backtest: fix order update_time update in the matching engine

fixes: #631
This commit is contained in:
c9s 2022-05-22 02:40:12 +08:00
parent f06ec76618
commit 18fc68f6c6
No known key found for this signature in database
GPG Key ID: 7385E7E464CB0A54
3 changed files with 94 additions and 25 deletions

View File

@ -160,7 +160,7 @@ const ordersToMarkets = (interval, orders) => {
let endTime = (startTime + intervalSecs); let endTime = (startTime + intervalSecs);
// skip the marker in the same interval of the last marker // skip the marker in the same interval of the last marker
if (t < endTime) { if (t < endTime) {
continue // continue
} }
} }

View File

@ -7,6 +7,7 @@ import (
"time" "time"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/sirupsen/logrus"
"github.com/c9s/bbgo/pkg/fixedpoint" "github.com/c9s/bbgo/pkg/fixedpoint"
"github.com/c9s/bbgo/pkg/types" "github.com/c9s/bbgo/pkg/types"
@ -23,15 +24,18 @@ func incTradeID() uint64 {
return atomic.AddUint64(&tradeID, 1) return atomic.AddUint64(&tradeID, 1)
} }
var klineMatchingLogger = logrus.WithField("backtest", "klineEngine")
// SimplePriceMatching implements a simple kline data driven matching engine for backtest // SimplePriceMatching implements a simple kline data driven matching engine for backtest
//go:generate callbackgen -type SimplePriceMatching //go:generate callbackgen -type SimplePriceMatching
type SimplePriceMatching struct { type SimplePriceMatching struct {
Symbol string Symbol string
Market types.Market Market types.Market
mu sync.Mutex mu sync.Mutex
bidOrders []types.Order bidOrders []types.Order
askOrders []types.Order askOrders []types.Order
closedOrders []types.Order
LastPrice fixedpoint.Value LastPrice fixedpoint.Value
LastKLine types.KLine LastKLine types.KLine
@ -118,11 +122,9 @@ func (m *SimplePriceMatching) PlaceOrder(o types.SubmitOrder) (closedOrders *typ
return nil, nil, fmt.Errorf("order quantity %s is less than minQuantity %s, order: %+v", o.Quantity.String(), m.Market.MinQuantity.String(), o) return nil, nil, fmt.Errorf("order quantity %s is less than minQuantity %s, order: %+v", o.Quantity.String(), m.Market.MinQuantity.String(), o)
} }
if !price.IsZero() { quoteQuantity := o.Quantity.Mul(price)
quoteQuantity := o.Quantity.Mul(price) if quoteQuantity.Compare(m.Market.MinNotional) < 0 {
if quoteQuantity.Compare(m.Market.MinNotional) < 0 { return nil, nil, fmt.Errorf("order amount %s is less than minNotional %s, order: %+v", quoteQuantity.String(), m.Market.MinNotional.String(), o)
return nil, nil, fmt.Errorf("order amount %s is less than minNotional %s, order: %+v", quoteQuantity.String(), m.Market.MinNotional.String(), o)
}
} }
switch o.Side { switch o.Side {
@ -147,7 +149,7 @@ func (m *SimplePriceMatching) PlaceOrder(o types.SubmitOrder) (closedOrders *typ
m.EmitOrderUpdate(order) m.EmitOrderUpdate(order)
// emit trade before we publish order // emit trade before we publish order
trade := m.newTradeFromOrder(order, false) trade := m.newTradeFromOrder(&order, false)
m.executeTrade(trade) m.executeTrade(trade)
// update the order status // update the order status
@ -184,11 +186,9 @@ func (m *SimplePriceMatching) executeTrade(trade types.Trade) {
// execute trade, update account balances // execute trade, update account balances
if trade.IsBuyer { if trade.IsBuyer {
err = m.Account.UseLockedBalance(m.Market.QuoteCurrency, trade.Price.Mul(trade.Quantity)) err = m.Account.UseLockedBalance(m.Market.QuoteCurrency, trade.Price.Mul(trade.Quantity))
m.Account.AddBalance(m.Market.BaseCurrency, trade.Quantity.Sub(trade.Fee.Div(trade.Price))) m.Account.AddBalance(m.Market.BaseCurrency, trade.Quantity.Sub(trade.Fee.Div(trade.Price)))
} else { } else {
err = m.Account.UseLockedBalance(m.Market.BaseCurrency, trade.Quantity) err = m.Account.UseLockedBalance(m.Market.BaseCurrency, trade.Quantity)
m.Account.AddBalance(m.Market.QuoteCurrency, trade.Quantity.Mul(trade.Price).Sub(trade.Fee)) m.Account.AddBalance(m.Market.QuoteCurrency, trade.Quantity.Mul(trade.Price).Sub(trade.Fee))
} }
@ -201,7 +201,7 @@ func (m *SimplePriceMatching) executeTrade(trade types.Trade) {
return return
} }
func (m *SimplePriceMatching) newTradeFromOrder(order types.Order, isMaker bool) types.Trade { func (m *SimplePriceMatching) newTradeFromOrder(order *types.Order, isMaker bool) types.Trade {
// BINANCE uses 0.1% for both maker and taker // BINANCE uses 0.1% for both maker and taker
// MAX uses 0.050% for maker and 0.15% for taker // MAX uses 0.050% for maker and 0.15% for taker
var feeRate fixedpoint.Value var feeRate fixedpoint.Value
@ -258,6 +258,8 @@ func (m *SimplePriceMatching) newTradeFromOrder(order types.Order, isMaker bool)
} }
func (m *SimplePriceMatching) BuyToPrice(price fixedpoint.Value) (closedOrders []types.Order, trades []types.Trade) { func (m *SimplePriceMatching) BuyToPrice(price fixedpoint.Value) (closedOrders []types.Order, trades []types.Trade) {
klineMatchingLogger.Debugf("kline buy to price %s", price.String())
var askOrders []types.Order var askOrders []types.Order
for _, o := range m.askOrders { for _, o := range m.askOrders {
@ -320,19 +322,24 @@ func (m *SimplePriceMatching) BuyToPrice(price fixedpoint.Value) (closedOrders [
m.askOrders = askOrders m.askOrders = askOrders
m.LastPrice = price m.LastPrice = price
for _, o := range closedOrders { for i := range closedOrders {
trade := m.newTradeFromOrder(o, true) o := closedOrders[i]
trade := m.newTradeFromOrder(&o, true)
m.executeTrade(trade) m.executeTrade(trade)
closedOrders[i] = o
trades = append(trades, trade) trades = append(trades, trade)
m.EmitOrderUpdate(o) m.EmitOrderUpdate(o)
} }
m.closedOrders = append(m.closedOrders, closedOrders...)
return closedOrders, trades return closedOrders, trades
} }
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())
var sellPrice = price var sellPrice = price
var bidOrders []types.Order var bidOrders []types.Order
for _, o := range m.bidOrders { for _, o := range m.bidOrders {
@ -370,9 +377,6 @@ func (m *SimplePriceMatching) SellToPrice(price fixedpoint.Value) (closedOrders
case types.OrderTypeLimit, types.OrderTypeLimitMaker: case types.OrderTypeLimit, types.OrderTypeLimitMaker:
if sellPrice.Compare(o.Price) <= 0 { if sellPrice.Compare(o.Price) <= 0 {
if o.Price.Compare(m.LastKLine.High) > 0 {
o.Price = m.LastKLine.High
}
o.ExecutedQuantity = o.Quantity o.ExecutedQuantity = o.Quantity
o.Status = types.OrderStatusFilled o.Status = types.OrderStatusFilled
closedOrders = append(closedOrders, o) closedOrders = append(closedOrders, o)
@ -388,14 +392,17 @@ func (m *SimplePriceMatching) SellToPrice(price fixedpoint.Value) (closedOrders
m.bidOrders = bidOrders m.bidOrders = bidOrders
m.LastPrice = price m.LastPrice = price
for _, o := range closedOrders { for i := range closedOrders {
trade := m.newTradeFromOrder(o, true) o := closedOrders[i]
trade := m.newTradeFromOrder(&o, true)
m.executeTrade(trade) m.executeTrade(trade)
closedOrders[i] = o
trades = append(trades, trade) trades = append(trades, trade)
m.EmitOrderUpdate(o) m.EmitOrderUpdate(o)
} }
m.closedOrders = append(m.closedOrders, closedOrders...)
return closedOrders, trades return closedOrders, trades
} }
@ -410,7 +417,8 @@ func (m *SimplePriceMatching) processKLine(kline types.KLine) {
m.BuyToPrice(kline.High) m.BuyToPrice(kline.High)
} }
if kline.Low.Compare(kline.Close) > 0 { // 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.SellToPrice(kline.Low)
m.BuyToPrice(kline.Close) m.BuyToPrice(kline.Close)
} else { } else {

View File

@ -21,7 +21,69 @@ func newLimitOrder(symbol string, side types.SideType, price, quantity float64)
} }
} }
func TestSimplePriceMatching_LimitOrder(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"),
}
t1 := time.Date(2021, 7, 1, 0, 0, 0, 0, time.UTC)
engine := &SimplePriceMatching{
Account: account,
Market: market,
CurrentTime: t1,
}
for i := 0; i <= 5; i++ {
var p = 20000.0 + float64(i)*1000.0
_, _, err := engine.PlaceOrder(newLimitOrder("BTCUSDT", types.SideTypeBuy, p, 0.001))
assert.NoError(t, err)
}
t2 := t1.Add(time.Minute)
// should match 25000, 24000
k := newKLine("BTCUSDT", types.Interval1m, t2, 26000, 27000, 23000, 25000)
assert.Equal(t, t2.Add(time.Minute-time.Millisecond), k.EndTime.Time())
engine.processKLine(k)
assert.Equal(t, 3, len(engine.bidOrders))
assert.Len(t, engine.bidOrders, 3)
assert.Equal(t, 3, len(engine.closedOrders))
for _, o := range engine.closedOrders {
assert.Equal(t, k.EndTime.Time(), o.UpdateTime.Time())
}
}
func newKLine(symbol string, interval types.Interval, startTime time.Time, o, h, l, c float64) types.KLine {
return types.KLine{
Symbol: symbol,
StartTime: types.Time(startTime),
EndTime: types.Time(startTime.Add(interval.Duration() - time.Millisecond)),
Interval: interval,
Open: fixedpoint.NewFromFloat(o),
High: fixedpoint.NewFromFloat(h),
Low: fixedpoint.NewFromFloat(l),
Close: fixedpoint.NewFromFloat(c),
Closed: true,
}
}
func TestSimplePriceMatching_PlaceLimitOrder(t *testing.T) {
account := &types.Account{ account := &types.Account{
MakerFeeRate: fixedpoint.NewFromFloat(0.075 * 0.01), MakerFeeRate: fixedpoint.NewFromFloat(0.075 * 0.01),
TakerFeeRate: fixedpoint.NewFromFloat(0.075 * 0.01), TakerFeeRate: fixedpoint.NewFromFloat(0.075 * 0.01),
@ -44,9 +106,8 @@ func TestSimplePriceMatching_LimitOrder(t *testing.T) {
} }
engine := &SimplePriceMatching{ engine := &SimplePriceMatching{
CurrentTime: time.Now(), Account: account,
Account: account, Market: market,
Market: market,
} }
for i := 0; i < 5; i++ { for i := 0; i < 5; i++ {