Skip to content

Commit

Permalink
🐛 check available margin for correct trader (#179)
Browse files Browse the repository at this point in the history
* 🐛 check available margin for correct trader

* remove logs

* add err logs
  • Loading branch information
atvanguard authored Mar 18, 2024
1 parent 62c931a commit 4a1e8e9
Show file tree
Hide file tree
Showing 2 changed files with 128 additions and 54 deletions.
55 changes: 30 additions & 25 deletions plugin/evm/orderbook/matching_pipeline.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package orderbook

import (
"fmt"
"math"
"math/big"
"sync"
Expand Down Expand Up @@ -201,8 +202,9 @@ func (pipeline *MatchingPipeline) runLiquidations(liquidablePositions []Liquidab
// compatibility with existing tests
marginMap[order.Trader] = big.NewInt(0)
}
_isExecutable, requiredMargin := isExecutable(&order, fillAmount, minAllowableMargin, takerFee, liquidationBounds[market].Upperbound, marginMap[order.Trader])
if !_isExecutable {
requiredMargin, err := isExecutable(&order, fillAmount, minAllowableMargin, takerFee, liquidationBounds[market].Upperbound, marginMap[order.Trader])
if err != nil {
log.Error("order is not executable", "order", order, "err", err)
numOrdersExhausted++
continue
}
Expand All @@ -228,8 +230,9 @@ func (pipeline *MatchingPipeline) runLiquidations(liquidablePositions []Liquidab
if marginMap[order.Trader] == nil {
marginMap[order.Trader] = big.NewInt(0)
}
isExecutable, requiredMargin := isExecutable(&order, fillAmount, minAllowableMargin, takerFee, liquidationBounds[market].Upperbound, marginMap[order.Trader])
if !isExecutable {
requiredMargin, err := isExecutable(&order, fillAmount, minAllowableMargin, takerFee, liquidationBounds[market].Upperbound, marginMap[order.Trader])
if err != nil {
log.Error("order is not executable", "order", order, "err", err)
numOrdersExhausted++
continue
}
Expand Down Expand Up @@ -261,8 +264,9 @@ func (pipeline *MatchingPipeline) runMatchingEngine(lotp LimitOrderTxProcessor,
}
numOrdersExhausted := 0
for j := 0; j < len(shortOrders); j++ {
fillAmount := areMatchingOrders(longOrders[i], shortOrders[j], marginMap, minAllowableMargin, takerFee, upperBound)
if fillAmount == nil {
fillAmount, err := areMatchingOrders(longOrders[i], shortOrders[j], marginMap, minAllowableMargin, takerFee, upperBound)
if err != nil {
log.Error("orders not matcheable", "longOrder", longOrders[i], "shortOrder", shortOrders[i], "err", err)
continue
}
longOrders[i], shortOrders[j] = ExecuteMatchedOrders(lotp, longOrders[i], shortOrders[j], fillAmount)
Expand All @@ -277,48 +281,49 @@ func (pipeline *MatchingPipeline) runMatchingEngine(lotp LimitOrderTxProcessor,
}
}

func areMatchingOrders(longOrder, shortOrder Order, marginMap map[common.Address]*big.Int, minAllowableMargin, takerFee, upperBound *big.Int) *big.Int {
func areMatchingOrders(longOrder, shortOrder Order, marginMap map[common.Address]*big.Int, minAllowableMargin, takerFee, upperBound *big.Int) (*big.Int, error) {
if longOrder.Price.Cmp(shortOrder.Price) == -1 {
return nil
return nil, fmt.Errorf("long order price %s is less than short order price %s", longOrder.Price, shortOrder.Price)
}
blockDiff := longOrder.BlockNumber.Cmp(shortOrder.BlockNumber)
if blockDiff == -1 && (longOrder.OrderType == IOC || shortOrder.isPostOnly()) ||
blockDiff == 1 && (shortOrder.OrderType == IOC || longOrder.isPostOnly()) {
return nil
return nil, fmt.Errorf("resting order semantics mismatch")
}
fillAmount := utils.BigIntMinAbs(longOrder.GetUnFilledBaseAssetQuantity(), shortOrder.GetUnFilledBaseAssetQuantity())
if fillAmount.Sign() == 0 {
return nil
return nil, fmt.Errorf("no fill amount")
}

_isExecutable, longMargin := isExecutable(&longOrder, fillAmount, minAllowableMargin, takerFee, upperBound, marginMap[longOrder.Trader])
if !_isExecutable {
return nil
longMargin, err := isExecutable(&longOrder, fillAmount, minAllowableMargin, takerFee, upperBound, marginMap[longOrder.Trader])
if err != nil {
return nil, err
}

var shortMargin *big.Int = big.NewInt(0)
_isExecutable, shortMargin = isExecutable(&shortOrder, fillAmount, minAllowableMargin, takerFee, upperBound, marginMap[longOrder.Trader])
if !_isExecutable {
return nil
shortMargin, err := isExecutable(&shortOrder, fillAmount, minAllowableMargin, takerFee, upperBound, marginMap[shortOrder.Trader])
if err != nil {
return nil, err
}
marginMap[longOrder.Trader].Sub(marginMap[longOrder.Trader], longMargin)
marginMap[shortOrder.Trader].Sub(marginMap[shortOrder.Trader], shortMargin)
return fillAmount
return fillAmount, nil
}

func isExecutable(order *Order, fillAmount, minAllowableMargin, takerFee, upperBound, availableMargin *big.Int) (bool, *big.Int) {
func isExecutable(order *Order, fillAmount, minAllowableMargin, takerFee, upperBound, availableMargin *big.Int) (*big.Int, error) {
if order.OrderType == Limit || order.ReduceOnly {
return true, big.NewInt(0) // no extra margin required because for limit orders it is already reserved
return big.NewInt(0), nil // no extra margin required because for limit orders it is already reserved
}
requiredMargin := big.NewInt(0)
if order.OrderType == IOC {
requiredMargin := getRequiredMargin(order, fillAmount, minAllowableMargin, takerFee, upperBound)
return requiredMargin.Cmp(availableMargin) <= 0, requiredMargin
requiredMargin = getRequiredMargin(order, fillAmount, minAllowableMargin, takerFee, upperBound)
}
if order.OrderType == Signed {
requiredMargin := getRequiredMargin(order, fillAmount, minAllowableMargin, big.NewInt(0) /* signed orders are always maker */, upperBound)
return requiredMargin.Cmp(availableMargin) <= 0, requiredMargin
requiredMargin = getRequiredMargin(order, fillAmount, minAllowableMargin, big.NewInt(0) /* signed orders are always maker */, upperBound)
}
return false, big.NewInt(0)
if requiredMargin.Cmp(availableMargin) > 0 {
return nil, fmt.Errorf("insufficient margin. trader %s, required: %s, available: %s", order.Trader, requiredMargin, availableMargin)
}
return requiredMargin, nil
}

func getRequiredMargin(order *Order, fillAmount, minAllowableMargin, takerFee, upperBound *big.Int) *big.Int {
Expand Down
127 changes: 98 additions & 29 deletions plugin/evm/orderbook/matching_pipeline_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package orderbook

import (
"fmt"
"math/big"
"testing"
"time"
Expand Down Expand Up @@ -246,7 +247,7 @@ func TestRunMatchingEngine(t *testing.T) {
fillAmount1 := longOrder1.BaseAssetQuantity
fillAmount2 := longOrder2.BaseAssetQuantity
marginMap := map[common.Address]*big.Int{
common.HexToAddress("0x22Bb736b64A0b4D4081E103f83bccF864F0404aa"): big.NewInt(1e9), // $1000
trader: big.NewInt(0), // limit order doesn't need any available margin
}
lotp.On("ExecuteMatchedOrdersTx", longOrder1, shortOrder1, fillAmount1).Return(nil)
lotp.On("ExecuteMatchedOrdersTx", longOrder2, shortOrder2, fillAmount2).Return(nil)
Expand Down Expand Up @@ -276,7 +277,7 @@ func TestRunMatchingEngine(t *testing.T) {
lotp.On("PurgeLocalTx").Return(nil)
lotp.On("ExecuteMatchedOrdersTx", longOrder, shortOrder, fillAmount).Return(nil)
marginMap := map[common.Address]*big.Int{
common.HexToAddress("0x22Bb736b64A0b4D4081E103f83bccF864F0404aa"): big.NewInt(1e9), // $1000
trader: big.NewInt(0), // limit order doesn't need any available margin
}
pipeline.runMatchingEngine(lotp, longOrders, shortOrders, marginMap, minAllowableMargin, takerFee, upperBound)
lotp.AssertCalled(t, "ExecuteMatchedOrdersTx", longOrder, shortOrder, fillAmount)
Expand Down Expand Up @@ -316,7 +317,7 @@ func TestRunMatchingEngine(t *testing.T) {
lotp.On("PurgeLocalTx").Return(nil)
log.Info("longOrder1", "longOrder1", longOrder1)
marginMap := map[common.Address]*big.Int{
common.HexToAddress("0x22Bb736b64A0b4D4081E103f83bccF864F0404aa"): big.NewInt(1e9), // $1000
trader: big.NewInt(0), // limit order doesn't need any available margin
}
pipeline.runMatchingEngine(lotp, longOrders, shortOrders, marginMap, minAllowableMargin, takerFee, upperBound)
log.Info("longOrder1", "longOrder1", longOrder1)
Expand Down Expand Up @@ -525,44 +526,46 @@ func TestAreMatchingOrders(t *testing.T) {
longOrder_ := Order{
Market: 1,
PositionType: LONG,
BaseAssetQuantity: big.NewInt(10),
BaseAssetQuantity: hu.Mul1e18(big.NewInt(10)),
Trader: trader,
FilledBaseAssetQuantity: big.NewInt(0),
Salt: big.NewInt(1),
Price: big.NewInt(100),
Price: hu.Mul1e6(big.NewInt(100)),
ReduceOnly: false,
LifecycleList: []Lifecycle{Lifecycle{}},
BlockNumber: big.NewInt(21),
RawOrder: &LimitOrder{
BaseOrder: hu.BaseOrder{
AmmIndex: big.NewInt(1),
Trader: trader,
BaseAssetQuantity: big.NewInt(10),
Price: big.NewInt(100),
BaseAssetQuantity: hu.Mul1e18(big.NewInt(10)),
Price: hu.Mul1e6(big.NewInt(100)),
Salt: big.NewInt(1),
ReduceOnly: false,
},
PostOnly: false,
},
OrderType: Limit,
}

shortTrader := common.HexToAddress("0xc413Fa79AdE66224F560BD7693F8bEc81746Bf92")
shortOrder_ := Order{
Market: 1,
PositionType: SHORT,
BaseAssetQuantity: big.NewInt(-10),
Trader: trader,
BaseAssetQuantity: hu.Mul1e18(big.NewInt(-10)),
Trader: shortTrader,
FilledBaseAssetQuantity: big.NewInt(0),
Salt: big.NewInt(2),
Price: big.NewInt(100),
Price: hu.Mul1e6(big.NewInt(100)),
ReduceOnly: false,
LifecycleList: []Lifecycle{Lifecycle{}},
BlockNumber: big.NewInt(21),
RawOrder: &LimitOrder{
BaseOrder: hu.BaseOrder{
AmmIndex: big.NewInt(1),
Trader: trader,
BaseAssetQuantity: big.NewInt(-10),
Price: big.NewInt(100),
BaseAssetQuantity: hu.Mul1e18(big.NewInt(-10)),
Price: hu.Mul1e6(big.NewInt(100)),
Salt: big.NewInt(2),
ReduceOnly: false,
},
Expand All @@ -577,10 +580,10 @@ func TestAreMatchingOrders(t *testing.T) {

longOrder.Price = big.NewInt(80)
marginMap := map[common.Address]*big.Int{
common.HexToAddress("0x22Bb736b64A0b4D4081E103f83bccF864F0404aa"): big.NewInt(1e9), // $1000
trader: big.NewInt(0), // limit order doesn't need any available margin
}
actualFillAmount := areMatchingOrders(longOrder, shortOrder, marginMap, minAllowableMargin, takerFee, upperBound)

actualFillAmount, err := areMatchingOrders(longOrder, shortOrder, marginMap, minAllowableMargin, takerFee, upperBound)
assert.EqualError(t, err, fmt.Errorf("long order price %s is less than short order price %s", longOrder.Price.String(), shortOrder.Price.String()).Error())
assert.Nil(t, actualFillAmount)
})

Expand All @@ -598,9 +601,10 @@ func TestAreMatchingOrders(t *testing.T) {
ExpireAt: big.NewInt(0),
}
marginMap := map[common.Address]*big.Int{
common.HexToAddress("0x22Bb736b64A0b4D4081E103f83bccF864F0404aa"): big.NewInt(1e9), // $1000
trader: big.NewInt(0), // limit order doesn't need any available margin
}
actualFillAmount := areMatchingOrders(longOrder, shortOrder, marginMap, minAllowableMargin, takerFee, upperBound)
actualFillAmount, err := areMatchingOrders(longOrder, shortOrder, marginMap, minAllowableMargin, takerFee, upperBound)
assert.EqualError(t, err, fmt.Errorf("resting order semantics mismatch").Error())
assert.Nil(t, actualFillAmount)
})
t.Run("short order is post only", func(t *testing.T) {
Expand All @@ -609,9 +613,10 @@ func TestAreMatchingOrders(t *testing.T) {

shortOrder.RawOrder.(*LimitOrder).PostOnly = true
marginMap := map[common.Address]*big.Int{
common.HexToAddress("0x22Bb736b64A0b4D4081E103f83bccF864F0404aa"): big.NewInt(1e9), // $1000
trader: big.NewInt(0), // limit order doesn't need any available margin
}
actualFillAmount := areMatchingOrders(longOrder, shortOrder, marginMap, minAllowableMargin, takerFee, upperBound)
actualFillAmount, err := areMatchingOrders(longOrder, shortOrder, marginMap, minAllowableMargin, takerFee, upperBound)
assert.EqualError(t, err, fmt.Errorf("resting order semantics mismatch").Error())
assert.Nil(t, actualFillAmount)
})
})
Expand All @@ -630,9 +635,10 @@ func TestAreMatchingOrders(t *testing.T) {
ExpireAt: big.NewInt(0),
}
marginMap := map[common.Address]*big.Int{
common.HexToAddress("0x22Bb736b64A0b4D4081E103f83bccF864F0404aa"): big.NewInt(1e9), // $1000
trader: big.NewInt(0), // limit order doesn't need any available margin
}
actualFillAmount := areMatchingOrders(longOrder, shortOrder, marginMap, minAllowableMargin, takerFee, upperBound)
actualFillAmount, err := areMatchingOrders(longOrder, shortOrder, marginMap, minAllowableMargin, takerFee, upperBound)
assert.EqualError(t, err, fmt.Errorf("resting order semantics mismatch").Error())
assert.Nil(t, actualFillAmount)
})
t.Run("longOrder is post only", func(t *testing.T) {
Expand All @@ -641,9 +647,10 @@ func TestAreMatchingOrders(t *testing.T) {

longOrder.RawOrder.(*LimitOrder).PostOnly = true
marginMap := map[common.Address]*big.Int{
common.HexToAddress("0x22Bb736b64A0b4D4081E103f83bccF864F0404aa"): big.NewInt(1e9), // $1000
trader: big.NewInt(0), // limit order doesn't need any available margin
}
actualFillAmount := areMatchingOrders(longOrder, shortOrder, marginMap, minAllowableMargin, takerFee, upperBound)
actualFillAmount, err := areMatchingOrders(longOrder, shortOrder, marginMap, minAllowableMargin, takerFee, upperBound)
assert.EqualError(t, err, fmt.Errorf("resting order semantics mismatch").Error())
assert.Nil(t, actualFillAmount)
})
})
Expand All @@ -654,22 +661,84 @@ func TestAreMatchingOrders(t *testing.T) {

longOrder.FilledBaseAssetQuantity = longOrder.BaseAssetQuantity
marginMap := map[common.Address]*big.Int{
common.HexToAddress("0x22Bb736b64A0b4D4081E103f83bccF864F0404aa"): big.NewInt(1e9), // $1000
trader: big.NewInt(0), // limit order doesn't need any available margin
}
actualFillAmount := areMatchingOrders(longOrder, shortOrder, marginMap, minAllowableMargin, takerFee, upperBound)
actualFillAmount, err := areMatchingOrders(longOrder, shortOrder, marginMap, minAllowableMargin, takerFee, upperBound)
assert.EqualError(t, err, fmt.Errorf("no fill amount").Error())
assert.Nil(t, actualFillAmount)
})

t.Run("success", func(t *testing.T) {
longOrder := deepCopyOrder(&longOrder_)
shortOrder := deepCopyOrder(&shortOrder_)

longOrder.FilledBaseAssetQuantity = big.NewInt(5)
longOrder.FilledBaseAssetQuantity = hu.Mul1e18(big.NewInt(5))
marginMap := map[common.Address]*big.Int{
common.HexToAddress("0x22Bb736b64A0b4D4081E103f83bccF864F0404aa"): big.NewInt(1e9), // $1000
trader: big.NewInt(0),
shortTrader: big.NewInt(0),
}
actualFillAmount := areMatchingOrders(longOrder, shortOrder, marginMap, minAllowableMargin, takerFee, upperBound)
assert.Equal(t, big.NewInt(5), actualFillAmount)
actualFillAmount, err := areMatchingOrders(longOrder, shortOrder, marginMap, minAllowableMargin, takerFee, upperBound)
assert.Nil(t, err)
assert.Equal(t, hu.Mul1e18(big.NewInt(5)), actualFillAmount)
})

t.Run("test ioc/signed orders", func(t *testing.T) {
t.Run("long trader has insufficient margin", func(t *testing.T) {
longOrder := deepCopyOrder(&longOrder_) // longOrder_ has block 21
longOrder.OrderType = IOC

shortOrder := deepCopyOrder(&shortOrder_) // shortOrder_ has block 2
shortOrder.OrderType = Signed

expectedFillAmount := longOrder.BaseAssetQuantity
marginMap := map[common.Address]*big.Int{
trader: big.NewInt(0),
shortTrader: big.NewInt(0),
}
actualFillAmount, err := areMatchingOrders(longOrder, shortOrder, marginMap, minAllowableMargin, takerFee, upperBound)
requiredMargin := hu.GetRequiredMargin(longOrder.Price, expectedFillAmount, minAllowableMargin, takerFee)
assert.EqualError(t, err, fmt.Errorf("insufficient margin. trader %s, required: %s, available: %s", trader, requiredMargin, big.NewInt(0)).Error())
assert.Nil(t, actualFillAmount)
})

t.Run("short trader has insufficient margin", func(t *testing.T) {
longOrder := deepCopyOrder(&longOrder_) // longOrder_ has block 21
longOrder.OrderType = IOC

shortOrder := deepCopyOrder(&shortOrder_) // shortOrder_ has block 2
shortOrder.OrderType = Signed

expectedFillAmount := longOrder.BaseAssetQuantity
longRequiredMargin := hu.GetRequiredMargin(longOrder.Price, expectedFillAmount, minAllowableMargin, takerFee)
shortRequiredMargin := hu.GetRequiredMargin(shortOrder.Price, expectedFillAmount, minAllowableMargin, big.NewInt(0))
marginMap := map[common.Address]*big.Int{
trader: longRequiredMargin,
shortTrader: big.NewInt(0),
}
actualFillAmount, err := areMatchingOrders(longOrder, shortOrder, marginMap, minAllowableMargin, takerFee, upperBound)
assert.EqualError(t, err, fmt.Errorf("insufficient margin. trader %s, required: %s, available: %s", shortTrader, shortRequiredMargin, big.NewInt(0)).Error())
assert.Nil(t, actualFillAmount)
})

t.Run("[success] match ioc order with signed order", func(t *testing.T) {
longOrder := deepCopyOrder(&longOrder_) // longOrder_ has block 21
longOrder.OrderType = IOC

shortOrder := deepCopyOrder(&shortOrder_) // shortOrder_ has block 2
shortOrder.OrderType = Signed

longOrder.FilledBaseAssetQuantity = hu.Mul1e18(big.NewInt(4))
expectedFillAmount := hu.Mul1e18(big.NewInt(6))
longRequiredMargin := hu.GetRequiredMargin(longOrder.Price, expectedFillAmount, minAllowableMargin, takerFee)
shortRequiredMargin := hu.GetRequiredMargin(shortOrder.Price, expectedFillAmount, minAllowableMargin, big.NewInt(0))
marginMap := map[common.Address]*big.Int{
trader: longRequiredMargin,
shortTrader: shortRequiredMargin,
}
actualFillAmount, err := areMatchingOrders(longOrder, shortOrder, marginMap, minAllowableMargin, takerFee, upperBound)
assert.Nil(t, err)
assert.Equal(t, expectedFillAmount, actualFillAmount)
})
})
}

Expand Down

0 comments on commit 4a1e8e9

Please sign in to comment.