code.vegaprotocol.io/vega@v0.79.0/core/execution/amm/pool_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 "strconv" 20 "testing" 21 22 "code.vegaprotocol.io/vega/core/events" 23 "code.vegaprotocol.io/vega/core/execution/amm/mocks" 24 "code.vegaprotocol.io/vega/core/types" 25 vgcrypto "code.vegaprotocol.io/vega/libs/crypto" 26 "code.vegaprotocol.io/vega/libs/num" 27 "code.vegaprotocol.io/vega/libs/ptr" 28 "code.vegaprotocol.io/vega/logging" 29 30 "github.com/golang/mock/gomock" 31 "github.com/stretchr/testify/assert" 32 "github.com/stretchr/testify/require" 33 ) 34 35 func TestAMMPool(t *testing.T) { 36 t.Run("test volume between prices", testTradeableVolumeInRange) 37 t.Run("test volume between prices when AMM closing", testTradeableVolumeInRangeClosing) 38 t.Run("test tradable volume at price", testTradableVolumeAtPrice) 39 t.Run("test best price", testBestPrice) 40 t.Run("test pool logic with position factor", testPoolPositionFactor) 41 t.Run("test one sided pool", testOneSidedPool) 42 t.Run("test near zero volume curve triggers and error", testNearZeroCurveErrors) 43 t.Run("test volume between prices when closing", testTradeableVolumeInRangeClosing) 44 t.Run("test sparse AMM", testSparseAMM) 45 } 46 47 func testTradeableVolumeInRange(t *testing.T) { 48 p := newTestPool(t) 49 defer p.ctrl.Finish() 50 51 tests := []struct { 52 name string 53 price1 *num.Uint 54 price2 *num.Uint 55 position int64 56 side types.Side 57 expectedVolume uint64 58 }{ 59 { 60 name: "full volume upper curve", 61 price1: num.NewUint(2000), 62 price2: num.NewUint(2200), 63 side: types.SideBuy, 64 expectedVolume: 635, 65 }, 66 { 67 name: "full volume upper curve with bound creep", 68 price1: num.NewUint(1500), 69 price2: num.NewUint(3500), 70 side: types.SideBuy, 71 expectedVolume: 635, 72 }, 73 { 74 name: "full volume lower curve", 75 price1: num.NewUint(1800), 76 price2: num.NewUint(2000), 77 side: types.SideSell, 78 expectedVolume: 702, 79 }, 80 { 81 name: "full volume lower curve with bound creep", 82 price1: num.NewUint(500), 83 price2: num.NewUint(2500), 84 side: types.SideSell, 85 expectedVolume: 702, 86 }, 87 { 88 name: "buy trade causes sign to flip and full volume crosses curves", 89 price1: num.NewUint(500), 90 price2: num.NewUint(3500), 91 side: types.SideBuy, 92 expectedVolume: 1335, 93 position: 700, // position at full lower boundary, incoming is by so whole volume of both curves is available 94 }, 95 { 96 name: "sell trade causes sign to flip and full volume crosses curves", 97 price1: num.NewUint(500), 98 price2: num.NewUint(3500), 99 side: types.SideSell, 100 expectedVolume: 1337, 101 position: -700, // position at full upper boundary, incoming is by so whole volume of both curves is available 102 }, 103 { 104 name: "buy trade causes sign to flip and partial volume across both curves", 105 price1: num.NewUint(500), 106 price2: num.NewUint(3500), 107 side: types.SideBuy, 108 expectedVolume: 985, 109 position: 350, 110 }, 111 { 112 name: "sell trade causes sign to flip and partial volume across both curves", 113 price1: num.NewUint(500), 114 price2: num.NewUint(3500), 115 side: types.SideSell, 116 expectedVolume: 1052, 117 position: -350, 118 }, 119 { 120 name: "AMM is long and price range is fully in short section", 121 price1: num.NewUint(1900), 122 price2: num.NewUint(2200), 123 side: types.SideSell, 124 expectedVolume: 0, 125 position: 700, 126 }, 127 { 128 name: "AMM is short and price range is fully in long section", 129 price1: num.NewUint(1900), 130 price2: num.NewUint(2100), 131 side: types.SideBuy, 132 expectedVolume: 0, 133 position: -700, 134 }, 135 } 136 137 for _, tt := range tests { 138 t.Run(tt.name, func(t *testing.T) { 139 ensurePositionN(t, p.pos, tt.position, num.UintZero(), 1) 140 volume := p.pool.TradableVolumeInRange(tt.side, tt.price1, tt.price2) 141 assert.Equal(t, int(tt.expectedVolume), int(volume)) 142 }) 143 } 144 } 145 146 func testTradeableVolumeInRangeClosing(t *testing.T) { 147 p := newTestPool(t) 148 defer p.ctrl.Finish() 149 150 // pool is reducing its 151 p.pool.status = types.AMMPoolStatusReduceOnly 152 153 tests := []struct { 154 name string 155 price1 *num.Uint 156 price2 *num.Uint 157 position int64 158 side types.Side 159 expectedVolume uint64 160 nposcalls int 161 }{ 162 { 163 name: "0 position, 0 buy volume", 164 price1: num.NewUint(1800), 165 price2: num.NewUint(2200), 166 side: types.SideBuy, 167 expectedVolume: 0, 168 nposcalls: 1, 169 }, 170 { 171 name: "0 position, 0 sell volume", 172 price1: num.NewUint(1800), 173 price2: num.NewUint(2200), 174 side: types.SideSell, 175 expectedVolume: 0, 176 nposcalls: 1, 177 }, 178 { 179 name: "long position, 0 volume for incoming SELL", 180 price1: num.NewUint(1800), 181 price2: num.NewUint(2200), 182 side: types.SideSell, 183 position: 10, 184 expectedVolume: 0, 185 nposcalls: 1, 186 }, 187 { 188 name: "long position, 10 volume for incoming BUY", 189 price1: num.NewUint(1800), 190 price2: num.NewUint(2200), 191 side: types.SideBuy, 192 position: 10, 193 expectedVolume: 10, 194 nposcalls: 2, 195 }, 196 { 197 name: "short position, 0 volume for incoming BUY", 198 price1: num.NewUint(1800), 199 price2: num.NewUint(2200), 200 side: types.SideBuy, 201 position: -10, 202 expectedVolume: 0, 203 nposcalls: 1, 204 }, 205 { 206 name: "short position, 10 volume for incoming SELL", 207 price1: num.NewUint(1800), 208 price2: num.NewUint(2200), 209 side: types.SideSell, 210 position: -10, 211 expectedVolume: 10, 212 nposcalls: 2, 213 }, 214 { 215 name: "asking for SELL volume but for prices outside of price ranges", 216 price1: num.NewUint(2000), 217 price2: num.NewUint(2200), 218 side: types.SideBuy, 219 position: 10, 220 expectedVolume: 0, 221 nposcalls: 2, 222 }, 223 { 224 name: "asking for BUY volume but for prices outside of price ranges", 225 price1: num.NewUint(1800), 226 price2: num.NewUint(1850), 227 side: types.SideSell, 228 position: -10, 229 expectedVolume: 0, 230 nposcalls: 2, 231 }, 232 { 233 name: "asking for partial closing volume when long", 234 price1: num.NewUint(1800), 235 price2: num.NewUint(1850), 236 side: types.SideBuy, 237 position: 702, 238 expectedVolume: 186, 239 nposcalls: 2, 240 }, 241 { 242 name: "asking for partial closing volume when short", 243 price1: num.NewUint(2100), 244 price2: num.NewUint(2150), 245 side: types.SideSell, 246 position: -635, 247 expectedVolume: 155, 248 nposcalls: 2, 249 }, 250 } 251 252 for _, tt := range tests { 253 t.Run(tt.name, func(t *testing.T) { 254 ensurePositionN(t, p.pos, tt.position, num.UintZero(), tt.nposcalls) 255 volume := p.pool.TradableVolumeInRange(tt.side, tt.price1, tt.price2) 256 assert.Equal(t, int(tt.expectedVolume), int(volume)) 257 }) 258 } 259 } 260 261 func testTradableVolumeAtPrice(t *testing.T) { 262 p := newTestPool(t) 263 defer p.ctrl.Finish() 264 265 tests := []struct { 266 name string 267 price *num.Uint 268 position int64 269 side types.Side 270 expectedVolume uint64 271 }{ 272 { 273 name: "full volume upper curve", 274 price: num.NewUint(2200), 275 side: types.SideBuy, 276 expectedVolume: 635, 277 }, 278 { 279 name: "full volume lower curve", 280 price: num.NewUint(1800), 281 side: types.SideSell, 282 expectedVolume: 702, 283 }, 284 { 285 name: "no volume upper, wrong side", 286 price: num.NewUint(2200), 287 side: types.SideSell, 288 expectedVolume: 0, 289 }, 290 { 291 name: "no volume lower, wrong side", 292 price: num.NewUint(1800), 293 side: types.SideBuy, 294 expectedVolume: 0, 295 }, 296 { 297 name: "no volume at fair-price buy", 298 price: num.NewUint(2000), 299 side: types.SideBuy, 300 expectedVolume: 0, 301 }, 302 { 303 name: "no volume at fair-price sell", 304 price: num.NewUint(2000), 305 side: types.SideSell, 306 expectedVolume: 0, 307 }, 308 } 309 310 for _, tt := range tests { 311 t.Run(tt.name, func(t *testing.T) { 312 ensurePositionN(t, p.pos, tt.position, num.UintZero(), 1) 313 volume := p.pool.TradableVolumeForPrice(tt.side, tt.price) 314 assert.Equal(t, int(tt.expectedVolume), int(volume)) 315 }) 316 } 317 } 318 319 func TestTradeableVolumeWhenAtBoundary(t *testing.T) { 320 // from ticket 11389 this replicates a scenario found during fuzz testing 321 submit := &types.SubmitAMM{ 322 AMMBaseCommand: types.AMMBaseCommand{ 323 Party: vgcrypto.RandomHash(), 324 MarketID: vgcrypto.RandomHash(), 325 SlippageTolerance: num.DecimalFromFloat(0.1), 326 }, 327 CommitmentAmount: num.MustUintFromString("2478383748073213000000", 10), 328 Parameters: &types.ConcentratedLiquidityParameters{ 329 Base: num.NewUint(676540), 330 LowerBound: num.NewUint(671272), 331 UpperBound: nil, 332 LeverageAtLowerBound: ptr.From(num.DecimalFromFloat(39.1988064541227)), 333 LeverageAtUpperBound: nil, 334 }, 335 } 336 337 p := newTestPoolWithSubmission(t, 338 num.DecimalFromInt64(1000), 339 num.DecimalFromFloat(10000000000000000), 340 submit, 341 0, 342 ) 343 defer p.ctrl.Finish() 344 345 // when position is zero fair-price should be the base 346 ensurePositionN(t, p.pos, 0, num.UintZero(), 2) 347 fp := p.pool.FairPrice() 348 assert.Equal(t, "6765400000000000000000", fp.String()) 349 350 fullLong := 12546 351 352 // volume from base -> low is 12546, but in reality it is 12546.4537027400278, but we can only trade int volume. 353 volume := p.pool.TradableVolumeInRange(types.SideSell, num.MustUintFromString("6712720000000000000000", 10), num.MustUintFromString("6765400000000000000000", 10)) 354 assert.Equal(t, fullLong, int(volume)) 355 356 // now lets pretend the AMM has fully traded out in that direction, best price will be near but not quite the lower bound 357 ensurePositionN(t, p.pos, int64(fullLong), num.UintZero(), 2) 358 fp = p.pool.FairPrice() 359 assert.Equal(t, "6712721893865935337785", fp.String()) 360 assert.True(t, fp.GTE(num.MustUintFromString("6712720000000000000000", 10))) 361 362 // now the fair-price is not *quite* on the lower boundary but the volume between it at the lower bound should be 0. 363 volume = p.pool.TradableVolumeInRange(types.SideSell, num.MustUintFromString("6712720000000000000000", 10), fp) 364 assert.Equal(t, 0, int(volume)) 365 } 366 367 func testPoolPositionFactor(t *testing.T) { 368 p := newTestPoolWithPositionFactor(t, num.DecimalFromInt64(1000)) 369 defer p.ctrl.Finish() 370 371 ensurePositionN(t, p.pos, 0, num.UintZero(), 1) 372 volume := p.pool.TradableVolumeInRange(types.SideBuy, num.NewUint(2000), num.NewUint(2200)) 373 // with position factot of 1 the volume is 635 374 assert.Equal(t, int(635395), int(volume)) 375 376 ensurePositionN(t, p.pos, 0, num.UintZero(), 1) 377 volume = p.pool.TradableVolumeInRange(types.SideSell, num.NewUint(1800), num.NewUint(2000)) 378 // with position factot of 1 the volume is 702 379 assert.Equal(t, int(702411), int(volume)) 380 381 ensurePositionN(t, p.pos, -1, num.NewUint(2000), 1) 382 // now best price should be the same as if the factor were 1, since its a price and not a volume 383 fairPrice := p.pool.FairPrice() 384 assert.Equal(t, "2001", fairPrice.String()) 385 } 386 387 func testBestPrice(t *testing.T) { 388 p := newTestPool(t) 389 defer p.ctrl.Finish() 390 391 tests := []struct { 392 name string 393 position int64 394 expectedPrice string 395 side types.Side 396 }{ 397 { 398 name: "best price sell", 399 expectedPrice: "2000", 400 position: 1, 401 side: types.SideSell, 402 }, 403 { 404 name: "best price buy", 405 expectedPrice: "1998", 406 position: 1, 407 side: types.SideBuy, 408 }, 409 { 410 name: "best price buy, amm fully long", 411 expectedPrice: "", 412 position: 702, 413 side: types.SideBuy, 414 }, 415 { 416 name: "best price sell, amm fully short", 417 expectedPrice: "", 418 position: -635, 419 side: types.SideSell, 420 }, 421 } 422 423 for _, tt := range tests { 424 t.Run(tt.name, func(t *testing.T) { 425 ensurePositionN(t, p.pos, tt.position, num.UintZero(), 2) 426 quote, ok := p.pool.BestPrice(tt.side) 427 if tt.expectedPrice == "" { 428 assert.False(t, ok) 429 } else { 430 assert.True(t, ok) 431 require.Equal(t, tt.expectedPrice, quote.String()) 432 } 433 }) 434 } 435 } 436 437 func testOneSidedPool(t *testing.T) { 438 // a pool with no liquidity below 439 p := newTestPoolWithRanges(t, nil, num.NewUint(2000), num.NewUint(2200)) 440 defer p.ctrl.Finish() 441 442 // side with liquidity returns volume 443 ensurePositionN(t, p.pos, 0, num.UintZero(), 1) 444 volume := p.pool.TradableVolumeInRange(types.SideBuy, num.NewUint(2000), num.NewUint(2200)) 445 assert.Equal(t, int(635), int(volume)) 446 447 // empty side returns no volume 448 ensurePositionN(t, p.pos, 0, num.UintZero(), 1) 449 volume = p.pool.TradableVolumeInRange(types.SideSell, num.NewUint(1800), num.NewUint(2000)) 450 assert.Equal(t, int(0), int(volume)) 451 452 // pool with short position and incoming sell only reports volume up to base 453 // empty side returns no volume 454 ensurePositionN(t, p.pos, -10, num.UintZero(), 1) 455 volume = p.pool.TradableVolumeInRange(types.SideSell, num.NewUint(1800), num.NewUint(2200)) 456 assert.Equal(t, int(10), int(volume)) 457 458 // fair price at 0 position is still ok 459 ensurePosition(t, p.pos, 0, num.UintZero()) 460 price := p.pool.FairPrice() 461 assert.Equal(t, "2000", price.String()) 462 463 // fair price at short position is still ok 464 ensurePosition(t, p.pos, -10, num.UintZero()) 465 price = p.pool.FairPrice() 466 assert.Equal(t, "2003", price.String()) 467 468 // fair price when long should panic since AMM should never be able to get into that state 469 // fair price at short position is still ok 470 ensurePosition(t, p.pos, 10, num.UintZero()) 471 assert.Panics(t, func() { p.pool.FairPrice() }) 472 } 473 474 func testNearZeroCurveErrors(t *testing.T) { 475 baseCmd := types.AMMBaseCommand{ 476 Party: vgcrypto.RandomHash(), 477 MarketID: vgcrypto.RandomHash(), 478 SlippageTolerance: num.DecimalFromFloat(0.1), 479 } 480 481 submit := &types.SubmitAMM{ 482 AMMBaseCommand: baseCmd, 483 CommitmentAmount: num.NewUint(1000), 484 Parameters: &types.ConcentratedLiquidityParameters{ 485 Base: num.NewUint(1900), 486 LowerBound: num.NewUint(1800), 487 UpperBound: num.NewUint(2000), 488 LeverageAtLowerBound: ptr.From(num.DecimalFromFloat(50)), 489 LeverageAtUpperBound: ptr.From(num.DecimalFromFloat(50)), 490 }, 491 } 492 // test that creating a pool with a near zero volume curve will error 493 pool, err := newBasicPoolWithSubmit(t, submit) 494 assert.Nil(t, pool) 495 assert.ErrorContains(t, err, "commitment amount too low") 496 497 // test that a pool with higher commitment amount will not error 498 submit.CommitmentAmount = num.NewUint(100000) 499 pool, err = newBasicPoolWithSubmit(t, submit) 500 assert.NotNil(t, pool) 501 assert.NoError(t, err) 502 503 // test that amending a pool to a near zero volume curve will error 504 amend := &types.AmendAMM{ 505 AMMBaseCommand: baseCmd, 506 CommitmentAmount: num.NewUint(100), 507 } 508 509 _, err = pool.Update( 510 amend, 511 &types.RiskFactor{Short: num.DecimalFromFloat(0.02), Long: num.DecimalFromFloat(0.02)}, 512 &types.ScalingFactors{InitialMargin: num.DecimalFromFloat(1.25)}, 513 num.DecimalZero(), 0, 514 ) 515 assert.ErrorContains(t, err, "commitment amount too low") 516 517 amend.CommitmentAmount = num.NewUint(1000000) 518 _, err = pool.Update( 519 amend, 520 &types.RiskFactor{Short: num.DecimalFromFloat(0.02), Long: num.DecimalFromFloat(0.02)}, 521 &types.ScalingFactors{InitialMargin: num.DecimalFromFloat(1.25)}, 522 num.DecimalZero(), 0, 523 ) 524 assert.NoError(t, err) 525 } 526 527 func testSparseAMM(t *testing.T) { 528 baseCmd := types.AMMBaseCommand{ 529 Party: vgcrypto.RandomHash(), 530 MarketID: vgcrypto.RandomHash(), 531 SlippageTolerance: num.DecimalFromFloat(0.1), 532 } 533 534 submit := &types.SubmitAMM{ 535 AMMBaseCommand: baseCmd, 536 CommitmentAmount: num.NewUint(1000), 537 Parameters: &types.ConcentratedLiquidityParameters{ 538 Base: num.NewUint(1900), 539 LowerBound: num.NewUint(1800), 540 UpperBound: num.NewUint(2000), 541 LeverageAtLowerBound: ptr.From(num.DecimalFromFloat(50)), 542 LeverageAtUpperBound: ptr.From(num.DecimalFromFloat(50)), 543 }, 544 } 545 // test that creating a pool with a near zero volume curve will error 546 pool, err := newBasicPoolWithSubmit(t, submit) 547 assert.Nil(t, pool) 548 assert.ErrorContains(t, err, "commitment amount too low") 549 } 550 551 func assertOrderPrices(t *testing.T, orders []*types.Order, side types.Side, st, nd int) { 552 t.Helper() 553 require.Equal(t, nd-st+1, len(orders)) 554 for i, o := range orders { 555 price := st + i 556 assert.Equal(t, side, o.Side) 557 assert.Equal(t, strconv.FormatInt(int64(price), 10), o.Price.String()) 558 } 559 } 560 561 func newBasicPoolWithSubmit(t *testing.T, submit *types.SubmitAMM) (*Pool, error) { 562 t.Helper() 563 ctrl := gomock.NewController(t) 564 col := mocks.NewMockCollateral(ctrl) 565 pos := mocks.NewMockPosition(ctrl) 566 567 pos.EXPECT().GetPositionsByParty(gomock.Any()).AnyTimes().Return( 568 []events.MarketPosition{&marketPosition{size: 0, averageEntry: nil}}, 569 ) 570 571 sqrter := &Sqrter{cache: map[string]num.Decimal{}} 572 573 return NewPool( 574 logging.NewTestLogger(), 575 vgcrypto.RandomHash(), 576 vgcrypto.RandomHash(), 577 vgcrypto.RandomHash(), 578 submit, 579 sqrter.sqrt, 580 col, 581 pos, 582 &types.RiskFactor{ 583 Short: num.DecimalFromFloat(0.02), 584 Long: num.DecimalFromFloat(0.02), 585 }, 586 &types.ScalingFactors{ 587 InitialMargin: num.DecimalFromFloat(1.25), // this is 1/0.8 which is margin_usage_at_bound_above in the note-book 588 }, 589 num.DecimalZero(), 590 num.DecimalOne(), 591 num.DecimalOne(), 592 num.NewUint(10000), 593 0, 594 num.DecimalZero(), 595 num.DecimalZero(), 596 ) 597 } 598 599 func ensurePositionN(t *testing.T, p *mocks.MockPosition, pos int64, averageEntry *num.Uint, times int) { 600 t.Helper() 601 602 if times < 0 { 603 p.EXPECT().GetPositionsByParty(gomock.Any()).AnyTimes().Return( 604 []events.MarketPosition{&marketPosition{size: pos, averageEntry: averageEntry}}, 605 ) 606 } else { 607 p.EXPECT().GetPositionsByParty(gomock.Any()).Times(times).Return( 608 []events.MarketPosition{&marketPosition{size: pos, averageEntry: averageEntry}}, 609 ) 610 } 611 } 612 613 func ensurePosition(t *testing.T, p *mocks.MockPosition, pos int64, averageEntry *num.Uint) { 614 t.Helper() 615 616 ensurePositionN(t, p, pos, averageEntry, 1) 617 } 618 619 func ensureBalancesN(t *testing.T, col *mocks.MockCollateral, balance uint64, times int) { 620 t.Helper() 621 622 // split the balance equally across general and margin 623 split := balance / 2 624 gen := &types.Account{ 625 Balance: num.NewUint(split), 626 } 627 mar := &types.Account{ 628 Balance: num.NewUint(balance - split), 629 } 630 631 if times < 0 { 632 col.EXPECT().GetPartyGeneralAccount(gomock.Any(), gomock.Any()).AnyTimes().Return(gen, nil) 633 col.EXPECT().GetPartyMarginAccount(gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes().Return(mar, nil) 634 } else { 635 col.EXPECT().GetPartyGeneralAccount(gomock.Any(), gomock.Any()).Times(times).Return(gen, nil) 636 col.EXPECT().GetPartyMarginAccount(gomock.Any(), gomock.Any(), gomock.Any()).Times(times).Return(mar, nil) 637 } 638 } 639 640 func ensureBalances(t *testing.T, col *mocks.MockCollateral, balance uint64) { 641 t.Helper() 642 ensureBalancesN(t, col, balance, 1) 643 } 644 645 func TestNotebook(t *testing.T) { 646 // Note that these were verified using Tom's jupyter notebook, so don't go arbitrarily changing the numbers 647 // without re-verifying! 648 649 p := newTestPool(t) 650 defer p.ctrl.Finish() 651 652 base := num.NewUint(2000) 653 low := num.NewUint(1800) 654 up := num.NewUint(2200) 655 656 pos := int64(0) 657 658 ensurePositionN(t, p.pos, pos, num.UintZero(), 1) 659 volume := p.pool.TradableVolumeInRange(types.SideSell, base, low) 660 assert.Equal(t, int(702), int(volume)) 661 662 ensurePositionN(t, p.pos, pos, num.UintZero(), 1) 663 volume = p.pool.TradableVolumeInRange(types.SideBuy, up, base) 664 assert.Equal(t, int(635), int(volume)) 665 666 lowmid := num.NewUint(1900) 667 upmid := num.NewUint(2100) 668 669 ensurePositionN(t, p.pos, pos, num.UintZero(), 1) 670 volume = p.pool.TradableVolumeInRange(types.SideSell, low, lowmid) 671 assert.Equal(t, int(365), int(volume)) 672 673 ensurePositionN(t, p.pos, pos, num.UintZero(), 1) 674 volume = p.pool.TradableVolumeInRange(types.SideBuy, upmid, up) 675 assert.Equal(t, int(306), int(volume)) 676 677 ensurePosition(t, p.pos, -500, upmid.Clone()) 678 fairPrice := p.pool.FairPrice() 679 assert.Equal(t, "2155", fairPrice.String()) 680 681 ensurePosition(t, p.pos, 500, lowmid.Clone()) 682 fairPrice = p.pool.FairPrice() 683 assert.Equal(t, "1854", fairPrice.String()) 684 685 // fair price is 2000 and the AMM quotes a best-buy at 1999 so incoming SELL should have a price <= 1999 686 ensurePositionN(t, p.pos, 0, lowmid.Clone(), 2) 687 price := p.pool.PriceForVolume(100, types.SideSell) 688 assert.Equal(t, "1984", price.String()) 689 690 // fair price is 2000 and the AMM quotes a best-buy at 2001 so incoming BUY should have a price >= 2001 691 ensurePositionN(t, p.pos, 0, lowmid.Clone(), 2) 692 price = p.pool.PriceForVolume(100, types.SideBuy) 693 assert.Equal(t, "2014", price.String()) 694 } 695 696 type tstPool struct { 697 pool *Pool 698 col *mocks.MockCollateral 699 pos *mocks.MockPosition 700 ctrl *gomock.Controller 701 submission *types.SubmitAMM 702 } 703 704 func newTestPool(t *testing.T) *tstPool { 705 t.Helper() 706 return newTestPoolWithPositionFactor(t, num.DecimalOne()) 707 } 708 709 func newTestPoolWithRanges(t *testing.T, low, base, high *num.Uint) *tstPool { 710 t.Helper() 711 return newTestPoolWithOpts(t, num.DecimalOne(), low, base, high, num.NewUint(100000), 0) 712 } 713 714 func newTestPoolWithPositionFactor(t *testing.T, positionFactor num.Decimal) *tstPool { 715 t.Helper() 716 return newTestPoolWithOpts(t, positionFactor, num.NewUint(1800), num.NewUint(2000), num.NewUint(2200), num.NewUint(100000), 0) 717 } 718 719 func newTestPoolWithOpts(t *testing.T, positionFactor num.Decimal, low, base, high *num.Uint, commitment *num.Uint, allowedEmptyAMMLevels uint64) *tstPool { 720 t.Helper() 721 ctrl := gomock.NewController(t) 722 col := mocks.NewMockCollateral(ctrl) 723 pos := mocks.NewMockPosition(ctrl) 724 725 sqrter := &Sqrter{cache: map[string]num.Decimal{}} 726 727 submit := &types.SubmitAMM{ 728 AMMBaseCommand: types.AMMBaseCommand{ 729 Party: vgcrypto.RandomHash(), 730 MarketID: vgcrypto.RandomHash(), 731 SlippageTolerance: num.DecimalFromFloat(0.1), 732 }, 733 // 0000000000000 734 CommitmentAmount: commitment, 735 Parameters: &types.ConcentratedLiquidityParameters{ 736 Base: base, 737 LowerBound: low, 738 UpperBound: high, 739 LeverageAtLowerBound: ptr.From(num.DecimalFromFloat(50)), 740 LeverageAtUpperBound: ptr.From(num.DecimalFromFloat(50)), 741 }, 742 } 743 744 pool, err := NewPool( 745 logging.NewTestLogger(), 746 vgcrypto.RandomHash(), 747 vgcrypto.RandomHash(), 748 vgcrypto.RandomHash(), 749 submit, 750 sqrter.sqrt, 751 col, 752 pos, 753 &types.RiskFactor{ 754 Short: num.DecimalFromFloat(0.02), 755 Long: num.DecimalFromFloat(0.02), 756 }, 757 &types.ScalingFactors{ 758 InitialMargin: num.DecimalFromFloat(1.25), // this is 1/0.8 which is margin_usage_at_bound_above in the note-book 759 }, 760 num.DecimalZero(), 761 num.DecimalOne(), 762 positionFactor, 763 num.NewUint(100000), 764 allowedEmptyAMMLevels, 765 num.DecimalZero(), 766 num.DecimalZero(), 767 ) 768 assert.NoError(t, err) 769 770 return &tstPool{ 771 submission: submit, 772 pool: pool, 773 col: col, 774 pos: pos, 775 ctrl: ctrl, 776 } 777 } 778 779 func newTestPoolWithSubmission(t *testing.T, positionFactor, priceFactor num.Decimal, submit *types.SubmitAMM, allowedEmptyAMMLevels uint64) *tstPool { 780 t.Helper() 781 ctrl := gomock.NewController(t) 782 col := mocks.NewMockCollateral(ctrl) 783 pos := mocks.NewMockPosition(ctrl) 784 785 sqrter := &Sqrter{cache: map[string]num.Decimal{}} 786 787 pool, err := NewPool( 788 logging.NewTestLogger(), 789 vgcrypto.RandomHash(), 790 vgcrypto.RandomHash(), 791 vgcrypto.RandomHash(), 792 submit, 793 sqrter.sqrt, 794 col, 795 pos, 796 &types.RiskFactor{ 797 Short: num.DecimalFromFloat(0.009937604878885509), 798 Long: num.DecimalFromFloat(0.00984363574304481), 799 }, 800 &types.ScalingFactors{ 801 InitialMargin: num.DecimalFromFloat(1.5), // this is 1/0.8 which is margin_usage_at_bound_above in the note-book 802 }, 803 num.DecimalFromFloat(0), 804 priceFactor, 805 positionFactor, 806 num.NewUint(1000), 807 allowedEmptyAMMLevels, 808 num.DecimalZero(), 809 num.DecimalZero(), 810 ) 811 require.NoError(t, err) 812 813 return &tstPool{ 814 submission: submit, 815 pool: pool, 816 col: col, 817 pos: pos, 818 ctrl: ctrl, 819 } 820 } 821 822 type marketPosition struct { 823 size int64 824 averageEntry *num.Uint 825 } 826 827 func (m marketPosition) AverageEntryPrice() *num.Uint { return m.averageEntry.Clone() } 828 func (m marketPosition) Party() string { return "" } 829 func (m marketPosition) Size() int64 { return m.size } 830 func (m marketPosition) Buy() int64 { return 0 } 831 func (m marketPosition) Sell() int64 { return 0 } 832 func (m marketPosition) Price() *num.Uint { return num.UintZero() } 833 func (m marketPosition) BuySumProduct() *num.Uint { return num.UintZero() } 834 func (m marketPosition) SellSumProduct() *num.Uint { return num.UintZero() } 835 func (m marketPosition) VWBuy() *num.Uint { return num.UintZero() } 836 func (m marketPosition) VWSell() *num.Uint { return num.UintZero() }