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