Skip to content

Commit

Permalink
Check margin requirement for IOC orders in matching (#141)
Browse files Browse the repository at this point in the history
* Check margin requirement for IOC orders in matching

* Add a counter for overall panics

* Fix cancelOrdersWithLowMargin

* Margin check refactor (#143)

* refactor runMatchingEngine

* fix tests

* margin adjustments

* fix tests

* Add error log when order is not found

---------

Co-authored-by: atvanguard <[email protected]>
  • Loading branch information
lumos42 and atvanguard authored Dec 30, 2023
1 parent 1e2cfeb commit 445022b
Show file tree
Hide file tree
Showing 13 changed files with 259 additions and 96 deletions.
1 change: 1 addition & 0 deletions plugin/evm/limit_order.go
Original file line number Diff line number Diff line change
Expand Up @@ -462,6 +462,7 @@ func executeFuncAndRecoverPanic(fn func(), panicMessage string, panicCounter met

log.Error(panicMessage, "errorMessage", errorMessage, "stack_trace", string(debug.Stack()))
panicCounter.Inc(1)
orderbook.AllPanicsCounter.Inc(1)
}
}()
fn()
Expand Down
6 changes: 6 additions & 0 deletions plugin/evm/orderbook/config_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ type IConfigService interface {
GetCumulativePremiumFraction(market Market) *big.Int
GetAcceptableBounds(market Market) (*big.Int, *big.Int)
GetAcceptableBoundsForLiquidation(market Market) (*big.Int, *big.Int)
GetTakerFee() *big.Int
}

type ConfigService struct {
Expand Down Expand Up @@ -102,3 +103,8 @@ func (cs *ConfigService) GetCumulativePremiumFraction(market Market) *big.Int {
markets := bibliophile.GetMarkets(cs.getStateAtCurrentBlock())
return bibliophile.GetCumulativePremiumFraction(cs.getStateAtCurrentBlock(), markets[market])
}

func (cs *ConfigService) GetTakerFee() *big.Int {
takerFee := bibliophile.GetTakerFee(cs.getStateAtCurrentBlock())
return hu.Div(hu.Mul(takerFee, big.NewInt(8)), big.NewInt(10)) // 20% discount, which is applied to everyone currently
}
32 changes: 24 additions & 8 deletions plugin/evm/orderbook/contract_events_processor.go
Original file line number Diff line number Diff line change
Expand Up @@ -155,9 +155,10 @@ func (cep *ContractEventsProcessor) handleOrderBookEvent(event *types.Log) {
return
}
orderId := event.Topics[1]
errorString := args["err"].(string)
if !removed {
log.Info("OrderMatchingError", "args", args, "orderId", orderId.String(), "TxHash", event.TxHash, "number", event.BlockNumber)
if err := cep.database.SetOrderStatus(orderId, Execution_Failed, args["err"].(string), event.BlockNumber); err != nil {
if err := cep.database.SetOrderStatus(orderId, Execution_Failed, errorString, event.BlockNumber); err != nil {
log.Error("error in SetOrderStatus", "method", "OrderMatchingError", "err", err)
return
}
Expand Down Expand Up @@ -704,17 +705,32 @@ func (cep *ContractEventsProcessor) updateMetrics(logs []*types.Log) {

metricName := fmt.Sprintf("%s/%s", "events", event_.Name)

if !event.Removed {
metrics.GetOrRegisterCounter(metricName, nil).Inc(1)
} else {
metrics.GetOrRegisterCounter(metricName, nil).Dec(1)
}
metrics.GetOrRegisterCounter(metricName, nil).Inc(1)

switch event_.Name {
case "OrderAccepted":
orderAcceptedCount++
orderAcceptedCount += 1
case "OrderCancelAccepted":
orderCancelledCount++
orderCancelledCount += 1
case "OrderMatchingError":
// separate metrics for combination of order type and error string - for more granular analysis
args := map[string]interface{}{}
err := cep.orderBookABI.UnpackIntoMap(args, "OrderMatchingError", event.Data)
if err != nil {
log.Error("error in orderBookAbi.UnpackIntoMap", "method", "OrderMatchingError", "err", err)
return
}
orderId := event.Topics[1]
errorString := args["err"].(string)

order := cep.database.GetOrderById(orderId)
if order != nil {
ordertype := order.OrderType
metricName := fmt.Sprintf("%s/%s/%s/%s", "events", "OrderMatchingError", ordertype, utils.RemoveSpacesAndSpecialChars(errorString))
metrics.GetOrRegisterCounter(metricName, nil).Inc(1)
} else {
log.Error("updateMetrics - error in getting order", "event", "OrderMatchingError")
}
}
}

Expand Down
1 change: 1 addition & 0 deletions plugin/evm/orderbook/hubbleutils/margin_math.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ type HubbleState struct {
ActiveMarkets []Market
MinAllowableMargin *big.Int
MaintenanceMargin *big.Int
TakerFee *big.Int
}

type UserState struct {
Expand Down
12 changes: 6 additions & 6 deletions plugin/evm/orderbook/liquidations_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ func TestGetLiquidableTraders(t *testing.T) {
OraclePrices: map[Market]*big.Int{market: hu.Mul1e6(big.NewInt(110))},
ActiveMarkets: []hu.Market{market},
}
liquidablePositions, _ := db.GetNaughtyTraders(hState)
liquidablePositions, _, _ := db.GetNaughtyTraders(hState)
assert.Equal(t, 0, len(liquidablePositions))
})

Expand All @@ -44,7 +44,7 @@ func TestGetLiquidableTraders(t *testing.T) {
ActiveMarkets: []hu.Market{market},
MaintenanceMargin: db.configService.getMaintenanceMargin(),
}
liquidablePositions, _ := db.GetNaughtyTraders(hState)
liquidablePositions, _, _ := db.GetNaughtyTraders(hState)
assert.Equal(t, 0, len(liquidablePositions))
})

Expand Down Expand Up @@ -109,7 +109,7 @@ func TestGetLiquidableTraders(t *testing.T) {
marginFraction := calcMarginFraction(_trader, hState)
assert.Equal(t, new(big.Int).Div(hu.Mul1e6(new(big.Int).Add(new(big.Int).Sub(marginLong, pendingFundingLong), unrealizePnL)), notionalPosition), marginFraction)

liquidablePositions, _ := db.GetNaughtyTraders(hState)
liquidablePositions, _, _ := db.GetNaughtyTraders(hState)
assert.Equal(t, 0, len(liquidablePositions))
})

Expand Down Expand Up @@ -165,7 +165,7 @@ func TestGetLiquidableTraders(t *testing.T) {
marginFraction := calcMarginFraction(_trader, hState)
assert.Equal(t, new(big.Int).Div(hu.Mul1e6(new(big.Int).Add(new(big.Int).Sub(marginLong, pendingFundingLong), unrealizePnL)), notionalPosition), marginFraction)

liquidablePositions, _ := db.GetNaughtyTraders(hState)
liquidablePositions, _, _ := db.GetNaughtyTraders(hState)
assert.Equal(t, 0, len(liquidablePositions))
})
})
Expand Down Expand Up @@ -228,7 +228,7 @@ func TestGetLiquidableTraders(t *testing.T) {
marginFraction := calcMarginFraction(_trader, hState)
assert.Equal(t, new(big.Int).Div(hu.Mul1e6(new(big.Int).Add(new(big.Int).Sub(marginShort, pendingFundingShort), unrealizePnL)), notionalPosition), marginFraction)

liquidablePositions, _ := db.GetNaughtyTraders(hState)
liquidablePositions, _, _ := db.GetNaughtyTraders(hState)
assert.Equal(t, 0, len(liquidablePositions))
})

Expand Down Expand Up @@ -283,7 +283,7 @@ func TestGetLiquidableTraders(t *testing.T) {
marginFraction := calcMarginFraction(_trader, hState)
assert.Equal(t, new(big.Int).Div(hu.Mul1e6(new(big.Int).Add(new(big.Int).Sub(marginShort, pendingFundingShort), unrealizePnL)), notionalPosition), marginFraction)

liquidablePositions, _ := db.GetNaughtyTraders(hState)
liquidablePositions, _, _ := db.GetNaughtyTraders(hState)
assert.Equal(t, 0, len(liquidablePositions))
})
})
Expand Down
71 changes: 64 additions & 7 deletions plugin/evm/orderbook/matching_pipeline.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,19 +79,21 @@ func (pipeline *MatchingPipeline) Run(blockNumber *big.Int) bool {
ActiveMarkets: markets,
MinAllowableMargin: pipeline.configService.getMinAllowableMargin(),
MaintenanceMargin: pipeline.configService.getMaintenanceMargin(),
TakerFee: pipeline.configService.GetTakerFee(),
}

// build trader map
liquidablePositions, ordersToCancel := pipeline.db.GetNaughtyTraders(hState)
liquidablePositions, ordersToCancel, marginMap := pipeline.db.GetNaughtyTraders(hState)
cancellableOrderIds := pipeline.cancelLimitOrders(ordersToCancel)
orderMap := make(map[Market]*Orders)
for _, market := range markets {
orderMap[market] = pipeline.fetchOrders(market, hState.OraclePrices[market], cancellableOrderIds, blockNumber)
}
pipeline.runLiquidations(liquidablePositions, orderMap, hState.OraclePrices)
pipeline.runLiquidations(liquidablePositions, orderMap, hState.OraclePrices, marginMap)
for _, market := range markets {
// @todo should we prioritize matching in any particular market?
pipeline.runMatchingEngine(pipeline.lotp, orderMap[market].longOrders, orderMap[market].shortOrders)
upperBound, _ := pipeline.configService.GetAcceptableBounds(market)
pipeline.runMatchingEngine(pipeline.lotp, orderMap[market].longOrders, orderMap[market].shortOrders, marginMap, hState.MinAllowableMargin, hState.TakerFee, upperBound)
}

orderBookTxsCount := pipeline.lotp.GetOrderBookTxsCount()
Expand Down Expand Up @@ -185,7 +187,7 @@ func (pipeline *MatchingPipeline) fetchOrders(market Market, underlyingPrice *bi
return &Orders{longOrders, shortOrders}
}

func (pipeline *MatchingPipeline) runLiquidations(liquidablePositions []LiquidablePosition, orderMap map[Market]*Orders, underlyingPrices map[Market]*big.Int) {
func (pipeline *MatchingPipeline) runLiquidations(liquidablePositions []LiquidablePosition, orderMap map[Market]*Orders, underlyingPrices map[Market]*big.Int, marginMap map[common.Address]*big.Int) {
if len(liquidablePositions) == 0 {
return
}
Expand All @@ -204,6 +206,8 @@ func (pipeline *MatchingPipeline) runLiquidations(liquidablePositions []Liquidab
liquidationBounds[market] = S{Upperbound: upperbound, Lowerbound: lowerbound}
}

minAllowableMargin := pipeline.configService.getMinAllowableMargin()
takerFee := pipeline.configService.GetTakerFee()
for _, liquidable := range liquidablePositions {
market := liquidable.Market
numOrdersExhausted := 0
Expand All @@ -215,6 +219,16 @@ func (pipeline *MatchingPipeline) runLiquidations(liquidablePositions []Liquidab
break
}
fillAmount := utils.BigIntMinAbs(liquidable.GetUnfilledSize(), order.GetUnFilledBaseAssetQuantity())
requiredMargin := getRequiredMargin(&order, fillAmount, minAllowableMargin, takerFee, liquidationBounds[market].Upperbound)
if marginMap[order.Trader] == nil {
// compatibility with existing tests
marginMap[order.Trader] = big.NewInt(0)
}
if requiredMargin.Cmp(marginMap[order.Trader]) == 1 {
numOrdersExhausted++
continue
}
marginMap[order.Trader].Sub(marginMap[order.Trader], requiredMargin) // deduct available margin for this run
pipeline.lotp.ExecuteLiquidation(liquidable.Address, order, fillAmount)
order.FilledBaseAssetQuantity.Add(order.FilledBaseAssetQuantity, fillAmount)
liquidable.FilledSize.Add(liquidable.FilledSize, fillAmount)
Expand All @@ -233,6 +247,15 @@ func (pipeline *MatchingPipeline) runLiquidations(liquidablePositions []Liquidab
break
}
fillAmount := utils.BigIntMinAbs(liquidable.GetUnfilledSize(), order.GetUnFilledBaseAssetQuantity())
requiredMargin := getRequiredMargin(&order, fillAmount, minAllowableMargin, takerFee, liquidationBounds[market].Upperbound)
if marginMap[order.Trader] == nil {
marginMap[order.Trader] = big.NewInt(0)
}
if requiredMargin.Cmp(marginMap[order.Trader]) == 1 {
numOrdersExhausted++
continue
}
marginMap[order.Trader].Sub(marginMap[order.Trader], requiredMargin) // deduct available margin for this run
pipeline.lotp.ExecuteLiquidation(liquidable.Address, order, fillAmount)
order.FilledBaseAssetQuantity.Sub(order.FilledBaseAssetQuantity, fillAmount)
liquidable.FilledSize.Sub(liquidable.FilledSize, fillAmount)
Expand All @@ -246,20 +269,21 @@ func (pipeline *MatchingPipeline) runLiquidations(liquidablePositions []Liquidab
orderMap[market].shortOrders = orderMap[market].shortOrders[numOrdersExhausted:]
}
if liquidable.GetUnfilledSize().Sign() != 0 {
unquenchedLiquidationsCounter.Inc(1)
log.Info("unquenched liquidation", "liquidable", liquidable)
}
}
}

func (pipeline *MatchingPipeline) runMatchingEngine(lotp LimitOrderTxProcessor, longOrders []Order, shortOrders []Order) {
func (pipeline *MatchingPipeline) runMatchingEngine(lotp LimitOrderTxProcessor, longOrders []Order, shortOrders []Order, marginMap map[common.Address]*big.Int, minAllowableMargin, takerFee, upperBound *big.Int) {
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++ {
fillAmount := areMatchingOrders(longOrders[i], shortOrders[j])
fillAmount := areMatchingOrders(longOrders[i], shortOrders[j], marginMap, minAllowableMargin, takerFee, upperBound)
if fillAmount == nil {
continue
}
Expand All @@ -275,7 +299,7 @@ func (pipeline *MatchingPipeline) runMatchingEngine(lotp LimitOrderTxProcessor,
}
}

func areMatchingOrders(longOrder, shortOrder Order) *big.Int {
func areMatchingOrders(longOrder, shortOrder Order, marginMap map[common.Address]*big.Int, minAllowableMargin, takerFee, upperBound *big.Int) *big.Int {
if longOrder.Price.Cmp(shortOrder.Price) == -1 {
return nil
}
Expand All @@ -288,9 +312,42 @@ func areMatchingOrders(longOrder, shortOrder Order) *big.Int {
if fillAmount.Sign() == 0 {
return nil
}

// for ioc orders, check that they have enough margin to execute the trade
longMargin := big.NewInt(0)
shortMargin := big.NewInt(0)
if longOrder.OrderType == IOC {
longMargin := getRequiredMargin(&longOrder, fillAmount, minAllowableMargin, takerFee, upperBound)
if longMargin.Cmp(marginMap[longOrder.Trader]) == 1 {
return nil
}
}
if shortOrder.OrderType == IOC {
shortMargin := getRequiredMargin(&shortOrder, fillAmount, minAllowableMargin, takerFee, upperBound)
if shortMargin.Cmp(marginMap[shortOrder.Trader]) == 1 {
return nil
}
}
marginMap[longOrder.Trader].Sub(marginMap[longOrder.Trader], longMargin)
marginMap[shortOrder.Trader].Sub(marginMap[shortOrder.Trader], shortMargin)
return fillAmount
}

func getRequiredMargin(order *Order, fillAmount, minAllowableMargin, takerFee, upperBound *big.Int) *big.Int {
if order.OrderType != IOC {
return big.NewInt(0) // no extra margin required because for limit orders it is already reserved
// @todo change for signed orders
}
price := order.Price
if order.BaseAssetQuantity.Sign() == -1 && order.Price.Cmp(upperBound) == -1 {
price = upperBound
}
quoteAsset := hu.Div1e18(hu.Mul(fillAmount, price)) // fillAmount is scaled by 18 decimals
requiredMargin := hu.Div1e6(hu.Mul(minAllowableMargin, quoteAsset))
_takerFee := hu.Div1e6(hu.Mul(quoteAsset, takerFee))
return hu.Add(requiredMargin, _takerFee)
}

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)
Expand Down
Loading

0 comments on commit 445022b

Please sign in to comment.