diff --git a/CHANGELOG.md b/CHANGELOG.md index e80af4099a..88c4560590 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ - [11714](https://github.com/vegaprotocol/vega/issues/11714) - Improve `AMM` performance by caching best prices and volumes. - [11642](https://github.com/vegaprotocol/vega/issues/11642) - `AMMs` with empty price levels are now allowed. - [11685](https://github.com/vegaprotocol/vega/issues/11685) - Automated purchase support added. +- [11726](https://github.com/vegaprotocol/vega/issues/11726) - Combined `AMM` uncrossing orders for better performance when uncrossing the book. - [11711](https://github.com/vegaprotocol/vega/issues/11711) - Manage closed team membership by updating the allow list. - [11722](https://github.com/vegaprotocol/vega/issues/11722) - Expose active protocol automated purchase identifier in market data API. diff --git a/core/integration/features/amm/0090-VAMM-auction.feature b/core/integration/features/amm/0090-VAMM-auction.feature index 6bac9a36b2..408e1dd4d7 100644 --- a/core/integration/features/amm/0090-VAMM-auction.feature +++ b/core/integration/features/amm/0090-VAMM-auction.feature @@ -98,7 +98,7 @@ Feature: vAMM rebasing when created or amended When the opening auction period ends for market "ETH/MAR22" Then the following trades should be executed: | buyer | price | size | seller | is amm | - | vamm1-id | 100 | 46 | vamm2-id | true | + | vamm1-id | 100 | 91 | vamm2-id | true | Then the network moves ahead "1" blocks @@ -406,7 +406,7 @@ Feature: vAMM rebasing when created or amended When the opening auction period ends for market "ETH/MAR22" Then the following trades should be executed: | buyer | price | size | seller | is amm | - | vamm2-id | 100 | 46 | vamm1-id | true | + | vamm2-id | 100 | 91 | vamm1-id | true | Then the network moves ahead "1" blocks @@ -548,4 +548,50 @@ Feature: vAMM rebasing when created or amended And the market data for the market "ETH/MAR22" should be: | mark price | trading mode | best bid price | best offer price | - | 155 | TRADING_MODE_CONTINUOUS | 109 | 0 | \ No newline at end of file + | 155 | TRADING_MODE_CONTINUOUS | 109 | 0 | + + + @VAMM3 + Scenario: Two AMMs crossed with large order expansion + + Then the parties submit the following AMM: + | party | market id | amount | slippage | base | lower bound | upper bound | proposed fee | + | vamm1 | ETH/MAR22 | 1000000 | 0.05 | 150 | 100 | 200 | 0.03 | + Then the AMM pool status should be: + | party | market id | amount | status | base | lower bound | upper bound | + | vamm1 | ETH/MAR22 | 1000000 | STATUS_ACTIVE | 150 | 100 | 200 | + + And the market data for the market "ETH/MAR22" should be: + | trading mode | indicative price | indicative volume | + | TRADING_MODE_OPENING_AUCTION | 0 | 0 | + + Then the parties submit the following AMM: + | party | market id | amount | slippage | base | lower bound | upper bound | proposed fee | + | vamm2 | ETH/MAR22 | 1000000 | 0.05 | 250 | 200 | 300 | 0.03 | + Then the AMM pool status should be: + | party | market id | amount | status | base | lower bound | upper bound | + | vamm2 | ETH/MAR22 | 1000000 | STATUS_ACTIVE | 250 | 200 | 300 | + + + And set the following AMM sub account aliases: + | party | market id | alias | + | vamm1 | ETH/MAR22 | vamm1-id | + | vamm2 | ETH/MAR22 | vamm2-id | + + And the market data for the market "ETH/MAR22" should be: + | trading mode | indicative price | indicative volume | + | TRADING_MODE_OPENING_AUCTION | 201 | 2238 | + + When the opening auction period ends for market "ETH/MAR22" + + # note that this is one big order instead of a hundred small orders + Then the following trades should be executed: + | buyer | price | size | seller | is amm | + | vamm2-id | 201 | 2238 | vamm1-id | true | + + Then the network moves ahead "1" blocks + + # two AMMs are now prices at ~100 which is between their base values + And the market data for the market "ETH/MAR22" should be: + | mark price | trading mode | best bid price | best offer price | + | 201 | TRADING_MODE_CONTINUOUS | 202 | 204 | \ No newline at end of file diff --git a/core/matching/indicative_price_and_volume.go b/core/matching/indicative_price_and_volume.go index 3483af832f..1d270f1042 100644 --- a/core/matching/indicative_price_and_volume.go +++ b/core/matching/indicative_price_and_volume.go @@ -492,11 +492,30 @@ func (ipv *IndicativePriceAndVolume) ExtractOffbookOrders(price *num.Uint, side cpm = func(p *num.Uint) bool { return p.GT(price) } } + var combined *types.Order for _, o := range oo { if cpm(o.Price) { continue } - orders = append(orders, o) + + // we want to combine all the uncrossing orders into one big one of the combined volume so that + // we only uncross with 1 order and not 1000s of expanded ones for a single AMM. We can take the price + // to the best of the lot so that it trades -- it'll get overridden by the uncrossing price after uncrossing + // anyway. + if combined == nil { + combined = o.Clone() + orders = append(orders, combined) + } else { + combined.Size += o.Size + combined.Remaining += o.Remaining + + if side == types.SideBuy { + combined.Price = num.Max(combined.Price, o.Price) + } else { + combined.Price = num.Min(combined.Price, o.Price) + } + } + volume += o.Size // if we're extracted enough we can stop now diff --git a/core/matching/orderbook_amm_test.go b/core/matching/orderbook_amm_test.go index 0e6302e8b4..70dc9be2a4 100644 --- a/core/matching/orderbook_amm_test.go +++ b/core/matching/orderbook_amm_test.go @@ -305,17 +305,19 @@ func TestIndicativeTradesAMMOnly(t *testing.T) { expectCrossedAMMs(t, tst, 100, 150) tst.book.EnterAuction() - ret := []*types.Order{createOrder(t, tst, 100, num.NewUint(100))} + ret := createOrder(t, tst, 10, num.NewUint(100)) tst.obs.EXPECT().SubmitOrder(gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes().DoAndReturn( func(o *types.Order, _, _ *num.Uint) []*types.Order { - o.Remaining = 0 - return ret + ret.Size = o.Remaining + ret.Remaining = o.Remaining + return []*types.Order{ret.Clone()} }, ) trades, err := tst.book.GetIndicativeTrades() require.NoError(t, err) - assert.Equal(t, 26, len(trades)) + assert.Equal(t, 1, len(trades)) + assert.Equal(t, 260, int(trades[0].Size)) } func TestIndicativeTradesAMMOrderbookNotCrosses(t *testing.T) { @@ -337,17 +339,19 @@ func TestIndicativeTradesAMMOrderbookNotCrosses(t *testing.T) { _, err = tst.book.SubmitOrder(o) require.NoError(t, err) - ret := []*types.Order{createOrder(t, tst, 100, num.NewUint(100))} - tst.obs.EXPECT().SubmitOrder(gomock.Any(), gomock.Any(), gomock.Any()).Times(26).DoAndReturn( + ret := createOrder(t, tst, 10, num.NewUint(100)) + tst.obs.EXPECT().SubmitOrder(gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes().DoAndReturn( func(o *types.Order, _, _ *num.Uint) []*types.Order { - o.Remaining = 0 - return ret + ret.Size = o.Remaining + ret.Remaining = o.Remaining + return []*types.Order{ret.Clone()} }, ) trades, err := tst.book.GetIndicativeTrades() require.NoError(t, err) - assert.Equal(t, 26, len(trades)) + assert.Equal(t, 1, len(trades)) + assert.Equal(t, 260, int(trades[0].Size)) } func TestIndicativeTradesAMMCrossedOrders(t *testing.T) { @@ -358,15 +362,9 @@ func TestIndicativeTradesAMMCrossedOrders(t *testing.T) { expectCrossedAMMs(t, tst, 100, 150) tst.book.EnterAuction() - // submit an order each side outside of the crossed region - o := createOrder(t, tst, 10, num.NewUint(110)) - o.Side = types.SideBuy - _, err := tst.book.SubmitOrder(o) - require.NoError(t, err) - - o = createOrder(t, tst, 5, num.NewUint(125)) + o := createOrder(t, tst, 5, num.NewUint(125)) o.Side = types.SideSell - _, err = tst.book.SubmitOrder(o) + _, err := tst.book.SubmitOrder(o) require.NoError(t, err) o = createOrder(t, tst, 5, num.NewUint(126)) @@ -374,18 +372,19 @@ func TestIndicativeTradesAMMCrossedOrders(t *testing.T) { _, err = tst.book.SubmitOrder(o) require.NoError(t, err) - ret := []*types.Order{createOrder(t, tst, 100, num.NewUint(100))} - tst.obs.EXPECT().SubmitOrder(gomock.Any(), gomock.Any(), gomock.Any()).Times(2).Return(ret) + ret := createOrder(t, tst, 250, num.NewUint(100)) + tst.obs.EXPECT().SubmitOrder(gomock.Any(), gomock.Any(), gomock.Any()).Times(1).Return([]*types.Order{ret.Clone()}) - ret = []*types.Order{createOrder(t, tst, 5, num.NewUint(100))} - tst.obs.EXPECT().SubmitOrder(gomock.Any(), gomock.Any(), gomock.Any()).Times(1).Return(ret) - - ret = []*types.Order{createOrder(t, tst, 100, num.NewUint(100))} - tst.obs.EXPECT().SubmitOrder(gomock.Any(), gomock.Any(), gomock.Any()).Times(23).Return(ret) + tst.obs.EXPECT().SubmitOrder(gomock.Any(), gomock.Any(), gomock.Any()).Times(1).Return(nil) trades, err := tst.book.GetIndicativeTrades() require.NoError(t, err) - assert.Equal(t, 27, len(trades)) + assert.Equal(t, 3, len(trades)) + var total int + for _, t := range trades { + total += int(t.Size) + } + assert.Equal(t, 260, total) } func TestUncrossedBookDoesNotExpandAMMs(t *testing.T) {