From c7fac85ff5cf6d580c48a5fa89a2934fdda64e66 Mon Sep 17 00:00:00 2001 From: wwestgarth Date: Tue, 23 Jul 2024 15:31:30 +0100 Subject: [PATCH] fix: ensure AMM best bid/ask is capped at boundaries to we not not over state available volume --- core/execution/amm/engine.go | 4 +-- core/execution/amm/engine_test.go | 52 +++++++++++++++++++++++++++++-- 2 files changed, 52 insertions(+), 4 deletions(-) diff --git a/core/execution/amm/engine.go b/core/execution/amm/engine.go index 96865f93646..a97008aa14f 100644 --- a/core/execution/amm/engine.go +++ b/core/execution/amm/engine.go @@ -328,7 +328,7 @@ func (e *Engine) BestPricesAndVolumes() (*num.Uint, uint64, *num.Uint, uint64) { fp := pool.BestPrice(nil) // get the volume on the buy side by simulating an incoming sell order - bid := num.UintZero().Sub(fp, pool.oneTick) + bid := num.Max(pool.lower.low, num.UintZero().Sub(fp, pool.oneTick)) volume := pool.TradableVolumeInRange(types.SideSell, fp.Clone(), bid) if volume != 0 { @@ -341,7 +341,7 @@ func (e *Engine) BestPricesAndVolumes() (*num.Uint, uint64, *num.Uint, uint64) { } // get the volume on the sell side by simulating an incoming buy order - ask := num.UintZero().Add(fp, pool.oneTick) + ask := num.Min(pool.upper.high, num.UintZero().Add(fp, pool.oneTick)) volume = pool.TradableVolumeInRange(types.SideBuy, fp.Clone(), ask) if volume != 0 { if bestAsk == nil || ask.LT(bestAsk) { diff --git a/core/execution/amm/engine_test.go b/core/execution/amm/engine_test.go index a2f0095d3d2..6f616d85696 100644 --- a/core/execution/amm/engine_test.go +++ b/core/execution/amm/engine_test.go @@ -426,6 +426,49 @@ func testBestPricesAndVolume(t *testing.T) { assert.Equal(t, avolume, avAt) } +func TestBestPricesAndVolumeNearBound(t *testing.T) { + tst := getTestEngineWithFactors(t, num.DecimalFromInt64(100), num.DecimalFromFloat(10)) + + // create three pools + party, subAccount := getParty(t, tst) + submit := getPoolSubmission(t, party, tst.marketID) + + expectSubaccountCreation(t, tst, party, subAccount) + whenAMMIsSubmitted(t, tst, submit) + + tst.pos.EXPECT().GetPositionsByParty(gomock.Any()).Times(5).Return( + []events.MarketPosition{&marketPosition{size: 0, averageEntry: num.NewUint(0)}}, + ) + + bid, bvolume, ask, avolume := tst.engine.BestPricesAndVolumes() + assert.Equal(t, "199900", bid.String()) + assert.Equal(t, "200100", ask.String()) + assert.Equal(t, 1250, int(bvolume)) + assert.Equal(t, 1192, int(avolume)) + + // lets move its position so that the fair price is within one tick of the AMMs upper boundary + tst.pos.EXPECT().GetPositionsByParty(gomock.Any()).Times(5).Return( + []events.MarketPosition{&marketPosition{size: -222000, averageEntry: num.NewUint(0)}}, + ) + + bid, bvolume, ask, avolume = tst.engine.BestPricesAndVolumes() + assert.Equal(t, "219890", bid.String()) + assert.Equal(t, "220000", ask.String()) // make sure we are capped to the boundary and not 220090 + assert.Equal(t, 1034, int(bvolume)) + assert.Equal(t, 103, int(avolume)) + + // lets move its position so that the fair price is within one tick of the AMMs upper boundary + tst.pos.EXPECT().GetPositionsByParty(gomock.Any()).Times(5).Return( + []events.MarketPosition{&marketPosition{size: 270400, averageEntry: num.NewUint(0)}}, + ) + + bid, bvolume, ask, avolume = tst.engine.BestPricesAndVolumes() + assert.Equal(t, "180000", bid.String()) // make sure we are capped to the boundary and not 179904 + assert.Equal(t, "180104", ask.String()) + assert.Equal(t, 58, int(bvolume)) + assert.Equal(t, 1463, int(avolume)) +} + func testClosingReduceOnlyPool(t *testing.T) { ctx := context.Background() tst := getTestEngine(t) @@ -709,7 +752,7 @@ type tstEngine struct { assetID string } -func getTestEngine(t *testing.T) *tstEngine { +func getTestEngineWithFactors(t *testing.T, priceFactor, positionFactor num.Decimal) *tstEngine { t.Helper() ctrl := gomock.NewController(t) col := mocks.NewMockCollateral(ctrl) @@ -730,7 +773,7 @@ func getTestEngine(t *testing.T) *tstEngine { parties := cmocks.NewMockParties(ctrl) parties.EXPECT().AssignDeriveKey(gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes() - eng := New(logging.NewTestLogger(), broker, col, marketID, assetID, pos, num.DecimalOne(), num.DecimalOne(), mat, parties) + eng := New(logging.NewTestLogger(), broker, col, marketID, assetID, pos, priceFactor, positionFactor, mat, parties) // do an ontick to initialise the idgen ctx := vgcontext.WithTraceID(context.Background(), vgcrypto.RandomHash()) @@ -748,6 +791,11 @@ func getTestEngine(t *testing.T) *tstEngine { } } +func getTestEngine(t *testing.T) *tstEngine { + t.Helper() + return getTestEngineWithFactors(t, num.DecimalOne(), num.DecimalOne()) +} + func getAccount(balance uint64) *types.Account { return &types.Account{ Balance: num.NewUint(balance),