Skip to content

Commit

Permalink
allow grace period for market orders to be filled (#168)
Browse files Browse the repository at this point in the history
* allow grace period for market orders to be filled

* dont remove IOC orders based on block number

* remove KEEP_IF_MATCHEABLE orderStatus

* trigger matching if possible when signed order is placed

* add log

* check bidsHead only if !isLongOrder

* fix test
  • Loading branch information
atvanguard authored Mar 7, 2024
1 parent e8433a1 commit 772ea72
Show file tree
Hide file tree
Showing 7 changed files with 144 additions and 79 deletions.
6 changes: 5 additions & 1 deletion plugin/evm/gossiper_orders.go
Original file line number Diff line number Diff line change
Expand Up @@ -158,10 +158,14 @@ func (h *GossipHandler) HandleSignedOrders(nodeID ids.NodeID, msg message.Signed
// re-gossip orders, but not when we already knew the orders
ordersToGossip := make([]*hu.SignedOrder, 0)
for _, order := range orders {
_, err := tradingAPI.PlaceOrder(order)
_, shouldTriggerMatching, err := tradingAPI.PlaceOrder(order)
if err == nil {
h.stats.IncSignedOrdersGossipReceivedNew()
ordersToGossip = append(ordersToGossip, order)
if shouldTriggerMatching {
log.Info("received new match-able signed order, triggering matching pipeline...")
h.vm.limitOrderProcesser.RunMatchingPipeline()
}
} else if err == hu.ErrOrderAlreadyExists {
h.stats.IncSignedOrdersGossipReceivedKnown()
} else {
Expand Down
5 changes: 4 additions & 1 deletion plugin/evm/limit_order.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ type LimitOrderProcesser interface {
GetOrderBookAPI() *orderbook.OrderBookAPI
GetTestingAPI() *orderbook.TestingAPI
GetTradingAPI() *orderbook.TradingAPI
RunMatchingPipeline()
}

type limitOrderProcesser struct {
Expand Down Expand Up @@ -72,7 +73,8 @@ func NewLimitOrderProcesser(ctx *snow.Context, txPool *txpool.TxPool, shutdownCh
contractEventProcessor := orderbook.NewContractEventsProcessor(memoryDb, signedObAddy)

matchingPipeline := orderbook.NewMatchingPipeline(memoryDb, lotp, configService)
// if any of the following values are changed, the nodes will need to be restarted
// if any of the following values are changed, the nodes will need to be restarted.
// This is also true for local testing. once contracts are deployed it's mandatory to restart the nodes
hState := &hu.HubbleState{
Assets: matchingPipeline.GetCollaterals(),
ActiveMarkets: matchingPipeline.GetActiveMarkets(),
Expand Down Expand Up @@ -347,6 +349,7 @@ func (lop *limitOrderProcesser) runMatchingTimer() {

case <-lop.shutdownChan:
lop.matchingPipeline.MatchingTicker.Stop()
lop.matchingPipeline.SanitaryTicker.Stop()
return
}
}
Expand Down
2 changes: 1 addition & 1 deletion plugin/evm/order_api.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ func (api *OrderAPI) PlaceSignedOrders(ctx context.Context, input string) (Place
continue
}

orderId, err := api.tradingAPI.PlaceOrder(order)
orderId, _, err := api.tradingAPI.PlaceOrder(order)
orderResponse.OrderId = orderId.String()
if err != nil {
orderResponse.Error = err.Error()
Expand Down
2 changes: 1 addition & 1 deletion plugin/evm/orderbook/matching_pipeline.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ func (pipeline *MatchingPipeline) Run(blockNumber *big.Int) bool {
}

orderBookTxsCount := pipeline.lotp.GetOrderBookTxsCount()
log.Info("MatchingPipeline:Run", "orderBookTxsCount", orderBookTxsCount)
log.Info("MatchingPipeline:Complete", "orderBookTxsCount", orderBookTxsCount)
if orderBookTxsCount > 0 {
pipeline.lotp.SetOrderBookTxsBlockNumber(blockNumber.Uint64())
return true
Expand Down
100 changes: 35 additions & 65 deletions plugin/evm/orderbook/memory_database.go
Original file line number Diff line number Diff line change
Expand Up @@ -299,54 +299,13 @@ func (db *InMemoryDatabase) Accept(acceptedBlockNumber, blockTimestamp uint64) {
shortOrders := db.getShortOrdersWithoutLock(Market(m), nil, nil, false)

for _, longOrder := range longOrders {
status := shouldRemove(acceptedBlockNumber, blockTimestamp, longOrder)
if status == KEEP_IF_MATCHEABLE {
matchFound := false
for _, shortOrder := range shortOrders {
if longOrder.Price.Cmp(shortOrder.Price) < 0 {
break // because the short orders are sorted in ascending order of price, there is no point in checking further
}
// an IOC order even if has a price overlap can only be matched if the order came before it (or same block)
if longOrder.BlockNumber.Uint64() >= shortOrder.BlockNumber.Uint64() {
matchFound = true
break
} /* else {
dont break here because there might be an a short order with higher price that came before the IOC longOrder in question
} */
}
if !matchFound {
status = REMOVE
}
}

if status == REMOVE {
if shouldRemove(acceptedBlockNumber, blockTimestamp, longOrder) == REMOVE {
db.deleteOrderWithoutLock(longOrder.Id)
}
}

for _, shortOrder := range shortOrders {
status := shouldRemove(acceptedBlockNumber, blockTimestamp, shortOrder)
if status == KEEP_IF_MATCHEABLE {
matchFound := false
for _, longOrder := range longOrders {
if longOrder.Price.Cmp(shortOrder.Price) < 0 {
break // because the long orders are sorted in descending order of price, there is no point in checking further
}
// an IOC order even if has a price overlap can only be matched if the order came before it (or same block)
if shortOrder.BlockNumber.Uint64() >= longOrder.BlockNumber.Uint64() {
matchFound = true
break
}
/* else {
dont break here because there might be an a long order with lower price that came before the IOC shortOrder in question
} */
}
if !matchFound {
status = REMOVE
}
}

if status == REMOVE {
if shouldRemove(acceptedBlockNumber, blockTimestamp, shortOrder) == REMOVE {
db.deleteOrderWithoutLock(shortOrder.Id)
}
}
Expand All @@ -358,7 +317,6 @@ type OrderStatus uint8
const (
KEEP OrderStatus = iota
REMOVE
KEEP_IF_MATCHEABLE
)

func shouldRemove(acceptedBlockNumber, blockTimestamp uint64, order Order) OrderStatus {
Expand All @@ -378,15 +336,6 @@ func shouldRemove(acceptedBlockNumber, blockTimestamp uint64, order Order) Order
if expireAt.Sign() > 0 && expireAt.Int64() < int64(blockTimestamp) {
return REMOVE
}

// IOC order can not matched with any order that came after it (same block is allowed)
// we can only surely say about orders that came at <= acceptedBlockNumber
if order.OrderType == IOC {
if order.BlockNumber.Uint64() > acceptedBlockNumber {
return KEEP
}
return KEEP_IF_MATCHEABLE
}
return KEEP
}

Expand Down Expand Up @@ -1292,10 +1241,11 @@ func getOrderIdx(orders []*Order, orderId common.Hash) int {
}

type OrderValidationFields struct {
Exists bool
PosSize *big.Int
AsksHead *big.Int
BidsHead *big.Int
Exists bool
PosSize *big.Int
AsksHead *big.Int
BidsHead *big.Int
ShouldTriggerMatching bool
}

func (db *InMemoryDatabase) GetOrderValidationFields(orderId common.Hash, order *hu.SignedOrder) OrderValidationFields {
Expand All @@ -1315,20 +1265,40 @@ func (db *InMemoryDatabase) GetOrderValidationFields(orderId common.Hash, order
}

// market data
// allow some grace to market orders to be filled and accept post-only orders that might fill them
// iterate until we find a short order that is not an IOC order.
isLongOrder := order.BaseAssetQuantity.Sign() > 0
shouldTriggerMatching := false
asksHead := big.NewInt(0)
if len(db.ShortOrders[marketId]) > 0 {
asksHead = db.ShortOrders[marketId][0].Price
if isLongOrder && len(db.ShortOrders[marketId]) > 0 {
for _, _order := range db.ShortOrders[marketId] {
if _order.OrderType != IOC {
asksHead = _order.Price
break
} else if _order.Price.Cmp(order.Price) <= 0 {
shouldTriggerMatching = true
}
}
}

bidsHead := big.NewInt(0)
if len(db.LongOrders[marketId]) > 0 {
bidsHead = db.LongOrders[marketId][0].Price
if !isLongOrder && len(db.LongOrders[marketId]) > 0 {
for _, _order := range db.LongOrders[marketId] {
if _order.OrderType != IOC {
bidsHead = _order.Price
break
} else if _order.Price.Cmp(order.Price) >= 0 {
shouldTriggerMatching = true
}
}
}

return OrderValidationFields{
Exists: false,
PosSize: posSize,
AsksHead: asksHead,
BidsHead: bidsHead,
Exists: false,
PosSize: posSize,
AsksHead: asksHead,
BidsHead: bidsHead,
ShouldTriggerMatching: shouldTriggerMatching,
}
}

Expand Down
88 changes: 88 additions & 0 deletions plugin/evm/orderbook/memory_database_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1063,3 +1063,91 @@ func TestSampleImpactPrice(t *testing.T) {
})
})
}

func TestGetOrderValidationFields(t *testing.T) {
db := getDatabase()

t.Run("bidsHead is unaffected by IOC orders", func(t *testing.T) {
signedOrder := &hu.SignedOrder{
LimitOrder: LimitOrder{
BaseOrder: hu.BaseOrder{
AmmIndex: big.NewInt(0),
Trader: common.HexToAddress("0x70997970C51812dc3A010C7d01b50e0d17dc79C8"),
BaseAssetQuantity: big.NewInt(-5000000000000000000),
Price: big.NewInt(1000000000),
Salt: big.NewInt(1688994806105),
ReduceOnly: false,
},
PostOnly: true,
},
OrderType: 2,
ExpireAt: big.NewInt(1688994854),
}
orderId, _ := signedOrder.Hash()

// no orders, ask and bids head should be 0
fields := db.GetOrderValidationFields(orderId, signedOrder)
assert.Equal(t, big.NewInt(0), fields.BidsHead)
assert.Equal(t, big.NewInt(0), fields.AsksHead)

// send a bid at $100
order1 := createLimitOrder(LONG, "0x70997970C51812dc3A010C7d01b50e0d17dc79C8", big.NewInt(1), big.NewInt(100), Placed, big.NewInt(2), big.NewInt(1688994806105))
db.Add(&order1)
fields = db.GetOrderValidationFields(orderId, signedOrder)
assert.Equal(t, big.NewInt(100), fields.BidsHead)
assert.Equal(t, big.NewInt(0), fields.AsksHead)

// send a market market bid at $101
// assert that bidsHead remains at $101 so signed orders at (100, 101) can be accepted and matched
order2 := createIOCOrder(LONG, "0x22Bb736b64A0b4D4081E103f83bccF864F0404aa", big.NewInt(1e18), big.NewInt(101), Placed, big.NewInt(2), big.NewInt(2), big.NewInt(10))
db.Add(&order2)
fields = db.GetOrderValidationFields(orderId, signedOrder)
assert.Equal(t, big.NewInt(100), fields.BidsHead)
assert.Equal(t, big.NewInt(0), fields.AsksHead)

db.Delete(order1.Id)
db.Delete(order2.Id)
})

t.Run("asksHead is unaffected by IOC orders", func(t *testing.T) {
signedOrder := &hu.SignedOrder{
LimitOrder: LimitOrder{
BaseOrder: hu.BaseOrder{
AmmIndex: big.NewInt(0),
Trader: common.HexToAddress("0x70997970C51812dc3A010C7d01b50e0d17dc79C8"),
BaseAssetQuantity: big.NewInt(5000000000000000000),
Price: big.NewInt(1000000000),
Salt: big.NewInt(1688994806105),
ReduceOnly: false,
},
PostOnly: true,
},
OrderType: 2,
ExpireAt: big.NewInt(1688994854),
}
orderId, _ := signedOrder.Hash()

// no orders, ask and bids head should be 0
fields := db.GetOrderValidationFields(orderId, signedOrder)
assert.Equal(t, big.NewInt(0), fields.BidsHead)
assert.Equal(t, big.NewInt(0), fields.AsksHead)

// send a bid at $100
order1 := createLimitOrder(SHORT, "0x70997970C51812dc3A010C7d01b50e0d17dc79C8", big.NewInt(-1), big.NewInt(100), Placed, big.NewInt(2), big.NewInt(1688994806105))
db.Add(&order1)
fields = db.GetOrderValidationFields(orderId, signedOrder)
assert.Equal(t, big.NewInt(0), fields.BidsHead)
assert.Equal(t, big.NewInt(100), fields.AsksHead)

// send a market market bid at $101
// assert that bidsHead remains at $101 so signed orders at (100, 101) can be accepted and matched
order2 := createIOCOrder(SHORT, "0x22Bb736b64A0b4D4081E103f83bccF864F0404aa", big.NewInt(-1), big.NewInt(99), Placed, big.NewInt(2), big.NewInt(2), big.NewInt(10))
db.Add(&order2)
fields = db.GetOrderValidationFields(orderId, signedOrder)
assert.Equal(t, big.NewInt(0), fields.BidsHead)
assert.Equal(t, big.NewInt(100), fields.AsksHead)

db.Delete(order1.Id)
db.Delete(order2.Id)
})
}
20 changes: 10 additions & 10 deletions plugin/evm/orderbook/trading_apis.go
Original file line number Diff line number Diff line change
Expand Up @@ -335,15 +335,15 @@ func (api *TradingAPI) StreamMarketTrades(ctx context.Context, market Market, bl
}

// @todo cache api.configService values to avoid db lookups on every order placement
func (api *TradingAPI) PlaceOrder(order *hu.SignedOrder) (common.Hash, error) {
func (api *TradingAPI) PlaceOrder(order *hu.SignedOrder) (common.Hash, bool, error) {
orderId, err := order.Hash()
if err != nil {
return common.Hash{}, fmt.Errorf("failed to hash order: %s", err)
return common.Hash{}, false, fmt.Errorf("failed to hash order: %s", err)
}
fields := api.db.GetOrderValidationFields(orderId, order)
// P1. Order is not already in memdb
if fields.Exists {
return orderId, hu.ErrOrderAlreadyExists
return orderId, false, hu.ErrOrderAlreadyExists
}
marketId := int(order.AmmIndex.Int64())
trader, signer, err := hu.ValidateSignedOrder(
Expand All @@ -358,11 +358,11 @@ func (api *TradingAPI) PlaceOrder(order *hu.SignedOrder) (common.Hash, error) {
},
)
if err != nil {
return orderId, err
return orderId, false, err
}
if trader != signer && !api.configService.IsTradingAuthority(trader, signer) {
log.Error("not trading authority", "trader", trader.String(), "signer", signer.String())
return orderId, hu.ErrNoTradingAuthority
return orderId, false, hu.ErrNoTradingAuthority
}

requiredMargin := big.NewInt(0)
Expand All @@ -373,11 +373,11 @@ func (api *TradingAPI) PlaceOrder(order *hu.SignedOrder) (common.Hash, error) {
requiredMargin = hu.GetRequiredMargin(order.Price, hu.Abs(order.BaseAssetQuantity), minAllowableMargin, big.NewInt(0))
availableMargin := api.db.GetMarginAvailableForMakerbook(trader, hu.ArrayToMap(api.configService.GetUnderlyingPrices()))
if availableMargin.Cmp(requiredMargin) == -1 {
return orderId, hu.ErrInsufficientMargin
return orderId, false, hu.ErrInsufficientMargin
}
} else {
// @todo P3. Sum of all reduce only orders should not exceed the total position size
return orderId, errors.New("reduce only orders via makerbook are not supported yet")
return orderId, false, errors.New("reduce only orders via makerbook are not supported yet")
}

// P4. Post only order shouldn't cross the market
Expand All @@ -389,13 +389,13 @@ func (api *TradingAPI) PlaceOrder(order *hu.SignedOrder) (common.Hash, error) {
asksHead := fields.AsksHead
bidsHead := fields.BidsHead
if (orderSide == hu.Side(hu.Short) && bidsHead.Sign() != 0 && order.Price.Cmp(bidsHead) != 1) || (orderSide == hu.Side(hu.Long) && asksHead.Sign() != 0 && order.Price.Cmp(asksHead) != -1) {
return orderId, hu.ErrCrossingMarket
return orderId, false, hu.ErrCrossingMarket
}
}

// P5. HasReferrer
if !api.configService.HasReferrer(order.Trader) {
return orderId, hu.ErrNoReferrer
return orderId, false, hu.ErrNoReferrer
}

// validations passed, add to db
Expand Down Expand Up @@ -454,7 +454,7 @@ func (api *TradingAPI) PlaceOrder(order *hu.SignedOrder) (common.Hash, error) {
traderFeed.Send(traderEvent)
}()

return orderId, nil
return orderId, fields.ShouldTriggerMatching, nil
}

func writeOrderToFile(order Order) {
Expand Down

0 comments on commit 772ea72

Please sign in to comment.