Merge pull request #773 from c9s/fix/backtest-taker-order

fix: fix backtest taker order execution
This commit is contained in:
Yo-An Lin 2022-06-28 18:13:22 +08:00 committed by GitHub
commit ee59fc447d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 170 additions and 91 deletions

View File

@ -126,15 +126,19 @@ func (m *SimplePriceMatching) CancelOrder(o types.Order) (types.Order, error) {
// PlaceOrder returns the created order object, executed trade (if any) and error
func (m *SimplePriceMatching) PlaceOrder(o types.SubmitOrder) (*types.Order, *types.Trade, error) {
if o.Type == types.OrderTypeMarket {
if m.LastPrice.IsZero() {
panic("unexpected error: for market order, the last price can not be zero")
}
}
isTaker := o.Type == types.OrderTypeMarket || isLimitTakerOrder(o, m.LastPrice)
// price for checking account balance, default price
price := o.Price
switch o.Type {
case types.OrderTypeMarket:
if m.LastPrice.IsZero() {
panic("unexpected: last price can not be zero")
}
price = m.LastPrice
case types.OrderTypeLimit, types.OrderTypeStopLimit, types.OrderTypeLimitMaker:
price = o.Price
@ -167,7 +171,7 @@ func (m *SimplePriceMatching) PlaceOrder(o types.SubmitOrder) (*types.Order, *ty
orderID := incOrderID()
order := m.newOrder(o, orderID)
if o.Type == types.OrderTypeMarket {
if isTaker {
// emit the order update for Status:New
m.EmitOrderUpdate(order)
@ -175,13 +179,12 @@ func (m *SimplePriceMatching) PlaceOrder(o types.SubmitOrder) (*types.Order, *ty
var order2 = order
// emit trade before we publish order
trade := m.newTradeFromOrder(&order2, false)
trade := m.newTradeFromOrder(&order2, false, m.LastPrice)
m.executeTrade(trade)
// update the order status
order2.Status = types.OrderStatusFilled
order2.ExecutedQuantity = order2.Quantity
order2.Price = price
order2.IsWorking = false
// let the exchange emit the "FILLED" order update (we need the closed order)
@ -240,26 +243,21 @@ func (m *SimplePriceMatching) executeTrade(trade types.Trade) {
m.EmitBalanceUpdate(m.Account.Balances())
}
func (m *SimplePriceMatching) newTradeFromOrder(order *types.Order, isMaker bool) types.Trade {
func (m *SimplePriceMatching) getFeeRate(isMaker bool) (feeRate fixedpoint.Value) {
// BINANCE uses 0.1% for both maker and taker
// MAX uses 0.050% for maker and 0.15% for taker
var feeRate fixedpoint.Value
if isMaker {
feeRate = m.Account.MakerFeeRate
} else {
feeRate = m.Account.TakerFeeRate
}
return feeRate
}
price := order.Price
switch order.Type {
case types.OrderTypeMarket, types.OrderTypeStopMarket:
if m.LastPrice.IsZero() {
panic("unexpected: last price can not be zero")
}
price = m.LastPrice
}
func (m *SimplePriceMatching) newTradeFromOrder(order *types.Order, isMaker bool, price fixedpoint.Value) types.Trade {
// BINANCE uses 0.1% for both maker and taker
// MAX uses 0.050% for maker and 0.15% for taker
var feeRate = m.getFeeRate(isMaker)
var quoteQuantity = order.Quantity.Mul(price)
var fee fixedpoint.Value
var feeCurrency string
@ -268,17 +266,7 @@ func (m *SimplePriceMatching) newTradeFromOrder(order *types.Order, isMaker bool
feeCurrency = FeeToken
fee = quoteQuantity.Mul(feeRate)
} else {
switch order.Side {
case types.SideTypeBuy:
fee = order.Quantity.Mul(feeRate)
feeCurrency = m.Market.BaseCurrency
case types.SideTypeSell:
fee = quoteQuantity.Mul(feeRate)
feeCurrency = m.Market.QuoteCurrency
}
fee, feeCurrency = calculateNativeOrderFee(order, m.Market, feeRate)
}
// update order time
@ -288,7 +276,7 @@ func (m *SimplePriceMatching) newTradeFromOrder(order *types.Order, isMaker bool
return types.Trade{
ID: id,
OrderID: order.OrderID,
Exchange: "backtest",
Exchange: types.ExchangeBacktest,
Price: price,
Quantity: order.Quantity,
QuoteQuantity: quoteQuantity,
@ -302,8 +290,8 @@ func (m *SimplePriceMatching) newTradeFromOrder(order *types.Order, isMaker bool
}
}
// BuyToPrice means price go up and the limit sell should be triggered
func (m *SimplePriceMatching) BuyToPrice(price fixedpoint.Value) (closedOrders []types.Order, trades []types.Trade) {
// buyToPrice means price go up and the limit sell should be triggered
func (m *SimplePriceMatching) buyToPrice(price fixedpoint.Value) (closedOrders []types.Order, trades []types.Trade) {
klineMatchingLogger.Debugf("kline buy to price %s", price.String())
var bidOrders []types.Order
@ -418,7 +406,7 @@ func (m *SimplePriceMatching) BuyToPrice(price fixedpoint.Value) (closedOrders [
for i := range closedOrders {
o := closedOrders[i]
trade := m.newTradeFromOrder(&o, true)
trade := m.newTradeFromOrder(&o, true, o.Price)
m.executeTrade(trade)
closedOrders[i] = o
@ -432,9 +420,9 @@ func (m *SimplePriceMatching) BuyToPrice(price fixedpoint.Value) (closedOrders [
return closedOrders, trades
}
// SellToPrice simulates the price trend in down direction.
// sellToPrice simulates the price trend in down direction.
// When price goes down, buy orders should be executed, and the stop orders should be triggered.
func (m *SimplePriceMatching) SellToPrice(price fixedpoint.Value) (closedOrders []types.Order, trades []types.Trade) {
func (m *SimplePriceMatching) sellToPrice(price fixedpoint.Value) (closedOrders []types.Order, trades []types.Trade) {
klineMatchingLogger.Debugf("kline sell to price %s", price.String())
// in this section we handle --- the price goes lower, and we trigger the stop sell
@ -488,8 +476,10 @@ func (m *SimplePriceMatching) SellToPrice(price fixedpoint.Value) (closedOrders
switch o.Type {
case types.OrderTypeStopMarket:
// should we trigger the order
if o.StopPrice.Compare(price) < 0 {
// price goes down and if the stop price is still lower than the current price
// or the stop price is not touched
// then we should skip this order
if price.Compare(o.StopPrice) > 0 {
bidOrders = append(bidOrders, o)
break
}
@ -501,9 +491,10 @@ func (m *SimplePriceMatching) SellToPrice(price fixedpoint.Value) (closedOrders
closedOrders = append(closedOrders, o)
case types.OrderTypeStopLimit:
// if the price is lower than the stop order price
// we should trigger the stop order
if o.StopPrice.Compare(price) < 0 {
// price goes down and if the stop price is still lower than the current price
// or the stop price is not touched
// then we should skip this order
if price.Compare(o.StopPrice) > 0 {
bidOrders = append(bidOrders, o)
break
}
@ -539,7 +530,7 @@ func (m *SimplePriceMatching) SellToPrice(price fixedpoint.Value) (closedOrders
for i := range closedOrders {
o := closedOrders[i]
trade := m.newTradeFromOrder(&o, true)
trade := m.newTradeFromOrder(&o, true, o.Price)
m.executeTrade(trade)
closedOrders[i] = o
@ -579,40 +570,40 @@ func (m *SimplePriceMatching) processKLine(kline types.KLine) {
m.LastPrice = kline.Open
} else {
if m.LastPrice.Compare(kline.Open) > 0 {
m.SellToPrice(kline.Open)
m.sellToPrice(kline.Open)
} else {
m.BuyToPrice(kline.Open)
m.buyToPrice(kline.Open)
}
}
switch kline.Direction() {
case types.DirectionDown:
if kline.High.Compare(kline.Open) >= 0 {
m.BuyToPrice(kline.High)
m.buyToPrice(kline.High)
}
// if low is lower than close, sell to low first, and then buy up to close
if kline.Low.Compare(kline.Close) < 0 {
m.SellToPrice(kline.Low)
m.BuyToPrice(kline.Close)
m.sellToPrice(kline.Low)
m.buyToPrice(kline.Close)
} else {
m.SellToPrice(kline.Close)
m.sellToPrice(kline.Close)
}
case types.DirectionUp:
if kline.Low.Compare(kline.Open) <= 0 {
m.SellToPrice(kline.Low)
m.sellToPrice(kline.Low)
}
if kline.High.Compare(kline.Close) > 0 {
m.BuyToPrice(kline.High)
m.SellToPrice(kline.Close)
m.buyToPrice(kline.High)
m.sellToPrice(kline.Close)
} else {
m.BuyToPrice(kline.Close)
m.buyToPrice(kline.Close)
}
default: // no trade up or down
if m.LastPrice.IsZero() {
m.BuyToPrice(kline.Close)
m.buyToPrice(kline.Close)
}
}
@ -631,3 +622,28 @@ func (m *SimplePriceMatching) newOrder(o types.SubmitOrder, orderID uint64) type
UpdateTime: types.Time(m.CurrentTime),
}
}
func calculateNativeOrderFee(order *types.Order, market types.Market, feeRate fixedpoint.Value) (fee fixedpoint.Value, feeCurrency string) {
switch order.Side {
case types.SideTypeBuy:
fee = order.Quantity.Mul(feeRate)
feeCurrency = market.BaseCurrency
case types.SideTypeSell:
quoteQuantity := order.Quantity.Mul(order.Price)
fee = quoteQuantity.Mul(feeRate)
feeCurrency = market.QuoteCurrency
}
return fee, feeCurrency
}
func isLimitTakerOrder(o types.SubmitOrder, currentPrice fixedpoint.Value) bool {
if currentPrice.IsZero() {
return false
}
return o.Type == types.OrderTypeLimit && ((o.Side == types.SideTypeBuy && o.Price.Compare(currentPrice) >= 0) ||
(o.Side == types.SideTypeSell && o.Price.Compare(currentPrice) <= 0))
}

View File

@ -46,6 +46,7 @@ func TestSimplePriceMatching_orderUpdate(t *testing.T) {
Market: market,
CurrentTime: t1,
closedOrders: make(map[uint64]types.Order),
LastPrice: fixedpoint.NewFromFloat(25000),
}
orderUpdateCnt := 0
@ -66,11 +67,12 @@ func TestSimplePriceMatching_orderUpdate(t *testing.T) {
}
})
// maker order
_, _, err := engine.PlaceOrder(newLimitOrder("BTCUSDT", types.SideTypeBuy, 24000.0, 0.1))
assert.NoError(t, err)
assert.Equal(t, 1, orderUpdateCnt) // should got new status
assert.Equal(t, 1, orderUpdateNewStatusCnt) // should got new status
assert.Equal(t, 0, orderUpdateFilledStatusCnt) // should got new status
assert.Equal(t, 1, orderUpdateCnt) // should get new status
assert.Equal(t, 1, orderUpdateNewStatusCnt) // should get new status
assert.Equal(t, 0, orderUpdateFilledStatusCnt) // should get new status
assert.Equal(t, types.OrderStatusNew, lastOrder.Status)
assert.Equal(t, fixedpoint.NewFromFloat(0.0), lastOrder.ExecutedQuantity)
@ -89,23 +91,8 @@ func TestSimplePriceMatching_orderUpdate(t *testing.T) {
}
func TestSimplePriceMatching_processKLine(t *testing.T) {
account := &types.Account{
MakerFeeRate: fixedpoint.NewFromFloat(0.075 * 0.01),
TakerFeeRate: fixedpoint.NewFromFloat(0.075 * 0.01),
}
account.UpdateBalances(types.BalanceMap{
"USDT": {Currency: "USDT", Available: fixedpoint.NewFromFloat(10000.0)},
})
market := types.Market{
Symbol: "BTCUSDT",
PricePrecision: 8,
VolumePrecision: 8,
QuoteCurrency: "USDT",
BaseCurrency: "BTC",
MinNotional: fixedpoint.MustNewFromString("0.001"),
MinAmount: fixedpoint.MustNewFromString("10.0"),
MinQuantity: fixedpoint.MustNewFromString("0.001"),
}
account := getTestAccount()
market := getTestMarket()
t1 := time.Date(2021, 7, 1, 0, 0, 0, 0, time.UTC)
engine := &SimplePriceMatching{
@ -113,6 +100,7 @@ func TestSimplePriceMatching_processKLine(t *testing.T) {
Market: market,
CurrentTime: t1,
closedOrders: make(map[uint64]types.Order),
LastPrice: fixedpoint.NewFromFloat(30000.0),
}
for i := 0; i <= 5; i++ {
@ -124,7 +112,7 @@ func TestSimplePriceMatching_processKLine(t *testing.T) {
t2 := t1.Add(time.Minute)
// should match 25000, 24000
k := newKLine("BTCUSDT", types.Interval1m, t2, 26000, 27000, 23000, 25000)
k := newKLine("BTCUSDT", types.Interval1m, t2, 30000, 27000, 23000, 25000)
assert.Equal(t, t2.Add(time.Minute-time.Millisecond), k.EndTime.Time())
engine.processKLine(k)
@ -151,6 +139,8 @@ func newKLine(symbol string, interval types.Interval, startTime time.Time, o, h,
}
}
// getTestMarket returns the BTCUSDT market information
// for tests, we always use BTCUSDT
func getTestMarket() types.Market {
market := types.Market{
Symbol: "BTCUSDT",
@ -209,13 +199,13 @@ func TestSimplePriceMatching_StopLimitOrderBuy(t *testing.T) {
assert.Equal(t, 2, len(engine.bidOrders))
assert.Equal(t, 1, len(engine.askOrders))
closedOrders, trades := engine.BuyToPrice(fixedpoint.NewFromFloat(20000.0))
closedOrders, trades := engine.buyToPrice(fixedpoint.NewFromFloat(20000.0))
assert.Len(t, closedOrders, 0, "price change far from the price should not trigger the stop buy")
assert.Len(t, trades, 0, "price change far from the price should not trigger the stop buy")
assert.Equal(t, 2, len(engine.bidOrders), "bid orders should be the same")
assert.Equal(t, 1, len(engine.askOrders), "ask orders should be the same")
closedOrders, trades = engine.BuyToPrice(fixedpoint.NewFromFloat(21001.0))
closedOrders, trades = engine.buyToPrice(fixedpoint.NewFromFloat(21001.0))
assert.Len(t, closedOrders, 1, "should trigger the stop buy order")
assert.Len(t, trades, 1, "should have stop order trade executed")
@ -241,7 +231,7 @@ func TestSimplePriceMatching_StopLimitOrderBuy(t *testing.T) {
assert.NotNil(t, createdOrder, "place stop order should not trigger the stop buy")
assert.Len(t, engine.bidOrders, 2)
closedOrders, trades = engine.SellToPrice(fixedpoint.NewFromFloat(20500.0))
closedOrders, trades = engine.sellToPrice(fixedpoint.NewFromFloat(20500.0))
assert.Len(t, closedOrders, 1, "should trigger the stop buy order")
assert.Len(t, trades, 1, "should have stop order trade executed")
assert.Len(t, engine.bidOrders, 1, "should left one bid order")
@ -279,13 +269,13 @@ func TestSimplePriceMatching_StopLimitOrderSell(t *testing.T) {
assert.Equal(t, 1, len(engine.bidOrders))
assert.Equal(t, 2, len(engine.askOrders))
closedOrders, trades := engine.SellToPrice(fixedpoint.NewFromFloat(21500.0))
closedOrders, trades := engine.sellToPrice(fixedpoint.NewFromFloat(21500.0))
assert.Len(t, closedOrders, 0, "price change far from the price should not trigger the stop buy")
assert.Len(t, trades, 0, "price change far from the price should not trigger the stop buy")
assert.Equal(t, 1, len(engine.bidOrders))
assert.Equal(t, 2, len(engine.askOrders))
closedOrders, trades = engine.SellToPrice(fixedpoint.NewFromFloat(20990.0))
closedOrders, trades = engine.sellToPrice(fixedpoint.NewFromFloat(20990.0))
assert.Len(t, closedOrders, 1, "should trigger the stop sell order")
assert.Len(t, trades, 1, "should have stop order trade executed")
assert.Equal(t, 1, len(engine.bidOrders))
@ -313,7 +303,7 @@ func TestSimplePriceMatching_StopLimitOrderSell(t *testing.T) {
assert.Nil(t, trade, "place stop order should not trigger the stop sell")
assert.NotNil(t, createdOrder, "place stop order should not trigger the stop sell")
closedOrders, trades = engine.BuyToPrice(fixedpoint.NewFromFloat(21000.0))
closedOrders, trades = engine.buyToPrice(fixedpoint.NewFromFloat(21000.0))
if assert.Len(t, closedOrders, 1, "should trigger the stop sell order") {
assert.Len(t, trades, 1, "should have stop order trade executed")
assert.Equal(t, types.SideTypeSell, closedOrders[0].Side)
@ -348,11 +338,11 @@ func TestSimplePriceMatching_StopMarketOrderSell(t *testing.T) {
assert.Nil(t, trade, "place stop order should not trigger the stop sell")
assert.NotNil(t, createdOrder, "place stop order should not trigger the stop sell")
closedOrders, trades := engine.SellToPrice(fixedpoint.NewFromFloat(21500.0))
closedOrders, trades := engine.sellToPrice(fixedpoint.NewFromFloat(21500.0))
assert.Len(t, closedOrders, 0, "price change far from the price should not trigger the stop buy")
assert.Len(t, trades, 0, "price change far from the price should not trigger the stop buy")
closedOrders, trades = engine.SellToPrice(fixedpoint.NewFromFloat(20990.0))
closedOrders, trades = engine.sellToPrice(fixedpoint.NewFromFloat(20990.0))
assert.Len(t, closedOrders, 1, "should trigger the stop sell order")
assert.Len(t, trades, 1, "should have stop order trade executed")
@ -384,11 +374,11 @@ func TestSimplePriceMatching_PlaceLimitOrder(t *testing.T) {
assert.Len(t, engine.bidOrders, 5)
assert.Len(t, engine.askOrders, 5)
closedOrders, trades := engine.SellToPrice(fixedpoint.NewFromFloat(8100.0))
closedOrders, trades := engine.sellToPrice(fixedpoint.NewFromFloat(8100.0))
assert.Len(t, closedOrders, 0)
assert.Len(t, trades, 0)
closedOrders, trades = engine.SellToPrice(fixedpoint.NewFromFloat(8000.0))
closedOrders, trades = engine.sellToPrice(fixedpoint.NewFromFloat(8000.0))
assert.Len(t, closedOrders, 1)
assert.Len(t, trades, 1)
for _, trade := range trades {
@ -399,15 +389,15 @@ func TestSimplePriceMatching_PlaceLimitOrder(t *testing.T) {
assert.Equal(t, types.SideTypeBuy, o.Side)
}
closedOrders, trades = engine.SellToPrice(fixedpoint.NewFromFloat(7000.0))
closedOrders, trades = engine.sellToPrice(fixedpoint.NewFromFloat(7000.0))
assert.Len(t, closedOrders, 4)
assert.Len(t, trades, 4)
closedOrders, trades = engine.BuyToPrice(fixedpoint.NewFromFloat(8900.0))
closedOrders, trades = engine.buyToPrice(fixedpoint.NewFromFloat(8900.0))
assert.Len(t, closedOrders, 0)
assert.Len(t, trades, 0)
closedOrders, trades = engine.BuyToPrice(fixedpoint.NewFromFloat(9000.0))
closedOrders, trades = engine.buyToPrice(fixedpoint.NewFromFloat(9000.0))
assert.Len(t, closedOrders, 1)
assert.Len(t, trades, 1)
for _, o := range closedOrders {
@ -417,7 +407,76 @@ func TestSimplePriceMatching_PlaceLimitOrder(t *testing.T) {
assert.Equal(t, types.SideTypeSell, trade.Side)
}
closedOrders, trades = engine.BuyToPrice(fixedpoint.NewFromFloat(9500.0))
closedOrders, trades = engine.buyToPrice(fixedpoint.NewFromFloat(9500.0))
assert.Len(t, closedOrders, 4)
assert.Len(t, trades, 4)
}
func Test_calculateNativeOrderFee(t *testing.T) {
market := getTestMarket()
t.Run("sellOrder", func(t *testing.T) {
order := types.Order{
SubmitOrder: types.SubmitOrder{
Symbol: market.Symbol,
Side: types.SideTypeSell,
Type: types.OrderTypeLimit,
Quantity: fixedpoint.NewFromFloat(0.1),
Price: fixedpoint.NewFromFloat(20000.0),
TimeInForce: types.TimeInForceGTC,
},
}
feeRate := fixedpoint.MustNewFromString("0.075%")
fee, feeCurrency := calculateNativeOrderFee(&order, market, feeRate)
assert.Equal(t, "1.5", fee.String())
assert.Equal(t, "USDT", feeCurrency)
})
t.Run("buyOrder", func(t *testing.T) {
order := types.Order{
SubmitOrder: types.SubmitOrder{
Symbol: market.Symbol,
Side: types.SideTypeBuy,
Type: types.OrderTypeLimit,
Quantity: fixedpoint.NewFromFloat(0.1),
Price: fixedpoint.NewFromFloat(20000.0),
TimeInForce: types.TimeInForceGTC,
},
}
feeRate := fixedpoint.MustNewFromString("0.075%")
fee, feeCurrency := calculateNativeOrderFee(&order, market, feeRate)
assert.Equal(t, "0.000075", fee.String())
assert.Equal(t, "BTC", feeCurrency)
})
}
func TestSimplePriceMatching_LimitTakerOrder(t *testing.T) {
account := getTestAccount()
market := getTestMarket()
engine := &SimplePriceMatching{
Account: account,
Market: market,
closedOrders: make(map[uint64]types.Order),
LastPrice: fixedpoint.NewFromFloat(20000.0),
}
closedOrder, trade, err := engine.PlaceOrder(newLimitOrder("BTCUSDT", types.SideTypeBuy, 21000.0, 1.0))
assert.NoError(t, err)
if assert.NotNil(t, closedOrder) {
if assert.NotNil(t, trade) {
assert.Equal(t, "20000", trade.Price.String())
assert.False(t, trade.IsMaker, "should be taker")
}
}
closedOrder, trade, err = engine.PlaceOrder(newLimitOrder("BTCUSDT", types.SideTypeSell, 19000.0, 1.0))
assert.NoError(t, err)
if assert.NotNil(t, closedOrder) {
assert.Equal(t, "19000", closedOrder.Price.String())
if assert.NotNil(t, trade) {
assert.Equal(t, "20000", trade.Price.String())
assert.False(t, trade.IsMaker, "should be taker")
}
}
}

View File

@ -116,9 +116,13 @@ type SubmitOrder struct {
Side SideType `json:"side" db:"side"`
Type OrderType `json:"orderType" db:"order_type"`
Quantity fixedpoint.Value `json:"quantity" db:"quantity"`
Price fixedpoint.Value `json:"price" db:"price"`
StopPrice fixedpoint.Value `json:"stopPrice,omitempty" db:"stop_price"`
Quantity fixedpoint.Value `json:"quantity" db:"quantity"`
Price fixedpoint.Value `json:"price" db:"price"`
// AveragePrice is only used in back-test currently
AveragePrice fixedpoint.Value `json:"averagePrice"`
StopPrice fixedpoint.Value `json:"stopPrice,omitempty" db:"stop_price"`
Market Market `json:"-" db:"-"`