bbgo_origin/pkg/backtest/matching.go

464 lines
11 KiB
Go
Raw Normal View History

2020-11-07 08:08:20 +00:00
package backtest
import (
2020-11-09 08:34:35 +00:00
"fmt"
"sync"
"sync/atomic"
2020-11-07 08:08:20 +00:00
"time"
"github.com/pkg/errors"
2020-11-07 08:08:20 +00:00
"github.com/c9s/bbgo/pkg/fixedpoint"
"github.com/c9s/bbgo/pkg/types"
)
// DefaultFeeRate set the fee rate for most cases
// BINANCE uses 0.1% for both maker and taker
2020-11-08 19:01:40 +00:00
// for BNB holders, it's 0.075% for both maker and taker
// MAX uses 0.050% for maker and 0.15% for taker
const DefaultFeeRate = 0.075 * 0.01
var orderID uint64 = 1
var tradeID uint64 = 1
2020-11-07 08:08:20 +00:00
func incOrderID() uint64 {
return atomic.AddUint64(&orderID, 1)
2020-11-07 08:08:20 +00:00
}
func incTradeID() uint64 {
return atomic.AddUint64(&tradeID, 1)
}
// SimplePriceMatching implements a simple kline data driven matching engine for backtest
//go:generate callbackgen -type SimplePriceMatching
type SimplePriceMatching struct {
Symbol string
Market types.Market
2020-11-07 08:08:20 +00:00
mu sync.Mutex
bidOrders []types.Order
askOrders []types.Order
2020-11-07 08:08:20 +00:00
LastPrice fixedpoint.Value
2021-03-15 18:13:52 +00:00
LastKLine types.KLine
CurrentTime time.Time
Account *types.Account
2021-12-05 04:10:45 +00:00
MakerFeeRate fixedpoint.Value `json:"makerFeeRate"`
TakerFeeRate fixedpoint.Value `json:"takerFeeRate"`
tradeUpdateCallbacks []func(trade types.Trade)
orderUpdateCallbacks []func(order types.Order)
2020-11-10 06:18:04 +00:00
balanceUpdateCallbacks []func(balances types.BalanceMap)
2020-11-07 08:08:20 +00:00
}
func (m *SimplePriceMatching) CancelOrder(o types.Order) (types.Order, error) {
found := false
2020-11-07 08:08:20 +00:00
switch o.Side {
2020-11-07 08:08:20 +00:00
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()
2020-11-07 08:08:20 +00:00
case types.SideTypeSell:
m.mu.Lock()
var orders []types.Order
2020-11-10 06:18:04 +00:00
for _, order := range m.askOrders {
if o.OrderID == order.OrderID {
found = true
continue
}
orders = append(orders, order)
}
2020-11-10 06:18:04 +00:00
m.askOrders = orders
m.mu.Unlock()
2020-11-07 08:08:20 +00:00
}
if !found {
2020-11-09 08:34:35 +00:00
return o, fmt.Errorf("cancel order failed, order %d not found: %+v", o.OrderID, o)
}
switch o.Side {
case types.SideTypeBuy:
2020-11-10 06:18:04 +00:00
if err := m.Account.UnlockBalance(m.Market.QuoteCurrency, fixedpoint.NewFromFloat(o.Price*o.Quantity)); err != nil {
return o, err
}
case types.SideTypeSell:
2020-11-10 06:18:04 +00:00
if err := m.Account.UnlockBalance(m.Market.BaseCurrency, fixedpoint.NewFromFloat(o.Quantity)); err != nil {
return o, err
}
2020-11-07 08:08:20 +00:00
}
o.Status = types.OrderStatusCanceled
m.EmitOrderUpdate(o)
2020-11-10 06:18:04 +00:00
m.EmitBalanceUpdate(m.Account.Balances())
return o, nil
2020-11-07 08:08:20 +00:00
}
func (m *SimplePriceMatching) PlaceOrder(o types.SubmitOrder) (closedOrders *types.Order, trades *types.Trade, err error) {
// price for checking account balance
price := o.Price
switch o.Type {
case types.OrderTypeMarket:
price = m.LastPrice.Float64()
case types.OrderTypeLimit, types.OrderTypeLimitMaker:
price = o.Price
}
2022-01-29 09:44:42 +00:00
if o.Quantity < m.Market.MinQuantity {
return nil, nil, fmt.Errorf("order quantity %f is less than minQuantity %f, order: %+v", o.Quantity, m.Market.MinQuantity, o)
}
quoteQuantity := o.Quantity * price
if quoteQuantity < m.Market.MinNotional {
return nil, nil, fmt.Errorf("order amount %f is less than minNotional %f, order: %+v", quoteQuantity, m.Market.MinNotional, o)
}
switch o.Side {
case types.SideTypeBuy:
2022-01-29 09:44:42 +00:00
if err := m.Account.LockBalance(m.Market.QuoteCurrency, fixedpoint.NewFromFloat(quoteQuantity)); err != nil {
return nil, nil, err
}
case types.SideTypeSell:
2022-01-29 09:44:42 +00:00
if err := m.Account.LockBalance(m.Market.BaseCurrency, fixedpoint.NewFromFloat(o.Quantity)); err != nil {
return nil, nil, err
}
}
2020-11-10 06:18:04 +00:00
m.EmitBalanceUpdate(m.Account.Balances())
2020-11-10 06:18:04 +00:00
// start from one
orderID := incOrderID()
order := m.newOrder(o, orderID)
if o.Type == types.OrderTypeMarket {
m.EmitOrderUpdate(order)
// emit trade before we publish order
trade := m.newTradeFromOrder(order, false)
m.executeTrade(trade)
// update the order status
order.Status = types.OrderStatusFilled
order.ExecutedQuantity = order.Quantity
order.Price = price
m.EmitOrderUpdate(order)
2020-11-10 06:18:04 +00:00
m.EmitBalanceUpdate(m.Account.Balances())
return &order, &trade, nil
}
// for limit maker orders
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)
return &order, nil, nil
}
func (m *SimplePriceMatching) executeTrade(trade types.Trade) {
var err error
// execute trade, update account balances
if trade.IsBuyer {
2020-11-10 06:18:04 +00:00
err = m.Account.UseLockedBalance(m.Market.QuoteCurrency, fixedpoint.NewFromFloat(trade.Price*trade.Quantity))
2021-12-10 07:03:15 +00:00
m.Account.AddBalance(m.Market.BaseCurrency, fixedpoint.NewFromFloat(trade.Quantity))
} else {
2020-11-10 06:18:04 +00:00
err = m.Account.UseLockedBalance(m.Market.BaseCurrency, fixedpoint.NewFromFloat(trade.Quantity))
2021-12-10 07:03:15 +00:00
m.Account.AddBalance(m.Market.QuoteCurrency, fixedpoint.NewFromFloat(trade.Quantity*trade.Price))
}
if err != nil {
panic(errors.Wrapf(err, "executeTrade exception, wanted to use more than the locked balance"))
}
m.EmitTradeUpdate(trade)
2020-11-10 06:18:04 +00:00
m.EmitBalanceUpdate(m.Account.Balances())
return
}
func (m *SimplePriceMatching) newTradeFromOrder(order types.Order, isMaker bool) types.Trade {
// BINANCE uses 0.1% for both maker and taker
// MAX uses 0.050% for maker and 0.15% for taker
2021-12-05 04:10:45 +00:00
var feeRate = DefaultFeeRate
if isMaker {
if m.MakerFeeRate > 0 {
feeRate = m.MakerFeeRate.Float64()
}
} else {
if m.TakerFeeRate > 0 {
feeRate = m.TakerFeeRate.Float64()
}
}
price := order.Price
switch order.Type {
case types.OrderTypeMarket, types.OrderTypeStopMarket:
price = m.LastPrice.Float64()
}
var fee float64
var feeCurrency string
switch order.Side {
case types.SideTypeBuy:
2021-12-05 04:10:45 +00:00
fee = order.Quantity * feeRate
feeCurrency = m.Market.BaseCurrency
case types.SideTypeSell:
fee = order.Quantity * price * feeRate
feeCurrency = m.Market.QuoteCurrency
}
var id = incTradeID()
return types.Trade{
2021-12-23 05:15:27 +00:00
ID: id,
OrderID: order.OrderID,
Exchange: "backtest",
Price: price,
Quantity: order.Quantity,
QuoteQuantity: order.Quantity * price,
Symbol: order.Symbol,
Side: order.Side,
IsBuyer: order.Side == types.SideTypeBuy,
IsMaker: isMaker,
2021-05-19 17:32:26 +00:00
Time: types.Time(m.CurrentTime),
Fee: fee,
FeeCurrency: feeCurrency,
}
}
func (m *SimplePriceMatching) BuyToPrice(price fixedpoint.Value) (closedOrders []types.Order, trades []types.Trade) {
var priceF = price.Float64()
var askOrders []types.Order
2020-11-08 19:17:02 +00:00
for _, o := range m.askOrders {
switch o.Type {
case types.OrderTypeStopMarket:
// should we trigger the order
2020-11-08 19:17:02 +00:00
if priceF <= o.StopPrice {
// not triggering it, put it back
askOrders = append(askOrders, o)
break
}
2020-11-08 19:17:02 +00:00
o.Type = types.OrderTypeMarket
o.ExecutedQuantity = o.Quantity
o.Price = priceF
o.Status = types.OrderStatusFilled
closedOrders = append(closedOrders, o)
2020-11-08 19:17:02 +00:00
trade := m.newTradeFromOrder(o, false)
m.executeTrade(trade)
2020-11-08 19:17:02 +00:00
trades = append(trades, trade)
m.EmitOrderUpdate(o)
case types.OrderTypeStopLimit:
// should we trigger the order?
if priceF <= o.StopPrice {
askOrders = append(askOrders, o)
2020-11-08 19:17:02 +00:00
break
}
2020-11-08 19:17:02 +00:00
o.Type = types.OrderTypeLimit
2020-11-08 19:17:02 +00:00
// is it a taker order?
if priceF >= o.Price {
o.ExecutedQuantity = o.Quantity
o.Status = types.OrderStatusFilled
closedOrders = append(closedOrders, o)
2020-11-08 19:17:02 +00:00
trade := m.newTradeFromOrder(o, false)
m.executeTrade(trade)
2020-11-08 19:17:02 +00:00
trades = append(trades, trade)
2020-11-08 19:17:02 +00:00
m.EmitOrderUpdate(o)
} else {
2020-11-08 19:17:02 +00:00
// maker order
askOrders = append(askOrders, o)
}
case types.OrderTypeLimit, types.OrderTypeLimitMaker:
if priceF >= o.Price {
o.ExecutedQuantity = o.Quantity
o.Status = types.OrderStatusFilled
closedOrders = append(closedOrders, o)
trade := m.newTradeFromOrder(o, true)
m.executeTrade(trade)
trades = append(trades, trade)
m.EmitOrderUpdate(o)
} else {
askOrders = append(askOrders, o)
}
default:
askOrders = append(askOrders, o)
}
}
m.askOrders = askOrders
m.LastPrice = price
return closedOrders, trades
}
func (m *SimplePriceMatching) SellToPrice(price fixedpoint.Value) (closedOrders []types.Order, trades []types.Trade) {
var sellPrice = price.Float64()
var bidOrders []types.Order
for _, o := range m.bidOrders {
switch o.Type {
case types.OrderTypeStopMarket:
// should we trigger the order
if sellPrice <= o.StopPrice {
o.ExecutedQuantity = o.Quantity
o.Price = sellPrice
o.Status = types.OrderStatusFilled
closedOrders = append(closedOrders, o)
trade := m.newTradeFromOrder(o, false)
m.executeTrade(trade)
trades = append(trades, trade)
m.EmitOrderUpdate(o)
} else {
bidOrders = append(bidOrders, o)
}
case types.OrderTypeStopLimit:
// should we trigger the order
if sellPrice <= o.StopPrice {
o.Type = types.OrderTypeLimit
if sellPrice <= o.Price {
o.ExecutedQuantity = o.Quantity
o.Status = types.OrderStatusFilled
closedOrders = append(closedOrders, o)
trade := m.newTradeFromOrder(o, false)
m.executeTrade(trade)
trades = append(trades, trade)
m.EmitOrderUpdate(o)
} else {
bidOrders = append(bidOrders, o)
}
} else {
bidOrders = append(bidOrders, o)
}
case types.OrderTypeLimit, types.OrderTypeLimitMaker:
if sellPrice <= o.Price {
o.ExecutedQuantity = o.Quantity
o.Status = types.OrderStatusFilled
closedOrders = append(closedOrders, o)
trade := m.newTradeFromOrder(o, true)
m.executeTrade(trade)
trades = append(trades, trade)
m.EmitOrderUpdate(o)
} else {
bidOrders = append(bidOrders, o)
}
default:
bidOrders = append(bidOrders, o)
}
}
m.bidOrders = bidOrders
m.LastPrice = price
return closedOrders, trades
}
2020-11-10 06:18:04 +00:00
func (m *SimplePriceMatching) processKLine(kline types.KLine) {
m.CurrentTime = kline.EndTime.Time()
2021-03-15 18:13:52 +00:00
m.LastKLine = kline
2020-12-04 02:18:51 +00:00
switch kline.Direction() {
case types.DirectionDown:
if kline.High >= kline.Open {
2020-11-10 06:18:04 +00:00
m.BuyToPrice(fixedpoint.NewFromFloat(kline.High))
}
if kline.Low > kline.Close {
2020-11-10 06:18:04 +00:00
m.SellToPrice(fixedpoint.NewFromFloat(kline.Low))
2020-11-10 11:06:20 +00:00
m.BuyToPrice(fixedpoint.NewFromFloat(kline.Close))
} else {
m.SellToPrice(fixedpoint.NewFromFloat(kline.Close))
}
2020-11-10 06:18:04 +00:00
2020-12-04 02:18:51 +00:00
case types.DirectionUp:
if kline.Low <= kline.Open {
2020-11-10 06:18:04 +00:00
m.SellToPrice(fixedpoint.NewFromFloat(kline.Low))
}
if kline.High > kline.Close {
2020-11-10 06:18:04 +00:00
m.BuyToPrice(fixedpoint.NewFromFloat(kline.High))
2020-11-10 11:06:20 +00:00
m.SellToPrice(fixedpoint.NewFromFloat(kline.Close))
} else {
m.BuyToPrice(fixedpoint.NewFromFloat(kline.Close))
}
2022-01-29 09:44:42 +00:00
default: // no trade up or down
if m.LastPrice == 0 {
m.BuyToPrice(fixedpoint.NewFromFloat(kline.Close))
}
2020-11-07 08:08:20 +00:00
}
}
func (m *SimplePriceMatching) newOrder(o types.SubmitOrder, orderID uint64) types.Order {
2020-11-07 08:08:20 +00:00
return types.Order{
OrderID: orderID,
2020-11-07 08:08:20 +00:00
SubmitOrder: o,
Exchange: types.ExchangeBacktest,
2020-11-07 08:08:20 +00:00
Status: types.OrderStatusNew,
ExecutedQuantity: 0,
2020-11-10 06:18:04 +00:00
IsWorking: true,
2021-05-19 17:32:26 +00:00
CreationTime: types.Time(m.CurrentTime),
UpdateTime: types.Time(m.CurrentTime),
2020-11-07 08:08:20 +00:00
}
}