From 4527359a0de3ef1d6569efea57fcf4ff28a05298 Mon Sep 17 00:00:00 2001 From: wwestgarth Date: Wed, 9 Oct 2024 12:52:00 +0100 Subject: [PATCH] fix: when uncrossing after auction refine approximately expanded AMMs in the volume maximising range --- core/execution/amm/engine.go | 16 +- core/execution/amm/shape.go | 21 +- core/execution/amm/shape_test.go | 291 +++++++++--------- core/execution/common/interfaces.go | 2 +- .../common/liquidity_provision_fees.go | 9 +- .../liquidity_provision_snapshot_test.go | 4 +- core/execution/common/mocks_amm/mocks.go | 7 +- .../features/amm/0090-VAMM-006-014.feature | 2 +- .../amm/0090-VAMM-auction-refine.feature | 160 ++++++++++ .../features/amm/0090-VAMM-auction.feature | 3 +- core/matching/cached_orderbook.go | 4 +- core/matching/can_uncross_test.go | 16 +- core/matching/indicative_price_and_volume.go | 119 ++++--- core/matching/mocks/mocks.go | 7 +- core/matching/orderbook.go | 114 +++++-- core/matching/orderbook_amm_test.go | 8 +- core/types/amm.go | 7 + 17 files changed, 529 insertions(+), 261 deletions(-) create mode 100644 core/integration/features/amm/0090-VAMM-auction-refine.feature diff --git a/core/execution/amm/engine.go b/core/execution/amm/engine.go index 7b5a141c747..d84506e0970 100644 --- a/core/execution/amm/engine.go +++ b/core/execution/amm/engine.go @@ -970,32 +970,30 @@ func (e *Engine) UpdateSubAccountBalance( // OrderbookShape expands all registered AMM's into orders between the given prices. If `ammParty` is supplied then just the pool // with that party id is expanded. -func (e *Engine) OrderbookShape(st, nd *num.Uint, ammParty *string) ([]*types.Order, []*types.Order) { +func (e *Engine) OrderbookShape(st, nd *num.Uint, ammParty *string) []*types.OrderbookShapeResult { if ammParty == nil { // no party give, expand all registered - buys, sells := []*types.Order{}, []*types.Order{} + res := make([]*types.OrderbookShapeResult, 0, len(e.poolsCpy)) for _, p := range e.poolsCpy { - b, s := p.OrderbookShape(st, nd, e.idgen) - buys = append(buys, b...) - sells = append(sells, s...) + res = append(res, p.OrderbookShape(st, nd, e.idgen)) } - return buys, sells + return res } // asked to expand just one AMM, lets find it, first amm-party -> owning party owner, ok := e.ammParties[*ammParty] if !ok { - return nil, nil + return nil } // now owning party -> pool p, ok := e.pools[owner] if !ok { - return nil, nil + return nil } // expand it - return p.OrderbookShape(st, nd, e.idgen) + return []*types.OrderbookShapeResult{p.OrderbookShape(st, nd, e.idgen)} } func (e *Engine) GetAMMPoolsBySubAccount() map[string]common.AMMPool { diff --git a/core/execution/amm/shape.go b/core/execution/amm/shape.go index f27f5ca9ee8..71e81ece072 100644 --- a/core/execution/amm/shape.go +++ b/core/execution/amm/shape.go @@ -449,10 +449,10 @@ func (sm *shapeMaker) adjustRegion() bool { return true } -func (sm *shapeMaker) makeShape() ([]*types.Order, []*types.Order) { +func (sm *shapeMaker) makeShape() { if !sm.adjustRegion() { // if there is no overlap between the input region and the AMM's bounds then there are no orders - return sm.buys, sm.sells + return } // create accurate orders at the boundary of the adjusted region (even if we are going to make approximate internal steps) @@ -481,15 +481,22 @@ func (sm *shapeMaker) makeShape() ([]*types.Order, []*types.Order) { logging.Int("sells", len(sm.sells)), ) } - return sm.buys, sm.sells } -func (p *Pool) OrderbookShape(from, to *num.Uint, idgen *idgeneration.IDGenerator) ([]*types.Order, []*types.Order) { - return newShapeMaker( +func (p *Pool) OrderbookShape(from, to *num.Uint, idgen *idgeneration.IDGenerator) *types.OrderbookShapeResult { + sm := newShapeMaker( p.log, p, from, to, - idgen). - makeShape() + idgen) + + sm.makeShape() + + return &types.OrderbookShapeResult{ + AmmParty: sm.pool.AMMParty, + Buys: sm.buys, + Sells: sm.sells, + Approx: sm.approx, + } } diff --git a/core/execution/amm/shape_test.go b/core/execution/amm/shape_test.go index d9b805fdcda..25c19b7fa4e 100644 --- a/core/execution/amm/shape_test.go +++ b/core/execution/amm/shape_test.go @@ -52,52 +52,53 @@ func testOrderbookShapeZeroPosition(t *testing.T) { // when range [7, 10] expect orders at prices (7, 8, 9) // there will be no order at price 10 since that is the pools fair-price and it quotes +/-1 eitherside ensurePositionN(t, p.pos, 0, num.UintZero(), 2) - buys, sells := p.pool.OrderbookShape(low, base, nil) - assertOrderPrices(t, buys, types.SideBuy, 7, 9) - assert.Equal(t, 0, len(sells)) + r := p.pool.OrderbookShape(low, base, nil) + + assertOrderPrices(t, r.Buys, types.SideBuy, 7, 9) + assert.Equal(t, 0, len(r.Sells)) // when range [7, 9] expect orders at prices (7, 8, 9) ensurePositionN(t, p.pos, 0, num.UintZero(), 2) - buys, sells = p.pool.OrderbookShape(low, num.NewUint(9), nil) - assertOrderPrices(t, buys, types.SideBuy, 7, 9) - assert.Equal(t, 0, len(sells)) + r = p.pool.OrderbookShape(low, num.NewUint(9), nil) + assertOrderPrices(t, r.Buys, types.SideBuy, 7, 9) + assert.Equal(t, 0, len(r.Sells)) // when range [10, 13] expect orders at prices (11, 12, 13) // there will be no order at price 10 since that is the pools fair-price and it quotes +/-1 eitherside ensurePositionN(t, p.pos, 0, num.UintZero(), 2) - buys, sells = p.pool.OrderbookShape(base, high, nil) - assert.Equal(t, 0, len(buys)) - assertOrderPrices(t, sells, types.SideSell, 11, 13) + r = p.pool.OrderbookShape(base, high, nil) + assert.Equal(t, 0, len(r.Buys)) + assertOrderPrices(t, r.Sells, types.SideSell, 11, 13) // when range [11, 13] expect orders at prices (11, 12, 13) ensurePositionN(t, p.pos, 0, num.UintZero(), 2) - buys, sells = p.pool.OrderbookShape(num.NewUint(11), high, nil) - assert.Equal(t, 0, len(buys)) - assertOrderPrices(t, sells, types.SideSell, 11, 13) + r = p.pool.OrderbookShape(num.NewUint(11), high, nil) + assert.Equal(t, 0, len(r.Buys)) + assertOrderPrices(t, r.Sells, types.SideSell, 11, 13) // whole range from [7, 10] will have buys (7, 8, 9) and sells (11, 12, 13) ensurePositionN(t, p.pos, 0, num.UintZero(), 2) - buys, sells = p.pool.OrderbookShape(low, high, nil) - assertOrderPrices(t, buys, types.SideBuy, 7, 9) - assertOrderPrices(t, sells, types.SideSell, 11, 13) + r = p.pool.OrderbookShape(low, high, nil) + assertOrderPrices(t, r.Buys, types.SideBuy, 7, 9) + assertOrderPrices(t, r.Sells, types.SideSell, 11, 13) // mid both curves spanning buys and sells, range from [8, 12] will have buys (8, 9) and sells (11, 12) ensurePositionN(t, p.pos, 0, num.UintZero(), 2) - buys, sells = p.pool.OrderbookShape(num.NewUint(8), num.NewUint(12), nil) - assertOrderPrices(t, buys, types.SideBuy, 8, 9) - assertOrderPrices(t, sells, types.SideSell, 11, 12) + r = p.pool.OrderbookShape(num.NewUint(8), num.NewUint(12), nil) + assertOrderPrices(t, r.Buys, types.SideBuy, 8, 9) + assertOrderPrices(t, r.Sells, types.SideSell, 11, 12) // range (8, 8) should return a single buy order at price 8, which is a bit counter intuitive ensurePositionN(t, p.pos, 0, num.UintZero(), 2) - buys, sells = p.pool.OrderbookShape(num.NewUint(8), num.NewUint(8), nil) - assertOrderPrices(t, buys, types.SideBuy, 8, 8) - assert.Equal(t, 0, len(sells)) + r = p.pool.OrderbookShape(num.NewUint(8), num.NewUint(8), nil) + assertOrderPrices(t, r.Buys, types.SideBuy, 8, 8) + assert.Equal(t, 0, len(r.Sells)) // range (10, 10) should return only the orders at the fair-price, which is 0 orders ensurePositionN(t, p.pos, 0, num.UintZero(), 2) - buys, sells = p.pool.OrderbookShape(num.NewUint(10), num.NewUint(10), nil) - assert.Equal(t, 0, len(buys)) - assert.Equal(t, 0, len(sells)) + r = p.pool.OrderbookShape(num.NewUint(10), num.NewUint(10), nil) + assert.Equal(t, 0, len(r.Buys)) + assert.Equal(t, 0, len(r.Sells)) } func testOrderbookShapeLong(t *testing.T) { @@ -115,27 +116,27 @@ func testOrderbookShapeLong(t *testing.T) { // range [7, 10] with have buy order (7) and sell orders (9, 10) ensurePositionN(t, p.pos, position, num.UintZero(), 2) - buys, sells := p.pool.OrderbookShape(low, base, nil) - assertOrderPrices(t, buys, types.SideBuy, 7, 7) - assertOrderPrices(t, sells, types.SideSell, 9, 10) + r := p.pool.OrderbookShape(low, base, nil) + assertOrderPrices(t, r.Buys, types.SideBuy, 7, 7) + assertOrderPrices(t, r.Sells, types.SideSell, 9, 10) // range [10, 13] with have sell orders (10, 11, 12, 13) ensurePositionN(t, p.pos, position, num.UintZero(), 2) - buys, sells = p.pool.OrderbookShape(base, high, nil) - assert.Equal(t, 0, len(buys)) - assertOrderPrices(t, sells, types.SideSell, 10, 13) + r = p.pool.OrderbookShape(base, high, nil) + assert.Equal(t, 0, len(r.Buys)) + assertOrderPrices(t, r.Sells, types.SideSell, 10, 13) // whole range will have buys at (7) and sells at (9, 10, 11, 12, 13) ensurePositionN(t, p.pos, position, num.UintZero(), 2) - buys, sells = p.pool.OrderbookShape(low, high, nil) - assertOrderPrices(t, buys, types.SideBuy, 7, 7) - assertOrderPrices(t, sells, types.SideSell, 9, 13) + r = p.pool.OrderbookShape(low, high, nil) + assertOrderPrices(t, r.Buys, types.SideBuy, 7, 7) + assertOrderPrices(t, r.Sells, types.SideSell, 9, 13) // query at fair price returns no orders ensurePositionN(t, p.pos, position, num.UintZero(), 2) - buys, sells = p.pool.OrderbookShape(num.NewUint(8), num.NewUint(8), nil) - assert.Equal(t, 0, len(buys)) - assert.Equal(t, 0, len(sells)) + r = p.pool.OrderbookShape(num.NewUint(8), num.NewUint(8), nil) + assert.Equal(t, 0, len(r.Buys)) + assert.Equal(t, 0, len(r.Sells)) } func testOrderbookShapeShort(t *testing.T) { @@ -153,27 +154,27 @@ func testOrderbookShapeShort(t *testing.T) { // range [7, 10] with have buy order (7,8,9,10) ensurePositionN(t, p.pos, position, num.UintZero(), 2) - buys, sells := p.pool.OrderbookShape(low, base, nil) - assertOrderPrices(t, buys, types.SideBuy, 7, 10) - assert.Equal(t, 0, len(sells)) + r := p.pool.OrderbookShape(low, base, nil) + assertOrderPrices(t, r.Buys, types.SideBuy, 7, 10) + assert.Equal(t, 0, len(r.Sells)) // range [10, 13] with have buy orders (10, 11) and sell orders (13) ensurePositionN(t, p.pos, position, num.UintZero(), 2) - buys, sells = p.pool.OrderbookShape(base, high, nil) - assertOrderPrices(t, buys, types.SideBuy, 10, 11) - assertOrderPrices(t, sells, types.SideSell, 13, 13) + r = p.pool.OrderbookShape(base, high, nil) + assertOrderPrices(t, r.Buys, types.SideBuy, 10, 11) + assertOrderPrices(t, r.Sells, types.SideSell, 13, 13) // whole range will have buys at (7,8,9,10,11) and sells at (13) ensurePositionN(t, p.pos, position, num.UintZero(), 2) - buys, sells = p.pool.OrderbookShape(low, high, nil) - assertOrderPrices(t, buys, types.SideBuy, 7, 11) - assertOrderPrices(t, sells, types.SideSell, 13, 13) + r = p.pool.OrderbookShape(low, high, nil) + assertOrderPrices(t, r.Buys, types.SideBuy, 7, 11) + assertOrderPrices(t, r.Sells, types.SideSell, 13, 13) // query at fair price returns no orders ensurePositionN(t, p.pos, position, num.UintZero(), 2) - buys, sells = p.pool.OrderbookShape(num.NewUint(12), num.NewUint(12), nil) - assert.Equal(t, 0, len(buys)) - assert.Equal(t, 0, len(sells)) + r = p.pool.OrderbookShape(num.NewUint(12), num.NewUint(12), nil) + assert.Equal(t, 0, len(r.Buys)) + assert.Equal(t, 0, len(r.Sells)) } func testOrderbookShapeLimited(t *testing.T) { @@ -189,19 +190,19 @@ func testOrderbookShapeLimited(t *testing.T) { p.pool.maxCalculationLevels = num.NewUint(10) ensurePositionN(t, p.pos, position, num.UintZero(), 2) - buys, sells := p.pool.OrderbookShape(low, base, nil) - assert.Equal(t, 11, len(buys)) - assert.Equal(t, 0, len(sells)) + r := p.pool.OrderbookShape(low, base, nil) + assert.Equal(t, 11, len(r.Buys)) + assert.Equal(t, 0, len(r.Sells)) ensurePositionN(t, p.pos, position, num.UintZero(), 2) - buys, sells = p.pool.OrderbookShape(base, high, nil) - assert.Equal(t, 0, len(buys)) - assert.Equal(t, 11, len(sells)) + r = p.pool.OrderbookShape(base, high, nil) + assert.Equal(t, 0, len(r.Buys)) + assert.Equal(t, 11, len(r.Sells)) ensurePositionN(t, p.pos, position, num.UintZero(), 2) - buys, sells = p.pool.OrderbookShape(low, high, nil) - assert.Equal(t, 6, len(buys)) - assert.Equal(t, 6, len(sells)) + r = p.pool.OrderbookShape(low, high, nil) + assert.Equal(t, 6, len(r.Buys)) + assert.Equal(t, 6, len(r.Sells)) } func testOrderbookShapeStepOverFairPrice(t *testing.T) { @@ -222,19 +223,19 @@ func testOrderbookShapeStepOverFairPrice(t *testing.T) { require.Equal(t, "26", p.pool.FairPrice().String()) ensurePositionN(t, p.pos, position, num.UintZero(), 2) - buys, sells := p.pool.OrderbookShape(low, base, nil) - assert.Equal(t, 4, len(buys)) - assert.Equal(t, 8, len(sells)) + r := p.pool.OrderbookShape(low, base, nil) + assert.Equal(t, 4, len(r.Buys)) + assert.Equal(t, 8, len(r.Sells)) ensurePositionN(t, p.pos, position, num.UintZero(), 2) - buys, sells = p.pool.OrderbookShape(base, high, nil) - assert.Equal(t, 0, len(buys)) - assert.Equal(t, 12, len(sells)) + r = p.pool.OrderbookShape(base, high, nil) + assert.Equal(t, 0, len(r.Buys)) + assert.Equal(t, 12, len(r.Sells)) ensurePositionN(t, p.pos, position, num.UintZero(), 2) - buys, sells = p.pool.OrderbookShape(low, high, nil) - assert.Equal(t, 3, len(buys)) - assert.Equal(t, 10, len(sells)) + r = p.pool.OrderbookShape(low, high, nil) + assert.Equal(t, 3, len(r.Buys)) + assert.Equal(t, 10, len(r.Sells)) } func testOrderbookShapeNoStepOverFairPrice(t *testing.T) { @@ -249,19 +250,19 @@ func testOrderbookShapeNoStepOverFairPrice(t *testing.T) { p.pool.maxCalculationLevels = num.NewUint(6) ensurePositionN(t, p.pos, position, num.UintZero(), 2) - buys, sells := p.pool.OrderbookShape(low, base, nil) - assert.Equal(t, 7, len(buys)) - assert.Equal(t, 0, len(sells)) + r := p.pool.OrderbookShape(low, base, nil) + assert.Equal(t, 7, len(r.Buys)) + assert.Equal(t, 0, len(r.Sells)) ensurePositionN(t, p.pos, position, num.UintZero(), 2) - buys, sells = p.pool.OrderbookShape(base, high, nil) - assert.Equal(t, 0, len(buys)) - assert.Equal(t, 7, len(sells)) + r = p.pool.OrderbookShape(base, high, nil) + assert.Equal(t, 0, len(r.Buys)) + assert.Equal(t, 7, len(r.Sells)) ensurePositionN(t, p.pos, position, num.UintZero(), 2) - buys, sells = p.pool.OrderbookShape(low, high, nil) - assert.Equal(t, 4, len(buys)) - assert.Equal(t, 4, len(sells)) + r = p.pool.OrderbookShape(low, high, nil) + assert.Equal(t, 4, len(r.Buys)) + assert.Equal(t, 4, len(r.Sells)) } func testOrderbookShapeReduceOnly(t *testing.T) { @@ -278,9 +279,9 @@ func testOrderbookShapeReduceOnly(t *testing.T) { // AMM is position 0 it will have no orders position := int64(0) ensurePositionN(t, p.pos, position, num.UintZero(), 2) - buys, sells := p.pool.OrderbookShape(low, base, nil) - assert.Equal(t, 0, len(buys)) - assert.Equal(t, 0, len(sells)) + r := p.pool.OrderbookShape(low, base, nil) + assert.Equal(t, 0, len(r.Buys)) + assert.Equal(t, 0, len(r.Sells)) // AMM is long and will have a fair-price of 8 and so will only have orders from 8 -> base position = int64(17980) @@ -289,9 +290,9 @@ func testOrderbookShapeReduceOnly(t *testing.T) { // range [7, 13] will have only sellf orders (9, 10) ensurePositionN(t, p.pos, position, num.UintZero(), 2) - buys, sells = p.pool.OrderbookShape(low, high, nil) - assert.Equal(t, 0, len(buys)) - assertOrderPrices(t, sells, types.SideSell, 9, 10) + r = p.pool.OrderbookShape(low, high, nil) + assert.Equal(t, 0, len(r.Buys)) + assertOrderPrices(t, r.Sells, types.SideSell, 9, 10) // AMM is short and will have a fair-price of 12 position = int64(-20000) @@ -300,9 +301,9 @@ func testOrderbookShapeReduceOnly(t *testing.T) { // range [10, 13] with have buy orders (10, 11) ensurePositionN(t, p.pos, position, num.UintZero(), 2) - buys, sells = p.pool.OrderbookShape(base, high, nil) - assertOrderPrices(t, buys, types.SideBuy, 10, 11) - assert.Equal(t, 0, len(sells)) + r = p.pool.OrderbookShape(base, high, nil) + assertOrderPrices(t, r.Buys, types.SideBuy, 10, 11) + assert.Equal(t, 0, len(r.Sells)) } func testOrderbookShapeBoundaryOrder(t *testing.T) { @@ -318,14 +319,14 @@ func testOrderbookShapeBoundaryOrder(t *testing.T) { // limit the number of orders in the expansion p.pool.maxCalculationLevels = num.NewUint(5) - buys, sells := p.pool.OrderbookShape(midlow, midhigh, nil) - assert.Equal(t, 4, len(buys)) - assert.Equal(t, 4, len(sells)) + r := p.pool.OrderbookShape(midlow, midhigh, nil) + assert.Equal(t, 4, len(r.Buys)) + assert.Equal(t, 4, len(r.Sells)) // we're in approximate mode but we still require an exact order at the boundaries of the shape range // check that the price for the first by is midlow, and the last sell is midhigh - assert.Equal(t, midlow.String(), buys[0].Price.String()) - assert.Equal(t, midhigh.String(), sells[(len(sells)-1)].Price.String()) + assert.Equal(t, midlow.String(), r.Buys[0].Price.String()) + assert.Equal(t, midhigh.String(), r.Sells[(len(r.Sells)-1)].Price.String()) } func testOrderbookSubTick(t *testing.T) { @@ -357,12 +358,12 @@ func testOrderbookSubTick(t *testing.T) { // region of 1000 -> 1383 from := num.NewUint(1000) to := num.NewUint(1383) - buys, sells := p.pool.OrderbookShape(from, to, nil) + r := p.pool.OrderbookShape(from, to, nil) - assert.Equal(t, 4, len(buys)) - assert.Equal(t, bp.String(), buys[3].Price.String()) + assert.Equal(t, 4, len(r.Buys)) + assert.Equal(t, bp.String(), r.Buys[3].Price.String()) - assert.Equal(t, 0, len(sells)) + assert.Equal(t, 0, len(r.Sells)) } func testClosingCloseToBase(t *testing.T) { @@ -391,35 +392,35 @@ func testClosingCloseToBase(t *testing.T) { // region of 1000 -> 1383 from := num.NewUint(1000) to := num.NewUint(2000) - buys, sells := p.pool.OrderbookShape(from, to, nil) + r := p.pool.OrderbookShape(from, to, nil) // should have one sell of volume 1 - assert.Equal(t, 0, len(buys)) - assert.Equal(t, 1, len(sells)) - assert.Equal(t, 1, int(sells[0].Size)) - assert.Equal(t, "14", sells[0].OriginalPrice.String()) + assert.Equal(t, 0, len(r.Buys)) + assert.Equal(t, 1, len(r.Sells)) + assert.Equal(t, 1, int(r.Sells[0].Size)) + assert.Equal(t, "14", r.Sells[0].OriginalPrice.String()) // and it is short one position = int64(-1) ensurePositionN(t, p.pos, position, num.UintZero(), 2) - buys, sells = p.pool.OrderbookShape(from, to, nil) + r = p.pool.OrderbookShape(from, to, nil) // should have one sell of volume 1 - assert.Equal(t, 1, len(buys)) - assert.Equal(t, 0, len(sells)) - assert.Equal(t, 1, int(buys[0].Size)) - assert.Equal(t, "16", buys[0].OriginalPrice.String()) + assert.Equal(t, 1, len(r.Buys)) + assert.Equal(t, 0, len(r.Sells)) + assert.Equal(t, 1, int(r.Buys[0].Size)) + assert.Equal(t, "16", r.Buys[0].OriginalPrice.String()) // no position position = int64(0) ensurePositionN(t, p.pos, position, num.UintZero(), 2) - buys, sells = p.pool.OrderbookShape(from, to, nil) + r = p.pool.OrderbookShape(from, to, nil) // should have one sell of volume 1 - assert.Equal(t, 0, len(buys)) - assert.Equal(t, 0, len(sells)) + assert.Equal(t, 0, len(r.Buys)) + assert.Equal(t, 0, len(r.Sells)) } func testPointExpansionAtFairPrice(t *testing.T) { @@ -430,9 +431,9 @@ func testPointExpansionAtFairPrice(t *testing.T) { // range [10, 10] fair price is 10, no orders ensurePositionN(t, p.pos, 0, num.UintZero(), 2) - buys, sells := p.pool.OrderbookShape(base, base, nil) - assert.Equal(t, 0, len(buys)) - assert.Equal(t, 0, len(sells)) + r := p.pool.OrderbookShape(base, base, nil) + assert.Equal(t, 0, len(r.Buys)) + assert.Equal(t, 0, len(r.Sells)) // now try with a one sided curve where the input range shrinks to a point-expansion p = newTestPoolWithRanges(t, num.NewUint(7), num.NewUint(10), nil) @@ -440,9 +441,9 @@ func testPointExpansionAtFairPrice(t *testing.T) { // range [10, 1000] but sell curve is empty so effective range is [10, 10] at fair-price ensurePositionN(t, p.pos, 0, num.UintZero(), 2) - buys, sells = p.pool.OrderbookShape(base, num.NewUint(1000), nil) - assert.Equal(t, 0, len(buys)) - assert.Equal(t, 0, len(sells)) + r = p.pool.OrderbookShape(base, num.NewUint(1000), nil) + assert.Equal(t, 0, len(r.Buys)) + assert.Equal(t, 0, len(r.Sells)) // now try with a one sided curve where the input range shrinks to a point-expansion p = newTestPoolWithRanges(t, nil, num.NewUint(10), num.NewUint(13)) @@ -450,9 +451,9 @@ func testPointExpansionAtFairPrice(t *testing.T) { // range [1, 10] but buy curve is empty so effective range is [10, 10] at fair-price ensurePositionN(t, p.pos, 0, num.UintZero(), 2) - buys, sells = p.pool.OrderbookShape(num.NewUint(1), base, nil) - assert.Equal(t, 0, len(buys)) - assert.Equal(t, 0, len(sells)) + r = p.pool.OrderbookShape(num.NewUint(1), base, nil) + assert.Equal(t, 0, len(r.Buys)) + assert.Equal(t, 0, len(r.Sells)) } func TestOrderbookShapeSparseAMM(t *testing.T) { @@ -467,56 +468,56 @@ func TestOrderbookShapeSparseAMM(t *testing.T) { // when range [99, 198] expect orders at ticks of 9 from 99 -> 189 // there will be no order at price 200 since that is the pools fair-price and it quotes +/-1 eitherside ensurePositionN(t, p.pos, 0, num.UintZero(), 2) - buys, sells := p.pool.OrderbookShape(low, base, nil) - assert.Equal(t, 14, len(buys)) - assert.Equal(t, 0, len(sells)) + r := p.pool.OrderbookShape(low, base, nil) + assert.Equal(t, 14, len(r.Buys)) + assert.Equal(t, 0, len(r.Sells)) // check boundary orders - assert.Equal(t, "99", buys[0].Price.String()) - assert.Equal(t, "191", buys[(len(buys)-1)].Price.String()) + assert.Equal(t, "99", r.Buys[0].Price.String()) + assert.Equal(t, "191", r.Buys[(len(r.Buys)-1)].Price.String()) // when range [99, 191] expect orders at prices 100 -> 191 ensurePositionN(t, p.pos, 0, num.UintZero(), 2) - buys, sells = p.pool.OrderbookShape(low, num.NewUint(189), nil) - assert.Equal(t, 14, len(buys)) - assert.Equal(t, 0, len(sells)) + r = p.pool.OrderbookShape(low, num.NewUint(189), nil) + assert.Equal(t, 14, len(r.Buys)) + assert.Equal(t, 0, len(r.Sells)) // check boundary orders - assert.Equal(t, "99", buys[0].Price.String()) - assert.Equal(t, "189", buys[(len(buys)-1)].Price.String()) + assert.Equal(t, "99", r.Buys[0].Price.String()) + assert.Equal(t, "189", r.Buys[(len(r.Buys)-1)].Price.String()) // when range [198, 297] expect orders at prices 207 -> 297 // there will be no order at price 198 since that is the pools fair-price and it quotes +/-1 eitherside ensurePositionN(t, p.pos, 0, num.UintZero(), 2) - buys, sells = p.pool.OrderbookShape(base, high, nil) - assert.Equal(t, 0, len(buys)) - assert.Equal(t, 12, len(sells)) + r = p.pool.OrderbookShape(base, high, nil) + assert.Equal(t, 0, len(r.Buys)) + assert.Equal(t, 12, len(r.Sells)) // check boundary orders - assert.Equal(t, "203", sells[0].Price.String()) - assert.Equal(t, "297", sells[(len(sells)-1)].Price.String()) + assert.Equal(t, "203", r.Sells[0].Price.String()) + assert.Equal(t, "297", r.Sells[(len(r.Sells)-1)].Price.String()) // when range [207, 297] expect orders at prices 207 -> 297 ensurePositionN(t, p.pos, 0, num.UintZero(), 2) - buys, sells = p.pool.OrderbookShape(num.NewUint(207), high, nil) - assert.Equal(t, 0, len(buys)) - assert.Equal(t, 11, len(sells)) + r = p.pool.OrderbookShape(num.NewUint(207), high, nil) + assert.Equal(t, 0, len(r.Buys)) + assert.Equal(t, 11, len(r.Sells)) // check boundary orders - assert.Equal(t, "207", sells[0].Price.String()) - assert.Equal(t, "297", sells[(len(sells)-1)].Price.String()) + assert.Equal(t, "207", r.Sells[0].Price.String()) + assert.Equal(t, "297", r.Sells[(len(r.Sells)-1)].Price.String()) // range (8, 8) should return a single buy order at price 8, which is a bit counter intuitive ensurePositionN(t, p.pos, 0, num.UintZero(), 2) - buys, sells = p.pool.OrderbookShape(num.NewUint(117), num.NewUint(117), nil) - assertOrderPrices(t, buys, types.SideBuy, 117, 117) - assert.Equal(t, 0, len(sells)) + r = p.pool.OrderbookShape(num.NewUint(117), num.NewUint(117), nil) + assertOrderPrices(t, r.Buys, types.SideBuy, 117, 117) + assert.Equal(t, 0, len(r.Sells)) // range (10, 10) should return only the orders at the fair-price, which is 0 orders ensurePositionN(t, p.pos, 0, num.UintZero(), 2) - buys, sells = p.pool.OrderbookShape(num.NewUint(198), num.NewUint(198), nil) - assert.Equal(t, 0, len(buys)) - assert.Equal(t, 0, len(sells)) + r = p.pool.OrderbookShape(num.NewUint(198), num.NewUint(198), nil) + assert.Equal(t, 0, len(r.Buys)) + assert.Equal(t, 0, len(r.Sells)) } func TestOrderbookShapeSparseAMMBoundaryOrders(t *testing.T) { @@ -539,12 +540,12 @@ func TestOrderbookShapeSparseAMMBoundaryOrders(t *testing.T) { defer p.ctrl.Finish() ensurePositionN(t, p.pos, 0, nil, -1) - buys, sells := p.pool.OrderbookShape( + r := p.pool.OrderbookShape( num.MustUintFromString("1958381716019393098944", 10), num.MustUintFromString("4897350000000000000000", 10), nil, ) - assert.Equal(t, 0, len(buys)) - require.Equal(t, "1958381716019393098944", sells[0].Price.String()) - require.Equal(t, "2461090000000000000000", sells[len(sells)-1].Price.String()) + assert.Equal(t, 0, len(r.Buys)) + require.Equal(t, "1958381716019393098944", r.Sells[0].Price.String()) + require.Equal(t, "2461090000000000000000", r.Sells[len(r.Sells)-1].Price.String()) } diff --git a/core/execution/common/interfaces.go b/core/execution/common/interfaces.go index ec75126ec10..9e16e6c2d71 100644 --- a/core/execution/common/interfaces.go +++ b/core/execution/common/interfaces.go @@ -339,7 +339,7 @@ type EquityLikeShares interface { } type AMMPool interface { - OrderbookShape(from, to *num.Uint, idgen *idgeneration.IDGenerator) ([]*types.Order, []*types.Order) + OrderbookShape(from, to *num.Uint, idgen *idgeneration.IDGenerator) *types.OrderbookShapeResult LiquidityFee() num.Decimal CommitmentAmount() *num.Uint } diff --git a/core/execution/common/liquidity_provision_fees.go b/core/execution/common/liquidity_provision_fees.go index f7e53b2c780..b876dd6dc55 100644 --- a/core/execution/common/liquidity_provision_fees.go +++ b/core/execution/common/liquidity_provision_fees.go @@ -159,13 +159,14 @@ func (m *MarketLiquidity) updateAMMCommitment(count int64) { pools := m.amm.GetAMMPoolsBySubAccount() for ammParty, pool := range pools { - buy, sell := pool.OrderbookShape(minP, maxP, nil) + r := pool.OrderbookShape(minP, maxP, nil) + buyTotal, sellTotal := num.UintZero(), num.UintZero() - for _, b := range buy { + for _, b := range r.Buys { size := num.UintFromUint64(b.Size) buyTotal.AddSum(size.Mul(size, b.Price)) } - for _, s := range sell { + for _, s := range r.Sells { size := num.UintFromUint64(s.Size) sellTotal.AddSum(size.Mul(size, s.Price)) } @@ -183,7 +184,7 @@ func (m *MarketLiquidity) updateAMMCommitment(count int64) { score := as.score if !skipScore { bb, ba := num.DecimalFromUint(bestB), num.DecimalFromUint(bestA) - score = m.liquidityEngine.GetPartyLiquidityScore(append(buy, sell...), bb, ba, minP, maxP) + score = m.liquidityEngine.GetPartyLiquidityScore(append(r.Buys, r.Sells...), bb, ba, minP, maxP) } // set the stake and score diff --git a/core/execution/common/liquidity_provision_snapshot_test.go b/core/execution/common/liquidity_provision_snapshot_test.go index b70f9e3a42f..9a22b19d080 100644 --- a/core/execution/common/liquidity_provision_snapshot_test.go +++ b/core/execution/common/liquidity_provision_snapshot_test.go @@ -83,8 +83,8 @@ func TestAMMStateSnapshot(t *testing.T) { type dummyAMM struct{} -func (d dummyAMM) OrderbookShape(from, to *num.Uint, idgen *idgeneration.IDGenerator) ([]*types.Order, []*types.Order) { - return nil, nil +func (d dummyAMM) OrderbookShape(from, to *num.Uint, idgen *idgeneration.IDGenerator) *types.OrderbookShapeResult { + return &types.OrderbookShapeResult{} } func (d dummyAMM) LiquidityFee() num.Decimal { diff --git a/core/execution/common/mocks_amm/mocks.go b/core/execution/common/mocks_amm/mocks.go index 84b9ca52ae0..4da3383b272 100644 --- a/core/execution/common/mocks_amm/mocks.go +++ b/core/execution/common/mocks_amm/mocks.go @@ -67,12 +67,11 @@ func (mr *MockAMMPoolMockRecorder) LiquidityFee() *gomock.Call { } // OrderbookShape mocks base method. -func (m *MockAMMPool) OrderbookShape(arg0, arg1 *num.Uint, arg2 *idgeneration.IDGenerator) ([]*types.Order, []*types.Order) { +func (m *MockAMMPool) OrderbookShape(arg0, arg1 *num.Uint, arg2 *idgeneration.IDGenerator) *types.OrderbookShapeResult { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "OrderbookShape", arg0, arg1, arg2) - ret0, _ := ret[0].([]*types.Order) - ret1, _ := ret[1].([]*types.Order) - return ret0, ret1 + ret0, _ := ret[0].(*types.OrderbookShapeResult) + return ret0 } // OrderbookShape indicates an expected call of OrderbookShape. diff --git a/core/integration/features/amm/0090-VAMM-006-014.feature b/core/integration/features/amm/0090-VAMM-006-014.feature index 09a15cdff98..49ed03f993f 100644 --- a/core/integration/features/amm/0090-VAMM-006-014.feature +++ b/core/integration/features/amm/0090-VAMM-006-014.feature @@ -432,7 +432,7 @@ Feature: Ensure the vAMM positions follow the market correctly | party5 | 130 | 0 | 0 | | | vamm1-id | 0 | 0 | -257 | true | - @VAMM3 + @VAMM 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: diff --git a/core/integration/features/amm/0090-VAMM-auction-refine.feature b/core/integration/features/amm/0090-VAMM-auction-refine.feature new file mode 100644 index 00000000000..b393304fe90 --- /dev/null +++ b/core/integration/features/amm/0090-VAMM-auction-refine.feature @@ -0,0 +1,160 @@ +Feature: vAMM rebasing when created or amended + + Background: + Given the average block duration is "1" + And the margin calculator named "margin-calculator-1": + | search factor | initial factor | release factor | + | 1.2 | 1.5 | 1.7 | + And the simple risk model named "my-simple-risk-model": + | long | short | max move up | min move down | probability of trading | + | 0.00984363574304481 | 0.009937604878885509 | -1 | -1 | 0.2 | + And the liquidity monitoring parameters: + | name | triggering ratio | time window | scaling factor | + | lqm-params | 1.00 | 20s | 1 | + + And the following network parameters are set: + | name | value | + | market.value.windowLength | 60s | + | network.markPriceUpdateMaximumFrequency | 0s | + | limits.markets.maxPeggedOrders | 6 | + | market.auction.minimumDuration | 1 | + | market.fee.factors.infrastructureFee | 0.001 | + | market.fee.factors.makerFee | 0.004 | + | spam.protection.max.stopOrdersPerMarket | 5 | + | market.liquidity.equityLikeShareFeeFraction | 1 | + | market.amm.minCommitmentQuantum | 1 | + | market.liquidity.bondPenaltyParameter | 0.2 | + | market.liquidity.stakeToCcyVolume | 1 | + | market.liquidity.successorLaunchWindowLength | 1h | + | market.liquidity.sla.nonPerformanceBondPenaltySlope | 0 | + | market.liquidity.sla.nonPerformanceBondPenaltyMax | 0.6 | + | validators.epoch.length | 10s | + | market.liquidity.earlyExitPenalty | 0.25 | + | market.liquidity.maximumLiquidityFeeFactorLevel | 0.25 | + + And the following assets are registered: + | id | decimal places | + | USD | 18 | + And the fees configuration named "fees-config-1": + | maker fee | infrastructure fee | + | 0.0004 | 0.001 | + + And the liquidity sla params named "SLA-22": + | price range | commitment min time fraction | performance hysteresis epochs | sla competition factor | + | 0.5 | 0.6 | 1 | 1.0 | + + 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 | position decimal places | decimal places | allowed empty amm levels | linear slippage factor | + | ETH/MAR22 | USD | USD | lqm-params | my-simple-risk-model | margin-calculator-1 | 2 | fees-config-1 | default-none | default-eth-for-future | 1e0 | 0 | SLA-22 | 3 | 2 | 100 | 0.001 | + + # 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: + | party | asset | amount | + | lp1 | USD | 1000000000000000000000000 | + | lp2 | USD | 1000000000000000000000000 | + | lp3 | USD | 1000000000000000000000000 | + | party1 | USD | 1000000000000000000000000 | + | party2 | USD | 1000000000000000000000000 | + | party3 | USD | 1000000000000000000000000 | + | party4 | USD | 1000000000000000000000000 | + | party5 | USD | 1000000000000000000000000 | + | vamm1 | USD | 1000000000000000000000000 | + | vamm2 | USD | 1000000000000000000000000 | + | vamm3 | USD | 1000000000000000000000000 | + | vamm4 | USD | 1000000000000000000000000 | + + @VAMM @NoPerp + Scenario: two crossed AMMs at opening auction end where orderbook shape refining is required + + # 39.98867525723519 11.328128751075417 + + Then the parties submit the following AMM: + | party | market id | amount | slippage | base | upper bound | proposed fee | lower leverage | upper leverage | + | vamm1 | ETH/MAR22 | 2852341107003410000000 | 0.05 | 218564 | 281710 | 0.03 | 39.98867525723519 | 11.328128751075417 | + Then the AMM pool status should be: + | party | market id | amount | status | base | upper bound | lower leverage | upper leverage | + | vamm1 | ETH/MAR22 | 2852341107003410000000 | STATUS_ACTIVE | 218564 | 281710 | 39.98867525723519 | 11.328128751075417 | + + 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 | lower leverage | upper leverage | + | vamm2 | ETH/MAR22 | 8514633449978613000000 | 0.05 | 372056 | 172861 | 452663 | 0.03 | 87.09361695065867 | 92.95166117996257 | + Then the AMM pool status should be: + | party | market id | amount | status | base | lower bound | upper bound |lower leverage | upper leverage | + | vamm2 | ETH/MAR22 | 8514633449978613000000 | STATUS_ACTIVE | 372056 | 172861 | 452663 | 87.09361695065867 | 92.95166117996257 | + + + And set the following AMM sub account aliases: + | party | market id | alias | + | vamm1 | ETH/MAR22 | vamm1-id | + | vamm2 | ETH/MAR22 | vamm2-id | + + + + And the parties place the following orders: + | party | market id | side | volume | price | resulting trades | type | tif | + | party1 | ETH/MAR22 | buy | 1 | 2 | 0 | TYPE_LIMIT | TIF_GTC | + | party1 | ETH/MAR22 | buy | 1 | 2 | 0 | TYPE_LIMIT | TIF_GTC | + | party2 | ETH/MAR22 | buy | 26 | 398908 | 0 | TYPE_LIMIT | TIF_GTC | + | party2 | ETH/MAR22 | buy | 20 | 400384 | 0 | TYPE_LIMIT | TIF_GTC | + | party3 | ETH/MAR22 | buy | 36825 | 400600 | 0 | TYPE_LIMIT | TIF_GTC | + | party3 | ETH/MAR22 | buy | 35099 | 400602 | 0 | TYPE_LIMIT | TIF_GTC | + | party3 | ETH/MAR22 | buy | 33454 | 400604 | 0 | TYPE_LIMIT | TIF_GTC | + | party3 | ETH/MAR22 | buy | 31886 | 400606 | 0 | TYPE_LIMIT | TIF_GTC | + | party3 | ETH/MAR22 | buy | 30392 | 400608 | 0 | TYPE_LIMIT | TIF_GTC | + | party3 | ETH/MAR22 | buy | 28967 | 400610 | 0 | TYPE_LIMIT | TIF_GTC | + | party3 | ETH/MAR22 | buy | 27610 | 400612 | 0 | TYPE_LIMIT | TIF_GTC | + | party3 | ETH/MAR22 | buy | 26316 | 400614 | 0 | TYPE_LIMIT | TIF_GTC | + | party2 | ETH/MAR22 | buy | 21 | 400616 | 0 | TYPE_LIMIT | TIF_GTC | + | party3 | ETH/MAR22 | buy | 25082 | 400616 | 0 | TYPE_LIMIT | TIF_GTC | + | party3 | ETH/MAR22 | buy | 23907 | 400618 | 0 | TYPE_LIMIT | TIF_GTC | + | party4 | ETH/MAR22 | buy | 250000 | 402896 | 0 | TYPE_LIMIT | TIF_GTC | + | party4 | ETH/MAR22 | buy | 250000 | 402904 | 0 | TYPE_LIMIT | TIF_GTC | + | party4 | ETH/MAR22 | buy | 250000 | 403052 | 0 | TYPE_LIMIT | TIF_GTC | + | party4 | ETH/MAR22 | buy | 250000 | 403682 | 0 | TYPE_LIMIT | TIF_GTC | + | party4 | ETH/MAR22 | buy | 250000 | 403916 | 0 | TYPE_LIMIT | TIF_GTC | + | party4 | ETH/MAR22 | buy | 250000 | 404624 | 0 | TYPE_LIMIT | TIF_GTC | + | party3 | ETH/MAR22 | sell | 36823 | 400638 | 0 | TYPE_LIMIT | TIF_GTC | + | party3 | ETH/MAR22 | sell | 35097 | 400636 | 0 | TYPE_LIMIT | TIF_GTC | + | party3 | ETH/MAR22 | sell | 33452 | 400634 | 0 | TYPE_LIMIT | TIF_GTC | + | party3 | ETH/MAR22 | sell | 31885 | 400632 | 0 | TYPE_LIMIT | TIF_GTC | + | party3 | ETH/MAR22 | sell | 30390 | 400630 | 0 | TYPE_LIMIT | TIF_GTC | + | party3 | ETH/MAR22 | sell | 28966 | 400628 | 0 | TYPE_LIMIT | TIF_GTC | + | party3 | ETH/MAR22 | sell | 27608 | 400626 | 0 | TYPE_LIMIT | TIF_GTC | + | party3 | ETH/MAR22 | sell | 26315 | 400624 | 0 | TYPE_LIMIT | TIF_GTC | + | party3 | ETH/MAR22 | sell | 25081 | 400622 | 0 | TYPE_LIMIT | TIF_GTC | + | party3 | ETH/MAR22 | sell | 23906 | 400620 | 0 | TYPE_LIMIT | TIF_GTC | + | party2 | ETH/MAR22 | sell | 25 | 399916 | 0 | TYPE_LIMIT | TIF_GTC | + | party2 | ETH/MAR22 | sell | 23 | 399688 | 0 | TYPE_LIMIT | TIF_GTC | + | party2 | ETH/MAR22 | sell | 24 | 399064 | 0 | TYPE_LIMIT | TIF_GTC | + | party5 | ETH/MAR22 | sell | 250000 | 396612 | 0 | TYPE_LIMIT | TIF_GTC | + | party5 | ETH/MAR22 | sell | 250000 | 395916 | 0 | TYPE_LIMIT | TIF_GTC | + | party5 | ETH/MAR22 | sell | 250000 | 395688 | 0 | TYPE_LIMIT | TIF_GTC | + | party5 | ETH/MAR22 | sell | 250000 | 395072 | 0 | TYPE_LIMIT | TIF_GTC | + | party5 | ETH/MAR22 | sell | 250000 | 394926 | 0 | TYPE_LIMIT | TIF_GTC | + | party5 | ETH/MAR22 | sell | 250000 | 394920 | 0 | TYPE_LIMIT | TIF_GTC | + + + # now place some pegged orders which will cause a panic if the uncrossing is crossed + When the parties place the following pegged orders: + | party | market id | side | volume | pegged reference | offset | + | lp3 | ETH/MAR22 | buy | 100 | BID | 1 | + | lp3 | ETH/MAR22 | sell | 100 | ASK | 1 | + + And the network moves ahead "1" blocks + + And the market data for the market "ETH/MAR22" should be: + | trading mode | indicative price | indicative volume | + | TRADING_MODE_OPENING_AUCTION | 400267 | 1511468 | + + When the opening auction period ends for market "ETH/MAR22" + + 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 | + | 400618 | TRADING_MODE_CONTINUOUS | diff --git a/core/integration/features/amm/0090-VAMM-auction.feature b/core/integration/features/amm/0090-VAMM-auction.feature index 408e1dd4d72..f022cf3ee18 100644 --- a/core/integration/features/amm/0090-VAMM-auction.feature +++ b/core/integration/features/amm/0090-VAMM-auction.feature @@ -550,8 +550,7 @@ Feature: vAMM rebasing when created or amended | mark price | trading mode | best bid price | best offer price | | 155 | TRADING_MODE_CONTINUOUS | 109 | 0 | - - @VAMM3 + @VAMM Scenario: Two AMMs crossed with large order expansion Then the parties submit the following AMM: diff --git a/core/matching/cached_orderbook.go b/core/matching/cached_orderbook.go index 8707e974864..35b9ee0f7c9 100644 --- a/core/matching/cached_orderbook.go +++ b/core/matching/cached_orderbook.go @@ -200,7 +200,9 @@ func (b *CachedOrderBook) GetIndicativePriceAndVolume() (*num.Uint, uint64, type volume, cachedVolOk := b.cache.GetIndicativeVolume() side, cachedSideOk := b.cache.GetIndicativeUncrossingSide() if !cachedPriceOk || !cachedVolOk || !cachedSideOk { - price, volume, side, _ = b.OrderBook.GetIndicativePriceAndVolume() + r := b.OrderBook.GetIndicativePriceAndVolume() + price, volume, side = r.price, r.volume, r.side + b.cache.SetIndicativePrice(price.Clone()) b.cache.SetIndicativeVolume(volume) b.cache.SetIndicativeUncrossingSide(side) diff --git a/core/matching/can_uncross_test.go b/core/matching/can_uncross_test.go index f07d434b293..3bca5a27fc4 100644 --- a/core/matching/can_uncross_test.go +++ b/core/matching/can_uncross_test.go @@ -80,10 +80,10 @@ func TestBidAndAskPresentAfterAuction(t *testing.T) { assert.NoError(t, err) } - indicativePrice, indicativeVolume, indicativeSide, _ := book.GetIndicativePriceAndVolume() - assert.Equal(t, indicativePrice.Uint64(), uint64(1975)) - assert.Equal(t, int(indicativeVolume), 5) - assert.Equal(t, indicativeSide, types.SideBuy) + r := book.GetIndicativePriceAndVolume() + assert.Equal(t, r.price.Uint64(), uint64(1975)) + assert.Equal(t, int(r.volume), 5) + assert.Equal(t, r.side, types.SideBuy) assert.True(t, book.BidAndAskPresentAfterAuction()) } @@ -143,9 +143,9 @@ func TestBidAndAskPresentAfterAuctionInverse(t *testing.T) { assert.NoError(t, err) } - indicativePrice, indicativeVolume, indicativeSide, _ := book.GetIndicativePriceAndVolume() - assert.Equal(t, indicativePrice.Uint64(), uint64(1950)) - assert.Equal(t, int(indicativeVolume), 5) - assert.Equal(t, indicativeSide, types.SideBuy) + r := book.GetIndicativePriceAndVolume() + assert.Equal(t, r.price.Uint64(), uint64(1950)) + assert.Equal(t, int(r.volume), 5) + assert.Equal(t, r.side, types.SideBuy) assert.True(t, book.BidAndAskPresentAfterAuction()) } diff --git a/core/matching/indicative_price_and_volume.go b/core/matching/indicative_price_and_volume.go index 1d270f1042d..f9cd6d1e9d8 100644 --- a/core/matching/indicative_price_and_volume.go +++ b/core/matching/indicative_price_and_volume.go @@ -58,8 +58,9 @@ type ipvVolume struct { } type ipvGeneratedOffbook struct { - buy []*types.Order - sell []*types.Order + buy []*types.Order + sell []*types.Order + approx bool } func (g *ipvGeneratedOffbook) add(order *types.Order) { @@ -130,39 +131,44 @@ func (ipv *IndicativePriceAndVolume) buildInitialOffbookShape(offbook OffbookSou } // expand all AMM's into orders within the crossed region and add them to the price-level cache - buys, sells := offbook.OrderbookShape(min, max, nil) + r := offbook.OrderbookShape(min, max, nil) - for i := len(buys) - 1; i >= 0; i-- { - o := buys[i] - mpl, ok := mplm[*o.Price] - if !ok { - mpl = ipvPriceLevel{price: o.Price, buypl: ipvVolume{0, 0}, sellpl: ipvVolume{0, 0}} - } - // increment the volume at this level - mpl.buypl.volume += o.Size - mpl.buypl.offbookVolume += o.Size - mplm[*o.Price] = mpl + for _, shape := range r { + buys := shape.Buys + sells := shape.Sells - if ipv.generated[o.Party] == nil { - ipv.generated[o.Party] = &ipvGeneratedOffbook{} - } - ipv.generated[o.Party].add(o) - } + for i := len(buys) - 1; i >= 0; i-- { + o := buys[i] + mpl, ok := mplm[*o.Price] + if !ok { + mpl = ipvPriceLevel{price: o.Price, buypl: ipvVolume{0, 0}, sellpl: ipvVolume{0, 0}} + } + // increment the volume at this level + mpl.buypl.volume += o.Size + mpl.buypl.offbookVolume += o.Size + mplm[*o.Price] = mpl - for _, o := range sells { - mpl, ok := mplm[*o.Price] - if !ok { - mpl = ipvPriceLevel{price: o.Price, buypl: ipvVolume{0, 0}, sellpl: ipvVolume{0, 0}} + if ipv.generated[o.Party] == nil { + ipv.generated[o.Party] = &ipvGeneratedOffbook{approx: shape.Approx} + } + ipv.generated[o.Party].add(o) } - mpl.sellpl.volume += o.Size - mpl.sellpl.offbookVolume += o.Size - mplm[*o.Price] = mpl + for _, o := range sells { + mpl, ok := mplm[*o.Price] + if !ok { + mpl = ipvPriceLevel{price: o.Price, buypl: ipvVolume{0, 0}, sellpl: ipvVolume{0, 0}} + } + + mpl.sellpl.volume += o.Size + mpl.sellpl.offbookVolume += o.Size + mplm[*o.Price] = mpl - if ipv.generated[o.Party] == nil { - ipv.generated[o.Party] = &ipvGeneratedOffbook{} + if ipv.generated[o.Party] == nil { + ipv.generated[o.Party] = &ipvGeneratedOffbook{approx: shape.Approx} + } + ipv.generated[o.Party].add(o) } - ipv.generated[o.Party].add(o) } } @@ -184,29 +190,49 @@ func (ipv *IndicativePriceAndVolume) removeOffbookShape(party string) { delete(ipv.generated, party) } -func (ipv *IndicativePriceAndVolume) addOffbookShape(party *string, minPrice, maxPrice *num.Uint) { +func (ipv *IndicativePriceAndVolume) addOffbookShape(party *string, minPrice, maxPrice *num.Uint, excludeMin, excludeMax bool) { // recalculate new orders for the shape and add the volume in - buys, sells := ipv.offbook.OrderbookShape(minPrice, maxPrice, party) + r := ipv.offbook.OrderbookShape(minPrice, maxPrice, party) - // add buys backwards so that the best-bid is first - for i := len(buys) - 1; i >= 0; i-- { - o := buys[i] - ipv.AddVolumeAtPrice(o.Price, o.Size, o.Side, true) + for _, shape := range r { + buys := shape.Buys + sells := shape.Sells - if ipv.generated[o.Party] == nil { - ipv.generated[o.Party] = &ipvGeneratedOffbook{} + if len(buys) == 0 && len(sells) == 0 { + continue } - ipv.generated[o.Party].add(o) - } - // add buys fowards so that the best-ask is first - for _, o := range sells { - ipv.AddVolumeAtPrice(o.Price, o.Size, o.Side, true) + if _, ok := ipv.generated[shape.AmmParty]; !ok { + ipv.generated[shape.AmmParty] = &ipvGeneratedOffbook{approx: shape.Approx} + } + + // add buys backwards so that the best-bid is first + for i := len(buys) - 1; i >= 0; i-- { + o := buys[i] + + if excludeMin && o.Price.EQ(minPrice) { + continue + } + if excludeMax && o.Price.EQ(maxPrice) { + continue + } + + ipv.AddVolumeAtPrice(o.Price, o.Size, o.Side, true) + ipv.generated[shape.AmmParty].add(o) + } - if ipv.generated[o.Party] == nil { - ipv.generated[o.Party] = &ipvGeneratedOffbook{} + // add buys fowards so that the best-ask is first + for _, o := range sells { + if excludeMin && o.Price.EQ(minPrice) { + continue + } + if excludeMax && o.Price.EQ(maxPrice) { + continue + } + + ipv.AddVolumeAtPrice(o.Price, o.Size, o.Side, true) + ipv.generated[shape.AmmParty].add(o) } - ipv.generated[o.Party].add(o) } } @@ -221,7 +247,7 @@ func (ipv *IndicativePriceAndVolume) updateOffbookState(minPrice, maxPrice *num. return } - ipv.addOffbookShape(nil, minPrice, maxPrice) + ipv.addOffbookShape(nil, minPrice, maxPrice, false, false) } // this will be used to build the initial set of price levels, when the auction is being started. @@ -471,7 +497,7 @@ func (ipv *IndicativePriceAndVolume) GetCumulativePriceLevels(maxPrice, minPrice return cumulativeVolumes, maxTradable } -// ExtractOffbookOrders returns the cached expanded orders of AMM's in the crossed region of the given side. These +// ExtractOffbookOrders returns the cached expanded orders of AM M's in the crossed region of the given side. These // are the order that we will send in aggressively to uncrossed the book. func (ipv *IndicativePriceAndVolume) ExtractOffbookOrders(price *num.Uint, side types.Side, target uint64) []*types.Order { if target == 0 { @@ -515,7 +541,6 @@ func (ipv *IndicativePriceAndVolume) ExtractOffbookOrders(price *num.Uint, side 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/mocks/mocks.go b/core/matching/mocks/mocks.go index 0f854d39684..8cc753a90c2 100644 --- a/core/matching/mocks/mocks.go +++ b/core/matching/mocks/mocks.go @@ -65,12 +65,11 @@ func (mr *MockOffbookSourceMockRecorder) NotifyFinished() *gomock.Call { } // OrderbookShape mocks base method. -func (m *MockOffbookSource) OrderbookShape(arg0, arg1 *num.Uint, arg2 *string) ([]*types.Order, []*types.Order) { +func (m *MockOffbookSource) OrderbookShape(arg0, arg1 *num.Uint, arg2 *string) []*types.OrderbookShapeResult { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "OrderbookShape", arg0, arg1, arg2) - ret0, _ := ret[0].([]*types.Order) - ret1, _ := ret[1].([]*types.Order) - return ret0, ret1 + ret0, _ := ret[0].([]*types.OrderbookShapeResult) + return ret0 } // OrderbookShape indicates an expected call of OrderbookShape. diff --git a/core/matching/orderbook.go b/core/matching/orderbook.go index 66bbe38242e..a7226f5695a 100644 --- a/core/matching/orderbook.go +++ b/core/matching/orderbook.go @@ -48,7 +48,7 @@ type OffbookSource interface { BestPricesAndVolumes() (*num.Uint, uint64, *num.Uint, uint64) SubmitOrder(agg *types.Order, inner, outer *num.Uint) []*types.Order NotifyFinished() - OrderbookShape(st, nd *num.Uint, id *string) ([]*types.Order, []*types.Order) + OrderbookShape(st, nd *num.Uint, id *string) []*types.OrderbookShapeResult } // OrderBook represents the book holding all orders in the system. @@ -92,6 +92,17 @@ type CumulativeVolumeLevel struct { cumulativeAskOffbook uint64 } +type ipvResult struct { + price *num.Uint // uncrossing price + side types.Side // uncrossing side + volume uint64 // uncrossing volume + offbookVolume uint64 // how much of the uncrossing volume comes from AMMs + + // volume maximising range + vmrMin *num.Uint + vmrMax *num.Uint +} + func (b *OrderBook) Hash() []byte { return crypto.Hash(append(b.buy.Hash(), b.sell.Hash()...)) } @@ -349,7 +360,8 @@ func (b *OrderBook) canUncross(requireTrades bool) bool { if buyMatch && sellMatch { return true } - _, v, _, _ := b.GetIndicativePriceAndVolume() + r := b.GetIndicativePriceAndVolume() + // no buy orders remaining on the book after uncrossing, it buyMatches exactly vol := uint64(0) if !buyMatch { @@ -362,7 +374,7 @@ func (b *OrderBook) canUncross(requireTrades bool) bool { for _, o := range l.orders { vol += o.Remaining // we've filled the uncrossing volume, and found an order that is not GFA - if vol > v && o.TimeInForce != types.OrderTimeInForceGFA { + if vol > r.volume && o.TimeInForce != types.OrderTimeInForceGFA { buyMatch = true break } @@ -387,7 +399,7 @@ func (b *OrderBook) canUncross(requireTrades bool) bool { } for _, o := range l.orders { vol += o.Remaining - if vol > v && o.TimeInForce != types.OrderTimeInForceGFA { + if vol > r.volume && o.TimeInForce != types.OrderTimeInForceGFA { sellMatch = true break } @@ -397,15 +409,50 @@ func (b *OrderBook) canUncross(requireTrades bool) bool { return sellMatch } +// GetRefinedIndicativePriceAndVolume calculates a refined ipv. This is required if an AMM was approximately expanded and steps +// over the boundaries of the volume maximising range. +func (b *OrderBook) GetRefinedIndicativePriceAndVolume(r ipvResult) ipvResult { + ipv := b.indicativePriceAndVolume + + if len(ipv.generated) == 0 { + return r + } + + for party, gen := range ipv.generated { + if !gen.approx { + // this AMM was already expanded accurately so we don't need to refine it + continue + } + + // remove the volume from this AMM + ipv.removeOffbookShape(party) + + // and add it in three parts so that we can get accurate orders within the volume-maximising range + // min-price -> min-vmr -> max-vmr -> max-price + + ipv.addOffbookShape(ptr.From(party), ipv.lastMinPrice, r.vmrMin, false, false) + + if r.vmrMin.NEQ(r.vmrMax) { + ipv.addOffbookShape(ptr.From(party), r.vmrMin, r.vmrMax, true, true) + } + + ipv.addOffbookShape(ptr.From(party), r.vmrMax, ipv.lastMaxPrice, false, false) + + ipv.needsUpdate = true + } + + return b.GetIndicativePriceAndVolume() +} + // GetIndicativePriceAndVolume Calculates the indicative price and volume of the order book without modifying the order book state. -func (b *OrderBook) GetIndicativePriceAndVolume() (retprice *num.Uint, retvol uint64, retside types.Side, offbookVolume uint64) { +func (b *OrderBook) GetIndicativePriceAndVolume() ipvResult { // Generate a set of price level pairs with their maximum tradable volumes cumulativeVolumes, maxTradableAmount, err := b.buildCumulativePriceLevels() if err != nil { if b.log.GetLevel() <= logging.DebugLevel { b.log.Debug("could not get cumulative price levels", logging.Error(err)) } - return num.UintZero(), 0, types.SideUnspecified, 0 + return ipvResult{price: num.UintZero()} } // Pull out all prices that match that volume @@ -418,8 +465,11 @@ func (b *OrderBook) GetIndicativePriceAndVolume() (retprice *num.Uint, retvol ui // get the maximum volume price from the average of the maximum and minimum tradable price levels var ( - uncrossPrice = num.UintZero() - uncrossSide types.Side + uncrossPrice = num.UintZero() + uncrossSide types.Side + offbookVolume uint64 + vmrMin *num.Uint + vmrMax *num.Uint ) if len(prices) > 0 { // uncrossPrice = (prices[len(prices)-1] + prices[0]) / 2 @@ -427,6 +477,9 @@ func (b *OrderBook) GetIndicativePriceAndVolume() (retprice *num.Uint, retvol ui num.UintZero().Add(prices[len(prices)-1], prices[0]), num.NewUint(2), ) + + vmrMin = prices[len(prices)-1] + vmrMax = prices[0] } // See which side we should fully process when we uncross @@ -446,7 +499,14 @@ func (b *OrderBook) GetIndicativePriceAndVolume() (retprice *num.Uint, retvol ui } } - return uncrossPrice, maxTradableAmount, uncrossSide, offbookVolume + return ipvResult{ + price: uncrossPrice, + volume: maxTradableAmount, + side: uncrossSide, + offbookVolume: offbookVolume, + vmrMin: vmrMin, + vmrMax: vmrMax, + } } // GetIndicativePrice Calculates the indicative price of the order book without modifying the order book state. @@ -481,10 +541,10 @@ func (b *OrderBook) GetIndicativePrice() (retprice *num.Uint) { func (b *OrderBook) GetIndicativeTrades() ([]*types.Trade, error) { // Get the uncrossing price and which side has the most volume at that price - price, volume, uncrossSide, offbookVolume := b.GetIndicativePriceAndVolume() + r := b.GetIndicativePriceAndVolume() // If we have no uncrossing price, we have nothing to do - if price.IsZero() && volume == 0 { + if r.price.IsZero() && r.volume == 0 { return nil, nil } @@ -493,21 +553,21 @@ func (b *OrderBook) GetIndicativeTrades() ([]*types.Trade, error) { uncrossingSide *OrderBookSide ) - if uncrossSide == types.SideBuy { + if r.side == types.SideBuy { uncrossingSide = b.buy } else { uncrossingSide = b.sell } // extract uncrossing orders from all AMMs - uncrossOrders = b.indicativePriceAndVolume.ExtractOffbookOrders(price, uncrossSide, offbookVolume) + uncrossOrders = b.indicativePriceAndVolume.ExtractOffbookOrders(r.price, r.side, r.offbookVolume) // the remaining volume should now come from the orderbook - volume -= offbookVolume + volume := r.volume - r.offbookVolume // Remove all the orders from that side of the book up to the given volume - uncrossOrders = append(uncrossOrders, uncrossingSide.ExtractOrders(price, volume, false)...) - opSide := b.getOppositeSide(uncrossSide) + uncrossOrders = append(uncrossOrders, uncrossingSide.ExtractOrders(r.price, volume, false)...) + opSide := b.getOppositeSide(r.side) output := make([]*types.Trade, 0, len(uncrossOrders)) trades, err := opSide.fakeUncrossAuction(uncrossOrders) if err != nil { @@ -515,7 +575,7 @@ func (b *OrderBook) GetIndicativeTrades() ([]*types.Trade, error) { } // Update all the trades to have the correct uncrossing price for _, t := range trades { - t.Price = price.Clone() + t.Price = r.price.Clone() } output = append(output, trades...) @@ -547,30 +607,34 @@ func (b *OrderBook) buildCumulativePriceLevels() ([]CumulativeVolumeLevel, uint6 // if removeOrders is set to true then matched orders get removed from the book. func (b *OrderBook) uncrossBook() ([]*types.OrderConfirmation, error) { // Get the uncrossing price and which side has the most volume at that price - price, volume, uncrossSide, offbookVolume := b.GetIndicativePriceAndVolume() + r := b.GetIndicativePriceAndVolume() // If we have no uncrossing price, we have nothing to do - if price.IsZero() && volume == 0 { + if r.price.IsZero() && r.volume == 0 { return nil, nil } + // if any offbook order step over the boundaries of the volume maximising range we need to refine the + // AMM expansion in this region to get a more accurate uncrossing volume + r = b.GetRefinedIndicativePriceAndVolume(r) + var uncrossingSide *OrderBookSide - if uncrossSide == types.SideBuy { + if r.side == types.SideBuy { uncrossingSide = b.buy } else { uncrossingSide = b.sell } // extract uncrossing orders from all AMMs - uncrossOrders := b.indicativePriceAndVolume.ExtractOffbookOrders(price, uncrossSide, offbookVolume) + uncrossOrders := b.indicativePriceAndVolume.ExtractOffbookOrders(r.price, r.side, r.offbookVolume) // the remaining volume should now come from the orderbook - volume -= offbookVolume + volume := r.volume - r.offbookVolume // Remove all the orders from that side of the book up to the given volume - uncrossOrders = append(uncrossOrders, uncrossingSide.ExtractOrders(price, volume, true)...) - return b.uncrossBookSide(uncrossOrders, b.getOppositeSide(uncrossSide), price.Clone()) + uncrossOrders = append(uncrossOrders, uncrossingSide.ExtractOrders(r.price, volume, true)...) + return b.uncrossBookSide(uncrossOrders, b.getOppositeSide(r.side), r.price.Clone()) } // Takes extracted order from a side of the book, and uncross them @@ -882,7 +946,7 @@ func (b *OrderBook) UpdateAMM(party string) { return } - ipv.addOffbookShape(ptr.From(party), ipv.lastMinPrice, ipv.lastMaxPrice) + ipv.addOffbookShape(ptr.From(party), ipv.lastMinPrice, ipv.lastMaxPrice, false, false) ipv.needsUpdate = true } diff --git a/core/matching/orderbook_amm_test.go b/core/matching/orderbook_amm_test.go index 70dc9be2a4a..8e965225a56 100644 --- a/core/matching/orderbook_amm_test.go +++ b/core/matching/orderbook_amm_test.go @@ -421,8 +421,14 @@ func expectCrossedAMMs(t *testing.T, tst *tstOrderbook, min, max int) { orders1 := createOrderbookShape(t, tst, min, max, types.SideBuy, "A") orders2 := createOrderbookShape(t, tst, min, max, types.SideSell, "B") + res := []*types.OrderbookShapeResult{ + { + Buys: orders1, + Sells: orders2, + }, + } - tst.obs.EXPECT().OrderbookShape(gomock.Any(), gomock.Any(), gomock.Any()).Return(orders1, orders2) + tst.obs.EXPECT().OrderbookShape(gomock.Any(), gomock.Any(), gomock.Any()).Return(res) } type tstOrderbook struct { diff --git a/core/types/amm.go b/core/types/amm.go index 390ea1a3dc7..abab321f188 100644 --- a/core/types/amm.go +++ b/core/types/amm.go @@ -22,6 +22,13 @@ import ( eventspb "code.vegaprotocol.io/vega/protos/vega/events/v1" ) +type OrderbookShapeResult struct { + AmmParty string + Buys []*Order + Sells []*Order + Approx bool +} + // AMMBaseCommand these 3 parameters should be always specified // in both the the submit and amend commands. type AMMBaseCommand struct {