From b5ace406dc1b299580f154757a6b6ec21b30335e Mon Sep 17 00:00:00 2001 From: Shubham Date: Mon, 18 Sep 2023 11:28:15 +0530 Subject: [PATCH] Refactor order structures (#111) * Refactor order structures * Fix tests and add new test * update old order type * Review fixes * Review fixes * Review fix * Handle idx = -1 * Unit tests for Delete --------- Co-authored-by: atvanguard <3612498+atvanguard@users.noreply.github.com> --- plugin/evm/limit_order.go | 2 +- .../orderbook/contract_events_processor.go | 24 +- .../contract_events_processor_test.go | 40 +-- plugin/evm/orderbook/matching_pipeline.go | 8 - plugin/evm/orderbook/memory_database.go | 264 ++++++++++++------ plugin/evm/orderbook/memory_database_test.go | 182 +++++++++++- plugin/evm/orderbook/metrics.go | 3 + plugin/evm/orderbook/mocks.go | 5 + plugin/evm/orderbook/service.go | 18 +- plugin/evm/orderbook_test.go | 12 +- 10 files changed, 403 insertions(+), 155 deletions(-) diff --git a/plugin/evm/limit_order.go b/plugin/evm/limit_order.go index 00ab0cc8a7..db81f313f3 100644 --- a/plugin/evm/limit_order.go +++ b/plugin/evm/limit_order.go @@ -434,7 +434,7 @@ func (lop *limitOrderProcesser) UpdateLastPremiumFractionFromStorage() { } } - orderMap := lop.memoryDb.GetOrderBookData().OrderMap + orderMap := lop.memoryDb.GetOrderBookData().Orders for orderHash, order := range orderMap { if order.FilledBaseAssetQuantity.CmpAbs(order.BaseAssetQuantity) > 0 { log.Info("Order map cleanup - deleting order", "hash", orderHash.String(), "baseAssetQuantity", order.BaseAssetQuantity, "filledBaseAssetQuantity", order.FilledBaseAssetQuantity) diff --git a/plugin/evm/orderbook/contract_events_processor.go b/plugin/evm/orderbook/contract_events_processor.go index 5355848ce9..c1e19fa7b9 100644 --- a/plugin/evm/orderbook/contract_events_processor.go +++ b/plugin/evm/orderbook/contract_events_processor.go @@ -263,24 +263,24 @@ func (cep *ContractEventsProcessor) handleIOCOrderBookEvent(event *types.Log) { } orderId := event.Topics[2] if !removed { - order := IOCOrder{} - order.DecodeFromRawOrder(args["order"]) - limitOrder := Order{ + iocOrder := IOCOrder{} + iocOrder.DecodeFromRawOrder(args["order"]) + order := Order{ Id: orderId, - Market: Market(order.AmmIndex.Int64()), - PositionType: getPositionTypeBasedOnBaseAssetQuantity(order.BaseAssetQuantity), + Market: Market(iocOrder.AmmIndex.Int64()), + PositionType: getPositionTypeBasedOnBaseAssetQuantity(iocOrder.BaseAssetQuantity), Trader: getAddressFromTopicHash(event.Topics[1]), - BaseAssetQuantity: order.BaseAssetQuantity, + BaseAssetQuantity: iocOrder.BaseAssetQuantity, FilledBaseAssetQuantity: big.NewInt(0), - Price: order.Price, - RawOrder: &order, - Salt: order.Salt, - ReduceOnly: order.ReduceOnly, + Price: iocOrder.Price, + RawOrder: &iocOrder, + Salt: iocOrder.Salt, + ReduceOnly: iocOrder.ReduceOnly, BlockNumber: big.NewInt(int64(event.BlockNumber)), OrderType: IOC, } - log.Info("IOCOrder/OrderPlaced", "order", limitOrder, "number", event.BlockNumber) - cep.database.Add(&limitOrder) + log.Info("IOCOrder/OrderPlaced", "order", order, "number", event.BlockNumber) + cep.database.Add(&order) } else { log.Info("IOCOrder/OrderPlaced removed", "orderId", orderId.String(), "block", event.BlockHash.String(), "number", event.BlockNumber) cep.database.Delete(orderId) diff --git a/plugin/evm/orderbook/contract_events_processor_test.go b/plugin/evm/orderbook/contract_events_processor_test.go index b481115bb4..f17c051f4a 100644 --- a/plugin/evm/orderbook/contract_events_processor_test.go +++ b/plugin/evm/orderbook/contract_events_processor_test.go @@ -66,10 +66,10 @@ func TestProcessEvents(t *testing.T) { orderMatchedEventLog1 := getEventLog(OrderBookContractAddress, orderMatchedEventTopics1, orderMatchedEventData1, orderMatchedBlockNumber) cep.ProcessEvents([]*types.Log{longOrderAcceptedEventLog, shortOrderAcceptedEventLog, orderMatchedEventLog0, orderMatchedEventLog1}) - actualLongOrder := db.OrderMap[getIdFromLimitOrder(longOrder)] + actualLongOrder := db.Orders[getIdFromLimitOrder(longOrder)] assert.Equal(t, fillAmount, actualLongOrder.FilledBaseAssetQuantity) - actualShortOrder := db.OrderMap[getIdFromLimitOrder(shortOrder)] + actualShortOrder := db.Orders[getIdFromLimitOrder(shortOrder)] assert.Equal(t, big.NewInt(0).Neg(fillAmount), actualShortOrder.FilledBaseAssetQuantity) }) @@ -164,7 +164,7 @@ func TestOrderBookMarginAccountClearingHouseEventInLog(t *testing.T) { cep.ProcessAcceptedEvents([]*types.Log{marginAccountLog, clearingHouseLog}, true) //OrderBook log - OrderAccepted - actualLimitOrder := *db.GetOrderBookData().OrderMap[getIdFromLimitOrder(order)] + actualLimitOrder := *db.GetOrderBookData().Orders[getIdFromLimitOrder(order)] args := map[string]interface{}{} orderBookABI.UnpackIntoMap(args, "OrderAccepted", orderAcceptedEventData) assert.Equal(t, Market(ammIndex.Int64()), actualLimitOrder.Market) @@ -208,7 +208,7 @@ func TestHandleOrderBookEvent(t *testing.T) { orderAcceptedEventData := []byte{} log := getEventLog(OrderBookContractAddress, topics, orderAcceptedEventData, blockNumber) cep.ProcessEvents([]*types.Log{log}) - actualLimitOrder := db.GetOrderBookData().OrderMap[orderId] + actualLimitOrder := db.GetOrderBookData().Orders[orderId] assert.Nil(t, actualLimitOrder) }) t.Run("When data in log unpack succeeds", func(t *testing.T) { @@ -219,7 +219,7 @@ func TestHandleOrderBookEvent(t *testing.T) { log := getEventLog(OrderBookContractAddress, topics, orderAcceptedEventData, blockNumber) cep.ProcessEvents([]*types.Log{log}) - actualLimitOrder := db.GetOrderBookData().OrderMap[orderId] + actualLimitOrder := db.GetOrderBookData().Orders[orderId] args := map[string]interface{}{} orderBookABI.UnpackIntoMap(args, "OrderAccepted", orderAcceptedEventData) assert.Equal(t, Market(ammIndex.Int64()), actualLimitOrder.Market) @@ -260,7 +260,7 @@ func TestHandleOrderBookEvent(t *testing.T) { log := getEventLog(OrderBookContractAddress, topics, orderCancelAcceptedEventData, blockNumber) cep.ProcessEvents([]*types.Log{log}) orderId := getIdFromOrder(*limitOrder) - actualLimitOrder := db.GetOrderBookData().OrderMap[orderId] + actualLimitOrder := db.GetOrderBookData().Orders[orderId] assert.Equal(t, limitOrder, actualLimitOrder) }) t.Run("When data in log unpack succeeds", func(t *testing.T) { @@ -268,7 +268,7 @@ func TestHandleOrderBookEvent(t *testing.T) { log := getEventLog(OrderBookContractAddress, topics, orderCancelAcceptedEventData, blockNumber) orderId := getIdFromOrder(*limitOrder) cep.ProcessEvents([]*types.Log{log}) - actualLimitOrder := db.GetOrderBookData().OrderMap[orderId] + actualLimitOrder := db.GetOrderBookData().Orders[orderId] assert.Equal(t, Cancelled, actualLimitOrder.getOrderStatus().Status) }) }) @@ -643,12 +643,12 @@ func TestRemovedEvents(t *testing.T) { cep.ProcessEvents([]*types.Log{longOrderAcceptedEventLog}) // order exists in memory now - assert.Equal(t, db.OrderMap[longOrderId].Salt, longOrder.Salt) + assert.Equal(t, db.Orders[longOrderId].Salt, longOrder.Salt) // order should be deleted if OrderAccepted log is removed longOrderAcceptedEventLog.Removed = true cep.ProcessEvents([]*types.Log{longOrderAcceptedEventLog}) - assert.Nil(t, db.OrderMap[longOrderId]) + assert.Nil(t, db.Orders[longOrderId]) }) t.Run("un-cancel an order when OrderCancelAccepted is removed", func(t *testing.T) { @@ -656,7 +656,7 @@ func TestRemovedEvents(t *testing.T) { cep.ProcessEvents([]*types.Log{longOrderAcceptedEventLog}) // order exists in memory now - assert.Equal(t, db.OrderMap[longOrderId].Salt, longOrder.Salt) + assert.Equal(t, db.Orders[longOrderId].Salt, longOrder.Salt) // cancel it orderCancelAcceptedEvent := getEventFromABI(limitOrderrderBookABI, "OrderCancelAccepted") @@ -665,12 +665,12 @@ func TestRemovedEvents(t *testing.T) { orderCancelAcceptedLog := getEventLog(OrderBookContractAddress, orderCancelAcceptedEventTopics, orderCancelAcceptedEventData, blockNumber.Uint64()+2) cep.ProcessEvents([]*types.Log{orderCancelAcceptedLog}) - assert.Equal(t, db.OrderMap[longOrderId].getOrderStatus().Status, Cancelled) + assert.Equal(t, db.Orders[longOrderId].getOrderStatus().Status, Cancelled) // now uncancel it orderCancelAcceptedLog.Removed = true cep.ProcessEvents([]*types.Log{orderCancelAcceptedLog}) - assert.Equal(t, db.OrderMap[longOrderId].getOrderStatus().Status, Placed) + assert.Equal(t, db.Orders[longOrderId].getOrderStatus().Status, Placed) }) t.Run("un-fulfill an order when OrderMatched is removed", func(t *testing.T) { @@ -679,8 +679,8 @@ func TestRemovedEvents(t *testing.T) { cep.ProcessEvents([]*types.Log{longOrderAcceptedEventLog, shortOrderAcceptedEventLog}) // orders exist in memory now - assert.Equal(t, db.OrderMap[longOrderId].Salt, longOrder.Salt) - assert.Equal(t, db.OrderMap[shortOrderId].Salt, shortOrder.Salt) + assert.Equal(t, db.Orders[longOrderId].Salt, longOrder.Salt) + assert.Equal(t, db.Orders[shortOrderId].Salt, shortOrder.Salt) // fulfill them orderMatchedEvent := getEventFromABI(orderBookABI, "OrderMatched") @@ -689,12 +689,12 @@ func TestRemovedEvents(t *testing.T) { orderMatchedLog := getEventLog(OrderBookContractAddress, orderMatchedEventTopics, orderMatchedEventData, blockNumber.Uint64()+2) cep.ProcessEvents([]*types.Log{orderMatchedLog}) - assert.Equal(t, db.OrderMap[longOrderId].getOrderStatus().Status, FulFilled) + assert.Equal(t, db.Orders[longOrderId].getOrderStatus().Status, FulFilled) // now un-fulfill it orderMatchedLog.Removed = true cep.ProcessEvents([]*types.Log{orderMatchedLog}) - assert.Equal(t, db.OrderMap[longOrderId].getOrderStatus().Status, Placed) + assert.Equal(t, db.Orders[longOrderId].getOrderStatus().Status, Placed) }) t.Run("revert state of an order when OrderMatchingError is removed", func(t *testing.T) { @@ -707,8 +707,8 @@ func TestRemovedEvents(t *testing.T) { cep.ProcessEvents([]*types.Log{longOrderAcceptedEventLog}) // orders exist in memory now - assert.Equal(t, db.OrderMap[longOrderId].Salt, longOrder.Salt) - assert.Equal(t, db.OrderMap[longOrderId].getOrderStatus().Status, Placed) + assert.Equal(t, db.Orders[longOrderId].Salt, longOrder.Salt) + assert.Equal(t, db.Orders[longOrderId].getOrderStatus().Status, Placed) // fail matching orderMatchingError := getEventFromABI(orderBookABI, "OrderMatchingError") @@ -717,12 +717,12 @@ func TestRemovedEvents(t *testing.T) { orderMatchingErrorLog := getEventLog(OrderBookContractAddress, orderMatchingErrorTopics, orderMatchingErrorData, blockNumber.Uint64()+2) cep.ProcessEvents([]*types.Log{orderMatchingErrorLog}) - assert.Equal(t, db.OrderMap[longOrderId].getOrderStatus().Status, Execution_Failed) + assert.Equal(t, db.Orders[longOrderId].getOrderStatus().Status, Execution_Failed) // now un-fail it orderMatchingErrorLog.Removed = true cep.ProcessEvents([]*types.Log{orderMatchingErrorLog}) - assert.Equal(t, db.OrderMap[longOrderId].getOrderStatus().Status, Placed) + assert.Equal(t, db.Orders[longOrderId].getOrderStatus().Status, Placed) }) } diff --git a/plugin/evm/orderbook/matching_pipeline.go b/plugin/evm/orderbook/matching_pipeline.go index 7e4198cc8c..6da506ab1b 100644 --- a/plugin/evm/orderbook/matching_pipeline.go +++ b/plugin/evm/orderbook/matching_pipeline.go @@ -325,11 +325,3 @@ func removeOrdersWithIds(orders []Order, orderIds map[common.Hash]struct{}) []Or } return filteredOrders } - -func formatHashSlice(hashes []common.Hash) []string { - var formattedHashes []string - for _, hash := range hashes { - formattedHashes = append(formattedHashes, hash.String()) - } - return formattedHashes -} diff --git a/plugin/evm/orderbook/memory_database.go b/plugin/evm/orderbook/memory_database.go index 21071bf971..cb2d7062c9 100644 --- a/plugin/evm/orderbook/memory_database.go +++ b/plugin/evm/orderbook/memory_database.go @@ -19,7 +19,9 @@ import ( type InMemoryDatabase struct { mu *sync.RWMutex `json:"-"` - OrderMap map[common.Hash]*Order `json:"order_map"` // ID => order + Orders map[common.Hash]*Order `json:"order_map"` // ID => order + LongOrders map[Market][]*Order `json:"long_orders"` + ShortOrders map[Market][]*Order `json:"short_orders"` TraderMap map[common.Address]*Trader `json:"trader_map"` // address => trader info NextFundingTime uint64 `json:"next_funding_time"` LastPrice map[Market]*big.Int `json:"last_price"` @@ -29,12 +31,13 @@ type InMemoryDatabase struct { } func NewInMemoryDatabase(configService IConfigService) *InMemoryDatabase { - orderMap := map[common.Hash]*Order{} lastPrice := map[Market]*big.Int{} traderMap := map[common.Address]*Trader{} return &InMemoryDatabase{ - OrderMap: orderMap, + Orders: map[common.Hash]*Order{}, + LongOrders: map[Market][]*Order{}, + ShortOrders: map[Market][]*Order{}, TraderMap: traderMap, NextFundingTime: 0, LastPrice: lastPrice, @@ -213,6 +216,7 @@ type Trader struct { type LimitOrderDatabase interface { LoadFromSnapshot(snapshot Snapshot) error GetAllOrders() []Order + GetMarketOrders(market Market) []Order Add(order *Order) Delete(orderId common.Hash) UpdateFilledBaseAssetQuantity(quantity *big.Int, orderId common.Hash, blockNumber uint64) @@ -233,7 +237,7 @@ type LimitOrderDatabase interface { GetAllTraders() map[common.Address]Trader GetOrderBookData() InMemoryDatabase GetOrderBookDataCopy() (*InMemoryDatabase, error) - Accept(blockNumber uint64, blockTimestamp uint64) + Accept(acceptedBlockNumber uint64, blockTimestamp uint64) SetOrderStatus(orderId common.Hash, status Status, info string, blockNumber uint64) error RevertLastStatus(orderId common.Hash) error GetNaughtyTraders(oraclePrices map[Market]*big.Int, markets []Market) ([]LiquidablePosition, map[common.Address][]Order) @@ -253,11 +257,14 @@ func (db *InMemoryDatabase) LoadFromSnapshot(snapshot Snapshot) error { db.mu.Lock() defer db.mu.Unlock() - if snapshot.Data == nil || snapshot.Data.OrderMap == nil || snapshot.Data.TraderMap == nil || snapshot.Data.LastPrice == nil { + if snapshot.Data == nil || snapshot.Data.Orders == nil || snapshot.Data.LongOrders == nil || snapshot.Data.ShortOrders == nil || + snapshot.Data.TraderMap == nil || snapshot.Data.LastPrice == nil { return fmt.Errorf("invalid snapshot; snapshot=%+v", snapshot) } - db.OrderMap = snapshot.Data.OrderMap + db.Orders = snapshot.Data.Orders + db.LongOrders = snapshot.Data.LongOrders + db.ShortOrders = snapshot.Data.ShortOrders db.TraderMap = snapshot.Data.TraderMap db.LastPrice = snapshot.Data.LastPrice db.NextFundingTime = snapshot.Data.NextFundingTime @@ -298,7 +305,7 @@ func (db *InMemoryDatabase) Accept(acceptedBlockNumber, blockTimestamp uint64) { } if status == REMOVE { - delete(db.OrderMap, longOrder.Id) + db.deleteOrderWithoutLock(longOrder.Id) } } @@ -325,7 +332,7 @@ func (db *InMemoryDatabase) Accept(acceptedBlockNumber, blockTimestamp uint64) { } if status == REMOVE { - delete(db.OrderMap, shortOrder.Id) + db.deleteOrderWithoutLock(shortOrder.Id) } } } @@ -369,10 +376,10 @@ func (db *InMemoryDatabase) SetOrderStatus(orderId common.Hash, status Status, i db.mu.Lock() defer db.mu.Unlock() - if db.OrderMap[orderId] == nil { + if db.Orders[orderId] == nil { return fmt.Errorf("invalid orderId %s", orderId.Hex()) } - db.OrderMap[orderId].LifecycleList = append(db.OrderMap[orderId].LifecycleList, Lifecycle{blockNumber, status, info}) + db.Orders[orderId].LifecycleList = append(db.Orders[orderId].LifecycleList, Lifecycle{blockNumber, status, info}) return nil } @@ -380,13 +387,13 @@ func (db *InMemoryDatabase) RevertLastStatus(orderId common.Hash) error { db.mu.Lock() defer db.mu.Unlock() - if db.OrderMap[orderId] == nil { + if db.Orders[orderId] == nil { return fmt.Errorf("invalid orderId %s", orderId.Hex()) } - lifeCycleList := db.OrderMap[orderId].LifecycleList + lifeCycleList := db.Orders[orderId].LifecycleList if len(lifeCycleList) > 0 { - db.OrderMap[orderId].LifecycleList = lifeCycleList[:len(lifeCycleList)-1] + db.Orders[orderId].LifecycleList = lifeCycleList[:len(lifeCycleList)-1] } return nil } @@ -396,51 +403,157 @@ func (db *InMemoryDatabase) GetAllOrders() []Order { defer db.mu.RUnlock() allOrders := []Order{} - for _, order := range db.OrderMap { + for _, order := range db.Orders { allOrders = append(allOrders, deepCopyOrder(order)) } return allOrders } +func (db *InMemoryDatabase) GetMarketOrders(market Market) []Order { + db.mu.RLock() // only read lock required + defer db.mu.RUnlock() + + allOrders := []Order{} + for _, order := range db.LongOrders[market] { + allOrders = append(allOrders, deepCopyOrder(order)) + } + + for _, order := range db.ShortOrders[market] { + allOrders = append(allOrders, deepCopyOrder(order)) + } + + return allOrders +} + func (db *InMemoryDatabase) Add(order *Order) { db.mu.Lock() defer db.mu.Unlock() + market := order.Market order.LifecycleList = append(order.LifecycleList, Lifecycle{order.BlockNumber.Uint64(), Placed, ""}) - db.OrderMap[order.Id] = order + + var orders []*Order + var position int + if order.PositionType == LONG { + orders = db.LongOrders[market] + position = sort.Search(len(orders), func(i int) bool { + priceDiff := order.Price.Cmp(orders[i].Price) + if priceDiff == 1 { + return true + } else if priceDiff == 0 { + blockDiff := order.BlockNumber.Cmp(orders[i].BlockNumber) + if blockDiff == -1 { // order was placed before i + return true + } else if blockDiff == 0 { // order and i were placed in the same block + if order.OrderType == IOC { + // prioritize fulfilling IOC orders first, because they are short-lived + return true + } + } + } + return false + }) + + } else { + orders = db.ShortOrders[market] + position = sort.Search(len(orders), func(i int) bool { + priceDiff := order.Price.Cmp(orders[i].Price) + if priceDiff == -1 { + return true + } else if priceDiff == 0 { + blockDiff := order.BlockNumber.Cmp(orders[i].BlockNumber) + if blockDiff == -1 { // order was placed before i + return true + } else if blockDiff == 0 { // order and i were placed in the same block + if order.OrderType == IOC { + // prioritize fulfilling IOC orders first, because they are short-lived + return true + } + } + } + return false + }) + } + + // Insert the order at the determined position + orders = append(orders, &Order{}) // Add an empty order to the end + copy(orders[position+1:], orders[position:]) // Shift orders to the right + orders[position] = order // Insert new Order at the right position + + if order.PositionType == LONG { + db.LongOrders[market] = orders + } else { + db.ShortOrders[market] = orders + } + + db.Orders[order.Id] = order } func (db *InMemoryDatabase) Delete(orderId common.Hash) { db.mu.Lock() defer db.mu.Unlock() - delete(db.OrderMap, orderId) + db.deleteOrderWithoutLock(orderId) +} + +func (db *InMemoryDatabase) deleteOrderWithoutLock(orderId common.Hash) { + order := db.Orders[orderId] + if order == nil { + log.Error("In Delete - orderId does not exist in the db.Orders", "orderId", orderId.Hex()) + deleteOrderIdNotFoundCounter.Inc(1) + return + } + + market := order.Market + if order.PositionType == LONG { + orders := db.LongOrders[market] + idx := getOrderIdx(orders, orderId) + if idx == -1 { + log.Error("In Delete - orderId does not exist in the db.LongOrders", "orderId", orderId.Hex()) + deleteOrderIdNotFoundCounter.Inc(1) + } else { + orders = append(orders[:idx], orders[idx+1:]...) + db.LongOrders[market] = orders + } + } else { + orders := db.ShortOrders[market] + idx := getOrderIdx(orders, orderId) + if idx == -1 { + log.Error("In Delete - orderId does not exist in the db.ShortOrders", "orderId", orderId.Hex()) + deleteOrderIdNotFoundCounter.Inc(1) + } else { + orders = append(orders[:idx], orders[idx+1:]...) + db.ShortOrders[market] = orders + } + } + + delete(db.Orders, orderId) } func (db *InMemoryDatabase) UpdateFilledBaseAssetQuantity(quantity *big.Int, orderId common.Hash, blockNumber uint64) { db.mu.Lock() defer db.mu.Unlock() - limitOrder := db.OrderMap[orderId] - if limitOrder == nil { + order := db.Orders[orderId] + if order == nil { log.Error("In UpdateFilledBaseAssetQuantity - orderId does not exist in the database", "orderId", orderId.Hex()) metrics.GetOrRegisterCounter("update_filled_base_asset_quantity_order_id_not_found", nil).Inc(1) return } - if limitOrder.PositionType == LONG { - limitOrder.FilledBaseAssetQuantity.Add(limitOrder.FilledBaseAssetQuantity, quantity) // filled = filled + quantity + if order.PositionType == LONG { + order.FilledBaseAssetQuantity.Add(order.FilledBaseAssetQuantity, quantity) // filled = filled + quantity } - if limitOrder.PositionType == SHORT { - limitOrder.FilledBaseAssetQuantity.Sub(limitOrder.FilledBaseAssetQuantity, quantity) // filled = filled - quantity + if order.PositionType == SHORT { + order.FilledBaseAssetQuantity.Sub(order.FilledBaseAssetQuantity, quantity) // filled = filled - quantity } - if limitOrder.BaseAssetQuantity.Cmp(limitOrder.FilledBaseAssetQuantity) == 0 { - limitOrder.LifecycleList = append(limitOrder.LifecycleList, Lifecycle{blockNumber, FulFilled, ""}) + if order.BaseAssetQuantity.Cmp(order.FilledBaseAssetQuantity) == 0 { + order.LifecycleList = append(order.LifecycleList, Lifecycle{blockNumber, FulFilled, ""}) } - if quantity.Cmp(big.NewInt(0)) == -1 && limitOrder.getOrderStatus().Status == FulFilled { + if quantity.Cmp(big.NewInt(0)) == -1 && order.getOrderStatus().Status == FulFilled { // handling reorgs - limitOrder.LifecycleList = limitOrder.LifecycleList[:len(limitOrder.LifecycleList)-1] + order.LifecycleList = order.LifecycleList[:len(order.LifecycleList)-1] } } @@ -480,18 +593,22 @@ func (db *InMemoryDatabase) GetLongOrders(market Market, lowerbound *big.Int, bl func (db *InMemoryDatabase) getLongOrdersWithoutLock(market Market, lowerbound *big.Int, blockNumber *big.Int, shouldClean bool) []Order { var longOrders []Order - for _, order := range db.OrderMap { - if order.PositionType == LONG && order.Market == market && (lowerbound == nil || order.Price.Cmp(lowerbound) >= 0) { - if shouldClean { - if _order := db.getCleanOrder(order, blockNumber); _order != nil { - longOrders = append(longOrders, *_order) - } - } else { - longOrders = append(longOrders, *order) + + marketOrders := db.LongOrders[market] + for _, order := range marketOrders { + if lowerbound != nil && order.Price.Cmp(lowerbound) < 0 { + // because the long orders are sorted in descending order of price, there is no point in checking further + break + } + + if shouldClean { + if _order := db.getCleanOrder(order, blockNumber); _order != nil { + longOrders = append(longOrders, *_order) } + } else { + longOrders = append(longOrders, deepCopyOrder(order)) } } - sortLongOrders(longOrders) return longOrders } @@ -503,18 +620,22 @@ func (db *InMemoryDatabase) GetShortOrders(market Market, upperbound *big.Int, b func (db *InMemoryDatabase) getShortOrdersWithoutLock(market Market, upperbound *big.Int, blockNumber *big.Int, shouldClean bool) []Order { var shortOrders []Order - for _, order := range db.OrderMap { - if order.PositionType == SHORT && order.Market == market && (upperbound == nil || order.Price.Cmp(upperbound) <= 0) { - if shouldClean { - if _order := db.getCleanOrder(order, blockNumber); _order != nil { - shortOrders = append(shortOrders, *_order) - } - } else { - shortOrders = append(shortOrders, *order) + + marketOrders := db.ShortOrders[market] + + for _, order := range marketOrders { + if upperbound != nil && order.Price.Cmp(upperbound) > 0 { + // short orders are sorted in ascending order of price + break + } + if shouldClean { + if _order := db.getCleanOrder(order, blockNumber); _order != nil { + shortOrders = append(shortOrders, *_order) } + } else { + shortOrders = append(shortOrders, deepCopyOrder(order)) } } - sortShortOrders(shortOrders) return shortOrders } @@ -740,7 +861,7 @@ func (db *InMemoryDatabase) GetOrderById(orderId common.Hash) *Order { db.mu.RLock() defer db.mu.RUnlock() - order := db.OrderMap[orderId] + order := db.Orders[orderId] if order == nil { return nil } @@ -873,7 +994,7 @@ func (db *InMemoryDatabase) determineOrdersToCancel(addr common.Address, trader func (db *InMemoryDatabase) getTraderOrders(trader common.Address, orderType OrderType) []Order { traderOrders := []Order{} - for _, order := range db.OrderMap { + for _, order := range db.Orders { if order.Trader == trader && order.OrderType == orderType { traderOrders = append(traderOrders, deepCopyOrder(order)) } @@ -883,7 +1004,7 @@ func (db *InMemoryDatabase) getTraderOrders(trader common.Address, orderType Ord func (db *InMemoryDatabase) getAllTraderOrders(trader common.Address) []Order { traderOrders := []Order{} - for _, order := range db.OrderMap { + for _, order := range db.Orders { if order.Trader == trader { traderOrders = append(traderOrders, deepCopyOrder(order)) } @@ -918,46 +1039,6 @@ func (db *InMemoryDatabase) getReduceOnlyOrderDisplay(order *Order) *Order { } } -func sortLongOrders(orders []Order) { - sort.SliceStable(orders, func(i, j int) bool { - priceDiff := orders[i].Price.Cmp(orders[j].Price) - if priceDiff == 1 { - return true - } else if priceDiff == 0 { - blockDiff := orders[i].BlockNumber.Cmp(orders[j].BlockNumber) - if blockDiff == -1 { // i was placed before j - return true - } else if blockDiff == 0 { // i and j were placed in the same block - if orders[i].OrderType == IOC { - // prioritize fulfilling IOC orders first, because they are short lived - return true - } - } - } - return false - }) -} - -func sortShortOrders(orders []Order) { - sort.SliceStable(orders, func(i, j int) bool { - priceDiff := orders[i].Price.Cmp(orders[j].Price) - if priceDiff == -1 { - return true - } else if priceDiff == 0 { - blockDiff := orders[i].BlockNumber.Cmp(orders[j].BlockNumber) - if blockDiff == -1 { // i was placed before j - return true - } else if blockDiff == 0 { // i and j were placed in the same block - if orders[i].OrderType == IOC { - // prioritize fulfilling IOC orders first, because they are short lived - return true - } - } - } - return false - }) -} - func (db *InMemoryDatabase) GetOrderBookData() InMemoryDatabase { db.mu.RLock() defer db.mu.RUnlock() @@ -1073,3 +1154,12 @@ func deepCopyTrader(order *Trader) *Trader { Margin: margin, } } + +func getOrderIdx(orders []*Order, orderId common.Hash) int { + for i, order := range orders { + if order.Id == orderId { + return i + } + } + return -1 +} diff --git a/plugin/evm/orderbook/memory_database_test.go b/plugin/evm/orderbook/memory_database_test.go index 19a7befcac..bafd3b2f2c 100644 --- a/plugin/evm/orderbook/memory_database_test.go +++ b/plugin/evm/orderbook/memory_database_test.go @@ -26,13 +26,117 @@ func TestgetDatabase(t *testing.T) { assert.NotNil(t, inMemoryDatabase) } +func TestAddSequence(t *testing.T) { + baseAssetQuantity := big.NewInt(10) + db := getDatabase() + + t.Run("Long orders", func(t *testing.T) { + order1 := createLimitOrder(LONG, userAddress, baseAssetQuantity, big.NewInt(20), status, big.NewInt(2), big.NewInt(1)) + db.Add(&order1) + + assert.Equal(t, 1, len(db.Orders)) + assert.Equal(t, 1, len(db.LongOrders[market])) + assert.Equal(t, db.LongOrders[market][0].Id, order1.Id) + + order2 := createLimitOrder(LONG, userAddress, baseAssetQuantity, big.NewInt(21), status, big.NewInt(2), big.NewInt(2)) + db.Add(&order2) + + assert.Equal(t, 2, len(db.Orders)) + assert.Equal(t, 2, len(db.LongOrders[market])) + assert.Equal(t, db.LongOrders[market][0].Id, order2.Id) + assert.Equal(t, db.LongOrders[market][1].Id, order1.Id) + + order3 := createLimitOrder(LONG, userAddress, baseAssetQuantity, big.NewInt(19), status, big.NewInt(2), big.NewInt(3)) + db.Add(&order3) + + assert.Equal(t, 3, len(db.Orders)) + assert.Equal(t, 3, len(db.LongOrders[market])) + assert.Equal(t, db.LongOrders[market][0].Id, order2.Id) + assert.Equal(t, db.LongOrders[market][1].Id, order1.Id) + assert.Equal(t, db.LongOrders[market][2].Id, order3.Id) + + // block number + order4 := createLimitOrder(LONG, userAddress, baseAssetQuantity, big.NewInt(20), status, big.NewInt(3), big.NewInt(4)) + db.Add(&order4) + + assert.Equal(t, 4, len(db.Orders)) + assert.Equal(t, 4, len(db.LongOrders[market])) + assert.Equal(t, db.LongOrders[market][0].Id, order2.Id) + assert.Equal(t, db.LongOrders[market][1].Id, order1.Id) + assert.Equal(t, db.LongOrders[market][2].Id, order4.Id) + assert.Equal(t, db.LongOrders[market][3].Id, order3.Id) + + // ioc order + order5 := createIOCOrder(LONG, userAddress, baseAssetQuantity, big.NewInt(20), status, big.NewInt(2), big.NewInt(5), big.NewInt(2)) + db.Add(&order5) + + assert.Equal(t, 5, len(db.Orders)) + assert.Equal(t, 5, len(db.LongOrders[market])) + assert.Equal(t, db.LongOrders[market][0].Id, order2.Id) + assert.Equal(t, db.LongOrders[market][1].Id, order5.Id) + assert.Equal(t, db.LongOrders[market][2].Id, order1.Id) + assert.Equal(t, db.LongOrders[market][3].Id, order4.Id) + assert.Equal(t, db.LongOrders[market][4].Id, order3.Id) + }) + + t.Run("Short orders", func(t *testing.T) { + baseAssetQuantity = big.NewInt(-10) + order1 := createLimitOrder(SHORT, userAddress, baseAssetQuantity, big.NewInt(20), status, big.NewInt(2), big.NewInt(6)) + db.Add(&order1) + + assert.Equal(t, 6, len(db.Orders)) + assert.Equal(t, 1, len(db.ShortOrders[market])) + assert.Equal(t, db.ShortOrders[market][0].Id, order1.Id) + + order2 := createLimitOrder(SHORT, userAddress, baseAssetQuantity, big.NewInt(19), status, big.NewInt(2), big.NewInt(7)) + db.Add(&order2) + + assert.Equal(t, 7, len(db.Orders)) + assert.Equal(t, 2, len(db.ShortOrders[market])) + assert.Equal(t, db.ShortOrders[market][0].Id, order2.Id) + assert.Equal(t, db.ShortOrders[market][1].Id, order1.Id) + + order3 := createLimitOrder(SHORT, userAddress, baseAssetQuantity, big.NewInt(21), status, big.NewInt(2), big.NewInt(8)) + db.Add(&order3) + + assert.Equal(t, 8, len(db.Orders)) + assert.Equal(t, 3, len(db.ShortOrders[market])) + assert.Equal(t, db.ShortOrders[market][0].Id, order2.Id) + assert.Equal(t, db.ShortOrders[market][1].Id, order1.Id) + assert.Equal(t, db.ShortOrders[market][2].Id, order3.Id) + + // block number + order4 := createLimitOrder(SHORT, userAddress, baseAssetQuantity, big.NewInt(20), status, big.NewInt(3), big.NewInt(9)) + db.Add(&order4) + + assert.Equal(t, 9, len(db.Orders)) + assert.Equal(t, 4, len(db.ShortOrders[market])) + assert.Equal(t, db.ShortOrders[market][0].Id, order2.Id) + assert.Equal(t, db.ShortOrders[market][1].Id, order1.Id) + assert.Equal(t, db.ShortOrders[market][2].Id, order4.Id) + assert.Equal(t, db.ShortOrders[market][3].Id, order3.Id) + + // ioc order + order5 := createIOCOrder(SHORT, userAddress, baseAssetQuantity, big.NewInt(20), status, big.NewInt(2), big.NewInt(10), big.NewInt(2)) + db.Add(&order5) + + assert.Equal(t, 10, len(db.Orders)) + assert.Equal(t, 5, len(db.ShortOrders[market])) + assert.Equal(t, db.ShortOrders[market][0].Id, order2.Id) + assert.Equal(t, db.ShortOrders[market][1].Id, order5.Id) + assert.Equal(t, db.ShortOrders[market][2].Id, order1.Id) + assert.Equal(t, db.ShortOrders[market][3].Id, order4.Id) + assert.Equal(t, db.ShortOrders[market][4].Id, order3.Id) + }) +} + func TestAdd(t *testing.T) { baseAssetQuantity := big.NewInt(-10) inMemoryDatabase := getDatabase() salt := big.NewInt(time.Now().Unix()) limitOrder := createLimitOrder(positionType, userAddress, baseAssetQuantity, price, status, blockNumber, salt) inMemoryDatabase.Add(&limitOrder) - returnedOrder := inMemoryDatabase.OrderMap[limitOrder.Id] + returnedOrder := inMemoryDatabase.Orders[limitOrder.Id] assert.Equal(t, limitOrder.PositionType, returnedOrder.PositionType) assert.Equal(t, limitOrder.Trader, returnedOrder.Trader) assert.Equal(t, limitOrder.BaseAssetQuantity, returnedOrder.BaseAssetQuantity) @@ -204,6 +308,56 @@ func TestGetLongOrders(t *testing.T) { } } +func TestDeleteOrders(t *testing.T) { + db := getDatabase() + + order1 := createLimitOrder(SHORT, userAddress, big.NewInt(-10), big.NewInt(20), status, big.NewInt(2), big.NewInt(1)) + order2 := createLimitOrder(SHORT, userAddress, big.NewInt(-10), big.NewInt(19), status, big.NewInt(2), big.NewInt(2)) + order3 := createLimitOrder(SHORT, userAddress, big.NewInt(-10), big.NewInt(21), status, big.NewInt(2), big.NewInt(3)) + order4 := createLimitOrder(LONG, userAddress, big.NewInt(10), big.NewInt(20), status, big.NewInt(2), big.NewInt(4)) + order5 := createLimitOrder(LONG, userAddress, big.NewInt(10), big.NewInt(19), status, big.NewInt(2), big.NewInt(5)) + order6 := createLimitOrder(LONG, userAddress, big.NewInt(10), big.NewInt(21), status, big.NewInt(2), big.NewInt(6)) + + db.Add(&order1) + db.Add(&order2) + db.Add(&order3) + db.Add(&order4) + db.Add(&order5) + db.Add(&order6) + + assert.Equal(t, 6, len(db.Orders)) + assert.Equal(t, 3, len(db.ShortOrders[market])) + assert.Equal(t, 3, len(db.LongOrders[market])) + + db.Delete(order1.Id) + assert.Equal(t, 5, len(db.Orders)) + assert.Equal(t, 2, len(db.ShortOrders[market])) + assert.Equal(t, 3, len(db.LongOrders[market])) + assert.Equal(t, -1, getOrderIdx(db.ShortOrders[market], order1.Id)) + assert.Nil(t, db.Orders[order1.Id]) + + db.Delete(order5.Id) + assert.Equal(t, 4, len(db.Orders)) + assert.Equal(t, 2, len(db.ShortOrders[market])) + assert.Equal(t, 2, len(db.LongOrders[market])) + assert.Equal(t, -1, getOrderIdx(db.LongOrders[market], order5.Id)) + assert.Nil(t, db.Orders[order5.Id]) + + db.Delete(order3.Id) + assert.Equal(t, 3, len(db.Orders)) + assert.Equal(t, 1, len(db.ShortOrders[market])) + assert.Equal(t, 2, len(db.LongOrders[market])) + assert.Equal(t, -1, getOrderIdx(db.ShortOrders[market], order3.Id)) + assert.Nil(t, db.Orders[order3.Id]) + + db.Delete(order2.Id) + assert.Equal(t, 2, len(db.Orders)) + assert.Equal(t, 0, len(db.ShortOrders[market])) + assert.Equal(t, 2, len(db.LongOrders[market])) + assert.Equal(t, -1, getOrderIdx(db.ShortOrders[market], order2.Id)) + assert.Nil(t, db.Orders[order2.Id]) +} + func TestGetCancellableOrders(t *testing.T) { // also tests getTotalNotionalPositionAndUnrealizedPnl inMemoryDatabase := getDatabase() @@ -306,7 +460,7 @@ func TestUpdateFulfilledBaseAssetQuantityLimitOrder(t *testing.T) { filledQuantity := big.NewInt(2) inMemoryDatabase.UpdateFilledBaseAssetQuantity(filledQuantity, limitOrder.Id, 69) - updatedLimitOrder := inMemoryDatabase.OrderMap[limitOrder.Id] + updatedLimitOrder := inMemoryDatabase.Orders[limitOrder.Id] assert.Equal(t, updatedLimitOrder.FilledBaseAssetQuantity, big.NewInt(0).Neg(filledQuantity)) assert.Equal(t, updatedLimitOrder.FilledBaseAssetQuantity, filledQuantity.Mul(filledQuantity, big.NewInt(-1))) @@ -321,7 +475,7 @@ func TestUpdateFulfilledBaseAssetQuantityLimitOrder(t *testing.T) { filledQuantity := big.NewInt(2) inMemoryDatabase.UpdateFilledBaseAssetQuantity(filledQuantity, limitOrder.Id, 69) - updatedLimitOrder := inMemoryDatabase.OrderMap[limitOrder.Id] + updatedLimitOrder := inMemoryDatabase.Orders[limitOrder.Id] assert.Equal(t, updatedLimitOrder.FilledBaseAssetQuantity, filledQuantity) }) @@ -438,15 +592,15 @@ func TestAccept(t *testing.T) { err := inMemoryDatabase.SetOrderStatus(orderId1, FulFilled, "", 51) assert.Nil(t, err) - assert.Equal(t, inMemoryDatabase.OrderMap[orderId1].getOrderStatus().Status, FulFilled) + assert.Equal(t, inMemoryDatabase.Orders[orderId1].getOrderStatus().Status, FulFilled) inMemoryDatabase.Accept(51, 51) // fulfilled order is deleted - _, ok := inMemoryDatabase.OrderMap[orderId1] + _, ok := inMemoryDatabase.Orders[orderId1] assert.False(t, ok) // unfulfilled order still exists - _, ok = inMemoryDatabase.OrderMap[orderId2] + _, ok = inMemoryDatabase.Orders[orderId2] assert.True(t, ok) }) @@ -455,11 +609,11 @@ func TestAccept(t *testing.T) { orderId := addLimitOrder(inMemoryDatabase) err := inMemoryDatabase.SetOrderStatus(orderId, FulFilled, "", 51) assert.Nil(t, err) - assert.Equal(t, inMemoryDatabase.OrderMap[orderId].getOrderStatus().Status, FulFilled) + assert.Equal(t, inMemoryDatabase.Orders[orderId].getOrderStatus().Status, FulFilled) inMemoryDatabase.Accept(52, 52) - _, ok := inMemoryDatabase.OrderMap[orderId] + _, ok := inMemoryDatabase.Orders[orderId] assert.False(t, ok) }) @@ -468,11 +622,11 @@ func TestAccept(t *testing.T) { orderId := addLimitOrder(inMemoryDatabase) err := inMemoryDatabase.SetOrderStatus(orderId, FulFilled, "", 51) assert.Nil(t, err) - assert.Equal(t, inMemoryDatabase.OrderMap[orderId].getOrderStatus().Status, FulFilled) + assert.Equal(t, inMemoryDatabase.Orders[orderId].getOrderStatus().Status, FulFilled) inMemoryDatabase.Accept(50, 50) - _, ok := inMemoryDatabase.OrderMap[orderId] + _, ok := inMemoryDatabase.Orders[orderId] assert.True(t, ok) }) @@ -481,7 +635,7 @@ func TestAccept(t *testing.T) { orderId := addLimitOrder(inMemoryDatabase) inMemoryDatabase.Accept(50, 50) - _, ok := inMemoryDatabase.OrderMap[orderId] + _, ok := inMemoryDatabase.Orders[orderId] assert.True(t, ok) }) } @@ -502,7 +656,7 @@ func TestRevertLastStatus(t *testing.T) { err := inMemoryDatabase.RevertLastStatus(orderId) assert.Nil(t, err) - assert.Equal(t, len(inMemoryDatabase.OrderMap[orderId].LifecycleList), 0) + assert.Equal(t, len(inMemoryDatabase.Orders[orderId].LifecycleList), 0) }) t.Run("revert status for fulfilled order", func(t *testing.T) { @@ -514,8 +668,8 @@ func TestRevertLastStatus(t *testing.T) { err = inMemoryDatabase.RevertLastStatus(orderId) assert.Nil(t, err) - assert.Equal(t, len(inMemoryDatabase.OrderMap[orderId].LifecycleList), 1) - assert.Equal(t, inMemoryDatabase.OrderMap[orderId].LifecycleList[0].BlockNumber, uint64(2)) + assert.Equal(t, len(inMemoryDatabase.Orders[orderId].LifecycleList), 1) + assert.Equal(t, inMemoryDatabase.Orders[orderId].LifecycleList[0].BlockNumber, uint64(2)) }) t.Run("revert status for accepted + fulfilled order - expect error", func(t *testing.T) { diff --git a/plugin/evm/orderbook/metrics.go b/plugin/evm/orderbook/metrics.go index 794d6ee8ae..9c52d12cfa 100644 --- a/plugin/evm/orderbook/metrics.go +++ b/plugin/evm/orderbook/metrics.go @@ -28,4 +28,7 @@ var ( // lag between head and accepted block headBlockLagHistogram = metrics.NewRegisteredHistogram("head_block_lag", nil, metrics.ResettingSample(metrics.NewExpDecaySample(1028, 0.015))) + + // order id not found while deleting + deleteOrderIdNotFoundCounter = metrics.NewRegisteredCounter("delete_order_id_not_found", nil) ) diff --git a/plugin/evm/orderbook/mocks.go b/plugin/evm/orderbook/mocks.go index 02ebfe5e0c..eca6d6b7d4 100644 --- a/plugin/evm/orderbook/mocks.go +++ b/plugin/evm/orderbook/mocks.go @@ -32,6 +32,11 @@ func (db *MockLimitOrderDatabase) GetAllOrders() []Order { return args.Get(0).([]Order) } +func (db *MockLimitOrderDatabase) GetMarketOrders(market Market) []Order { + args := db.Called() + return args.Get(0).([]Order) +} + func (db *MockLimitOrderDatabase) Add(order *Order) { } diff --git a/plugin/evm/orderbook/service.go b/plugin/evm/orderbook/service.go index 07878e09ab..05921293d7 100644 --- a/plugin/evm/orderbook/service.go +++ b/plugin/evm/orderbook/service.go @@ -140,14 +140,18 @@ func (api *OrderBookAPI) GetOrderBook(ctx context.Context, marketStr string) (*O if err != nil { return nil, err } - allOrders := api.db.GetAllOrders() - orders := []OrderMin{} - for _, order := range allOrders { - if market == nil || order.Market == Market(*market) { - orders = append(orders, order.ToOrderMin()) - } + var orders []Order + if market == nil { + orders = api.db.GetAllOrders() + } else { + orders = api.db.GetMarketOrders(Market(*market)) + } + + responseOrders := []OrderMin{} + for _, order := range orders { + responseOrders = append(responseOrders, order.ToOrderMin()) } - return &OrderBookResponse{Orders: orders}, nil + return &OrderBookResponse{Orders: responseOrders}, nil } func parseMarket(marketStr string) (*int, error) { diff --git a/plugin/evm/orderbook_test.go b/plugin/evm/orderbook_test.go index c0a8d21e20..6c8b1d1592 100644 --- a/plugin/evm/orderbook_test.go +++ b/plugin/evm/orderbook_test.go @@ -1003,22 +1003,22 @@ func TestHubbleLogs(t *testing.T) { t.Logf("VM2 Orders: %+v", detail2) // verify that order 1, 2, 3 are in both VMs - if order := filterOrderMapBySalt(detail1.OrderMap, big.NewInt(101)); order == nil { + if order := filterOrderMapBySalt(detail1.Orders, big.NewInt(101)); order == nil { t.Fatalf("Order 1 is not in VM1") } - if order := filterOrderMapBySalt(detail2.OrderMap, big.NewInt(101)); order == nil { + if order := filterOrderMapBySalt(detail2.Orders, big.NewInt(101)); order == nil { t.Fatalf("Order 1 is not in VM2") } - if order := filterOrderMapBySalt(detail1.OrderMap, big.NewInt(102)); order == nil { + if order := filterOrderMapBySalt(detail1.Orders, big.NewInt(102)); order == nil { t.Fatalf("Order 2 is not in VM1") } - if order := filterOrderMapBySalt(detail2.OrderMap, big.NewInt(102)); order == nil { + if order := filterOrderMapBySalt(detail2.Orders, big.NewInt(102)); order == nil { t.Fatalf("Order 2 is not in VM2") } - if order := filterOrderMapBySalt(detail1.OrderMap, big.NewInt(104)); order == nil { + if order := filterOrderMapBySalt(detail1.Orders, big.NewInt(104)); order == nil { t.Fatalf("Order 3 is not in VM1") } - if order := filterOrderMapBySalt(detail2.OrderMap, big.NewInt(104)); order == nil { + if order := filterOrderMapBySalt(detail2.Orders, big.NewInt(104)); order == nil { t.Fatalf("Order 3 is not in VM2") } }