Skip to content

Commit

Permalink
Merge pull request #11617 from vegaprotocol/11616-rework-tradable-volume
Browse files Browse the repository at this point in the history
fix: calculate AMM tradable volume purely in position to avoid precis…
  • Loading branch information
wwestgarth authored Aug 27, 2024
2 parents 9d92195 + 890d2eb commit 8031219
Show file tree
Hide file tree
Showing 8 changed files with 219 additions and 89 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
- [11568](https://github.com/vegaprotocol/vega/issues/11568) - order book shape on closing `AMM` no longer panics.
- [11540](https://github.com/vegaprotocol/vega/issues/11540) - Fix spam check for spots to use not double count quantum.
- [11542](https://github.com/vegaprotocol/vega/issues/11542) - Fix non determinism in lottery ranking.
- [11616](https://github.com/vegaprotocol/vega/issues/11616) - `AMM` tradable volume now calculated purely in positions to prevent loss of precision.
- [11544](https://github.com/vegaprotocol/vega/issues/11544) - Fix empty candles stream.
- [11579](https://github.com/vegaprotocol/vega/issues/11579) - Spot calculate fee on amend, use order price if no amended price is provided.
- [11585](https://github.com/vegaprotocol/vega/issues/11585) - Initialise rebate stats service in API.
Expand Down
8 changes: 4 additions & 4 deletions core/execution/amm/engine_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -462,7 +462,7 @@ func TestBestPricesAndVolumeNearBound(t *testing.T) {
expectSubaccountCreation(t, tst, party, subAccount)
whenAMMIsSubmitted(t, tst, submit)

tst.pos.EXPECT().GetPositionsByParty(gomock.Any()).Times(5).Return(
tst.pos.EXPECT().GetPositionsByParty(gomock.Any()).Times(3).Return(
[]events.MarketPosition{&marketPosition{size: 0, averageEntry: num.NewUint(0)}},
)

Expand All @@ -473,7 +473,7 @@ func TestBestPricesAndVolumeNearBound(t *testing.T) {
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(
tst.pos.EXPECT().GetPositionsByParty(gomock.Any()).Times(3).Return(
[]events.MarketPosition{&marketPosition{size: -222000, averageEntry: num.NewUint(0)}},
)

Expand All @@ -484,15 +484,15 @@ func TestBestPricesAndVolumeNearBound(t *testing.T) {
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(
tst.pos.EXPECT().GetPositionsByParty(gomock.Any()).Times(3).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))
assert.Equal(t, 1460, int(avolume))
}

func testClosingReduceOnlyPool(t *testing.T) {
Expand Down
57 changes: 35 additions & 22 deletions core/execution/amm/pool.go
Original file line number Diff line number Diff line change
Expand Up @@ -607,39 +607,52 @@ func (p *Pool) TradableVolumeInRange(side types.Side, price1 *num.Uint, price2 *
st, nd = nd, st
}

fp := p.fairPrice()
// map the given st/nd prices into positions, then the difference is the volume
asPosition := func(price *num.Uint) int64 {
switch {
case price.GT(p.lower.high):
// in upper curve
if !p.upper.empty {
return p.upper.positionAtPrice(p.sqrt, num.Min(p.upper.high, price))
}
case price.LT(p.lower.high):
// in lower curve
if !p.lower.empty {
return p.lower.positionAtPrice(p.sqrt, num.Max(p.lower.low, price))
}
}
return 0
}

stP := asPosition(st)
ndP := asPosition(nd)

if side == types.SideSell {
// want all buy volume so everything below fair price
nd = num.Min(fp, nd)
// want all buy volume so everything below fair price, where the AMM is long
ndP = num.MaxV(pos, ndP)
}

if side == types.SideBuy {
// want all sell volume so everything above fair price
st = num.Max(fp, st)
// want all sell volume so everything above fair price, where the AMM is short
stP = num.MinV(pos, stP)
}

var other *curve
var volume uint64
// get the curve based on the pool's current position, if the position is zero we take the curve the trade will put us in
// e.g trading with an incoming buy order will make the pool short, so we take the upper curve.
if pos < 0 || (pos == 0 && side == types.SideBuy) {
volume = p.upper.volumeBetweenPrices(p.sqrt, st, nd)
other = p.lower
} else {
volume = p.lower.volumeBetweenPrices(p.sqrt, st, nd)
other = p.upper
if !p.closing() {
return uint64(stP - ndP)
}

if p.closing() {
return num.MinV(volume, uint64(num.AbsV(pos)))
if pos > 0 {
// if closing and long, we have no volume at short prices, so cap range to > 0
stP = num.MaxV(0, stP)
ndP = num.MaxV(0, ndP)
}

// if the position is non-zero, the incoming order could push us across to the other curve
// so we need to check for volume there too
if pos != 0 {
volume += other.volumeBetweenPrices(p.sqrt, st, nd)
if pos < 0 {
// if closing and short, we have no volume at long prices, so cap range to < 0
stP = num.MinV(0, stP)
ndP = num.MinV(0, ndP)
}
return volume
return num.MinV(uint64(stP-ndP), uint64(num.AbsV(pos)))
}

// getBalance returns the total balance of the pool i.e it's general account + it's margin account.
Expand Down
146 changes: 131 additions & 15 deletions core/execution/amm/pool_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ func TestAMMPool(t *testing.T) {
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)
t.Run("test volume between prices when closing", testTradeableVolumeInRangeClosing)
}

func testTradeableVolumeInRange(t *testing.T) {
Expand Down Expand Up @@ -85,7 +86,7 @@ func testTradeableVolumeInRange(t *testing.T) {
price1: num.NewUint(500),
price2: num.NewUint(3500),
side: types.SideBuy,
expectedVolume: 1337,
expectedVolume: 1335,
position: 700, // position at full lower boundary, incoming is by so whole volume of both curves is available
},
{
Expand All @@ -101,22 +102,137 @@ func testTradeableVolumeInRange(t *testing.T) {
price1: num.NewUint(500),
price2: num.NewUint(3500),
side: types.SideBuy,
expectedVolume: 986,
expectedVolume: 985,
position: 350,
},
{
name: "sell trade causes sign to flip and partial volume across both curves",
price1: num.NewUint(500),
price2: num.NewUint(3500),
side: types.SideSell,
expectedVolume: 1053,
expectedVolume: 1052,
position: -350,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ensurePositionN(t, p.pos, tt.position, num.UintZero(), 2)
ensurePositionN(t, p.pos, tt.position, num.UintZero(), 1)
volume := p.pool.TradableVolumeInRange(tt.side, tt.price1, tt.price2)
assert.Equal(t, int(tt.expectedVolume), int(volume))
})
}
}

func testTradeableVolumeInRangeClosing(t *testing.T) {
p := newTestPool(t)
defer p.ctrl.Finish()

// pool is reducing its
p.pool.status = types.AMMPoolStatusReduceOnly

tests := []struct {
name string
price1 *num.Uint
price2 *num.Uint
position int64
side types.Side
expectedVolume uint64
nposcalls int
}{
{
name: "0 position, 0 buy volume",
price1: num.NewUint(1800),
price2: num.NewUint(2200),
side: types.SideBuy,
expectedVolume: 0,
nposcalls: 1,
},
{
name: "0 position, 0 sell volume",
price1: num.NewUint(1800),
price2: num.NewUint(2200),
side: types.SideSell,
expectedVolume: 0,
nposcalls: 1,
},
{
name: "long position, 0 volume for incoming SELL",
price1: num.NewUint(1800),
price2: num.NewUint(2200),
side: types.SideSell,
position: 10,
expectedVolume: 0,
nposcalls: 1,
},
{
name: "long position, 10 volume for incoming BUY",
price1: num.NewUint(1800),
price2: num.NewUint(2200),
side: types.SideBuy,
position: 10,
expectedVolume: 10,
nposcalls: 2,
},
{
name: "short position, 0 volume for incoming BUY",
price1: num.NewUint(1800),
price2: num.NewUint(2200),
side: types.SideBuy,
position: -10,
expectedVolume: 0,
nposcalls: 1,
},
{
name: "short position, 10 volume for incoming SELL",
price1: num.NewUint(1800),
price2: num.NewUint(2200),
side: types.SideSell,
position: -10,
expectedVolume: 10,
nposcalls: 2,
},
{
name: "asking for SELL volume but for prices outside of price ranges",
price1: num.NewUint(2000),
price2: num.NewUint(2200),
side: types.SideBuy,
position: 10,
expectedVolume: 0,
nposcalls: 2,
},
{
name: "asking for BUY volume but for prices outside of price ranges",
price1: num.NewUint(1800),
price2: num.NewUint(1850),
side: types.SideSell,
position: -10,
expectedVolume: 0,
nposcalls: 2,
},
{
name: "asking for partial closing volume when long",
price1: num.NewUint(1800),
price2: num.NewUint(1850),
side: types.SideBuy,
position: 702,
expectedVolume: 186,
nposcalls: 2,
},
{
name: "asking for partial closing volume when short",
price1: num.NewUint(2100),
price2: num.NewUint(2150),
side: types.SideSell,
position: -635,
expectedVolume: 155,
nposcalls: 2,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ensurePositionN(t, p.pos, tt.position, num.UintZero(), tt.nposcalls)
volume := p.pool.TradableVolumeInRange(tt.side, tt.price1, tt.price2)
assert.Equal(t, int(tt.expectedVolume), int(volume))
})
Expand Down Expand Up @@ -149,7 +265,7 @@ func TestTradeableVolumeWhenAtBoundary(t *testing.T) {
defer p.ctrl.Finish()

// when position is zero fair-price should be the base
ensurePositionN(t, p.pos, 0, num.UintZero(), 3)
ensurePositionN(t, p.pos, 0, num.UintZero(), 2)
fp := p.pool.BestPrice(nil)
assert.Equal(t, "6765400000000000000000", fp.String())

Expand All @@ -160,7 +276,7 @@ func TestTradeableVolumeWhenAtBoundary(t *testing.T) {
assert.Equal(t, fullLong, int(volume))

// now lets pretend the AMM has fully traded out in that direction, best price will be near but not quite the lower bound
ensurePositionN(t, p.pos, int64(fullLong), num.UintZero(), 3)
ensurePositionN(t, p.pos, int64(fullLong), num.UintZero(), 2)
fp = p.pool.BestPrice(nil)
assert.Equal(t, "6712721893865935337785", fp.String())
assert.True(t, fp.GTE(num.MustUintFromString("6712720000000000000000", 10)))
Expand All @@ -174,12 +290,12 @@ func testPoolPositionFactor(t *testing.T) {
p := newTestPoolWithPositionFactor(t, num.DecimalFromInt64(1000))
defer p.ctrl.Finish()

ensurePositionN(t, p.pos, 0, num.UintZero(), 2)
ensurePositionN(t, p.pos, 0, num.UintZero(), 1)
volume := p.pool.TradableVolumeInRange(types.SideBuy, num.NewUint(2000), num.NewUint(2200))
// with position factot of 1 the volume is 635
assert.Equal(t, int(635395), int(volume))

ensurePositionN(t, p.pos, 0, num.UintZero(), 2)
ensurePositionN(t, p.pos, 0, num.UintZero(), 1)
volume = p.pool.TradableVolumeInRange(types.SideSell, num.NewUint(1800), num.NewUint(2000))
// with position factot of 1 the volume is 702
assert.Equal(t, int(702411), int(volume))
Expand Down Expand Up @@ -256,18 +372,18 @@ func testOneSidedPool(t *testing.T) {
defer p.ctrl.Finish()

// side with liquidity returns volume
ensurePositionN(t, p.pos, 0, num.UintZero(), 2)
ensurePositionN(t, p.pos, 0, num.UintZero(), 1)
volume := p.pool.TradableVolumeInRange(types.SideBuy, num.NewUint(2000), num.NewUint(2200))
assert.Equal(t, int(635), int(volume))

// empty side returns no volume
ensurePositionN(t, p.pos, 0, num.UintZero(), 2)
ensurePositionN(t, p.pos, 0, num.UintZero(), 1)
volume = p.pool.TradableVolumeInRange(types.SideSell, num.NewUint(1800), num.NewUint(2000))
assert.Equal(t, int(0), int(volume))

// pool with short position and incoming sell only reports volume up to base
// empty side returns no volume
ensurePositionN(t, p.pos, -10, num.UintZero(), 2)
ensurePositionN(t, p.pos, -10, num.UintZero(), 1)
volume = p.pool.TradableVolumeInRange(types.SideSell, num.NewUint(1800), num.NewUint(2200))
assert.Equal(t, int(10), int(volume))

Expand Down Expand Up @@ -440,22 +556,22 @@ func TestNotebook(t *testing.T) {

pos := int64(0)

ensurePositionN(t, p.pos, pos, num.UintZero(), 2)
ensurePositionN(t, p.pos, pos, num.UintZero(), 1)
volume := p.pool.TradableVolumeInRange(types.SideSell, base, low)
assert.Equal(t, int(702), int(volume))

ensurePositionN(t, p.pos, pos, num.UintZero(), 2)
ensurePositionN(t, p.pos, pos, num.UintZero(), 1)
volume = p.pool.TradableVolumeInRange(types.SideBuy, up, base)
assert.Equal(t, int(635), int(volume))

lowmid := num.NewUint(1900)
upmid := num.NewUint(2100)

ensurePositionN(t, p.pos, pos, num.UintZero(), 2)
ensurePositionN(t, p.pos, pos, num.UintZero(), 1)
volume = p.pool.TradableVolumeInRange(types.SideSell, low, lowmid)
assert.Equal(t, int(365), int(volume))

ensurePositionN(t, p.pos, pos, num.UintZero(), 2)
ensurePositionN(t, p.pos, pos, num.UintZero(), 1)
volume = p.pool.TradableVolumeInRange(types.SideBuy, upmid, up)
assert.Equal(t, int(306), int(volume))

Expand Down
8 changes: 4 additions & 4 deletions core/integration/features/amm/0090-VAMM-006-014.feature
Original file line number Diff line number Diff line change
Expand Up @@ -478,10 +478,10 @@ 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 | 120 |
| 95 | TRADING_MODE_CONTINUOUS | 100 | 120 | 120 | 121 | 119 |
And the following trades should be executed:
| buyer | price | size | seller | is amm |
| party5 | 114 | 64 | vamm1-id | true |
| party5 | 114 | 65 | vamm1-id | true |
# Check the resulting position, vAMM further increased their position
When the network moves ahead "1" blocks
Then the parties should have the following profit and loss:
Expand All @@ -490,5 +490,5 @@ Feature: Ensure the vAMM positions follow the market correctly
| party2 | -1 | -14 | 0 | |
| party3 | -350 | -6650 | 0 | |
| party4 | 420 | 7980 | 0 | |
| party5 | 64 | 0 | 0 | |
| vamm1-id | -134 | -1330 | 0 | true |
| party5 | 65 | 0 | 0 | |
| vamm1-id | -135 | -1330 | 0 | true |
Loading

0 comments on commit 8031219

Please sign in to comment.