From 2cbe43a61799f1cb0d855388fa24743ffbd8616e Mon Sep 17 00:00:00 2001 From: wwestgarth Date: Tue, 1 Oct 2024 13:45:26 +0100 Subject: [PATCH] fix: avoid trading across discontinuity at and AMMs base price --- CHANGELOG.md | 1 + core/execution/amm/engine.go | 16 +- core/execution/amm/engine_test.go | 13 +- .../features/amm/0090-VAMM-006-014.feature | 44 ++--- .../features/amm/0090-VAMM-028.feature | 2 +- .../features/amm/0090-VAMM-basic.feature | 169 +++++++++++++++++- 6 files changed, 212 insertions(+), 33 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2cc9d0c948..e80af4099a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,7 @@ - [11672](https://github.com/vegaprotocol/vega/issues/11672) - Add missing fees in GraphQL bindings. - [11681](https://github.com/vegaprotocol/vega/issues/11681) - Account for conflicts inserting funding payment records. +- [11715](https://github.com/vegaprotocol/vega/issues/11715) - Trade across an `AMMs` base price in two steps since the curves either side will be different. - [11684](https://github.com/vegaprotocol/vega/issues/11684) - Better error when `Arbitrum` bridge details are missing from validator configuration. - [11696](https://github.com/vegaprotocol/vega/issues/11696) - Add binding for estimate fees API. - [11699](https://github.com/vegaprotocol/vega/issues/11699) - Update factors of programs when they are updated. diff --git a/core/execution/amm/engine.go b/core/execution/amm/engine.go index d8ac6456ec..7b5a141c74 100644 --- a/core/execution/amm/engine.go +++ b/core/execution/amm/engine.go @@ -568,8 +568,22 @@ func (e *Engine) partition(agg *types.Order, inner, outer *num.Uint) ([]*Pool, [ // pool is active in range add it to the slice active = append(active, p) + // we hit a discontinuity where an AMM's two curves meet if we try to trade over its base-price + // so we partition the inner/outer price range at the base price so that we instead trade across it + // in two steps. + boundary := p.upper.low + if inner != nil && outer != nil { + if boundary.LT(outer) && boundary.GT(inner) { + bounds[boundary.String()] = boundary.Clone() + } + } else if outer == nil && boundary.GT(inner) { + bounds[boundary.String()] = boundary.Clone() + } else if inner == nil && boundary.LT(outer) { + bounds[boundary.String()] = boundary.Clone() + } + // if a pool's upper or lower boundary exists within (inner, outer) then we consider that a sub-level - boundary := p.upper.high + boundary = p.upper.high if outer == nil || boundary.LT(outer) { bounds[boundary.String()] = boundary.Clone() } diff --git a/core/execution/amm/engine_test.go b/core/execution/amm/engine_test.go index d084d04a91..a9ac6a735c 100644 --- a/core/execution/amm/engine_test.go +++ b/core/execution/amm/engine_test.go @@ -234,11 +234,14 @@ func testBasicSubmitOrder(t *testing.T) { assert.Equal(t, "2021", ba.String()) orders = tst.engine.SubmitOrder(agg, num.NewUint(2020), num.NewUint(1990)) - require.Len(t, orders, 1) - assert.Equal(t, "2004", orders[0].Price.String()) - // note that this volume being bigger than 242367 above means we've moved back to position, then flipped - // sign, and took volume from the other curve. - assert.Equal(t, 362325, int(orders[0].Size)) + + // 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. + require.Len(t, orders, 2) + assert.Equal(t, "2009", orders[0].Price.String()) + assert.Equal(t, 236855, int(orders[0].Size)) + + assert.Equal(t, "1994", orders[1].Price.String()) + assert.Equal(t, 125470, int(orders[1].Size)) } func testSubmitOrderAtBestPrice(t *testing.T) { diff --git a/core/integration/features/amm/0090-VAMM-006-014.feature b/core/integration/features/amm/0090-VAMM-006-014.feature index e14e4439f0..09a15cdff9 100644 --- a/core/integration/features/amm/0090-VAMM-006-014.feature +++ b/core/integration/features/amm/0090-VAMM-006-014.feature @@ -348,22 +348,23 @@ Feature: Ensure the vAMM positions follow the market correctly # In a single trade, move the mid price to 110 When the parties place the following orders: | party | market id | side | volume | price | resulting trades | type | tif | - | party4 | ETH/MAR22 | buy | 440 | 110 | 1 | TYPE_LIMIT | TIF_GTC | + | party4 | ETH/MAR22 | buy | 440 | 110 | 2 | TYPE_LIMIT | TIF_GTC | Then the market data for the market "ETH/MAR22" should be: | mark price | trading mode | ref price | mid price | static mid price | best offer price | best bid price | | 94 | TRADING_MODE_CONTINUOUS | 100 | 110 | 110 | 111 | 109 | And the following trades should be executed: | buyer | price | size | seller | is amm | - | party4 | 95 | 440 | vamm1-id | true | + | party4 | 94 | 371 | vamm1-id | true | + | party4 | 104 | 69 | vamm1-id | true | # Check the resulting position, vAMM switched from long to short When the network moves ahead "1" blocks Then the parties should have the following profit and loss: | party | volume | unrealised pnl | realised pnl | is amm | - | party1 | 1 | -5 | 0 | | - | party2 | -1 | 5 | 0 | | - | party3 | -371 | -371 | 0 | | - | party4 | 440 | 0 | 0 | | - | vamm1-id | -69 | 0 | 371 | true | + | party1 | 1 | 4 | 0 | | + | party2 | -1 | -4 | 0 | | + | party3 | -371 | -3710 | 0 | | + | party4 | 440 | 3710 | 0 | | + | vamm1-id | -69 | 0 | 0 | true | # Now return the mid price back to 100 When the parties place the following orders: @@ -371,7 +372,7 @@ Feature: Ensure the vAMM positions follow the market correctly | party5 | ETH/MAR22 | sell | 69 | 100 | 1 | TYPE_LIMIT | TIF_GTC | Then the market data for the market "ETH/MAR22" should be: | mark price | trading mode | ref price | mid price | static mid price | best offer price | best bid price | - | 95 | TRADING_MODE_CONTINUOUS | 100 | 100 | 100 | 101 | 99 | + | 104 | TRADING_MODE_CONTINUOUS | 100 | 100 | 100 | 101 | 99 | And the following trades should be executed: | buyer | price | size | seller | is amm | | vamm1-id | 104 | 69 | party5 | true | @@ -382,9 +383,9 @@ Feature: Ensure the vAMM positions follow the market correctly | party1 | 1 | 4 | 0 | | | party2 | -1 | -4 | 0 | | | party3 | -371 | -3710 | 0 | | - | party4 | 440 | 3960 | 0 | | + | party4 | 440 | 3710 | 0 | | | party5 | -69 | 0 | 0 | | - | vamm1-id | 0 | 0 | -250 | true | + | vamm1-id | 0 | 0 | 0 | true | @VAMM Scenario: 0090-VAMM-013: If other traders trade to move the market mid price to 90 and then move the mid price back to 100 in several trades of varying size, the vAMM will have a position of 0. @@ -431,7 +432,7 @@ Feature: Ensure the vAMM positions follow the market correctly | party5 | 130 | 0 | 0 | | | vamm1-id | 0 | 0 | -257 | true | - @VAMM + @VAMM3 Scenario: 0090-VAMM-014: If other traders trade to move the market mid price to 90 and then in one trade move the mid price to 110 then trade to move the mid price to 120 the vAMM will have a larger (more negative) but comparable position to if they had been moved straight from 100 to 120. # Move mid price to 90 When the parties place the following orders: @@ -455,22 +456,23 @@ Feature: Ensure the vAMM positions follow the market correctly # In a single trade, move the mid price to 110 When the parties place the following orders: | party | market id | side | volume | price | resulting trades | type | tif | - | party4 | ETH/MAR22 | buy | 420 | 110 | 1 | TYPE_LIMIT | TIF_GTC | + | party4 | ETH/MAR22 | buy | 420 | 110 | 2 | TYPE_LIMIT | TIF_GTC | Then the market data for the market "ETH/MAR22" should be: | mark price | trading mode | ref price | mid price | static mid price | best offer price | best bid price | | 95 | TRADING_MODE_CONTINUOUS | 100 | 110 | 110 | 111 | 109 | And the following trades should be executed: | buyer | price | size | seller | is amm | - | party4 | 95 | 420 | vamm1-id | true | + | party4 | 94 | 350 | vamm1-id | true | + | party4 | 104 | 70 | vamm1-id | true | # Check the resulting position, vAMM switched from long to short When the network moves ahead "1" blocks Then the parties should have the following profit and loss: | party | volume | unrealised pnl | realised pnl | is amm | - | party1 | 1 | -5 | 0 | | - | party2 | -1 | 5 | 0 | | - | party3 | -350 | 0 | 0 | | - | party4 | 420 | 0 | 0 | | - | vamm1-id | -70 | 0 | 0 | true | + | party1 | 1 | 4 | 0 | | + | party2 | -1 | -4 | 0 | | + | party3 | -350 | -3150 | 0 | | + | party4 | 420 | 3500 | 0 | | + | vamm1-id | -70 | 0 | -350 | true | # Now further increase the mid price, move it up to 120 When the parties place the following orders: @@ -478,7 +480,7 @@ Feature: Ensure the vAMM positions follow the market correctly | party5 | ETH/MAR22 | buy | 65 | 120 | 1 | TYPE_LIMIT | TIF_GTC | Then the market data for the market "ETH/MAR22" should be: | mark price | trading mode | ref price | mid price | static mid price | best offer price | best bid price | - | 95 | TRADING_MODE_CONTINUOUS | 100 | 120 | 120 | 121 | 119 | + | 104 | TRADING_MODE_CONTINUOUS | 100 | 120 | 120 | 121 | 119 | And the following trades should be executed: | buyer | price | size | seller | is amm | | party5 | 114 | 65 | vamm1-id | true | @@ -489,6 +491,6 @@ Feature: Ensure the vAMM positions follow the market correctly | party1 | 1 | 14 | 0 | | | party2 | -1 | -14 | 0 | | | party3 | -350 | -6650 | 0 | | - | party4 | 420 | 7980 | 0 | | + | party4 | 420 | 7700 | 0 | | | party5 | 65 | 0 | 0 | | - | vamm1-id | -135 | -1330 | 0 | true | + | vamm1-id | -135 | -700 | -350 | true | diff --git a/core/integration/features/amm/0090-VAMM-028.feature b/core/integration/features/amm/0090-VAMM-028.feature index d4b0a2c2a3..51664c9f25 100644 --- a/core/integration/features/amm/0090-VAMM-028.feature +++ b/core/integration/features/amm/0090-VAMM-028.feature @@ -418,7 +418,7 @@ Feature: Ensure the vAMM positions follow the market correctly # Now to move from 110 down to 90, the volume ought to be 421 (=347+74) When the parties place the following orders: | party | market id | side | volume | price | resulting trades | type | tif | - | party6 | ETH/MAR23 | sell | 421 | 90 | 1 | TYPE_LIMIT | TIF_GTC | + | party6 | ETH/MAR23 | sell | 421 | 90 | 2 | TYPE_LIMIT | TIF_GTC | Then the market data for the market "ETH/MAR23" should be: | mark price | trading mode | target stake | supplied stake | open interest | ref price | mid price | static mid price | best offer price | best bid price | | 100 | TRADING_MODE_CONTINUOUS | 13915 | 1000 | 348 | 100 | 90 | 90 | 91 | 89 | diff --git a/core/integration/features/amm/0090-VAMM-basic.feature b/core/integration/features/amm/0090-VAMM-basic.feature index 66d4bcf955..3804a30f4f 100644 --- a/core/integration/features/amm/0090-VAMM-basic.feature +++ b/core/integration/features/amm/0090-VAMM-basic.feature @@ -53,8 +53,8 @@ Feature: vAMM rebasing when created or amended | trading.terminated | TYPE_BOOLEAN | trading termination | And the markets: - | id | quote name | asset | liquidity monitoring | risk model | margin calculator | auction duration | fees | price monitoring | data source config | linear slippage factor | quadratic slippage factor | sla params | - | ETH/MAR22 | USD | USD | lqm-params | log-normal-risk-model | margin-calculator-1 | 2 | fees-config-1 | default-none | termination-oracle | 1e0 | 0 | SLA-22 | + | id | quote name | asset | liquidity monitoring | risk model | margin calculator | auction duration | fees | price monitoring | data source config | linear slippage factor | quadratic slippage factor | sla params | allowed empty amm levels | + | ETH/MAR22 | USD | USD | lqm-params | log-normal-risk-model | margin-calculator-1 | 2 | fees-config-1 | default-none | termination-oracle | 1e0 | 0 | SLA-22 | 5 | # Setting up the accounts and vAMM submission now is part of the background, because we'll be running scenarios 0090-VAMM-006 through 0090-VAMM-014 on this setup Given the parties deposit on asset's general account the following amount: @@ -69,6 +69,7 @@ Feature: vAMM rebasing when created or amended | party5 | USD | 1000000 | | vamm1 | USD | 1000000 | | vamm2 | USD | 1000000 | + | vamm3 | USD | 1000000 | And the parties place the following orders: @@ -77,8 +78,6 @@ Feature: vAMM rebasing when created or amended | party5 | ETH/MAR22 | buy | 20 | 90 | 0 | TYPE_LIMIT | TIF_GTC | lp1-b | | party1 | ETH/MAR22 | buy | 1 | 100 | 0 | TYPE_LIMIT | TIF_GTC | | | party2 | ETH/MAR22 | sell | 1 | 100 | 0 | TYPE_LIMIT | TIF_GTC | | - | party3 | ETH/MAR22 | sell | 10 | 110 | 0 | TYPE_LIMIT | TIF_GTC | | - | lp1 | ETH/MAR22 | sell | 10 | 160 | 0 | TYPE_LIMIT | TIF_GTC | lp1-s | When the opening auction period ends for market "ETH/MAR22" Then the following trades should be executed: | buyer | price | size | seller | @@ -212,13 +211,173 @@ Feature: vAMM rebasing when created or amended When the parties place the following orders: | party | market id | side | volume | price | resulting trades | type | tif | reference | - | party1 | ETH/MAR22 | buy | 2 | 200 | 1 | TYPE_LIMIT | TIF_GTC | | + | party1 | ETH/MAR22 | buy | 2 | 200 | 2 | TYPE_LIMIT | TIF_GTC | | Then the parties amend the following AMM: | party | market id | amount | slippage | base | lower bound | proposed fee | error | | vamm1 | ETH/MAR22 | 100000 | 0.05 | 100 | 95 | 0.03 | cannot remove upper bound when AMM is short | + @VAMM + Scenario: Two AMM's incoming order split pro-rata equally + + When the parties submit the following AMM: + | party | market id | amount | slippage | base | lower bound | upper bound | proposed fee | + | vamm2 | ETH/MAR22 | 100000 | 0.05 | 100 | 95 | 105 | 0.03 | + Then the AMM pool status should be: + | party | market id | amount | status | base | lower bound | upper bound | + | vamm2 | ETH/MAR22 | 100000 | STATUS_ACTIVE | 100 | 95 | 105 | + + And set the following AMM sub account aliases: + | party | market id | alias | + | vamm2 | ETH/MAR22 | vamm2-id | + + And the market data for the market "ETH/MAR22" should be: + | mark price | trading mode | best bid price | best offer price | best bid volume | best offer volume | + | 100 | TRADING_MODE_CONTINUOUS | 99 | 101 | 206 | 184 | + + + When the parties place the following orders: + | party | market id | side | volume | price | resulting trades | type | tif | reference | + | party1 | ETH/MAR22 | sell | 1000 | 10 | 2 | TYPE_LIMIT | TIF_GTC | | + Then the following trades should be executed: + | buyer | price | size | seller | is amm | + | vamm1-id | 97 | 500 | party1 | true | + | vamm2-id | 97 | 500 | party1 | true | + + And the market data for the market "ETH/MAR22" should be: + | mark price | trading mode | best bid price | best offer price | best bid volume | best offer volume | + | 100 | TRADING_MODE_CONTINUOUS | 95 | 96 | 70 | 150 | + + @VAMM + Scenario: Two AMM's incoming order split pro-rata unequally + + When the parties submit the following AMM: + | party | market id | amount | slippage | base | lower bound | upper bound | proposed fee | + | vamm2 | ETH/MAR22 | 75000 | 0.05 | 100 | 90 | 110 | 0.03 | + Then the AMM pool status should be: + | party | market id | amount | status | base | lower bound | upper bound | + | vamm2 | ETH/MAR22 | 75000 | STATUS_ACTIVE | 100 | 90 | 110 | + + And set the following AMM sub account aliases: + | party | market id | alias | + | vamm2 | ETH/MAR22 | vamm2-id | + + And the market data for the market "ETH/MAR22" should be: + | mark price | trading mode | best bid price | best offer price | best bid volume | best offer volume | + | 100 | TRADING_MODE_CONTINUOUS | 99 | 101 | 141 | 125 | + + + When the parties place the following orders: + | party | market id | side | volume | price | resulting trades | type | tif | reference | + | party1 | ETH/MAR22 | sell | 400 | 10 | 2 | TYPE_LIMIT | TIF_GTC | | + Then the following trades should be executed: + | buyer | price | size | seller | is amm | + | vamm1-id | 98 | 292 | party1 | true | + | vamm2-id | 98 | 108 | party1 | true | + + And the market data for the market "ETH/MAR22" should be: + | mark price | trading mode | best bid price | best offer price | best bid volume | best offer volume | + | 100 | TRADING_MODE_CONTINUOUS | 96 | 98 | 184 | 113 | + + + @VAMM + Scenario: Two AMM's incoming order split pro-rata through base price where one side is much wider + + When the parties submit the following AMM: + | party | market id | amount | slippage | base | lower bound | upper bound | proposed fee | + | vamm2 | ETH/MAR22 | 100000 | 0.05 | 100 | 95 | 500 | 0.03 | + Then the AMM pool status should be: + | party | market id | amount | status | base | lower bound | upper bound | + | vamm2 | ETH/MAR22 | 100000 | STATUS_ACTIVE | 100 | 95 | 500 | + + And set the following AMM sub account aliases: + | party | market id | alias | + | vamm2 | ETH/MAR22 | vamm2-id | + + And the market data for the market "ETH/MAR22" should be: + | mark price | trading mode | best bid price | best offer price | best bid volume | best offer volume | + | 100 | TRADING_MODE_CONTINUOUS | 99 | 101 | 206 | 92 | + + + When the parties place the following orders: + | party | market id | side | volume | price | resulting trades | type | tif | reference | + | party1 | ETH/MAR22 | sell | 400 | 10 | 2 | TYPE_LIMIT | TIF_GTC | | + Then the following trades should be executed: + | buyer | price | size | seller | is amm | + | vamm1-id | 99 | 200 | party1 | true | + | vamm2-id | 99 | 200 | party1 | true | + + And the market data for the market "ETH/MAR22" should be: + | mark price | trading mode | best bid price | best offer price | best bid volume | best offer volume | + | 100 | TRADING_MODE_CONTINUOUS | 97 | 99 | 232 | 194 | + + # now lets swing back across both AMM's base price, AMM2 will have much less volume on that side + When the parties place the following orders: + | party | market id | side | volume | price | resulting trades | type | tif | reference | + | party1 | ETH/MAR22 | buy | 600 | 115 | 4 | TYPE_LIMIT | TIF_GTC | | + Then the following trades should be executed: + | seller | price | size | buyer | is amm | + | vamm1-id | 98 | 200 | party1 | true | + | vamm2-id | 98 | 200 | party1 | true | + | vamm1-id | 101 | 199 | party1 | true | + | vamm2-id | 100 | 1 | party1 | true | + And the market data for the market "ETH/MAR22" should be: + | mark price | trading mode | best bid price | best offer price | best bid volume | best offer volume | + | 100 | TRADING_MODE_CONTINUOUS | 102 | 104 | 16 | 163 | + + + +@VAMM + Scenario: 2 AMM's incoming order split pro-rata through, two AMM's are low volume on opposite ends + + When the parties amend the following AMM: + | party | market id | amount | slippage | base | lower bound | upper bound | + | vamm1 | ETH/MAR22 | 500000 | 0.15 | 2000 | 1995 | 2300 | + + + When the parties submit the following AMM: + | party | market id | amount | slippage | base | lower bound | upper bound | proposed fee | + | vamm2 | ETH/MAR22 | 500000 | 0.05 | 2000 | 1700 | 2005 | 0.03 | + Then the AMM pool status should be: + | party | market id | amount | status | base | lower bound | upper bound | + | vamm2 | ETH/MAR22 | 500000 | STATUS_ACTIVE | 2000 | 1700 | 2005 | + + And set the following AMM sub account aliases: + | party | market id | alias | + | vamm2 | ETH/MAR22 | vamm2-id | + + And the market data for the market "ETH/MAR22" should be: + | mark price | trading mode | best bid price | best offer price | best bid volume | best offer volume | + | 100 | TRADING_MODE_CONTINUOUS | 1999 | 2001 | 25 | 23 | + + + When the parties place the following orders: + | party | market id | side | volume | price | resulting trades | type | tif | reference | + | party1 | ETH/MAR22 | sell | 100 | 10 | 2 | TYPE_LIMIT | TIF_GTC | | + Then the following trades should be executed: + | buyer | price | size | seller | is amm | + | vamm1-id | 1998 | 99 | party1 | true | + | vamm2-id | 1998 | 1 | party1 | true | + + And the market data for the market "ETH/MAR22" should be: + | mark price | trading mode | best bid price | best offer price | best bid volume | best offer volume | + | 100 | TRADING_MODE_CONTINUOUS | 1995 | 1997 | 30 | 22 | + + + # now lets swing back across both AMM's base price, AMM2 will have much less volume on that side + When the parties place the following orders: + | party | market id | side | volume | price | resulting trades | type | tif | reference | + | party1 | ETH/MAR22 | buy | 200 | 5000 | 4 | TYPE_LIMIT | TIF_GTC | | + Then the following trades should be executed: + | seller | price | size | buyer | is amm | + | vamm1-id | 1997 | 99 | party1 | true | + | vamm2-id | 1998 | 1 | party1 | true | + | vamm1-id | 2001 | 1 | party1 | true | + | vamm2-id | 2002 | 99 | party1 | true | + And the market data for the market "ETH/MAR22" should be: + | mark price | trading mode | best bid price | best offer price | best bid volume | best offer volume | + | 100 | TRADING_MODE_CONTINUOUS | 2004 | 2005 | 5 | 19 | \ No newline at end of file