Skip to content

Commit

Permalink
Make IOC orders great again (#110)
Browse files Browse the repository at this point in the history
* New DetermineFillPrice

* modify runMatchingEngine

* refactor Accept

* juror.determineLiquidationFillPrice

* fix tests

* remove comment

* migrate oever tests to juror
  • Loading branch information
atvanguard authored Sep 13, 2023
1 parent 64f6981 commit 01bd9d5
Show file tree
Hide file tree
Showing 10 changed files with 660 additions and 793 deletions.
45 changes: 31 additions & 14 deletions plugin/evm/orderbook/matching_pipeline.go
Original file line number Diff line number Diff line change
Expand Up @@ -225,35 +225,52 @@ func (pipeline *MatchingPipeline) runLiquidations(liquidablePositions []Liquidab
}

func (pipeline *MatchingPipeline) runMatchingEngine(lotp LimitOrderTxProcessor, longOrders []Order, shortOrders []Order) {
if len(longOrders) == 0 || len(shortOrders) == 0 {
return
}

matchingComplete := false
for i := 0; i < len(longOrders); i++ {
// if there are no short orders or if the price of the first long order is < the price of the first short order, then we can stop matching
if len(shortOrders) == 0 || longOrders[i].Price.Cmp(shortOrders[0].Price) == -1 {
break
}
numOrdersExhausted := 0
for j := 0; j < len(shortOrders); j++ {
var ordersMatched bool
longOrders[i], shortOrders[j], ordersMatched = matchLongAndShortOrder(lotp, longOrders[i], shortOrders[j])
if !ordersMatched {
matchingComplete = true
break

fillAmount := areMatchingOrders(longOrders[i], shortOrders[j])
if fillAmount == nil {
continue
}
longOrders[i], shortOrders[j] = ExecuteMatchedOrders(lotp, longOrders[i], shortOrders[j], fillAmount)
if shortOrders[j].GetUnFilledBaseAssetQuantity().Sign() == 0 {
numOrdersExhausted++
}
if longOrders[i].GetUnFilledBaseAssetQuantity().Sign() == 0 {
break
}
}
if matchingComplete {
break
}
shortOrders = shortOrders[numOrdersExhausted:]
}
}

func areMatchingOrders(longOrder, shortOrder Order) *big.Int {
if longOrder.Price.Cmp(shortOrder.Price) == -1 {
return nil
}
blockDiff := longOrder.BlockNumber.Cmp(shortOrder.BlockNumber)
if blockDiff == -1 && (longOrder.OrderType == IOCOrderType || shortOrder.isPostOnly()) ||
blockDiff == 1 && (shortOrder.OrderType == IOCOrderType || longOrder.isPostOnly()) {
return nil
}
fillAmount := utils.BigIntMinAbs(longOrder.GetUnFilledBaseAssetQuantity(), shortOrder.GetUnFilledBaseAssetQuantity())
if fillAmount.Sign() == 0 {
return nil
}
return fillAmount
}

func ExecuteMatchedOrders(lotp LimitOrderTxProcessor, longOrder, shortOrder Order, fillAmount *big.Int) (Order, Order) {
lotp.ExecuteMatchedOrdersTx(longOrder, shortOrder, fillAmount)
longOrder.FilledBaseAssetQuantity = big.NewInt(0).Add(longOrder.FilledBaseAssetQuantity, fillAmount)
shortOrder.FilledBaseAssetQuantity = big.NewInt(0).Sub(shortOrder.FilledBaseAssetQuantity, fillAmount)
return longOrder, shortOrder
}

func matchLongAndShortOrder(lotp LimitOrderTxProcessor, longOrder, shortOrder Order) (Order, Order, bool) {
fillAmount := utils.BigIntMinAbs(longOrder.GetUnFilledBaseAssetQuantity(), shortOrder.GetUnFilledBaseAssetQuantity())
if longOrder.Price.Cmp(shortOrder.Price) == -1 || fillAmount.Sign() == 0 {
Expand Down
155 changes: 129 additions & 26 deletions plugin/evm/orderbook/memory_database.go
Original file line number Diff line number Diff line change
Expand Up @@ -271,23 +271,102 @@ func (db *InMemoryDatabase) LoadFromSnapshot(snapshot Snapshot) error {
return nil
}

// assumes that lock is held by the caller
func (db *InMemoryDatabase) Accept(blockNumber uint64, blockTimestamp uint64) {
func (db *InMemoryDatabase) Accept(acceptedBlockNumber, blockTimestamp uint64) {
db.mu.Lock()
defer db.mu.Unlock()

for orderId, order := range db.OrderMap {
lifecycle := order.getOrderStatus()
if (lifecycle.Status == FulFilled || lifecycle.Status == Cancelled) && lifecycle.BlockNumber <= blockNumber {
delete(db.OrderMap, orderId)
continue
count := db.configService.GetActiveMarketsCount()
for m := int64(0); m < count; m++ {
longOrders := db.getLongOrdersWithoutLock(Market(m), nil, nil, false)
shortOrders := db.getShortOrdersWithoutLock(Market(m), nil, nil, false)

for _, longOrder := range longOrders {
status := shouldRemove(acceptedBlockNumber, blockTimestamp, longOrder)
if status == CHECK_FOR_MATCHES {
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 {
delete(db.OrderMap, longOrder.Id)
}
}
expireAt := order.getExpireAt()
if expireAt.Sign() > 0 && expireAt.Int64() < int64(blockTimestamp) {
delete(db.OrderMap, orderId)

for _, shortOrder := range shortOrders {
status := shouldRemove(acceptedBlockNumber, blockTimestamp, shortOrder)
if status == CHECK_FOR_MATCHES {
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 {
delete(db.OrderMap, shortOrder.Id)
}
}
}
}

type OrderStatus uint8

const (
KEEP OrderStatus = iota
REMOVE
CHECK_FOR_MATCHES
)

func shouldRemove(acceptedBlockNumber, blockTimestamp uint64, order Order) OrderStatus {
// check if there is any criteria to delete the order
// 1. Order is fulfilled or cancelled
lifecycle := order.getOrderStatus()
if (lifecycle.Status == FulFilled || lifecycle.Status == Cancelled) && lifecycle.BlockNumber <= acceptedBlockNumber {
return REMOVE
}

if order.OrderType != IOCOrderType {
return KEEP
}

// 2. if the order is expired
expireAt := order.getExpireAt()
if expireAt.Sign() > 0 && expireAt.Int64() < int64(blockTimestamp) {
return REMOVE
}

// 3. 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.BlockNumber.Uint64() > acceptedBlockNumber {
return KEEP
}
return CHECK_FOR_MATCHES
}

func (db *InMemoryDatabase) SetOrderStatus(orderId common.Hash, status Status, info string, blockNumber uint64) error {
Expand Down Expand Up @@ -400,12 +479,19 @@ func (db *InMemoryDatabase) UpdateNextSamplePITime(nextSamplePITime uint64) {
func (db *InMemoryDatabase) GetLongOrders(market Market, lowerbound *big.Int, blockNumber *big.Int) []Order {
db.mu.RLock()
defer db.mu.RUnlock()
return db.getLongOrdersWithoutLock(market, lowerbound, blockNumber, true)
}

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 _order := db.getCleanOrder(order, blockNumber); _order != nil {
longOrders = append(longOrders, *_order)
if shouldClean {
if _order := db.getCleanOrder(order, blockNumber); _order != nil {
longOrders = append(longOrders, *_order)
}
} else {
longOrders = append(longOrders, *order)
}
}
}
Expand All @@ -416,12 +502,19 @@ func (db *InMemoryDatabase) GetLongOrders(market Market, lowerbound *big.Int, bl
func (db *InMemoryDatabase) GetShortOrders(market Market, upperbound *big.Int, blockNumber *big.Int) []Order {
db.mu.RLock()
defer db.mu.RUnlock()
return db.getShortOrdersWithoutLock(market, upperbound, blockNumber, true)
}

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 _order := db.getCleanOrder(order, blockNumber); _order != nil {
shortOrders = append(shortOrders, *_order)
if shouldClean {
if _order := db.getCleanOrder(order, blockNumber); _order != nil {
shortOrders = append(shortOrders, *_order)
}
} else {
shortOrders = append(shortOrders, *order)
}
}
}
Expand Down Expand Up @@ -829,34 +922,44 @@ func (db *InMemoryDatabase) getReduceOnlyOrderDisplay(order *Order) *Order {
}
}

func sortLongOrders(orders []Order) []Order {
func sortLongOrders(orders []Order) {
sort.SliceStable(orders, func(i, j int) bool {
if orders[i].Price.Cmp(orders[j].Price) == 1 {
priceDiff := orders[i].Price.Cmp(orders[j].Price)
if priceDiff == 1 {
return true
}
if orders[i].Price.Cmp(orders[j].Price) == 0 {
if orders[i].BlockNumber.Cmp(orders[j].BlockNumber) == -1 {
} 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 == IOCOrderType {
// prioritize fulfilling IOC orders first, because they are short lived
return true
}
}
}
return false
})
return orders
}

func sortShortOrders(orders []Order) []Order {
func sortShortOrders(orders []Order) {
sort.SliceStable(orders, func(i, j int) bool {
if orders[i].Price.Cmp(orders[j].Price) == -1 {
priceDiff := orders[i].Price.Cmp(orders[j].Price)
if priceDiff == -1 {
return true
}
if orders[i].Price.Cmp(orders[j].Price) == 0 {
if orders[i].BlockNumber.Cmp(orders[j].BlockNumber) == -1 {
} 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 == IOCOrderType {
// prioritize fulfilling IOC orders first, because they are short lived
return true
}
}
}
return false
})
return orders
}

func (db *InMemoryDatabase) GetOrderBookData() InMemoryDatabase {
Expand Down
1 change: 0 additions & 1 deletion plugin/evm/orderbook/tx_processor.go
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,6 @@ func (lotp *limitOrderTxProcessor) ExecuteMatchedOrdersTx(longOrder Order, short
return err
}

// log.Info("ExecuteMatchedOrdersTx", "orders[0]", hex.EncodeToString(orders[0]), "orders[1]", hex.EncodeToString(orders[1]), "fillAmount", prettifyScaledBigInt(fillAmount, 18))
txHash, err := lotp.executeLocalTx(lotp.orderBookContractAddress, lotp.orderBookABI, "executeMatchedOrders", orders, fillAmount)
log.Info("ExecuteMatchedOrdersTx", "LongOrder", longOrder, "ShortOrder", shortOrder, "fillAmount", prettifyScaledBigInt(fillAmount, 18), "txHash", txHash.String(), "err", err)
return err
Expand Down
15 changes: 5 additions & 10 deletions precompile/contracts/bibliophile/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,6 @@ type BibliophileClient interface {
GetTakerFee() *big.Int
//orderbook
GetSize(market common.Address, trader *common.Address) *big.Int
DetermineFillPrice(marketId int64, longOrderPrice, shortOrderPrice, blockPlaced0, blockPlaced1 *big.Int) (*ValidateOrdersAndDetermineFillPriceOutput, error)
DetermineLiquidationFillPrice(marketId int64, baseAssetQuantity, price *big.Int) (*big.Int, error)
GetLongOpenOrdersAmount(trader common.Address, ammIndex *big.Int) *big.Int
GetShortOpenOrdersAmount(trader common.Address, ammIndex *big.Int) *big.Int
GetReduceOnlyAmount(trader common.Address, ammIndex *big.Int) *big.Int
Expand All @@ -44,6 +42,7 @@ type BibliophileClient interface {
GetBidsHead(market common.Address) *big.Int
GetAsksHead(market common.Address) *big.Int
GetUpperAndLowerBoundForMarket(marketId int64) (*big.Int, *big.Int)
GetAcceptableBoundsForLiquidation(marketId int64) (*big.Int, *big.Int)

GetAccessibleState() contract.AccessibleState
}
Expand Down Expand Up @@ -83,14 +82,6 @@ func (b *bibliophileClient) GetMarketAddressFromMarketID(marketID int64) common.
return getMarketAddressFromMarketID(marketID, b.accessibleState.GetStateDB())
}

func (b *bibliophileClient) DetermineFillPrice(marketId int64, longOrderPrice, shortOrderPrice, blockPlaced0, blockPlaced1 *big.Int) (*ValidateOrdersAndDetermineFillPriceOutput, error) {
return DetermineFillPrice(b.accessibleState.GetStateDB(), marketId, longOrderPrice, shortOrderPrice, blockPlaced0, blockPlaced1)
}

func (b *bibliophileClient) DetermineLiquidationFillPrice(marketId int64, baseAssetQuantity, price *big.Int) (*big.Int, error) {
return DetermineLiquidationFillPrice(b.accessibleState.GetStateDB(), marketId, baseAssetQuantity, price)
}

func (b *bibliophileClient) GetBlockPlaced(orderHash [32]byte) *big.Int {
return getBlockPlaced(b.accessibleState.GetStateDB(), orderHash)
}
Expand Down Expand Up @@ -155,6 +146,10 @@ func (b *bibliophileClient) GetUpperAndLowerBoundForMarket(marketId int64) (*big
return GetAcceptableBounds(b.accessibleState.GetStateDB(), marketId)
}

func (b *bibliophileClient) GetAcceptableBoundsForLiquidation(marketId int64) (*big.Int, *big.Int) {
return GetAcceptableBoundsForLiquidation(b.accessibleState.GetStateDB(), marketId)
}

func (b *bibliophileClient) GetBidsHead(market common.Address) *big.Int {
return getBidsHead(b.accessibleState.GetStateDB(), market)
}
Expand Down
29 changes: 7 additions & 22 deletions precompile/contracts/bibliophile/client_mock.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit 01bd9d5

Please sign in to comment.