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() }