package backtest import ( "fmt" "sync" "sync/atomic" "time" "github.com/pkg/errors" "github.com/sirupsen/logrus" "github.com/c9s/bbgo/pkg/fixedpoint" "github.com/c9s/bbgo/pkg/types" "github.com/c9s/bbgo/pkg/util" ) var orderID uint64 = 1 var tradeID uint64 = 1 func incOrderID() uint64 { return atomic.AddUint64(&orderID, 1) } func incTradeID() uint64 { return atomic.AddUint64(&tradeID, 1) } var klineMatchingLogger *logrus.Entry = nil // FeeToken is used to simulate the exchange platform fee token // This is to ease the back-testing environment for closing positions. const FeeToken = "FEE" var useFeeToken = true func init() { logger := logrus.New() if v, ok := util.GetEnvVarBool("DEBUG_MATCHING"); ok && v { logger.SetLevel(logrus.DebugLevel) } else { logger.SetLevel(logrus.ErrorLevel) } klineMatchingLogger = logger.WithField("backtest", "klineEngine") if v, ok := util.GetEnvVarBool("BACKTEST_USE_FEE_TOKEN"); ok { useFeeToken = v } } // SimplePriceMatching implements a simple kline data driven matching engine for backtest //go:generate callbackgen -type SimplePriceMatching type SimplePriceMatching struct { Symbol string Market types.Market mu sync.Mutex bidOrders []types.Order askOrders []types.Order closedOrders map[uint64]types.Order LastPrice fixedpoint.Value LastKLine types.KLine CurrentTime time.Time Account *types.Account tradeUpdateCallbacks []func(trade types.Trade) orderUpdateCallbacks []func(order types.Order) balanceUpdateCallbacks []func(balances types.BalanceMap) } func (m *SimplePriceMatching) CancelOrder(o types.Order) (types.Order, error) { found := false switch o.Side { case types.SideTypeBuy: m.mu.Lock() var orders []types.Order for _, order := range m.bidOrders { if o.OrderID == order.OrderID { found = true continue } orders = append(orders, order) } m.bidOrders = orders m.mu.Unlock() case types.SideTypeSell: m.mu.Lock() var orders []types.Order for _, order := range m.askOrders { if o.OrderID == order.OrderID { found = true continue } orders = append(orders, order) } m.askOrders = orders m.mu.Unlock() } if !found { return o, fmt.Errorf("cancel order failed, order %d not found: %+v", o.OrderID, o) } switch o.Side { case types.SideTypeBuy: if err := m.Account.UnlockBalance(m.Market.QuoteCurrency, o.Price.Mul(o.Quantity)); err != nil { return o, err } case types.SideTypeSell: if err := m.Account.UnlockBalance(m.Market.BaseCurrency, o.Quantity); err != nil { return o, err } } o.Status = types.OrderStatusCanceled m.EmitOrderUpdate(o) m.EmitBalanceUpdate(m.Account.Balances()) return o, nil } // 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: price = m.LastPrice case types.OrderTypeLimit, types.OrderTypeStopLimit, types.OrderTypeLimitMaker: price = o.Price } if o.Quantity.Compare(m.Market.MinQuantity) < 0 { return nil, nil, fmt.Errorf("order quantity %s is less than minQuantity %s, order: %+v", o.Quantity.String(), m.Market.MinQuantity.String(), o) } quoteQuantity := o.Quantity.Mul(price) 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) } switch o.Side { case types.SideTypeBuy: if err := m.Account.LockBalance(m.Market.QuoteCurrency, quoteQuantity); err != nil { return nil, nil, err } case types.SideTypeSell: if err := m.Account.LockBalance(m.Market.BaseCurrency, o.Quantity); err != nil { return nil, nil, err } } m.EmitBalanceUpdate(m.Account.Balances()) // start from one orderID := incOrderID() order := m.newOrder(o, orderID) if isTaker { // emit the order update for Status:New m.EmitOrderUpdate(order) // copy the order object to avoid side effect (for different callbacks) var order2 = order // emit trade before we publish order trade := m.newTradeFromOrder(&order2, false, m.LastPrice) m.executeTrade(trade) // update the order status order2.Status = types.OrderStatusFilled order2.ExecutedQuantity = order2.Quantity order2.IsWorking = false // let the exchange emit the "FILLED" order update (we need the closed order) // m.EmitOrderUpdate(order2) return &order2, &trade, nil } // For limit maker orders (open status) // TODO: handle limit taker order switch o.Side { case types.SideTypeBuy: m.mu.Lock() m.bidOrders = append(m.bidOrders, order) m.mu.Unlock() case types.SideTypeSell: m.mu.Lock() m.askOrders = append(m.askOrders, order) m.mu.Unlock() } m.EmitOrderUpdate(order) // emit order New status return &order, nil, nil } func (m *SimplePriceMatching) executeTrade(trade types.Trade) { var err error // execute trade, update account balances if trade.IsBuyer { err = m.Account.UseLockedBalance(m.Market.QuoteCurrency, trade.QuoteQuantity) // here the fee currency is the base currency q := trade.Quantity if trade.FeeCurrency == m.Market.BaseCurrency { q = q.Sub(trade.Fee) } m.Account.AddBalance(m.Market.BaseCurrency, q) } else { err = m.Account.UseLockedBalance(m.Market.BaseCurrency, trade.Quantity) // here the fee currency is the quote currency qq := trade.QuoteQuantity if trade.FeeCurrency == m.Market.QuoteCurrency { qq = qq.Sub(trade.Fee) } m.Account.AddBalance(m.Market.QuoteCurrency, qq) } if err != nil { panic(errors.Wrapf(err, "executeTrade exception, wanted to use more than the locked balance")) } m.EmitTradeUpdate(trade) m.EmitBalanceUpdate(m.Account.Balances()) } 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 if isMaker { feeRate = m.Account.MakerFeeRate } else { feeRate = m.Account.TakerFeeRate } return feeRate } 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 if useFeeToken { feeCurrency = FeeToken fee = quoteQuantity.Mul(feeRate) } else { fee, feeCurrency = calculateNativeOrderFee(order, m.Market, feeRate) } // update order time order.UpdateTime = types.Time(m.CurrentTime) var id = incTradeID() return types.Trade{ ID: id, OrderID: order.OrderID, Exchange: types.ExchangeBacktest, Price: price, Quantity: order.Quantity, QuoteQuantity: quoteQuantity, Symbol: order.Symbol, Side: order.Side, IsBuyer: order.Side == types.SideTypeBuy, IsMaker: isMaker, Time: types.Time(m.CurrentTime), Fee: fee, FeeCurrency: feeCurrency, } } // 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 for _, o := range m.bidOrders { switch o.Type { case types.OrderTypeStopMarket: // the price is still lower than the stop price, we will put the order back to the list if price.Compare(o.StopPrice) < 0 { // not triggering it, put it back bidOrders = append(bidOrders, o) break } o.Type = types.OrderTypeMarket o.ExecutedQuantity = o.Quantity o.Price = price o.Status = types.OrderStatusFilled closedOrders = append(closedOrders, o) case types.OrderTypeStopLimit: // the price is still lower than the stop price, we will put the order back to the list if price.Compare(o.StopPrice) < 0 { bidOrders = append(bidOrders, o) break } // convert this order to limit order // we use value object here, so it's a copy o.Type = types.OrderTypeLimit // is it a taker order? // higher than the current price, then it's a taker order if o.Price.Compare(price) >= 0 { // limit buy taker order, move it to the closed order // we assume that we have no price slippage here, so the latest price will be the executed price // TODO: simulate slippage here o.Price = price o.ExecutedQuantity = o.Quantity o.Status = types.OrderStatusFilled closedOrders = append(closedOrders, o) } else { // keep it as a maker order bidOrders = append(bidOrders, o) } default: bidOrders = append(bidOrders, o) } } m.bidOrders = bidOrders var askOrders []types.Order for _, o := range m.askOrders { switch o.Type { case types.OrderTypeStopMarket: // should we trigger the order if price.Compare(o.StopPrice) < 0 { // not triggering it, put it back askOrders = append(askOrders, o) break } o.Type = types.OrderTypeMarket o.ExecutedQuantity = o.Quantity o.Price = price o.Status = types.OrderStatusFilled closedOrders = append(closedOrders, o) case types.OrderTypeStopLimit: // should we trigger the order? if price.Compare(o.StopPrice) < 0 { askOrders = append(askOrders, o) break } o.Type = types.OrderTypeLimit // is it a taker order? // higher than the current price, then it's a taker order if o.Price.Compare(price) <= 0 { // limit sell order as taker, move it to the closed order // we assume that we have no price slippage here, so the latest price will be the executed price // TODO: simulate slippage here o.Price = price o.ExecutedQuantity = o.Quantity o.Status = types.OrderStatusFilled closedOrders = append(closedOrders, o) } else { // maker order askOrders = append(askOrders, o) } case types.OrderTypeLimit, types.OrderTypeLimitMaker: if price.Compare(o.Price) >= 0 { o.ExecutedQuantity = o.Quantity o.Status = types.OrderStatusFilled closedOrders = append(closedOrders, o) } else { askOrders = append(askOrders, o) } default: askOrders = append(askOrders, o) } } m.askOrders = askOrders m.LastPrice = price for i := range closedOrders { o := closedOrders[i] trade := m.newTradeFromOrder(&o, true, o.Price) m.executeTrade(trade) closedOrders[i] = o trades = append(trades, trade) m.EmitOrderUpdate(o) m.closedOrders[o.OrderID] = o } return closedOrders, trades } // 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) { klineMatchingLogger.Debugf("kline sell to price %s", price.String()) // in this section we handle --- the price goes lower, and we trigger the stop sell var askOrders []types.Order for _, o := range m.askOrders { switch o.Type { case types.OrderTypeStopMarket: // should we trigger the order if price.Compare(o.StopPrice) > 0 { askOrders = append(askOrders, o) break } o.Type = types.OrderTypeMarket o.ExecutedQuantity = o.Quantity o.Price = price o.Status = types.OrderStatusFilled closedOrders = append(closedOrders, o) case types.OrderTypeStopLimit: // if the price is lower than the stop price // we should trigger the stop sell order if price.Compare(o.StopPrice) > 0 { askOrders = append(askOrders, o) break } o.Type = types.OrderTypeLimit // handle TAKER SELL // if the order price is lower than the current price // it's a taker order if o.Price.Compare(price) <= 0 { o.Price = price o.ExecutedQuantity = o.Quantity o.Status = types.OrderStatusFilled closedOrders = append(closedOrders, o) } else { askOrders = append(askOrders, o) } default: askOrders = append(askOrders, o) } } m.askOrders = askOrders var bidOrders []types.Order for _, o := range m.bidOrders { switch o.Type { case types.OrderTypeStopMarket: // 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 } o.Type = types.OrderTypeMarket o.ExecutedQuantity = o.Quantity o.Price = price o.Status = types.OrderStatusFilled closedOrders = append(closedOrders, o) case types.OrderTypeStopLimit: // 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 } o.Type = types.OrderTypeLimit // taker order? if o.Price.Compare(price) >= 0 { o.Price = price o.ExecutedQuantity = o.Quantity o.Status = types.OrderStatusFilled closedOrders = append(closedOrders, o) } else { bidOrders = append(bidOrders, o) } case types.OrderTypeLimit, types.OrderTypeLimitMaker: if price.Compare(o.Price) <= 0 { o.ExecutedQuantity = o.Quantity o.Status = types.OrderStatusFilled closedOrders = append(closedOrders, o) } else { bidOrders = append(bidOrders, o) } default: bidOrders = append(bidOrders, o) } } m.bidOrders = bidOrders m.LastPrice = price for i := range closedOrders { o := closedOrders[i] trade := m.newTradeFromOrder(&o, true, o.Price) m.executeTrade(trade) closedOrders[i] = o trades = append(trades, trade) m.EmitOrderUpdate(o) m.closedOrders[o.OrderID] = o } return closedOrders, trades } func (m *SimplePriceMatching) getOrder(orderID uint64) (types.Order, bool) { if o, ok := m.closedOrders[orderID]; ok { return o, true } for _, o := range m.bidOrders { if o.OrderID == orderID { return o, true } } for _, o := range m.askOrders { if o.OrderID == orderID { return o, true } } return types.Order{}, false } func (m *SimplePriceMatching) processKLine(kline types.KLine) { m.CurrentTime = kline.EndTime.Time() if m.LastPrice.IsZero() { m.LastPrice = kline.Open } else { if m.LastPrice.Compare(kline.Open) > 0 { m.sellToPrice(kline.Open) } else { m.buyToPrice(kline.Open) } } switch kline.Direction() { case types.DirectionDown: if kline.High.Compare(kline.Open) >= 0 { 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) } else { m.sellToPrice(kline.Close) } case types.DirectionUp: if kline.Low.Compare(kline.Open) <= 0 { m.sellToPrice(kline.Low) } if kline.High.Compare(kline.Close) > 0 { m.buyToPrice(kline.High) m.sellToPrice(kline.Close) } else { m.buyToPrice(kline.Close) } default: // no trade up or down if m.LastPrice.IsZero() { m.buyToPrice(kline.Close) } } m.LastKLine = kline } func (m *SimplePriceMatching) newOrder(o types.SubmitOrder, orderID uint64) types.Order { return types.Order{ OrderID: orderID, SubmitOrder: o, Exchange: types.ExchangeBacktest, Status: types.OrderStatusNew, ExecutedQuantity: fixedpoint.Zero, IsWorking: true, CreationTime: types.Time(m.CurrentTime), 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)) }