Skip to content

Commit

Permalink
Merge pull request #11258 from vegaprotocol/feat-10784
Browse files Browse the repository at this point in the history
AMM handle "near-zero-volume" price levels
  • Loading branch information
karlem authored May 10, 2024
2 parents b1fa724 + 35650ff commit 7f66915
Show file tree
Hide file tree
Showing 13 changed files with 328 additions and 207 deletions.
23 changes: 20 additions & 3 deletions core/execution/amm/engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -656,7 +656,7 @@ func (e *Engine) SubmitAMM(

return err
}
pool := NewPool(
pool, err := NewPool(
poolID,
subAccount,
e.market.GetSettlementAsset(),
Expand All @@ -670,6 +670,17 @@ func (e *Engine) SubmitAMM(
e.priceFactor,
e.positionFactor,
)
if err != nil {
e.broker.Send(
events.NewAMMPoolEvent(
ctx, submit.Party, e.market.GetID(), subAccount, poolID,
submit.CommitmentAmount, submit.Parameters,
types.AMMPoolStatusRejected, types.AMMPoolStatusReasonCommitmentTooLow, // TODO Karel - check if this error is ok to return
),
)

return err
}

if targetPrice != nil {
if err := e.rebasePool(ctx, pool, targetPrice, submit.SlippageTolerance, idgen); err != nil {
Expand Down Expand Up @@ -720,7 +731,11 @@ func (e *Engine) AmendAMM(

oldCommitment := pool.Commitment.Clone()
fairPrice := pool.BestPrice(nil)
oldParams := pool.Update(amend, e.risk.GetRiskFactors(), e.risk.GetScalingFactors(), e.risk.GetSlippage())
oldParams, err := pool.Update(amend, e.risk.GetRiskFactors(), e.risk.GetScalingFactors(), e.risk.GetSlippage())
if err != nil {
return err
}

if err := e.rebasePool(ctx, pool, fairPrice, amend.SlippageTolerance, idgeneration.New(deterministicID)); err != nil {
// couldn't rebase the pool back to its original fair price so the amend is rejected
if err := e.updateSubAccountBalance(ctx, amend.Party, pool.SubAccount, oldCommitment); err != nil {
Expand All @@ -729,7 +744,9 @@ func (e *Engine) AmendAMM(
// restore updated parameters
pool.Parameters = oldParams
// restore curves
pool.setCurves(e.risk.GetRiskFactors(), e.risk.GetScalingFactors(), e.risk.GetSlippage())
if err := pool.setCurves(e.risk.GetRiskFactors(), e.risk.GetScalingFactors(), e.risk.GetSlippage()); err != nil {
e.log.Panic("could not restore curves after failed rebase", logging.Error(err))
}

return err
}
Expand Down
33 changes: 26 additions & 7 deletions core/execution/amm/pool.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ func NewPool(
linearSlippage num.Decimal,
priceFactor num.Decimal,
positionFactor num.Decimal,
) *Pool {
) (*Pool, error) {
oneTick, _ := num.UintFromDecimal(num.DecimalOne().Mul(priceFactor))
pool := &Pool{
ID: id,
Expand All @@ -127,8 +127,11 @@ func NewPool(
oneTick: oneTick,
status: types.AMMPoolStatusActive,
}
pool.setCurves(rf, sf, linearSlippage)
return pool
err := pool.setCurves(rf, sf, linearSlippage)
if err != nil {
return nil, err
}
return pool, nil
}

func NewPoolFromProto(
Expand Down Expand Up @@ -267,7 +270,7 @@ func (p *Pool) Update(
rf *types.RiskFactor,
sf *types.ScalingFactors,
linearSlippage num.Decimal,
) *types.ConcentratedLiquidityParameters {
) (*types.ConcentratedLiquidityParameters, error) {
if amend.CommitmentAmount != nil {
p.Commitment = amend.CommitmentAmount
}
Expand All @@ -276,8 +279,10 @@ func (p *Pool) Update(
}
oldParams := p.Parameters.Clone()
p.Parameters.ApplyUpdate(amend.Parameters)
p.setCurves(rf, sf, linearSlippage)
return oldParams
if err := p.setCurves(rf, sf, linearSlippage); err != nil {
return oldParams, err
}
return oldParams, nil
}

// emptyCurve creates the curve details that represent no liquidity.
Expand Down Expand Up @@ -375,7 +380,7 @@ func (p *Pool) setCurves(
rfs *types.RiskFactor,
sfs *types.ScalingFactors,
linearSlippage num.Decimal,
) {
) error {
// convert the bounds into asset precision
base, _ := num.UintFromDecimal(p.Parameters.Base.ToDecimal().Mul(p.priceFactor))
p.lower = emptyCurve(base)
Expand All @@ -395,6 +400,12 @@ func (p *Pool) setCurves(
p.positionFactor,
true,
)

highPriceMinusOne := num.UintZero().Sub(p.lower.high, p.oneTick)
// verify that the lower curve maintains sufficient volume from highPrice - 1 to the end of the curve.
if p.lower.volumeBetweenPrices(p.sqrt, highPriceMinusOne, p.lower.high) < 1 {
return fmt.Errorf("insufficient volume in the lower curve from high price - 1 to the end")
}
}

if p.Parameters.UpperBound != nil {
Expand All @@ -411,7 +422,15 @@ func (p *Pool) setCurves(
p.positionFactor,
false,
)

highPriceMinusOne := num.UintZero().Sub(p.upper.high, p.oneTick)
// verify that the upper curve maintains sufficient volume from highPrice - 1 to the end of the curve.
if p.upper.volumeBetweenPrices(p.sqrt, highPriceMinusOne, p.upper.high) < 1 {
return fmt.Errorf("insufficient volume in the lower curve from high price - 1 to the end")
}
}

return nil
}

// impliedPosition returns the position of the pool if its fair-price were the given price. `l` is
Expand Down
86 changes: 85 additions & 1 deletion core/execution/amm/pool_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ func TestAMMPool(t *testing.T) {
t.Run("test best price", testBestPrice)
t.Run("test pool logic with position factor", testPoolPositionFactor)
t.Run("test one sided pool", testOneSidedPool)
t.Run("test near zero volume curve triggers and error", testNearZeroCurveErrors)
}

func testTradeableVolumeInRange(t *testing.T) {
Expand Down Expand Up @@ -212,6 +213,88 @@ func testOneSidedPool(t *testing.T) {
assert.Panics(t, func() { p.pool.BestPrice(nil) })
}

func testNearZeroCurveErrors(t *testing.T) {
baseCmd := types.AMMBaseCommand{
Party: vgcrypto.RandomHash(),
MarketID: vgcrypto.RandomHash(),
SlippageTolerance: num.DecimalFromFloat(0.1),
}

submit := &types.SubmitAMM{
AMMBaseCommand: baseCmd,
CommitmentAmount: num.NewUint(1000),
Parameters: &types.ConcentratedLiquidityParameters{
Base: num.NewUint(1900),
LowerBound: num.NewUint(1800),
UpperBound: num.NewUint(2000),
LeverageAtLowerBound: ptr.From(num.DecimalFromFloat(50)),
LeverageAtUpperBound: ptr.From(num.DecimalFromFloat(50)),
},
}

// test that creating a pool with a near zero volume curve will error
pool, err := newBasicPoolWithSubmit(t, submit)
assert.Nil(t, pool)
assert.ErrorContains(t, err, "insufficient volume in the lower curve from high price - 1 to the end")

// test that a pool with higher commitment amount will not error
submit.CommitmentAmount = num.NewUint(100000)
pool, err = newBasicPoolWithSubmit(t, submit)
assert.NotNil(t, pool)
assert.NoError(t, err)

// test that amending a pool to a near zero volume curve will error
amend := &types.AmendAMM{
AMMBaseCommand: baseCmd,
CommitmentAmount: num.NewUint(100),
}

_, err = pool.Update(
amend,
&types.RiskFactor{Short: num.DecimalFromFloat(0.02), Long: num.DecimalFromFloat(0.02)},
&types.ScalingFactors{InitialMargin: num.DecimalFromFloat(1.25)},
num.DecimalZero(),
)
assert.ErrorContains(t, err, "insufficient volume in the lower curve from high price - 1 to the end")

amend.CommitmentAmount = num.NewUint(1000000)
_, err = pool.Update(
amend,
&types.RiskFactor{Short: num.DecimalFromFloat(0.02), Long: num.DecimalFromFloat(0.02)},
&types.ScalingFactors{InitialMargin: num.DecimalFromFloat(1.25)},
num.DecimalZero(),
)
assert.NoError(t, err)
}

func newBasicPoolWithSubmit(t *testing.T, submit *types.SubmitAMM) (*Pool, error) {
ctrl := gomock.NewController(t)
col := mocks.NewMockCollateral(ctrl)
pos := mocks.NewMockPosition(ctrl)

sqrter := &Sqrter{cache: map[string]num.Decimal{}}

return NewPool(
vgcrypto.RandomHash(),
vgcrypto.RandomHash(),
vgcrypto.RandomHash(),
submit,
sqrter.sqrt,
col,
pos,
&types.RiskFactor{
Short: num.DecimalFromFloat(0.02),
Long: num.DecimalFromFloat(0.02),
},
&types.ScalingFactors{
InitialMargin: num.DecimalFromFloat(1.25), // this is 1/0.8 which is margin_usage_at_bound_above in the note-book
},
num.DecimalZero(),
num.DecimalOne(),
num.DecimalOne(),
)
}

func ensurePositionN(t *testing.T, p *mocks.MockPosition, pos int64, averageEntry *num.Uint, times int) {
t.Helper()

Expand Down Expand Up @@ -346,7 +429,7 @@ func newTestPoolWithOpts(t *testing.T, positionFactor num.Decimal, low, base, hi
},
}

pool := NewPool(
pool, err := NewPool(
vgcrypto.RandomHash(),
vgcrypto.RandomHash(),
vgcrypto.RandomHash(),
Expand All @@ -365,6 +448,7 @@ func newTestPoolWithOpts(t *testing.T, positionFactor num.Decimal, low, base, hi
num.DecimalOne(),
positionFactor,
)
assert.NoError(t, err)

return &tstPool{
pool: pool,
Expand Down
12 changes: 6 additions & 6 deletions core/integration/features/amm/0012-POSR-032.feature
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ Feature: A network disposal order which crosses with volume implied by an vAMM s
| party4 | USD | 1000000 |
| party5 | USD | 1000000 |
| partyX | USD | 100 |
| vamm1 | USD | 10000 |
| vamm1 | USD | 100000 |

When the parties submit the following liquidity provision:
| id | party | market id | commitment amount | fee | lp type |
Expand Down Expand Up @@ -110,18 +110,18 @@ Feature: A network disposal order which crosses with volume implied by an vAMM s
| mark price | trading mode | target stake | supplied stake | open interest | ref price | mid price | static mid price |
| 100 | TRADING_MODE_CONTINUOUS | 79 | 1000 | 2 | 100 | 100 | 100 |
When the parties submit the following AMM:
| party | market id | amount | slippage | base | lower bound | upper bound | lower leverage | upper leverage | proposed fee |
| vamm1 | ETH/MAR22 | 10000 | 0.1 | 100 | 85 | 150 | 4 | 4 | 0.01 |
| party | market id | amount | slippage | base | lower bound | upper bound | lower leverage | upper leverage | proposed fee |
| vamm1 | ETH/MAR22 | 100000 | 0.1 | 100 | 85 | 150 | 4 | 4 | 0.01 |
Then the AMM pool status should be:
| party | market id | amount | status | base | lower bound | upper bound | lower leverage | upper leverage |
| vamm1 | ETH/MAR22 | 10000 | STATUS_ACTIVE | 100 | 85 | 150 | 4 | 4 |
| party | market id | amount | status | base | lower bound | upper bound | lower leverage | upper leverage |
| vamm1 | ETH/MAR22 | 100000 | STATUS_ACTIVE | 100 | 85 | 150 | 4 | 4 |

And set the following AMM sub account aliases:
| party | market id | alias |
| vamm1 | ETH/MAR22 | vamm1-id |
And the following transfers should happen:
| from | from account | to | to account | market id | amount | asset | is amm | type |
| vamm1 | ACCOUNT_TYPE_GENERAL | vamm1-id | ACCOUNT_TYPE_GENERAL | | 10000 | USD | true | TRANSFER_TYPE_AMM_SUBACCOUNT_LOW |
| vamm1 | ACCOUNT_TYPE_GENERAL | vamm1-id | ACCOUNT_TYPE_GENERAL | | 100000 | USD | true | TRANSFER_TYPE_AMM_SUBACCOUNT_LOW |

@VAMM
Scenario: the distressed party uncrosses with the vAMM orders
Expand Down
8 changes: 4 additions & 4 deletions core/integration/features/amm/0090-VAMM-001.feature
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ Feature: Test vAMM submission works as expected
| party1 | USD | 100000 |
| party2 | USD | 100000 |
| party3 | USD | 100000 |
| vamm1 | USD | 1000 |
| vamm1 | USD | 100000 |

When the parties submit the following liquidity provision:
| id | party | market id | commitment amount | fee | lp type |
Expand Down Expand Up @@ -90,14 +90,14 @@ Feature: Test vAMM submission works as expected
| 100 | TRADING_MODE_CONTINUOUS | 3600 | 94 | 106 | 39 | 1000 | 1 | 100 | 100 | 100 |
When the parties submit the following AMM:
| party | market id | amount | slippage | base | lower bound | upper bound | lower leverage | upper leverage | proposed fee |
| vamm1 | ETH/MAR22 | 1000 | 0.1 | 100 | 85 | 150 | 0.25 | 0.25 | 0.01 |
| vamm1 | ETH/MAR22 | 100000 | 0.1 | 100 | 85 | 150 | 0.25 | 0.25 | 0.01 |
Then the AMM pool status should be:
| party | market id | amount | status | base | lower bound | upper bound | lower leverage | upper leverage |
| vamm1 | ETH/MAR22 | 1000 | STATUS_ACTIVE | 100 | 85 | 150 | 0.25 | 0.25 |
| vamm1 | ETH/MAR22 | 100000 | STATUS_ACTIVE | 100 | 85 | 150 | 0.25 | 0.25 |

And set the following AMM sub account aliases:
| party | market id | alias |
| vamm1 | ETH/MAR22 | vamm1-acc |
And the following transfers should happen:
| from | from account | to | to account | market id | amount | asset | is amm | type |
| vamm1 | ACCOUNT_TYPE_GENERAL | vamm1-acc | ACCOUNT_TYPE_GENERAL | | 1000 | USD | true | TRANSFER_TYPE_AMM_SUBACCOUNT_LOW |
| vamm1 | ACCOUNT_TYPE_GENERAL | vamm1-acc | ACCOUNT_TYPE_GENERAL | | 100000 | USD | true | TRANSFER_TYPE_AMM_SUBACCOUNT_LOW |
8 changes: 4 additions & 4 deletions core/integration/features/amm/0090-VAMM-002.feature
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ Feature: Test vAMM submission works as expected
| party1 | USD | 100000 |
| party2 | USD | 100000 |
| party3 | USD | 100000 |
| vamm1 | USD | 1000 |
| vamm1 | USD | 100000 |

When the parties submit the following liquidity provision:
| id | party | market id | commitment amount | fee | lp type |
Expand Down Expand Up @@ -90,15 +90,15 @@ Feature: Test vAMM submission works as expected
| 100 | TRADING_MODE_CONTINUOUS | 3600 | 94 | 106 | 39 | 1000 | 1 | 100 | 100 | 100 |
When the parties submit the following AMM:
| party | market id | amount | slippage | base | lower bound | lower leverage | proposed fee |
| vamm1 | ETH/MAR22 | 1000 | 0.1 | 100 | 85 | 0.25 | 0.01 |
| vamm1 | ETH/MAR22 | 100000 | 0.1 | 100 | 85 | 0.25 | 0.01 |
Then the AMM pool status should be:
| party | market id | amount | status | base | lower bound | lower leverage |
| vamm1 | ETH/MAR22 | 1000 | STATUS_ACTIVE | 100 | 85 | 0.25 |
| vamm1 | ETH/MAR22 | 100000 | STATUS_ACTIVE | 100 | 85 | 0.25 |

And set the following AMM sub account aliases:
| party | market id | alias |
| vamm1 | ETH/MAR22 | vamm1-acc |
And the following transfers should happen:
| from | from account | to | to account | market id | amount | asset | is amm | type |
| vamm1 | ACCOUNT_TYPE_GENERAL | vamm1-acc | ACCOUNT_TYPE_GENERAL | | 1000 | USD | true | TRANSFER_TYPE_AMM_SUBACCOUNT_LOW |
| vamm1 | ACCOUNT_TYPE_GENERAL | vamm1-acc | ACCOUNT_TYPE_GENERAL | | 100000 | USD | true | TRANSFER_TYPE_AMM_SUBACCOUNT_LOW |

8 changes: 4 additions & 4 deletions core/integration/features/amm/0090-VAMM-003.feature
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ Feature: Test vAMM submission works as expected
| party1 | USD | 100000 |
| party2 | USD | 100000 |
| party3 | USD | 100000 |
| vamm1 | USD | 1000 |
| vamm1 | USD | 100000 |

When the parties submit the following liquidity provision:
| id | party | market id | commitment amount | fee | lp type |
Expand Down Expand Up @@ -90,15 +90,15 @@ Feature: Test vAMM submission works as expected
| 100 | TRADING_MODE_CONTINUOUS | 3600 | 94 | 106 | 39 | 1000 | 1 | 100 | 100 | 100 |
When the parties submit the following AMM:
| party | market id | amount | slippage | base | upper bound | upper leverage | proposed fee |
| vamm1 | ETH/MAR22 | 1000 | 0.1 | 100 | 150 | 0.25 | 0.01 |
| vamm1 | ETH/MAR22 | 100000 | 0.1 | 100 | 150 | 0.25 | 0.01 |
Then the AMM pool status should be:
| party | market id | amount | status | base | upper bound | upper leverage |
| vamm1 | ETH/MAR22 | 1000 | STATUS_ACTIVE | 100 | 150 | 0.25 |
| vamm1 | ETH/MAR22 | 100000 | STATUS_ACTIVE | 100 | 150 | 0.25 |

And set the following AMM sub account aliases:
| party | market id | alias |
| vamm1 | ETH/MAR22 | vamm1-acc |
And the following transfers should happen:
| from | from account | to | to account | market id | amount | asset | is amm | type |
| vamm1 | ACCOUNT_TYPE_GENERAL | vamm1-acc | ACCOUNT_TYPE_GENERAL | | 1000 | USD | true | TRANSFER_TYPE_AMM_SUBACCOUNT_LOW |
| vamm1 | ACCOUNT_TYPE_GENERAL | vamm1-acc | ACCOUNT_TYPE_GENERAL | | 100000 | USD | true | TRANSFER_TYPE_AMM_SUBACCOUNT_LOW |

Loading

0 comments on commit 7f66915

Please sign in to comment.