Skip to content

Commit

Permalink
Merge pull request #11629 from vegaprotocol/11583-bound-trade-interval
Browse files Browse the repository at this point in the history
fix: calculate rough bound on where to check for incoming trade volum…
  • Loading branch information
jeremyletang authored Sep 2, 2024
2 parents 6431330 + 11276e6 commit 6058ab1
Show file tree
Hide file tree
Showing 7 changed files with 103 additions and 73 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
- [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.
- [11583](https://github.com/vegaprotocol/vega/issues/11583) - Rough bound on price interval when matching with `AMMs` is now looser and calculated in the `AMM` engine.
- [11619](https://github.com/vegaprotocol/vega/issues/11619) - Fix `EstimatePositions` API for capped futures.
- [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
17 changes: 17 additions & 0 deletions core/execution/amm/engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -491,6 +491,23 @@ func (e *Engine) partition(agg *types.Order, inner, outer *num.Uint) ([]*Pool, [
}
}

if inner == nil {
// if inner is given as nil it means the matching engine is trading up to its first price level
// and so has no lower bound on the range. So we'll calculate one using best price of all pools
// note that if the incoming order is a buy the price range we need to evaluate is from
// fair-price -> best-ask -> outer, so we need to step one back. But then if we use fair-price exactly we
// risk hitting numerical problems and given this is just to exclude AMM's completely out of range we
// can be a bit looser and so step back again so that we evaluate from best-buy -> best-ask -> outer.
buy, _, ask, _ := e.BestPricesAndVolumes()
two := num.UintZero().AddSum(e.oneTick, e.oneTick)
if agg.Side == types.SideBuy && ask != nil {
inner = num.UintZero().Sub(ask, two)
}
if agg.Side == types.SideSell && buy != nil {
inner = num.UintZero().Add(buy, two)
}
}

// switch so that inner < outer to make it easier to reason with
if agg.Side == types.SideSell {
inner, outer = outer, inner
Expand Down
46 changes: 45 additions & 1 deletion core/integration/features/amm/0090-VAMM-auction.feature
Original file line number Diff line number Diff line change
Expand Up @@ -504,4 +504,48 @@ Feature: vAMM rebasing when created or amended
When the opening auction period ends for market "ETH/MAR22"
And the market data for the market "ETH/MAR22" should be:
| mark price | trading mode | best bid price | best offer price |
| 93 | TRADING_MODE_CONTINUOUS | 92 | 94 |
| 93 | TRADING_MODE_CONTINUOUS | 92 | 94 |


@VAMM
Scenario: AMM crossed with limit order, AMM pushed to boundary


And the parties place the following orders:
| party | market id | side | volume | price | resulting trades | type | tif | reference |
| lp1 | ETH/MAR22 | buy | 423 | 200 | 0 | TYPE_LIMIT | TIF_GTC | lp1-b |

Then the parties submit the following AMM:
| party | market id | amount | slippage | base | lower bound | upper bound | proposed fee |
| vamm1 | ETH/MAR22 | 100000 | 0.05 | 100 | 90 | 110 | 0.03 |
Then the AMM pool status should be:
| party | market id | amount | status | base | lower bound | upper bound |
| vamm1 | ETH/MAR22 | 100000 | STATUS_ACTIVE | 100 | 90 | 110 |


# 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 set the following AMM sub account aliases:
| party | market id | alias |
| vamm1 | ETH/MAR22 | vamm1-id |


And the market data for the market "ETH/MAR22" should be:
| trading mode | indicative price | indicative volume |
| TRADING_MODE_OPENING_AUCTION | 155 | 423 |


When the opening auction period ends for market "ETH/MAR22"

# the volume of this trade should be the entire volume of the AMM's sell curve
Then the following trades should be executed:
| buyer | price | size | seller | is amm |
| lp1 | 155 | 423 | vamm1-id | true |

And the market data for the market "ETH/MAR22" should be:
| mark price | trading mode | best bid price | best offer price |
| 155 | TRADING_MODE_CONTINUOUS | 109 | 0 |
46 changes: 7 additions & 39 deletions core/matching/orderbook.go
Original file line number Diff line number Diff line change
Expand Up @@ -491,16 +491,12 @@ func (b *OrderBook) GetIndicativeTrades() ([]*types.Trade, error) {
var (
uncrossOrders []*types.Order
uncrossingSide *OrderBookSide
uncrossBound *num.Uint
)

min, max := b.indicativePriceAndVolume.GetCrossedRegion()
if uncrossSide == types.SideBuy {
uncrossingSide = b.buy
uncrossBound = min
} else {
uncrossingSide = b.sell
uncrossBound = max
}

// extract uncrossing orders from all AMMs
Expand All @@ -513,7 +509,7 @@ func (b *OrderBook) GetIndicativeTrades() ([]*types.Trade, error) {
uncrossOrders = append(uncrossOrders, uncrossingSide.ExtractOrders(price, volume, false)...)
opSide := b.getOppositeSide(uncrossSide)
output := make([]*types.Trade, 0, len(uncrossOrders))
trades, err := opSide.fakeUncrossAuction(uncrossOrders, uncrossBound)
trades, err := opSide.fakeUncrossAuction(uncrossOrders)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -558,18 +554,12 @@ func (b *OrderBook) uncrossBook() ([]*types.OrderConfirmation, error) {
return nil, nil
}

var (
uncrossingSide *OrderBookSide
uncrossBound *num.Uint
)
var uncrossingSide *OrderBookSide

min, max := b.indicativePriceAndVolume.GetCrossedRegion()
if uncrossSide == types.SideBuy {
uncrossingSide = b.buy
uncrossBound = min
} else {
uncrossingSide = b.sell
uncrossBound = max
}

// extract uncrossing orders from all AMMs
Expand All @@ -580,15 +570,15 @@ func (b *OrderBook) uncrossBook() ([]*types.OrderConfirmation, error) {

// 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(), uncrossBound)
return b.uncrossBookSide(uncrossOrders, b.getOppositeSide(uncrossSide), price.Clone())
}

// Takes extracted order from a side of the book, and uncross them
// with the opposite side.
func (b *OrderBook) uncrossBookSide(
uncrossOrders []*types.Order,
opSide *OrderBookSide,
price, uncrossBound *num.Uint,
price *num.Uint,
) ([]*types.OrderConfirmation, error) {
var (
uncrossedOrder *types.OrderConfirmation
Expand All @@ -614,7 +604,7 @@ func (b *OrderBook) uncrossBookSide(
}

// try to get the market price value from the order
trades, affectedOrders, _, err := opSide.uncross(order, false, uncrossBound)
trades, affectedOrders, _, err := opSide.uncross(order, false)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -911,8 +901,7 @@ func (b *OrderBook) GetTrades(order *types.Order) ([]*types.Trade, error) {
b.latestTimestamp = order.CreatedAt
}

idealPrice := b.theoreticalBestTradePrice(order)
trades, err := b.getOppositeSide(order.Side).fakeUncross(order, true, idealPrice)
trades, err := b.getOppositeSide(order.Side).fakeUncross(order, true)
// it's fine for the error to be a wash trade here,
// it's just be stopped when really uncrossing.
if err != nil && err != ErrWashTrade {
Expand Down Expand Up @@ -961,25 +950,6 @@ func (b *OrderBook) ReSubmitSpecialOrders(order *types.Order) {
b.add(order)
}

// theoreticalBestTradePrice returns the best possible price the incoming order could trade
// as if the spread were as small as possible. This will be used to construct the first
// interval to query offbook orders matching with the other side.
func (b *OrderBook) theoreticalBestTradePrice(order *types.Order) *num.Uint {
bp, _, err := b.getSide(order.Side).BestPriceAndVolume()
if err != nil {
return nil
}

switch order.Side {
case types.SideBuy:
return bp.Add(bp, num.UintOne())
case types.SideSell:
return bp.Sub(bp, num.UintOne())
default:
panic("unexpected order side")
}
}

// SubmitOrder Add an order and attempt to uncross the book, returns a TradeSet protobuf message object.
func (b *OrderBook) SubmitOrder(order *types.Order) (*types.OrderConfirmation, error) {
if err := b.validateOrder(order); err != nil {
Expand All @@ -1006,9 +976,7 @@ func (b *OrderBook) SubmitOrder(order *types.Order) (*types.OrderConfirmation, e
// uncross with opposite

defer b.buy.uncrossFinished()

idealPrice := b.theoreticalBestTradePrice(order)
trades, impactedOrders, lastTradedPrice, err = b.getOppositeSide(order.Side).uncross(order, true, idealPrice)
trades, impactedOrders, lastTradedPrice, err = b.getOppositeSide(order.Side).uncross(order, true)
if !lastTradedPrice.IsZero() {
b.lastTradedPrice = lastTradedPrice
}
Expand Down
2 changes: 1 addition & 1 deletion core/matching/orderbook_amm_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -235,7 +235,7 @@ func testMatchOrdersBothSide(t *testing.T) {
assert.Len(t, trades, 8)

// uncross
expectOffbookOrders(t, tst, price, num.NewUint(oPrice-1), num.NewUint(120))
expectOffbookOrders(t, tst, price, nil, num.NewUint(120))
expectOffbookOrders(t, tst, price, num.NewUint(120), num.NewUint(110))
expectOffbookOrders(t, tst, price, num.NewUint(110), num.NewUint(90))
tst.obs.EXPECT().NotifyFinished().Times(1)
Expand Down
38 changes: 19 additions & 19 deletions core/matching/side.go
Original file line number Diff line number Diff line change
Expand Up @@ -408,7 +408,7 @@ func (s *OrderBookSide) GetVolume(price *num.Uint) (uint64, error) {

// fakeUncross returns hypothetical trades if the order book side were to be uncrossed with the agg order supplied,
// checkWashTrades checks non-FOK orders for wash trades if set to true (FOK orders are always checked for wash trades).
func (s *OrderBookSide) fakeUncross(agg *types.Order, checkWashTrades bool, idealPrice *num.Uint) ([]*types.Trade, error) {
func (s *OrderBookSide) fakeUncross(agg *types.Order, checkWashTrades bool) ([]*types.Trade, error) {
defer s.uncrossFinished()

// get a copy of the order passed in, so we can rely on fakeUncross to do its job
Expand All @@ -428,7 +428,7 @@ func (s *OrderBookSide) fakeUncross(agg *types.Order, checkWashTrades bool, idea
}

// first check for volume between the theoretical best price and the first price level
_, oo := s.uncrossOffbook(len(s.levels), fake, idealPrice, true)
_, oo := s.uncrossOffbook(len(s.levels), fake, true)
for _, order := range oo {
totalVolumeToFill += order.Remaining
}
Expand All @@ -448,7 +448,7 @@ func (s *OrderBookSide) fakeUncross(agg *types.Order, checkWashTrades bool, idea
}
}

_, oo := s.uncrossOffbook(i, fake, idealPrice, true)
_, oo := s.uncrossOffbook(i, fake, true)
for _, order := range oo {
totalVolumeToFill += order.Remaining
}
Expand Down Expand Up @@ -483,7 +483,7 @@ func (s *OrderBookSide) fakeUncross(agg *types.Order, checkWashTrades bool, idea
checkPrice = func(levelPrice *num.Uint) bool { return levelPrice.LT(agg.Price) }
}

trades, offbookOrders = s.uncrossOffbook(idx+1, fake, idealPrice, true)
trades, offbookOrders = s.uncrossOffbook(idx+1, fake, true)

// in here we iterate from the end, as it's easier to remove the
// price levels from the back of the slice instead of from the front
Expand All @@ -501,7 +501,7 @@ func (s *OrderBookSide) fakeUncross(agg *types.Order, checkWashTrades bool, idea
}

if fake.Remaining != 0 {
obTrades, obOrders := s.uncrossOffbook(idx, fake, idealPrice, true)
obTrades, obOrders := s.uncrossOffbook(idx, fake, true)
trades = append(trades, obTrades...)
offbookOrders = append(offbookOrders, obOrders...)
}
Expand All @@ -514,7 +514,7 @@ func (s *OrderBookSide) fakeUncross(agg *types.Order, checkWashTrades bool, idea
}

// fakeUncrossAuction returns hypothetical trades if the order book side were to be uncrossed with the agg orders supplied, wash trades are allowed.
func (s *OrderBookSide) fakeUncrossAuction(orders []*types.Order, bound *num.Uint) ([]*types.Trade, error) {
func (s *OrderBookSide) fakeUncrossAuction(orders []*types.Order) ([]*types.Trade, error) {
defer s.uncrossFinished()
// in here we iterate from the end, as it's easier to remove the
// price levels from the back of the slice instead of from the front
Expand Down Expand Up @@ -542,7 +542,7 @@ func (s *OrderBookSide) fakeUncrossAuction(orders []*types.Order, bound *num.Uin

for ; iOrder < len(orders); iOrder++ {
fake = orders[iOrder].Clone()
ntrades, _ = s.uncrossOffbook(len(s.levels), fake, bound, false)
ntrades, _ = s.uncrossOffbook(len(s.levels), fake, false)
trades = append(trades, ntrades...)

// no more to trade in this pre-orderbook region for AMM's, we now need to move to orderbook
Expand Down Expand Up @@ -580,7 +580,7 @@ func (s *OrderBookSide) fakeUncrossAuction(orders []*types.Order, bound *num.Uin
trades = append(trades, ntrades...)

if fake.Remaining != 0 {
ntrades, _ := s.uncrossOffbook(idx, fake, bound, true)
ntrades, _ := s.uncrossOffbook(idx, fake, true)
trades = append(trades, ntrades...)

// if we couldn't consume the whole order with this AMM volume in this region
Expand Down Expand Up @@ -617,15 +617,15 @@ func clonePriceLevel(lvl *PriceLevel) *PriceLevel {
// betweenLevels returns the inner, outer bounds for the given idx in the price levels.
// Usually this means (inner, outer) = (lvl[i].price, lvl[i-1].price) but we also handle
// the past the first and last price levels.
func (s *OrderBookSide) betweenLevels(idx int, first, last *num.Uint) (*num.Uint, *num.Uint) {
func (s *OrderBookSide) betweenLevels(idx int, last *num.Uint) (*num.Uint, *num.Uint) {
// there are no price levels, so between is from low to high
if len(s.levels) == 0 {
return first, last
return nil, last
}

// we're at the first price level
// we're at the first price level, we pass back nil for the first level since we do not know the bound for this
if idx == len(s.levels) {
return first, s.levels[idx-1].price
return nil, s.levels[idx-1].price
}

// we're at the last price level
Expand All @@ -641,13 +641,13 @@ func (s *OrderBookSide) uncrossFinished() {
}
}

func (s *OrderBookSide) uncrossOffbook(idx int, agg *types.Order, idealPrice *num.Uint, fake bool) ([]*types.Trade, []*types.Order) {
func (s *OrderBookSide) uncrossOffbook(idx int, agg *types.Order, fake bool) ([]*types.Trade, []*types.Order) {
if s.offbook == nil {
return nil, nil
}

// get the bounds between price levels for the given price level index
inner, outer := s.betweenLevels(idx, idealPrice, agg.Price)
inner, outer := s.betweenLevels(idx, agg.Price)

// submit the order to the offbook source for volume between those bounds
orders := s.offbook.SubmitOrder(agg, inner, outer)
Expand All @@ -668,7 +668,7 @@ func (s *OrderBookSide) uncrossOffbook(idx int, agg *types.Order, idealPrice *nu

// uncross returns trades after order book side gets uncrossed with the agg order supplied,
// checkWashTrades checks non-FOK orders for wash trades if set to true (FOK orders are always checked for wash trades).
func (s *OrderBookSide) uncross(agg *types.Order, checkWashTrades bool, theoreticalBestTrade *num.Uint) ([]*types.Trade, []*types.Order, *num.Uint, error) {
func (s *OrderBookSide) uncross(agg *types.Order, checkWashTrades bool) ([]*types.Trade, []*types.Order, *num.Uint, error) {
var (
trades []*types.Trade
impactedOrders []*types.Order
Expand All @@ -686,7 +686,7 @@ func (s *OrderBookSide) uncross(agg *types.Order, checkWashTrades bool, theoreti
}

if agg.TimeInForce == types.OrderTimeInForceFOK {
_, oo := s.uncrossOffbook(len(s.levels), fake, theoreticalBestTrade, true)
_, oo := s.uncrossOffbook(len(s.levels), fake, true)
for _, order := range oo {
totalVolumeToFill += order.Remaining
}
Expand All @@ -706,7 +706,7 @@ func (s *OrderBookSide) uncross(agg *types.Order, checkWashTrades bool, theoreti
// in case of network trades, we want to calculate an accurate average price to return
totalVolumeToFill += order.Remaining

_, oo := s.uncrossOffbook(i, fake, theoreticalBestTrade, true)
_, oo := s.uncrossOffbook(i, fake, true)
for _, order := range oo {
totalVolumeToFill += order.Remaining
}
Expand Down Expand Up @@ -742,7 +742,7 @@ func (s *OrderBookSide) uncross(agg *types.Order, checkWashTrades bool, theoreti
)

// first check for off source volume between the best theoretical price and the first price level
trades, impactedOrders = s.uncrossOffbook(idx+1, agg, theoreticalBestTrade, false)
trades, impactedOrders = s.uncrossOffbook(idx+1, agg, false)
filled = agg.Remaining == 0

// in here we iterate from the end, as it's easier to remove the
Expand All @@ -760,7 +760,7 @@ func (s *OrderBookSide) uncross(agg *types.Order, checkWashTrades bool, theoreti

if !filled {
// now check for off source volume between the price levels
ot, oo := s.uncrossOffbook(idx, agg, theoreticalBestTrade, false)
ot, oo := s.uncrossOffbook(idx, agg, false)
trades = append(trades, ot...)
impactedOrders = append(impactedOrders, oo...)
filled = agg.Remaining == 0
Expand Down
Loading

0 comments on commit 6058ab1

Please sign in to comment.