code.vegaprotocol.io/vega@v0.79.0/core/execution/amm/engine_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 amm 17 18 import ( 19 "context" 20 "testing" 21 "time" 22 23 bmocks "code.vegaprotocol.io/vega/core/broker/mocks" 24 "code.vegaprotocol.io/vega/core/events" 25 "code.vegaprotocol.io/vega/core/execution/amm/mocks" 26 "code.vegaprotocol.io/vega/core/execution/common" 27 cmocks "code.vegaprotocol.io/vega/core/execution/common/mocks" 28 "code.vegaprotocol.io/vega/core/types" 29 vgcontext "code.vegaprotocol.io/vega/libs/context" 30 vgcrypto "code.vegaprotocol.io/vega/libs/crypto" 31 "code.vegaprotocol.io/vega/libs/num" 32 "code.vegaprotocol.io/vega/libs/ptr" 33 "code.vegaprotocol.io/vega/logging" 34 v1 "code.vegaprotocol.io/vega/protos/vega/snapshot/v1" 35 36 "github.com/golang/mock/gomock" 37 "github.com/stretchr/testify/assert" 38 "github.com/stretchr/testify/require" 39 ) 40 41 var ( 42 riskFactors = &types.RiskFactor{Market: "", Short: num.DecimalOne(), Long: num.DecimalOne()} 43 scalingFactors = &types.ScalingFactors{InitialMargin: num.DecimalOne()} 44 slippage = num.DecimalOne() 45 ) 46 47 func TestSubmitAMM(t *testing.T) { 48 t.Run("test one pool per party", testOnePoolPerParty) 49 t.Run("test creation of sparse AMM", testSparseAMMEngine) 50 t.Run("test AMM snapshot", testAMMSnapshot) 51 } 52 53 func TestAMMTrading(t *testing.T) { 54 t.Run("test basic submit order", testBasicSubmitOrder) 55 t.Run("test submit order at best price", testSubmitOrderAtBestPrice) 56 t.Run("test submit market order", testSubmitMarketOrder) 57 t.Run("test submit market order unbounded", testSubmitMarketOrderUnbounded) 58 t.Run("test submit order pro rata", testSubmitOrderProRata) 59 t.Run("test best prices and volume", testBestPricesAndVolume) 60 61 t.Run("test submit buy order across AMM boundary", testSubmitOrderAcrossAMMBoundary) 62 t.Run("test submit sell order across AMM boundary", testSubmitOrderAcrossAMMBoundarySell) 63 } 64 65 func TestAmendAMM(t *testing.T) { 66 t.Run("test amend AMM which doesn't exist", testAmendAMMWhichDoesntExist) 67 t.Run("test amend AMM with sparse amend", testAmendAMMSparse) 68 t.Run("test amend AMM insufficient commitment", testAmendInsufficientCommitment) 69 t.Run("test amend AMM when position to large", testAmendWhenPositionLarge) 70 } 71 72 func TestClosingAMM(t *testing.T) { 73 t.Run("test closing a pool as reduce only when its position is 0", testClosingReduceOnlyPool) 74 t.Run("test amending closing pool makes it actives", testAmendMakesClosingPoolActive) 75 t.Run("test closing pool removed when position hits zero", testClosingPoolRemovedWhenPositionZero) 76 t.Run("test closing pool immediately", testClosingPoolImmediate) 77 } 78 79 func TestStoppingAMM(t *testing.T) { 80 t.Run("test stopping distressed AMM", testStoppingDistressedAMM) 81 t.Run("test AMM with no balance is stopped", testAMMWithNoBalanceStopped) 82 t.Run("test market closure", testMarketClosure) 83 } 84 85 func testOnePoolPerParty(t *testing.T) { 86 ctx := context.Background() 87 tst := getTestEngine(t) 88 89 party, subAccount := getParty(t, tst) 90 submit := getPoolSubmission(t, party, tst.marketID) 91 92 expectSubaccountCreation(t, tst, party, subAccount) 93 whenAMMIsSubmitted(t, tst, submit) 94 95 // when the party submits another, it is rejected 96 _, err := tst.engine.Create(ctx, submit, vgcrypto.RandomHash(), riskFactors, scalingFactors, slippage) 97 require.ErrorContains(t, err, "party already own a pool for market") 98 } 99 100 func testAmendAMMWhichDoesntExist(t *testing.T) { 101 ctx := context.Background() 102 tst := getTestEngine(t) 103 104 // make an amend when the party doesn't have a pool 105 party, _ := getParty(t, tst) 106 amend := getPoolAmendment(t, party, tst.marketID) 107 108 _, _, err := tst.engine.Amend(ctx, amend, riskFactors, scalingFactors, slippage) 109 require.ErrorIs(t, err, ErrNoPoolMatchingParty) 110 } 111 112 func testAmendAMMSparse(t *testing.T) { 113 ctx := context.Background() 114 tst := getTestEngine(t) 115 116 party, subAccount := getParty(t, tst) 117 submit := getPoolSubmission(t, party, tst.marketID) 118 expectSubaccountCreation(t, tst, party, subAccount) 119 whenAMMIsSubmitted(t, tst, submit) 120 121 amend := getPoolAmendment(t, party, tst.marketID) 122 // no amend to the commitment amount 123 amend.CommitmentAmount = nil 124 // no amend to the margin factors either 125 amend.Parameters.LeverageAtLowerBound = nil 126 amend.Parameters.LeverageAtUpperBound = nil 127 // to change something at least, inc the base + bounds by 1 128 amend.Parameters.Base.AddSum(num.UintOne()) 129 amend.Parameters.UpperBound.AddSum(num.UintOne()) 130 amend.Parameters.LowerBound.AddSum(num.UintOne()) 131 132 ensurePosition(t, tst.pos, 0, nil) 133 updated, _, err := tst.engine.Amend(ctx, amend, riskFactors, scalingFactors, slippage) 134 require.NoError(t, err) 135 136 tst.engine.Confirm(ctx, updated) 137 } 138 139 func testAmendInsufficientCommitment(t *testing.T) { 140 ctx := context.Background() 141 tst := getTestEngine(t) 142 143 party, subAccount := getParty(t, tst) 144 submit := getPoolSubmission(t, party, tst.marketID) 145 expectSubaccountCreation(t, tst, party, subAccount) 146 whenAMMIsSubmitted(t, tst, submit) 147 148 poolID := tst.engine.poolsCpy[0].ID 149 150 amend := getPoolAmendment(t, party, tst.marketID) 151 // no amend to the commitment amount 152 amend.CommitmentAmount = nil 153 154 // amend to super wide bounds so that the commitment is too thin to support the AMM 155 amend.Parameters.Base.AddSum(num.UintOne()) 156 amend.Parameters.UpperBound.AddSum(num.NewUint(1000000)) 157 amend.Parameters.LowerBound.AddSum(num.UintOne()) 158 159 _, _, err := tst.engine.Amend(ctx, amend, riskFactors, scalingFactors, slippage) 160 require.ErrorContains(t, err, "commitment amount too low") 161 162 // check that the original pool still exists 163 assert.Equal(t, poolID, tst.engine.poolsCpy[0].ID) 164 } 165 166 func testAmendWhenPositionLarge(t *testing.T) { 167 ctx := context.Background() 168 tst := getTestEngine(t) 169 170 party, subAccount := getParty(t, tst) 171 submit := getPoolSubmission(t, party, tst.marketID) 172 expectSubaccountCreation(t, tst, party, subAccount) 173 whenAMMIsSubmitted(t, tst, submit) 174 175 poolID := tst.engine.poolsCpy[0].ID 176 177 amend := getPoolAmendment(t, party, tst.marketID) 178 179 // lower commitment so that the AMM's position at the same price bounds will be less 180 amend.CommitmentAmount = num.NewUint(50000000000) 181 182 expectBalanceChecks(t, tst, party, subAccount, 100000000000) 183 ensurePosition(t, tst.pos, 20000000, nil) 184 _, _, err := tst.engine.Amend(ctx, amend, riskFactors, scalingFactors, slippage) 185 require.ErrorContains(t, err, "current position is outside of amended bounds") 186 187 // check that the original pool still exists 188 assert.Equal(t, poolID, tst.engine.poolsCpy[0].ID) 189 190 expectBalanceChecks(t, tst, party, subAccount, 100000000000) 191 ensurePosition(t, tst.pos, -20000000, nil) 192 _, _, err = tst.engine.Amend(ctx, amend, riskFactors, scalingFactors, slippage) 193 require.ErrorContains(t, err, "current position is outside of amended bounds") 194 195 // check that the original pool still exists 196 assert.Equal(t, poolID, tst.engine.poolsCpy[0].ID) 197 } 198 199 func testBasicSubmitOrder(t *testing.T) { 200 tst := getTestEngine(t) 201 202 party, subAccount := getParty(t, tst) 203 submit := getPoolSubmission(t, party, tst.marketID) 204 205 expectSubaccountCreation(t, tst, party, subAccount) 206 whenAMMIsSubmitted(t, tst, submit) 207 208 // now submit an order against it 209 agg := &types.Order{ 210 Size: 1000000, 211 Remaining: 1000000, 212 Side: types.SideBuy, 213 Price: num.NewUint(2100), 214 Type: types.OrderTypeLimit, 215 } 216 217 ensurePosition(t, tst.pos, 0, num.NewUint(0)) 218 orders := tst.engine.SubmitOrder(agg, num.NewUint(2000), num.NewUint(2020)) 219 require.Len(t, orders, 1) 220 assert.Equal(t, "2009", orders[0].Price.String()) 221 assert.Equal(t, 236855, int(orders[0].Size)) 222 223 // AMM is now short, but another order comes in that will flip its position to long 224 agg = &types.Order{ 225 Size: 1000000, 226 Remaining: 1000000, 227 Side: types.SideSell, 228 Price: num.NewUint(1900), 229 } 230 231 // fair-price is now 2020 232 bb, _, ba, _ := tst.engine.BestPricesAndVolumes() 233 assert.Equal(t, "2019", bb.String()) 234 assert.Equal(t, "2021", ba.String()) 235 236 orders = tst.engine.SubmitOrder(agg, num.NewUint(2020), num.NewUint(1990)) 237 238 // two orders because we have to split it when we trade across the base-price as thats where we move from one curve to the other. 239 require.Len(t, orders, 2) 240 assert.Equal(t, "2009", orders[0].Price.String()) 241 assert.Equal(t, 236855, int(orders[0].Size)) 242 243 assert.Equal(t, "1994", orders[1].Price.String()) 244 assert.Equal(t, 125470, int(orders[1].Size)) 245 } 246 247 func testSubmitOrderAtBestPrice(t *testing.T) { 248 tst := getTestEngine(t) 249 250 party, subAccount := getParty(t, tst) 251 submit := getPoolSubmission(t, party, tst.marketID) 252 253 expectSubaccountCreation(t, tst, party, subAccount) 254 whenAMMIsSubmitted(t, tst, submit) 255 256 // AMM has fair-price of 2000 so is willing to sell at 2001, send an incoming buy order at 2001 257 agg := &types.Order{ 258 Size: 1000000, 259 Remaining: 1000000, 260 Side: types.SideBuy, 261 Price: num.NewUint(2001), 262 Type: types.OrderTypeLimit, 263 } 264 265 ensurePosition(t, tst.pos, 0, num.NewUint(0)) 266 orders := tst.engine.SubmitOrder(agg, num.NewUint(2000), num.NewUint(2001)) 267 require.Len(t, orders, 1) 268 assert.Equal(t, "2000", orders[0].Price.String()) 269 assert.Equal(t, 11927, int(orders[0].Size)) 270 271 bb, _, ba, _ := tst.engine.BestPricesAndVolumes() 272 assert.Equal(t, "2002", ba.String()) 273 assert.Equal(t, "2000", bb.String()) 274 275 // now trade back with a price of 2000 276 agg = &types.Order{ 277 Size: 1000000, 278 Remaining: 1000000, 279 Side: types.SideSell, 280 Price: num.NewUint(2000), 281 Type: types.OrderTypeLimit, 282 } 283 orders = tst.engine.SubmitOrder(agg, num.NewUint(2001), num.NewUint(2000)) 284 require.Len(t, orders, 1) 285 assert.Equal(t, "2000", orders[0].Price.String()) 286 assert.Equal(t, 11927, int(orders[0].Size)) 287 } 288 289 func testSubmitMarketOrder(t *testing.T) { 290 tst := getTestEngine(t) 291 292 party, subAccount := getParty(t, tst) 293 submit := getPoolSubmission(t, party, tst.marketID) 294 295 expectSubaccountCreation(t, tst, party, subAccount) 296 whenAMMIsSubmitted(t, tst, submit) 297 298 // now submit an order against it 299 agg := &types.Order{ 300 Size: 1000000, 301 Remaining: 1000000, 302 Side: types.SideSell, 303 Price: num.NewUint(0), 304 Type: types.OrderTypeMarket, 305 } 306 307 ensurePosition(t, tst.pos, 0, num.NewUint(0)) 308 orders := tst.engine.SubmitOrder(agg, num.NewUint(2000), num.NewUint(1980)) 309 require.Len(t, orders, 1) 310 assert.Equal(t, "1989", orders[0].Price.String()) 311 assert.Equal(t, 251890, int(orders[0].Size)) 312 } 313 314 func testSubmitMarketOrderUnbounded(t *testing.T) { 315 tst := getTestEngine(t) 316 317 party, subAccount := getParty(t, tst) 318 submit := getPoolSubmission(t, party, tst.marketID) 319 320 expectSubaccountCreation(t, tst, party, subAccount) 321 whenAMMIsSubmitted(t, tst, submit) 322 323 // now submit an order against it 324 agg := &types.Order{ 325 Size: 1000000, 326 Remaining: 1000000, 327 Side: types.SideSell, 328 Price: num.NewUint(0), 329 Type: types.OrderTypeMarket, 330 } 331 332 ensurePosition(t, tst.pos, 0, num.NewUint(0)) 333 orders := tst.engine.SubmitOrder(agg, num.NewUint(1980), nil) 334 require.Len(t, orders, 1) 335 assert.Equal(t, "1960", orders[0].Price.String()) 336 assert.Equal(t, 1000000, int(orders[0].Size)) 337 } 338 339 func testSubmitOrderProRata(t *testing.T) { 340 tst := getTestEngine(t) 341 342 // create three pools 343 for i := 0; i < 3; i++ { 344 party, subAccount := getParty(t, tst) 345 submit := getPoolSubmission(t, party, tst.marketID) 346 347 expectSubaccountCreation(t, tst, party, subAccount) 348 whenAMMIsSubmitted(t, tst, submit) 349 } 350 351 ensurePositionN(t, tst.pos, 0, num.NewUint(0), 3) 352 353 // now submit an order against it 354 agg := &types.Order{ 355 Size: 666, 356 Remaining: 666, 357 Side: types.SideBuy, 358 Price: num.NewUint(2100), 359 } 360 orders := tst.engine.SubmitOrder(agg, num.NewUint(2010), num.NewUint(2020)) 361 require.Len(t, orders, 3) 362 for _, o := range orders { 363 assert.Equal(t, "2000", o.Price.String()) 364 assert.Equal(t, uint64(222), o.Size) 365 } 366 } 367 368 func testSubmitOrderAcrossAMMBoundary(t *testing.T) { 369 tst := getTestEngine(t) 370 371 // create three pools 372 for i := 0; i < 3; i++ { 373 party, subAccount := getParty(t, tst) 374 submit := getPoolSubmission(t, party, tst.marketID) 375 376 // going to shrink the boundaries 377 submit.Parameters.LowerBound.Add(submit.Parameters.LowerBound, num.NewUint(uint64(i*50))) 378 submit.Parameters.UpperBound.Sub(submit.Parameters.UpperBound, num.NewUint(uint64(i*50))) 379 380 expectSubaccountCreation(t, tst, party, subAccount) 381 whenAMMIsSubmitted(t, tst, submit) 382 } 383 384 ensureBalancesN(t, tst.col, 10000000000, -1) 385 ensurePositionN(t, tst.pos, 0, num.NewUint(0), -1) 386 387 // now submit an order against it 388 agg := &types.Order{ 389 Size: 1000000000000, 390 Remaining: 1000000000000, 391 Side: types.SideBuy, 392 Price: num.NewUint(2200), 393 } 394 395 // pools upper boundaries are 2100, 2150, 2200, and we submit a big order 396 // we expect to trade with each pool in these three chunks 397 // - first 3 orders with all pools from [2000, 2100] 398 // - then 2 orders with the longer two pools from [2100, 2150] 399 // - then 1 order just the last pool from [2150, 2200] 400 // so 6 orders in total 401 orders := tst.engine.SubmitOrder(agg, num.NewUint(2000), num.NewUint(2200)) 402 require.Len(t, orders, 6) 403 404 // first round, three orders moving all pool's to the upper boundary of the shortest 405 assert.Equal(t, "2049", orders[0].Price.String()) 406 assert.Equal(t, "2049", orders[1].Price.String()) 407 assert.Equal(t, "2049", orders[2].Price.String()) 408 409 // second round, 2 orders moving all pool's to the upper boundary of the second shortest 410 assert.Equal(t, "2124", orders[3].Price.String()) 411 assert.Equal(t, "2124", orders[4].Price.String()) 412 413 // third round, 1 orders moving the last pool to its boundary 414 assert.Equal(t, "2174", orders[5].Price.String()) 415 } 416 417 func testSubmitOrderAcrossAMMBoundarySell(t *testing.T) { 418 tst := getTestEngine(t) 419 420 // create three pools 421 for i := 0; i < 3; i++ { 422 party, subAccount := getParty(t, tst) 423 submit := getPoolSubmission(t, party, tst.marketID) 424 425 // going to shrink the boundaries 426 submit.Parameters.LowerBound.Add(submit.Parameters.LowerBound, num.NewUint(uint64(i*50))) 427 submit.Parameters.UpperBound.Sub(submit.Parameters.UpperBound, num.NewUint(uint64(i*50))) 428 429 expectSubaccountCreation(t, tst, party, subAccount) 430 whenAMMIsSubmitted(t, tst, submit) 431 } 432 433 ensureBalancesN(t, tst.col, 10000000000, -1) 434 ensurePositionN(t, tst.pos, 0, num.NewUint(0), -1) 435 436 // now submit an order against it 437 agg := &types.Order{ 438 Size: 1000000000000, 439 Remaining: 1000000000000, 440 Side: types.SideSell, 441 Price: num.NewUint(1800), 442 } 443 444 // pools lower boundaries are 1800, 1850, 1900, and we submit a big order 445 // we expect to trade with each pool in these three chunks 446 // - first 3 orders with all pools from [2000, 1900] 447 // - then 2 orders with the longer two pools from [1900, 1850] 448 // - then 1 order just the last pool from [1850, 1800] 449 // so 6 orders in total 450 // orders := tst.engine.SubmitOrder(agg, num.NewUint(2000), num.NewUint(1800)) 451 orders := tst.engine.SubmitOrder(agg, num.NewUint(2000), num.NewUint(1800)) 452 require.Len(t, orders, 6) 453 454 // first round, three orders moving all pool's to the upper boundary of the shortest 455 assert.Equal(t, "1949", orders[0].Price.String()) 456 assert.Equal(t, "1949", orders[1].Price.String()) 457 assert.Equal(t, "1949", orders[2].Price.String()) 458 459 // second round, 2 orders moving all pool's to the upper boundary of the second shortest 460 assert.Equal(t, "1874", orders[3].Price.String()) 461 assert.Equal(t, "1874", orders[4].Price.String()) 462 463 // third round, 1 orders moving the last pool to its boundary 464 assert.Equal(t, "1824", orders[5].Price.String()) 465 } 466 467 func testBestPricesAndVolume(t *testing.T) { 468 tst := getTestEngine(t) 469 470 // create three pools 471 for i := 0; i < 3; i++ { 472 party, subAccount := getParty(t, tst) 473 submit := getPoolSubmission(t, party, tst.marketID) 474 475 expectSubaccountCreation(t, tst, party, subAccount) 476 whenAMMIsSubmitted(t, tst, submit) 477 } 478 479 tst.pos.EXPECT().GetPositionsByParty(gomock.Any()).AnyTimes().Return( 480 []events.MarketPosition{&marketPosition{size: 0, averageEntry: num.NewUint(0)}}, 481 ) 482 483 bid, bvolume, ask, avolume := tst.engine.BestPricesAndVolumes() 484 assert.Equal(t, "1999", bid.String()) 485 assert.Equal(t, "2001", ask.String()) 486 assert.Equal(t, 37512, int(bvolume)) 487 assert.Equal(t, 35781, int(avolume)) 488 489 // test GetVolumeAtPrice returns the same volume given best bid/ask 490 bvAt := tst.engine.GetVolumeAtPrice(bid, types.SideSell) 491 assert.Equal(t, bvolume, bvAt) 492 avAt := tst.engine.GetVolumeAtPrice(ask, types.SideBuy) 493 assert.Equal(t, avolume, avAt) 494 } 495 496 func TestBestPricesAndVolumeNearBound(t *testing.T) { 497 tst := getTestEngineWithFactors(t, num.DecimalFromInt64(100), num.DecimalFromFloat(10), 0) 498 499 // create three pools 500 party, subAccount := getParty(t, tst) 501 submit := getPoolSubmission(t, party, tst.marketID) 502 503 expectSubaccountCreation(t, tst, party, subAccount) 504 whenAMMIsSubmitted(t, tst, submit) 505 506 tst.pos.EXPECT().GetPositionsByParty(gomock.Any()).Times(10).Return( 507 []events.MarketPosition{&marketPosition{size: 0, averageEntry: num.NewUint(0)}}, 508 ) 509 510 bid, bvolume, ask, avolume := tst.engine.BestPricesAndVolumes() 511 assert.Equal(t, "199900", bid.String()) 512 assert.Equal(t, "200100", ask.String()) 513 assert.Equal(t, 1250, int(bvolume)) 514 assert.Equal(t, 1192, int(avolume)) 515 516 // lets move its position so that the fair price is within one tick of the AMMs upper boundary 517 tst.pos.EXPECT().GetPositionsByParty(gomock.Any()).Times(10).Return( 518 []events.MarketPosition{&marketPosition{size: -222000, averageEntry: num.NewUint(0)}}, 519 ) 520 521 bid, bvolume, ask, avolume = tst.engine.BestPricesAndVolumes() 522 assert.Equal(t, "219890", bid.String()) 523 assert.Equal(t, "220000", ask.String()) // make sure we are capped to the boundary and not 220090 524 assert.Equal(t, 1034, int(bvolume)) 525 assert.Equal(t, 104, int(avolume)) 526 527 // lets move its position so that the fair price is within one tick of the AMMs upper boundary 528 tst.pos.EXPECT().GetPositionsByParty(gomock.Any()).Times(10).Return( 529 []events.MarketPosition{&marketPosition{size: 270400, averageEntry: num.NewUint(0)}}, 530 ) 531 532 bid, bvolume, ask, avolume = tst.engine.BestPricesAndVolumes() 533 assert.Equal(t, "180000", bid.String()) // make sure we are capped to the boundary and not 179904 534 assert.Equal(t, "180104", ask.String()) 535 assert.Equal(t, 62, int(bvolume)) 536 assert.Equal(t, 1460, int(avolume)) 537 } 538 539 func testClosingReduceOnlyPool(t *testing.T) { 540 ctx := context.Background() 541 tst := getTestEngine(t) 542 543 party, subAccount := getParty(t, tst) 544 submit := getPoolSubmission(t, party, tst.marketID) 545 546 expectSubaccountCreation(t, tst, party, subAccount) 547 whenAMMIsSubmitted(t, tst, submit) 548 549 // pool position is zero it should get removed right away with no fuss 550 ensurePosition(t, tst.pos, 0, num.UintZero()) 551 ensurePosition(t, tst.pos, 0, num.UintZero()) 552 expectSubAccountRelease(t, tst, party, subAccount) 553 cancel := getCancelSubmission(t, party, tst.marketID, types.AMMCancellationMethodReduceOnly) 554 mevt, err := tst.engine.CancelAMM(ctx, cancel) 555 require.NoError(t, err) 556 assert.Nil(t, mevt) // no closeout necessary so not event 557 tst.engine.OnMTM(ctx) 558 assert.Len(t, tst.engine.pools, 0) 559 } 560 561 func testClosingPoolImmediate(t *testing.T) { 562 ctx := context.Background() 563 tst := getTestEngine(t) 564 565 party, subAccount := getParty(t, tst) 566 submit := getPoolSubmission(t, party, tst.marketID) 567 568 expectSubaccountCreation(t, tst, party, subAccount) 569 whenAMMIsSubmitted(t, tst, submit) 570 571 // pool has a position but gets closed anyway 572 ensurePosition(t, tst.pos, 12, num.UintZero()) 573 expectSubAccountRelease(t, tst, party, subAccount) 574 cancel := getCancelSubmission(t, party, tst.marketID, types.AMMCancellationMethodImmediate) 575 mevt, err := tst.engine.CancelAMM(ctx, cancel) 576 require.NoError(t, err) 577 assert.Nil(t, mevt) // no closeout necessary so not event 578 assert.Len(t, tst.engine.pools, 0) 579 } 580 581 func testAmendMakesClosingPoolActive(t *testing.T) { 582 ctx := context.Background() 583 tst := getTestEngine(t) 584 585 party, subAccount := getParty(t, tst) 586 submit := getPoolSubmission(t, party, tst.marketID) 587 588 expectSubaccountCreation(t, tst, party, subAccount) 589 whenAMMIsSubmitted(t, tst, submit) 590 591 // pool position is non-zero so it''l hang around 592 ensurePosition(t, tst.pos, 12, num.UintZero()) 593 cancel := getCancelSubmission(t, party, tst.marketID, types.AMMCancellationMethodReduceOnly) 594 closeout, err := tst.engine.CancelAMM(ctx, cancel) 595 require.NoError(t, err) 596 assert.Nil(t, closeout) 597 tst.engine.OnMTM(ctx) 598 assert.Len(t, tst.engine.pools, 1) 599 assert.True(t, tst.engine.poolsCpy[0].closing()) 600 601 amend := getPoolAmendment(t, party, tst.marketID) 602 expectBalanceChecks(t, tst, party, subAccount, amend.CommitmentAmount.Uint64()) 603 ensurePosition(t, tst.pos, 0, num.UintZero()) 604 updated, _, err := tst.engine.Amend(ctx, amend, riskFactors, scalingFactors, slippage) 605 require.NoError(t, err) 606 tst.engine.Confirm(ctx, updated) 607 608 // pool is active again 609 assert.False(t, tst.engine.poolsCpy[0].closing()) 610 } 611 612 func testClosingPoolRemovedWhenPositionZero(t *testing.T) { 613 ctx := vgcontext.WithTraceID(context.Background(), vgcrypto.RandomHash()) 614 tst := getTestEngine(t) 615 616 party, subAccount := getParty(t, tst) 617 submit := getPoolSubmission(t, party, tst.marketID) 618 619 expectSubaccountCreation(t, tst, party, subAccount) 620 whenAMMIsSubmitted(t, tst, submit) 621 622 // pool position is non-zero so it''l hang around 623 ensurePosition(t, tst.pos, 12, num.UintZero()) 624 cancel := getCancelSubmission(t, party, tst.marketID, types.AMMCancellationMethodReduceOnly) 625 closeout, err := tst.engine.CancelAMM(ctx, cancel) 626 require.NoError(t, err) 627 assert.Nil(t, closeout) 628 tst.engine.OnMTM(ctx) 629 assert.True(t, tst.engine.poolsCpy[0].closing()) 630 631 // position is lower but non-zero 632 ensurePosition(t, tst.pos, 1, num.UintZero()) 633 tst.engine.OnMTM(ctx) 634 assert.True(t, tst.engine.poolsCpy[0].closing()) 635 636 // position is zero, it will get removed 637 ensurePositionN(t, tst.pos, 0, num.UintZero(), 2) 638 expectSubAccountRelease(t, tst, party, subAccount) 639 tst.engine.OnMTM(ctx) 640 assert.Len(t, tst.engine.poolsCpy, 0) 641 } 642 643 func testStoppingDistressedAMM(t *testing.T) { 644 ctx := context.Background() 645 tst := getTestEngine(t) 646 647 party, subAccount := getParty(t, tst) 648 submit := getPoolSubmission(t, party, tst.marketID) 649 650 expectSubaccountCreation(t, tst, party, subAccount) 651 whenAMMIsSubmitted(t, tst, submit) 652 653 // call remove distressed with a AMM's owner will not remove the pool 654 closed := []events.MarketPosition{ 655 mpos{party}, 656 } 657 tst.engine.RemoveDistressed(ctx, closed) 658 assert.Len(t, tst.engine.pools, 1) 659 660 // call remove distressed with a AMM's subacouunt will remove the pool 661 closed = []events.MarketPosition{ 662 mpos{subAccount}, 663 } 664 tst.engine.RemoveDistressed(ctx, closed) 665 assert.Len(t, tst.engine.pools, 0) 666 } 667 668 func testAMMWithNoBalanceStopped(t *testing.T) { 669 ctx := vgcontext.WithTraceID(context.Background(), vgcrypto.RandomHash()) 670 tst := getTestEngine(t) 671 672 party, subAccount := getParty(t, tst) 673 submit := getPoolSubmission(t, party, tst.marketID) 674 675 expectSubaccountCreation(t, tst, party, subAccount) 676 whenAMMIsSubmitted(t, tst, submit) 677 ensureBalances(t, tst.col, 10000) 678 tst.engine.OnTick(ctx, time.Now()) 679 assert.Len(t, tst.engine.pools, 1) 680 681 ensureBalances(t, tst.col, 0) 682 tst.engine.OnTick(ctx, time.Now()) 683 assert.Len(t, tst.engine.pools, 0) 684 } 685 686 func testMarketClosure(t *testing.T) { 687 ctx := vgcontext.WithTraceID(context.Background(), vgcrypto.RandomHash()) 688 tst := getTestEngine(t) 689 690 for i := 0; i < 10; i++ { 691 party, subAccount := getParty(t, tst) 692 submit := getPoolSubmission(t, party, tst.marketID) 693 694 expectSubaccountCreation(t, tst, party, subAccount) 695 whenAMMIsSubmitted(t, tst, submit) 696 expectSubAccountClose(t, tst, party, subAccount) 697 } 698 699 require.NoError(t, tst.engine.MarketClosing(ctx)) 700 require.Equal(t, 0, len(tst.engine.pools)) 701 require.Equal(t, 0, len(tst.engine.poolsCpy)) 702 require.Equal(t, 0, len(tst.engine.ammParties)) 703 } 704 705 func testSparseAMMEngine(t *testing.T) { 706 tst := getTestEngineWithFactors(t, num.DecimalOne(), num.DecimalOne(), 10) 707 708 party, subAccount := getParty(t, tst) 709 submit := getPoolSubmission(t, party, tst.marketID) 710 submit.CommitmentAmount = num.NewUint(100000) 711 712 expectSubaccountCreation(t, tst, party, subAccount) 713 whenAMMIsSubmitted(t, tst, submit) 714 715 tst.pos.EXPECT().GetPositionsByParty(gomock.Any()).AnyTimes().Return( 716 []events.MarketPosition{&marketPosition{size: 0, averageEntry: nil}}, 717 ) 718 bb, bv, ba, av := tst.engine.BestPricesAndVolumes() 719 assert.Equal(t, "1992", bb.String()) 720 assert.Equal(t, 1, int(bv)) 721 assert.Equal(t, "2009", ba.String()) 722 assert.Equal(t, 1, int(av)) 723 } 724 725 func testAMMSnapshot(t *testing.T) { 726 tst := getTestEngine(t) 727 728 // create three pools 729 for i := 0; i < 3; i++ { 730 party, subAccount := getParty(t, tst) 731 submit := getPoolSubmission(t, party, tst.marketID) 732 733 expectSubaccountCreation(t, tst, party, subAccount) 734 whenAMMIsSubmitted(t, tst, submit) 735 } 736 737 ensurePositionN(t, tst.pos, 0, num.NewUint(0), 3) 738 739 // now submit an order against it 740 agg := &types.Order{ 741 Size: 666, 742 Remaining: 666, 743 Side: types.SideBuy, 744 Price: num.NewUint(2100), 745 } 746 orders := tst.engine.SubmitOrder(agg, num.NewUint(2010), num.NewUint(2020)) 747 require.Len(t, orders, 3) 748 for _, o := range orders { 749 assert.Equal(t, "2000", o.Price.String()) 750 assert.Equal(t, uint64(222), o.Size) 751 } 752 753 bb1, bv1, ba1, av1 := tst.engine.BestPricesAndVolumes() 754 755 // now snapshot 756 state := tst.engine.IntoProto() 757 tst2 := getTestEngineWithProto(t, state) 758 759 // now do some stuff with it 760 ensurePositionN(t, tst2.pos, -222, num.NewUint(0), -1) 761 bb2, bv2, ba2, av2 := tst2.engine.BestPricesAndVolumes() 762 assert.Equal(t, bb1, bb2) 763 assert.Equal(t, bv1, bv2) 764 assert.Equal(t, ba1, ba2) 765 assert.Equal(t, av1, av2) 766 767 // now submit an order against it 768 agg = &types.Order{ 769 Size: 666, 770 Remaining: 666, 771 Side: types.SideSell, 772 Price: num.NewUint(1000), 773 } 774 orders = tst2.engine.SubmitOrder(agg, nil, nil) 775 require.Len(t, orders, 3) 776 for _, o := range orders { 777 assert.Equal(t, "2000", o.Price.String()) 778 assert.Equal(t, uint64(222), o.Size) 779 } 780 } 781 782 func expectSubaccountCreation(t *testing.T, tst *tstEngine, party, subAccount string) { 783 t.Helper() 784 785 // accounts are created 786 tst.col.EXPECT().CreatePartyAMMsSubAccounts(gomock.Any(), party, subAccount, tst.assetID, tst.marketID).Times(1) 787 } 788 789 func expectSubAccountRelease(t *testing.T, tst *tstEngine, party, subAccount string) { 790 t.Helper() 791 // account is update from party's main accounts 792 tst.col.EXPECT().SubAccountRelease( 793 gomock.Any(), 794 party, 795 subAccount, 796 tst.assetID, 797 tst.marketID, 798 gomock.Any(), 799 ).Times(1).Return([]*types.LedgerMovement{}, nil, nil) 800 } 801 802 func expectSubAccountClose(t *testing.T, tst *tstEngine, party, subAccount string) { 803 t.Helper() 804 tst.col.EXPECT().SubAccountClosed( 805 gomock.Any(), 806 party, 807 subAccount, 808 tst.assetID, 809 tst.marketID).Times(1).Return([]*types.LedgerMovement{}, nil) 810 } 811 812 func expectBalanceChecks(t *testing.T, tst *tstEngine, party, subAccount string, total uint64) { 813 t.Helper() 814 tst.col.EXPECT().GetPartyMarginAccount(tst.marketID, subAccount, tst.assetID).Times(1).Return(getAccount(0), nil) 815 tst.col.EXPECT().GetPartyGeneralAccount(subAccount, tst.assetID).Times(1).Return(getAccount(0), nil) 816 tst.col.EXPECT().GetPartyGeneralAccount(party, tst.assetID).Times(1).Return(getAccount(total), nil) 817 } 818 819 func whenAMMIsSubmitted(t *testing.T, tst *tstEngine, submission *types.SubmitAMM) { 820 t.Helper() 821 822 party := submission.Party 823 subAccount := DeriveAMMParty(party, tst.marketID, "AMMv1", 0) 824 expectBalanceChecks(t, tst, party, subAccount, submission.CommitmentAmount.Uint64()) 825 826 ensurePosition(t, tst.pos, 0, nil) 827 828 ctx := context.Background() 829 pool, err := tst.engine.Create(ctx, submission, vgcrypto.RandomHash(), riskFactors, scalingFactors, slippage) 830 require.NoError(t, err) 831 tst.engine.Confirm(ctx, pool) 832 } 833 834 func getParty(t *testing.T, tst *tstEngine) (string, string) { 835 t.Helper() 836 837 party := vgcrypto.RandomHash() 838 subAccount := DeriveAMMParty(party, tst.marketID, "AMMv1", 0) 839 return party, subAccount 840 } 841 842 func getPoolSubmission(t *testing.T, party, market string) *types.SubmitAMM { 843 t.Helper() 844 return &types.SubmitAMM{ 845 AMMBaseCommand: types.AMMBaseCommand{ 846 Party: party, 847 MarketID: market, 848 SlippageTolerance: num.DecimalFromFloat(0.1), 849 }, 850 CommitmentAmount: num.NewUint(10000000000), 851 Parameters: &types.ConcentratedLiquidityParameters{ 852 Base: num.NewUint(2000), 853 LowerBound: num.NewUint(1800), 854 UpperBound: num.NewUint(2200), 855 LeverageAtLowerBound: ptr.From(num.DecimalOne()), 856 LeverageAtUpperBound: ptr.From(num.DecimalOne()), 857 }, 858 } 859 } 860 861 func getPoolAmendment(t *testing.T, party, market string) *types.AmendAMM { 862 t.Helper() 863 return &types.AmendAMM{ 864 AMMBaseCommand: types.AMMBaseCommand{ 865 Party: party, 866 MarketID: market, 867 SlippageTolerance: num.DecimalFromFloat(0.1), 868 }, 869 CommitmentAmount: num.NewUint(10000000000), 870 Parameters: &types.ConcentratedLiquidityParameters{ 871 Base: num.NewUint(2100), 872 LowerBound: num.NewUint(1900), 873 UpperBound: num.NewUint(2300), 874 LeverageAtLowerBound: ptr.From(num.DecimalOne()), 875 LeverageAtUpperBound: ptr.From(num.DecimalOne()), 876 }, 877 } 878 } 879 880 func getCancelSubmission(t *testing.T, party, market string, method types.AMMCancellationMethod) *types.CancelAMM { 881 t.Helper() 882 return &types.CancelAMM{ 883 MarketID: market, 884 Party: party, 885 Method: method, 886 } 887 } 888 889 type tstEngine struct { 890 engine *Engine 891 broker *bmocks.MockBroker 892 col *mocks.MockCollateral 893 pos *mocks.MockPosition 894 parties *cmocks.MockParties 895 ctrl *gomock.Controller 896 897 marketID string 898 assetID string 899 } 900 901 func getTestEngineWithFactors(t *testing.T, priceFactor, positionFactor num.Decimal, allowedEmptyLevels uint64) *tstEngine { 902 t.Helper() 903 ctrl := gomock.NewController(t) 904 col := mocks.NewMockCollateral(ctrl) 905 pos := mocks.NewMockPosition(ctrl) 906 broker := bmocks.NewMockBroker(ctrl) 907 908 marketID := vgcrypto.RandomHash() 909 assetID := vgcrypto.RandomHash() 910 911 broker.EXPECT().Send(gomock.Any()).AnyTimes() 912 col.EXPECT().GetAssetQuantum(assetID).AnyTimes().Return(num.DecimalOne(), nil) 913 914 teams := cmocks.NewMockTeams(ctrl) 915 balanceChecker := cmocks.NewMockAccountBalanceChecker(ctrl) 916 917 mat := common.NewMarketActivityTracker(logging.NewTestLogger(), teams, balanceChecker, broker, col) 918 919 parties := cmocks.NewMockParties(ctrl) 920 parties.EXPECT().AssignDeriveKey(gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes() 921 922 eng := New(logging.NewTestLogger(), broker, col, marketID, assetID, pos, priceFactor, positionFactor, mat, parties, allowedEmptyLevels) 923 924 // do an ontick to initialise the idgen 925 ctx := vgcontext.WithTraceID(context.Background(), vgcrypto.RandomHash()) 926 eng.OnTick(ctx, time.Now()) 927 928 return &tstEngine{ 929 engine: eng, 930 broker: broker, 931 col: col, 932 pos: pos, 933 ctrl: ctrl, 934 parties: parties, 935 marketID: marketID, 936 assetID: assetID, 937 } 938 } 939 940 func getTestEngineWithProto(t *testing.T, state *v1.AmmState) *tstEngine { 941 t.Helper() 942 ctrl := gomock.NewController(t) 943 col := mocks.NewMockCollateral(ctrl) 944 pos := mocks.NewMockPosition(ctrl) 945 broker := bmocks.NewMockBroker(ctrl) 946 947 marketID := vgcrypto.RandomHash() 948 assetID := vgcrypto.RandomHash() 949 950 broker.EXPECT().Send(gomock.Any()).AnyTimes() 951 col.EXPECT().GetAssetQuantum(assetID).AnyTimes().Return(num.DecimalOne(), nil) 952 953 teams := cmocks.NewMockTeams(ctrl) 954 balanceChecker := cmocks.NewMockAccountBalanceChecker(ctrl) 955 956 mat := common.NewMarketActivityTracker(logging.NewTestLogger(), teams, balanceChecker, broker, col) 957 958 parties := cmocks.NewMockParties(ctrl) 959 parties.EXPECT().AssignDeriveKey(gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes() 960 961 priceFactor := num.DecimalOne() 962 positionFactor := num.DecimalOne() 963 964 eng, err := NewFromProto(logging.NewTestLogger(), broker, col, marketID, assetID, pos, state, priceFactor, positionFactor, mat, parties, 0) 965 require.NoError(t, err) 966 967 return &tstEngine{ 968 engine: eng, 969 broker: broker, 970 col: col, 971 pos: pos, 972 ctrl: ctrl, 973 parties: parties, 974 marketID: marketID, 975 assetID: assetID, 976 } 977 } 978 979 func getTestEngine(t *testing.T) *tstEngine { 980 t.Helper() 981 return getTestEngineWithFactors(t, num.DecimalOne(), num.DecimalOne(), 0) 982 } 983 984 func getAccount(balance uint64) *types.Account { 985 return &types.Account{ 986 Balance: num.NewUint(balance), 987 } 988 } 989 990 type mpos struct { 991 party string 992 } 993 994 func (m mpos) AverageEntryPrice() *num.Uint { return num.UintZero() } 995 func (m mpos) Party() string { return m.party } 996 func (m mpos) Size() int64 { return 0 } 997 func (m mpos) Buy() int64 { return 0 } 998 func (m mpos) Sell() int64 { return 0 } 999 func (m mpos) Price() *num.Uint { return num.UintZero() } 1000 func (m mpos) BuySumProduct() *num.Uint { return num.UintZero() } 1001 func (m mpos) SellSumProduct() *num.Uint { return num.UintZero() } 1002 func (m mpos) ClearPotentials() {} 1003 func (m mpos) VWBuy() *num.Uint { return num.UintZero() } 1004 func (m mpos) VWSell() *num.Uint { return num.UintZero() }