Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: calculate AMM tradable volume purely in position to avoid precis… #11617

Merged
merged 1 commit into from
Aug 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading