code.vegaprotocol.io/vega@v0.79.0/core/matching/orderbook_amm_test.go (about) 1 // Copyright (C) 2023 Gobalsky Labs Limited 2 // 3 // This program is free software: you can redistribute it and/or modify 4 // it under the terms of the GNU Affero General Public License as 5 // published by the Free Software Foundation, either version 3 of the 6 // License, or (at your option) any later version. 7 // 8 // This program is distributed in the hope that it will be useful, 9 // but WITHOUT ANY WARRANTY; without even the implied warranty of 10 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 // GNU Affero General Public License for more details. 12 // 13 // You should have received a copy of the GNU Affero General Public License 14 // along with this program. If not, see <http://www.gnu.org/licenses/>. 15 16 package matching_test 17 18 import ( 19 "testing" 20 21 "code.vegaprotocol.io/vega/core/matching" 22 "code.vegaprotocol.io/vega/core/matching/mocks" 23 "code.vegaprotocol.io/vega/core/types" 24 vgcrypto "code.vegaprotocol.io/vega/libs/crypto" 25 "code.vegaprotocol.io/vega/libs/num" 26 "code.vegaprotocol.io/vega/logging" 27 28 "github.com/golang/mock/gomock" 29 "github.com/stretchr/testify/assert" 30 "github.com/stretchr/testify/require" 31 ) 32 33 func TestOrderbookAMM(t *testing.T) { 34 t.Run("test empty book and AMM", testEmptyBookAndAMM) 35 t.Run("test empty book and matching AMM", testEmptyBookMatchingAMM) 36 t.Run("test empty book and matching AMM with incoming FOK", testEmptyBookMatchingAMMFOK) 37 t.Run("test matching between price levels", testMatchBetweenPriceLevels) 38 t.Run("test matching with orders on both sides", testMatchOrdersBothSide) 39 t.Run("test check book accounts for AMM volumes", testOnlyAMMOrders) 40 } 41 42 func testOnlyAMMOrders(t *testing.T) { 43 tst := getTestOrderBookWithAMM(t) 44 defer tst.ctrl.Finish() 45 sellPrice := num.NewUint(111) 46 buyPrice := num.NewUint(95) 47 // create some regular orders 48 sell := createOrder(t, tst, 10, sellPrice) 49 buy := createOrder(t, tst, 10, buyPrice) 50 buy.Side = types.SideBuy 51 tst.obs.EXPECT().SubmitOrder(gomock.Any(), gomock.Any(), gomock.Any()).Times(4).Return(nil) 52 tst.obs.EXPECT().NotifyFinished().Times(3) 53 _, err := tst.book.SubmitOrder(sell) 54 require.NoError(t, err) 55 _, err = tst.book.SubmitOrder(buy) 56 require.NoError(t, err) 57 // create some pegged orders 58 pob := createOrder(t, tst, 100, nil) 59 pob.Party = "B" 60 pob.Side = types.SideBuy 61 pob.PeggedOrder = &types.PeggedOrder{ 62 Reference: types.PeggedReferenceBestBid, 63 Offset: num.NewUint(10), 64 } 65 pos := createOrder(t, tst, 100, nil) 66 pos.Party = "S" 67 pos.PeggedOrder = &types.PeggedOrder{ 68 Reference: types.PeggedReferenceBestAsk, 69 Offset: num.NewUint(10), 70 } 71 require.NoError(t, err) 72 _, err = tst.book.SubmitOrder(pos) 73 require.NoError(t, err) 74 require.Equal(t, uint64(1), tst.book.GetPeggedOrdersCount()) 75 // now cancel the non-pegged orders 76 require.NoError(t, err) 77 one, zero := uint64(1), uint64(0) 78 79 // only buy orders 80 tst.obs.EXPECT().BestPricesAndVolumes().Times(1).Return(num.UintOne(), one, nil, zero) 81 check := tst.book.CheckBook() 82 require.False(t, check) 83 84 // buy and sell orders 85 tst.obs.EXPECT().BestPricesAndVolumes().Times(1).Return(num.UintOne(), one, num.UintOne(), one) 86 check = tst.book.CheckBook() 87 require.True(t, check) 88 } 89 90 func testEmptyBookAndAMM(t *testing.T) { 91 tst := getTestOrderBookWithAMM(t) 92 defer tst.ctrl.Finish() 93 price := num.NewUint(100) 94 95 // fake uncross 96 o := createOrder(t, tst, 100, price) 97 tst.obs.EXPECT().SubmitOrder(gomock.Any(), nil, price).Times(1) 98 tst.obs.EXPECT().NotifyFinished().Times(1) 99 trades, err := tst.book.GetTrades(o) 100 assert.NoError(t, err) 101 assert.Len(t, trades, 0) 102 103 // uncross 104 tst.obs.EXPECT().SubmitOrder(gomock.Any(), nil, price).Times(1) 105 tst.obs.EXPECT().NotifyFinished().Times(1) 106 conf, err := tst.book.SubmitOrder(o) 107 assert.NoError(t, err) 108 assert.Len(t, conf.PassiveOrdersAffected, 0) 109 assert.Len(t, conf.Trades, 0) 110 } 111 112 func testEmptyBookMatchingAMM(t *testing.T) { 113 tst := getTestOrderBookWithAMM(t) 114 defer tst.ctrl.Finish() 115 price := num.NewUint(100) 116 117 o := createOrder(t, tst, 1000, price) 118 generated := createGeneratedOrders(t, tst, price) 119 120 // fake uncross 121 tst.obs.EXPECT().SubmitOrder(gomock.Any(), nil, price).Times(1).Return(generated) 122 tst.obs.EXPECT().NotifyFinished().Times(1) 123 trades, err := tst.book.GetTrades(o) 124 assert.NoError(t, err) 125 assert.Len(t, trades, 2) 126 127 // uncross 128 tst.obs.EXPECT().SubmitOrder(gomock.Any(), nil, price).Times(1).Return(generated) 129 tst.obs.EXPECT().NotifyFinished().Times(1) 130 conf, err := tst.book.SubmitOrder(o) 131 assert.NoError(t, err) 132 assertConf(t, conf, 2, 10) 133 } 134 135 func testEmptyBookMatchingAMMFOK(t *testing.T) { 136 tst := getTestOrderBookWithAMM(t) 137 defer tst.ctrl.Finish() 138 price := num.NewUint(100) 139 140 o := createOrder(t, tst, 20, price) 141 generated := createGeneratedOrders(t, tst, price) 142 143 o.TimeInForce = types.OrderTimeInForceFOK 144 145 // fake uncross 146 tst.obs.EXPECT().SubmitOrder(gomock.Any(), nil, price).Times(2).Return(generated) 147 tst.obs.EXPECT().NotifyFinished().Times(2) 148 trades, err := tst.book.GetTrades(o) 149 assert.NoError(t, err) 150 assert.Len(t, trades, 2) 151 152 // uncross 153 tst.obs.EXPECT().SubmitOrder(gomock.Any(), nil, price).Times(2).Return(generated) 154 tst.obs.EXPECT().NotifyFinished().Times(2) 155 conf, err := tst.book.SubmitOrder(o) 156 assert.NoError(t, err) 157 assertConf(t, conf, 2, 10) 158 } 159 160 func testMatchBetweenPriceLevels(t *testing.T) { 161 tst := getTestOrderBookWithAMM(t) 162 defer tst.ctrl.Finish() 163 164 createPriceLevels(t, tst, 10, 165 num.NewUint(100), 166 num.NewUint(110), 167 num.NewUint(120), 168 ) 169 170 price := num.NewUint(90) 171 size := uint64(1000) 172 173 o := createOrder(t, tst, size, price) 174 generated := createGeneratedOrders(t, tst, price) 175 176 // price levels at 100, 110, 120, incoming order at 100 177 // expect it to consume all volume at the three levels, and between each level we'll submit to offbook 178 tst.obs.EXPECT().SubmitOrder(gomock.Any(), gomock.Any(), gomock.Any()).Times(4).Return(generated) 179 tst.obs.EXPECT().NotifyFinished().Times(1) 180 trades, err := tst.book.GetTrades(o) 181 assert.NoError(t, err) 182 183 // 3 trades with each price level, and then 2 trades from AMM in the intervals 184 // (nil, 120) (120, 110) (110, 100) (100, 90) 185 // so 3 + (2 * 4) = 11 186 assert.Len(t, trades, 11) 187 188 // uncross 189 expectOffbookOrders(t, tst, price, nil, num.NewUint(120)) 190 expectOffbookOrders(t, tst, price, num.NewUint(120), num.NewUint(110)) 191 expectOffbookOrders(t, tst, price, num.NewUint(110), num.NewUint(100)) 192 expectOffbookOrders(t, tst, price, num.NewUint(100), num.NewUint(90)) 193 tst.obs.EXPECT().NotifyFinished().Times(1) 194 195 conf, err := tst.book.SubmitOrder(o) 196 assert.NoError(t, err) 197 assertConf(t, conf, 11, 10) 198 } 199 200 func testMatchOrdersBothSide(t *testing.T) { 201 tst := getTestOrderBookWithAMM(t) 202 defer tst.ctrl.Finish() 203 204 createPriceLevels(t, tst, 10, 205 num.NewUint(120), 206 num.NewUint(110), 207 ) 208 209 // this one will be on the opposite side of the book as price levels 210 // sell order willing to sell at 130 211 oPrice := uint64(130) 212 tst.obs.EXPECT().SubmitOrder(gomock.Any(), gomock.Any(), gomock.Any()).Times(1) 213 tst.obs.EXPECT().NotifyFinished().Times(1) 214 o := createOrder(t, tst, 10, num.NewUint(oPrice)) 215 216 conf, err := tst.book.SubmitOrder(o) 217 assert.NoError(t, err) 218 assertConf(t, conf, 0, 0) 219 220 price := num.NewUint(90) 221 size := uint64(1000) 222 o = createOrder(t, tst, size, price) 223 generated := createGeneratedOrders(t, tst, price) 224 225 // price levels at 100, 110, 120, incoming order at 100 226 // expect it to consume all volume at the three levels, and between each level we'll submit to offbook 227 tst.obs.EXPECT().SubmitOrder(gomock.Any(), gomock.Any(), gomock.Any()).Times(3).Return(generated) 228 tst.obs.EXPECT().NotifyFinished().Times(1) 229 trades, err := tst.book.GetTrades(o) 230 assert.NoError(t, err) 231 232 // 3 trades with each price level, and then 2 trades from AMM in the intervals 233 // (nil, 120) (120, 110) (110, 100) (100, 90) 234 // so 3 + (2 * 4) = 11 235 assert.Len(t, trades, 8) 236 237 // uncross 238 expectOffbookOrders(t, tst, price, nil, num.NewUint(120)) 239 expectOffbookOrders(t, tst, price, num.NewUint(120), num.NewUint(110)) 240 expectOffbookOrders(t, tst, price, num.NewUint(110), num.NewUint(90)) 241 tst.obs.EXPECT().NotifyFinished().Times(1) 242 243 conf, err = tst.book.SubmitOrder(o) 244 assert.NoError(t, err) 245 assertConf(t, conf, 8, 10) 246 } 247 248 func TestAMMOnlyBestPrices(t *testing.T) { 249 tst := getTestOrderBookWithAMM(t) 250 defer tst.ctrl.Finish() 251 252 tst.obs.EXPECT().BestPricesAndVolumes().Return( 253 num.NewUint(1999), 254 uint64(10), 255 num.NewUint(2001), 256 uint64(9), 257 ).AnyTimes() 258 259 // Best 260 price, err := tst.book.GetBestAskPrice() 261 require.NoError(t, err) 262 assert.Equal(t, "2001", price.String()) 263 264 price, err = tst.book.GetBestBidPrice() 265 require.NoError(t, err) 266 assert.Equal(t, "1999", price.String()) 267 268 // Best and volume 269 price, volume, err := tst.book.BestOfferPriceAndVolume() 270 require.NoError(t, err) 271 assert.Equal(t, "2001", price.String()) 272 assert.Equal(t, uint64(9), volume) 273 274 price, volume, err = tst.book.BestBidPriceAndVolume() 275 require.NoError(t, err) 276 assert.Equal(t, "1999", price.String()) 277 assert.Equal(t, uint64(10), volume) 278 279 // Best static 280 price, err = tst.book.GetBestStaticAskPrice() 281 require.NoError(t, err) 282 assert.Equal(t, "2001", price.String()) 283 284 price, err = tst.book.GetBestStaticBidPrice() 285 require.NoError(t, err) 286 assert.Equal(t, "1999", price.String()) 287 288 // Best static and volume 289 price, volume, err = tst.book.GetBestStaticAskPriceAndVolume() 290 require.NoError(t, err) 291 assert.Equal(t, "2001", price.String()) 292 assert.Equal(t, uint64(9), volume) 293 294 price, volume, err = tst.book.GetBestStaticBidPriceAndVolume() 295 require.NoError(t, err) 296 assert.Equal(t, "1999", price.String()) 297 assert.Equal(t, uint64(10), volume) 298 } 299 300 func TestIndicativeTradesAMMOnly(t *testing.T) { 301 tst := getTestOrderBookWithAMM(t) 302 defer tst.ctrl.Finish() 303 tst.obs.EXPECT().NotifyFinished().Times(1) 304 305 expectCrossedAMMs(t, tst, 100, 150) 306 tst.book.EnterAuction() 307 308 ret := createOrder(t, tst, 10, num.NewUint(100)) 309 tst.obs.EXPECT().SubmitOrder(gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes().DoAndReturn( 310 func(o *types.Order, _, _ *num.Uint) []*types.Order { 311 ret.Size = o.Remaining 312 ret.Remaining = o.Remaining 313 return []*types.Order{ret.Clone()} 314 }, 315 ) 316 317 trades, err := tst.book.GetIndicativeTrades() 318 require.NoError(t, err) 319 assert.Equal(t, 1, len(trades)) 320 assert.Equal(t, 260, int(trades[0].Size)) 321 } 322 323 func TestIndicativeTradesAMMOrderbookNotCrosses(t *testing.T) { 324 tst := getTestOrderBookWithAMM(t) 325 defer tst.ctrl.Finish() 326 tst.obs.EXPECT().NotifyFinished().Times(1) 327 328 expectCrossedAMMs(t, tst, 100, 150) 329 tst.book.EnterAuction() 330 331 // submit an order each side outside of the crossed region 332 o := createOrder(t, tst, 10, num.NewUint(90)) 333 o.Side = types.SideBuy 334 _, err := tst.book.SubmitOrder(o) 335 require.NoError(t, err) 336 337 o = createOrder(t, tst, 10, num.NewUint(160)) 338 o.Side = types.SideSell 339 _, err = tst.book.SubmitOrder(o) 340 require.NoError(t, err) 341 342 ret := createOrder(t, tst, 10, num.NewUint(100)) 343 tst.obs.EXPECT().SubmitOrder(gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes().DoAndReturn( 344 func(o *types.Order, _, _ *num.Uint) []*types.Order { 345 ret.Size = o.Remaining 346 ret.Remaining = o.Remaining 347 return []*types.Order{ret.Clone()} 348 }, 349 ) 350 351 trades, err := tst.book.GetIndicativeTrades() 352 require.NoError(t, err) 353 assert.Equal(t, 1, len(trades)) 354 assert.Equal(t, 260, int(trades[0].Size)) 355 } 356 357 func TestIndicativeTradesAMMCrossedOrders(t *testing.T) { 358 tst := getTestOrderBookWithAMM(t) 359 defer tst.ctrl.Finish() 360 tst.obs.EXPECT().NotifyFinished().Times(1) 361 362 expectCrossedAMMs(t, tst, 100, 150) 363 tst.book.EnterAuction() 364 365 o := createOrder(t, tst, 5, num.NewUint(125)) 366 o.Side = types.SideSell 367 _, err := tst.book.SubmitOrder(o) 368 require.NoError(t, err) 369 370 o = createOrder(t, tst, 5, num.NewUint(126)) 371 o.Side = types.SideSell 372 _, err = tst.book.SubmitOrder(o) 373 require.NoError(t, err) 374 375 ret := createOrder(t, tst, 250, num.NewUint(100)) 376 tst.obs.EXPECT().SubmitOrder(gomock.Any(), gomock.Any(), gomock.Any()).Times(1).Return([]*types.Order{ret.Clone()}) 377 378 tst.obs.EXPECT().SubmitOrder(gomock.Any(), gomock.Any(), gomock.Any()).Times(1).Return(nil) 379 380 trades, err := tst.book.GetIndicativeTrades() 381 require.NoError(t, err) 382 assert.Equal(t, 3, len(trades)) 383 var total int 384 for _, t := range trades { 385 total += int(t.Size) 386 } 387 assert.Equal(t, 260, total) 388 } 389 390 func TestUncrossedBookDoesNotExpandAMMs(t *testing.T) { 391 tst := getTestOrderBookWithAMM(t) 392 defer tst.ctrl.Finish() 393 394 // AMM with buy at 99 and SELL at 101 395 tst.obs.EXPECT().BestPricesAndVolumes().Return(num.NewUint(uint64(99)), uint64(10), num.NewUint(uint64(101)), uint64(10)).AnyTimes() 396 397 // enter auction when not crossed we should not try to expand AMM's 398 tst.book.EnterAuction() 399 assert.Equal(t, "0", tst.book.GetIndicativePrice().String()) 400 } 401 402 func assertConf(t *testing.T, conf *types.OrderConfirmation, n int, size uint64) { 403 t.Helper() 404 assert.Len(t, conf.PassiveOrdersAffected, n) 405 assert.Len(t, conf.Trades, n) 406 for i := range conf.Trades { 407 assert.Equal(t, conf.Trades[i].Size, size) 408 assert.Equal(t, conf.PassiveOrdersAffected[i].Remaining, uint64(0)) 409 } 410 } 411 412 func expectOffbookOrders(t *testing.T, tst *tstOrderbook, price, first, last *num.Uint) { 413 t.Helper() 414 generated := createGeneratedOrders(t, tst, price) 415 tst.obs.EXPECT().SubmitOrder(gomock.Any(), first, last).Times(1).Return(generated) 416 } 417 418 func expectCrossedAMMs(t *testing.T, tst *tstOrderbook, min, max int) { 419 t.Helper() 420 tst.obs.EXPECT().BestPricesAndVolumes().Return(num.NewUint(uint64(max)), uint64(10), num.NewUint(uint64(min)), uint64(10)).AnyTimes() 421 422 orders1 := createOrderbookShape(t, tst, min, max, types.SideBuy, "A") 423 orders2 := createOrderbookShape(t, tst, min, max, types.SideSell, "B") 424 res := []*types.OrderbookShapeResult{ 425 { 426 Buys: orders1, 427 Sells: orders2, 428 }, 429 } 430 431 tst.obs.EXPECT().OrderbookShape(gomock.Any(), gomock.Any(), gomock.Any()).Return(res) 432 } 433 434 type tstOrderbook struct { 435 ctrl *gomock.Controller 436 book *matching.CachedOrderBook 437 obs *mocks.MockOffbookSource 438 marketID string 439 } 440 441 func createOrder(t *testing.T, tst *tstOrderbook, size uint64, price *num.Uint) *types.Order { 442 t.Helper() 443 return &types.Order{ 444 ID: vgcrypto.RandomHash(), 445 Status: types.OrderStatusActive, 446 MarketID: tst.marketID, 447 Party: "A", 448 Side: types.SideSell, 449 Price: price, 450 OriginalPrice: price, 451 Size: size, 452 Remaining: size, 453 TimeInForce: types.OrderTimeInForceGTC, 454 Type: types.OrderTypeLimit, 455 } 456 } 457 458 func createGeneratedOrders(t *testing.T, tst *tstOrderbook, price *num.Uint) []*types.Order { 459 t.Helper() 460 461 orders := []*types.Order{} 462 for i := 0; i < 2; i++ { 463 o := createOrder(t, tst, 10, price) 464 o.Side = types.OtherSide(o.Side) 465 o.Party = "C" 466 orders = append(orders, o) 467 } 468 469 return orders 470 } 471 472 func createPriceLevels(t *testing.T, tst *tstOrderbook, size uint64, levels ...*num.Uint) { 473 t.Helper() 474 475 tst.obs.EXPECT().SubmitOrder(gomock.Any(), gomock.Any(), gomock.Any()).Times(len(levels)) 476 tst.obs.EXPECT().NotifyFinished().Times(len(levels)) 477 for _, l := range levels { 478 o := createOrder(t, tst, size, l) 479 o.Side = types.OtherSide(o.Side) 480 o.Party = "B" 481 conf, err := tst.book.SubmitOrder(o) 482 require.NoError(t, err) 483 require.Len(t, conf.Trades, 0) 484 } 485 } 486 487 func createOrderbookShape(t *testing.T, tst *tstOrderbook, from, to int, side types.Side, party string) []*types.Order { 488 t.Helper() 489 490 orders := []*types.Order{} 491 for i := from; i <= to; i++ { 492 o := createOrder(t, tst, 10, num.NewUint(uint64(i))) 493 o.GeneratedOffbook = true 494 o.Side = side 495 o.Party = party 496 orders = append(orders, o) 497 } 498 return orders 499 } 500 501 func getTestOrderBookWithAMM(t *testing.T) *tstOrderbook { 502 t.Helper() 503 504 ctrl := gomock.NewController(t) 505 obs := mocks.NewMockOffbookSource(ctrl) 506 507 marketID := "testMarket" 508 book := matching.NewCachedOrderBook(logging.NewTestLogger(), matching.NewDefaultConfig(), "testMarket", false, peggedOrderCounterForTest) 509 book.SetOffbookSource(obs) 510 511 return &tstOrderbook{ 512 ctrl: ctrl, 513 book: book, 514 obs: obs, 515 marketID: marketID, 516 } 517 }