backtest: move fee mode functions to fee.go

This commit is contained in:
c9s 2022-09-01 13:48:33 +08:00
parent 8cd646668a
commit 10ed706ed6
No known key found for this signature in database
GPG Key ID: 7385E7E464CB0A54
4 changed files with 164 additions and 93 deletions

View File

@ -36,10 +36,10 @@ import (
"github.com/sirupsen/logrus"
"github.com/c9s/bbgo/pkg/cache"
"github.com/pkg/errors"
"github.com/c9s/bbgo/pkg/cache"
"github.com/c9s/bbgo/pkg/bbgo"
"github.com/c9s/bbgo/pkg/service"
"github.com/c9s/bbgo/pkg/types"
@ -138,12 +138,15 @@ func (e *Exchange) addMatchingBook(symbol string, market types.Market) {
}
func (e *Exchange) _addMatchingBook(symbol string, market types.Market) {
e.matchingBooks[symbol] = &SimplePriceMatching{
CurrentTime: e.currentTime,
Account: e.account,
Market: market,
closedOrders: make(map[uint64]types.Order),
matching := &SimplePriceMatching{
currentTime: e.currentTime,
account: e.account,
Market: market,
closedOrders: make(map[uint64]types.Order),
feeModeFunction: getFeeModeFunction(e.config.FeeMode),
}
e.matchingBooks[symbol] = matching
}
func (e *Exchange) NewStream() types.Stream {
@ -257,7 +260,7 @@ func (e *Exchange) QueryTicker(ctx context.Context, symbol string) (*types.Ticke
return nil, fmt.Errorf("matching engine is not initialized for symbol %s", symbol)
}
kline := matching.LastKLine
kline := matching.lastKLine
return &types.Ticker{
Time: kline.EndTime.Time(),
Volume: kline.Volume,
@ -382,7 +385,7 @@ func (e *Exchange) ConsumeKLine(k types.KLine) {
e.currentTime = kline1m.EndTime.Time()
// here we generate trades and order updates
matching.processKLine(kline1m)
matching.NextKLine = &k
matching.nextKLine = &k
for _, kline := range matching.klineCache {
e.MarketDataStream.EmitKLineClosed(kline)
for _, h := range e.Src.Callbacks {

83
pkg/backtest/fee.go Normal file
View File

@ -0,0 +1,83 @@
package backtest
import (
"github.com/c9s/bbgo/pkg/bbgo"
"github.com/c9s/bbgo/pkg/fixedpoint"
"github.com/c9s/bbgo/pkg/types"
)
type FeeModeFunction func(order *types.Order, market *types.Market, feeRate fixedpoint.Value) (fee fixedpoint.Value, feeCurrency string)
func feeModeFunctionToken(order *types.Order, _ *types.Market, feeRate fixedpoint.Value) (fee fixedpoint.Value, feeCurrency string) {
quoteQuantity := order.Quantity.Mul(order.Price)
feeCurrency = FeeToken
fee = quoteQuantity.Mul(feeRate)
return fee, feeCurrency
}
func feeModeFunctionNative(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 feeModeFunctionQuote(order *types.Order, market *types.Market, feeRate fixedpoint.Value) (fee fixedpoint.Value, feeCurrency string) {
feeCurrency = market.QuoteCurrency
quoteQuantity := order.Quantity.Mul(order.Price)
switch order.Side {
case types.SideTypeBuy:
fee = quoteQuantity.Mul(feeRate)
case types.SideTypeSell:
fee = quoteQuantity.Mul(feeRate)
}
return fee, feeCurrency
}
func getFeeModeFunction(feeMode bbgo.BackTestFeeMode) FeeModeFunction {
switch feeMode {
case bbgo.BackTestFeeModeNative:
return feeModeFunctionNative
case bbgo.BackTestFeeModeQuote:
return feeModeFunctionQuote
case bbgo.BackTestFeeModeToken:
return feeModeFunctionToken
default:
return feeModeFunctionNative
}
}
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
}

View File

@ -59,12 +59,14 @@ type SimplePriceMatching struct {
closedOrders map[uint64]types.Order
klineCache map[types.Interval]types.KLine
LastPrice fixedpoint.Value
LastKLine types.KLine
NextKLine *types.KLine
CurrentTime time.Time
lastPrice fixedpoint.Value
lastKLine types.KLine
nextKLine *types.KLine
currentTime time.Time
Account *types.Account
feeModeFunction FeeModeFunction
account *types.Account
tradeUpdateCallbacks []func(trade types.Trade)
orderUpdateCallbacks []func(order types.Order)
@ -109,38 +111,38 @@ func (m *SimplePriceMatching) CancelOrder(o types.Order) (types.Order, error) {
switch o.Side {
case types.SideTypeBuy:
if err := m.Account.UnlockBalance(m.Market.QuoteCurrency, o.Price.Mul(o.Quantity)); err != nil {
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 {
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())
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() {
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)
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.Market.TruncatePrice(m.LastPrice)
price = m.Market.TruncatePrice(m.lastPrice)
case types.OrderTypeStopMarket:
// the actual price might be different.
@ -165,17 +167,17 @@ func (m *SimplePriceMatching) PlaceOrder(o types.SubmitOrder) (*types.Order, *ty
switch o.Side {
case types.SideTypeBuy:
if err := m.Account.LockBalance(m.Market.QuoteCurrency, quoteQuantity); err != nil {
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 {
if err := m.account.LockBalance(m.Market.BaseCurrency, o.Quantity); err != nil {
return nil, nil, err
}
}
m.EmitBalanceUpdate(m.Account.Balances())
m.EmitBalanceUpdate(m.account.Balances())
// start from one
orderID := incOrderID()
@ -184,19 +186,19 @@ func (m *SimplePriceMatching) PlaceOrder(o types.SubmitOrder) (*types.Order, *ty
if isTaker {
var price fixedpoint.Value
if order.Type == types.OrderTypeMarket {
order.Price = m.Market.TruncatePrice(m.LastPrice)
order.Price = m.Market.TruncatePrice(m.lastPrice)
price = order.Price
} else if order.Type == types.OrderTypeLimit {
// if limit order's price is with the range of next kline
// we assume it will be traded as a maker trade, and is traded at its original price
// TODO: if it is treated as a maker trade, fee should be specially handled
// otherwise, set NextKLine.Close(i.e., m.LastPrice) to be the taker traded price
if m.NextKLine != nil && m.NextKLine.High.Compare(order.Price) > 0 && order.Side == types.SideTypeBuy {
if m.nextKLine != nil && m.nextKLine.High.Compare(order.Price) > 0 && order.Side == types.SideTypeBuy {
order.AveragePrice = order.Price
} else if m.NextKLine != nil && m.NextKLine.Low.Compare(order.Price) < 0 && order.Side == types.SideTypeSell {
} else if m.nextKLine != nil && m.nextKLine.Low.Compare(order.Price) < 0 && order.Side == types.SideTypeSell {
order.AveragePrice = order.Price
} else {
order.AveragePrice = m.Market.TruncatePrice(m.LastPrice)
order.AveragePrice = m.Market.TruncatePrice(m.lastPrice)
}
price = order.AveragePrice
}
@ -223,10 +225,10 @@ func (m *SimplePriceMatching) PlaceOrder(o types.SubmitOrder) (*types.Order, *ty
// the executed price is lower than the given price, so we will use less quote currency to buy the base asset.
amount := order.Price.Sub(order.AveragePrice).Mul(order.Quantity)
if amount.Sign() > 0 {
if err := m.Account.UnlockBalance(m.Market.QuoteCurrency, amount); err != nil {
if err := m.account.UnlockBalance(m.Market.QuoteCurrency, amount); err != nil {
return nil, nil, err
}
m.EmitBalanceUpdate(m.Account.Balances())
m.EmitBalanceUpdate(m.account.Balances())
}
case types.SideTypeSell:
@ -234,8 +236,8 @@ func (m *SimplePriceMatching) PlaceOrder(o types.SubmitOrder) (*types.Order, *ty
// the executed price is higher than the given price, so we will get more quote currency back
amount := order.AveragePrice.Sub(order.Price).Mul(order.Quantity)
if amount.Sign() > 0 {
m.Account.AddBalance(m.Market.QuoteCurrency, amount)
m.EmitBalanceUpdate(m.Account.Balances())
m.account.AddBalance(m.Market.QuoteCurrency, amount)
m.EmitBalanceUpdate(m.account.Balances())
}
}
}
@ -273,7 +275,7 @@ 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)
err = m.account.UseLockedBalance(m.Market.QuoteCurrency, trade.QuoteQuantity)
// here the fee currency is the base currency
q := trade.Quantity
@ -281,16 +283,16 @@ func (m *SimplePriceMatching) executeTrade(trade types.Trade) {
q = q.Sub(trade.Fee)
}
m.Account.AddBalance(m.Market.BaseCurrency, q)
m.account.AddBalance(m.Market.BaseCurrency, q)
} else {
err = m.Account.UseLockedBalance(m.Market.BaseCurrency, trade.Quantity)
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)
m.account.AddBalance(m.Market.QuoteCurrency, qq)
}
if err != nil {
@ -298,16 +300,16 @@ func (m *SimplePriceMatching) executeTrade(trade types.Trade) {
}
m.EmitTradeUpdate(trade)
m.EmitBalanceUpdate(m.Account.Balances())
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
feeRate = m.account.MakerFeeRate
} else {
feeRate = m.Account.TakerFeeRate
feeRate = m.account.TakerFeeRate
}
return feeRate
}
@ -320,15 +322,14 @@ func (m *SimplePriceMatching) newTradeFromOrder(order *types.Order, isMaker bool
var fee fixedpoint.Value
var feeCurrency string
if useFeeToken {
feeCurrency = FeeToken
fee = quoteQuantity.Mul(feeRate)
if m.feeModeFunction != nil {
fee, feeCurrency = m.feeModeFunction(order, &m.Market, feeRate)
} else {
fee, feeCurrency = calculateNativeOrderFee(order, m.Market, feeRate)
fee, feeCurrency = feeModeFunctionQuote(order, &m.Market, feeRate)
}
// update order time
order.UpdateTime = types.Time(m.CurrentTime)
order.UpdateTime = types.Time(m.currentTime)
var id = incTradeID()
return types.Trade{
@ -342,7 +343,7 @@ func (m *SimplePriceMatching) newTradeFromOrder(order *types.Order, isMaker bool
Side: order.Side,
IsBuyer: order.Side == types.SideTypeBuy,
IsMaker: isMaker,
Time: types.Time(m.CurrentTime),
Time: types.Time(m.currentTime),
Fee: fee,
FeeCurrency: feeCurrency,
}
@ -458,7 +459,7 @@ func (m *SimplePriceMatching) buyToPrice(price fixedpoint.Value) (closedOrders [
}
m.askOrders = askOrders
m.LastPrice = price
m.lastPrice = price
for i := range closedOrders {
o := closedOrders[i]
@ -587,7 +588,7 @@ func (m *SimplePriceMatching) sellToPrice(price fixedpoint.Value) (closedOrders
}
m.bidOrders = bidOrders
m.LastPrice = price
m.lastPrice = price
for i := range closedOrders {
o := closedOrders[i]
@ -631,12 +632,12 @@ func (m *SimplePriceMatching) getOrder(orderID uint64) (types.Order, bool) {
}
func (m *SimplePriceMatching) processKLine(kline types.KLine) {
m.CurrentTime = kline.EndTime.Time()
m.currentTime = kline.EndTime.Time()
if m.LastPrice.IsZero() {
m.LastPrice = kline.Open
if m.lastPrice.IsZero() {
m.lastPrice = kline.Open
} else {
if m.LastPrice.Compare(kline.Open) > 0 {
if m.lastPrice.Compare(kline.Open) > 0 {
m.sellToPrice(kline.Open)
} else {
m.buyToPrice(kline.Open)
@ -669,12 +670,12 @@ func (m *SimplePriceMatching) processKLine(kline types.KLine) {
m.buyToPrice(kline.Close)
}
default: // no trade up or down
if m.LastPrice.IsZero() {
if m.lastPrice.IsZero() {
m.buyToPrice(kline.Close)
}
}
m.LastKLine = kline
m.lastKLine = kline
}
func (m *SimplePriceMatching) newOrder(o types.SubmitOrder, orderID uint64) types.Order {
@ -685,27 +686,11 @@ func (m *SimplePriceMatching) newOrder(o types.SubmitOrder, orderID uint64) type
Status: types.OrderStatusNew,
ExecutedQuantity: fixedpoint.Zero,
IsWorking: true,
CreationTime: types.Time(m.CurrentTime),
UpdateTime: types.Time(m.CurrentTime),
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 isTakerOrder(o types.Order) bool {
if o.AveragePrice.IsZero() {
return false

View File

@ -44,11 +44,11 @@ func TestSimplePriceMatching_orderUpdate(t *testing.T) {
t1 := time.Date(2021, 7, 1, 0, 0, 0, 0, time.UTC)
engine := &SimplePriceMatching{
Account: account,
account: account,
Market: market,
CurrentTime: t1,
currentTime: t1,
closedOrders: make(map[uint64]types.Order),
LastPrice: fixedpoint.NewFromFloat(25000),
lastPrice: fixedpoint.NewFromFloat(25000),
}
orderUpdateCnt := 0
@ -97,11 +97,11 @@ func TestSimplePriceMatching_CancelOrder(t *testing.T) {
market := getTestMarket()
t1 := time.Date(2021, 7, 1, 0, 0, 0, 0, time.UTC)
engine := &SimplePriceMatching{
Account: account,
account: account,
Market: market,
CurrentTime: t1,
currentTime: t1,
closedOrders: make(map[uint64]types.Order),
LastPrice: fixedpoint.NewFromFloat(30000.0),
lastPrice: fixedpoint.NewFromFloat(30000.0),
}
createdOrder1, trade1, err := engine.PlaceOrder(newLimitOrder("BTCUSDT", types.SideTypeBuy, 20000.0, 0.1))
@ -139,11 +139,11 @@ func TestSimplePriceMatching_processKLine(t *testing.T) {
t1 := time.Date(2021, 7, 1, 0, 0, 0, 0, time.UTC)
engine := &SimplePriceMatching{
Account: account,
account: account,
Market: market,
CurrentTime: t1,
currentTime: t1,
closedOrders: make(map[uint64]types.Order),
LastPrice: fixedpoint.NewFromFloat(30000.0),
lastPrice: fixedpoint.NewFromFloat(30000.0),
}
for i := 0; i <= 5; i++ {
@ -216,10 +216,10 @@ func TestSimplePriceMatching_LimitBuyTakerOrder(t *testing.T) {
account := getTestAccount()
market := getTestMarket()
engine := &SimplePriceMatching{
Account: account,
account: account,
Market: market,
closedOrders: make(map[uint64]types.Order),
LastPrice: fixedpoint.NewFromFloat(19000.0),
lastPrice: fixedpoint.NewFromFloat(19000.0),
}
takerOrder := types.SubmitOrder{
@ -256,10 +256,10 @@ func TestSimplePriceMatching_StopLimitOrderBuy(t *testing.T) {
account := getTestAccount()
market := getTestMarket()
engine := &SimplePriceMatching{
Account: account,
account: account,
Market: market,
closedOrders: make(map[uint64]types.Order),
LastPrice: fixedpoint.NewFromFloat(19000.0),
lastPrice: fixedpoint.NewFromFloat(19000.0),
}
stopBuyOrder := types.SubmitOrder{
@ -299,7 +299,7 @@ func TestSimplePriceMatching_StopLimitOrderBuy(t *testing.T) {
assert.Equal(t, "21001", trades[0].Price.String())
assert.Equal(t, "22000", closedOrders[0].Price.String(), "order.Price should not be adjusted")
assert.Equal(t, fixedpoint.NewFromFloat(21001.0).String(), engine.LastPrice.String())
assert.Equal(t, fixedpoint.NewFromFloat(21001.0).String(), engine.lastPrice.String())
stopOrder2 := types.SubmitOrder{
Symbol: market.Symbol,
@ -326,10 +326,10 @@ func TestSimplePriceMatching_StopLimitOrderSell(t *testing.T) {
account := getTestAccount()
market := getTestMarket()
engine := &SimplePriceMatching{
Account: account,
account: account,
Market: market,
closedOrders: make(map[uint64]types.Order),
LastPrice: fixedpoint.NewFromFloat(22000.0),
lastPrice: fixedpoint.NewFromFloat(22000.0),
}
stopSellOrder := types.SubmitOrder{
@ -370,7 +370,7 @@ func TestSimplePriceMatching_StopLimitOrderSell(t *testing.T) {
assert.Equal(t, types.OrderTypeLimit, closedOrders[0].Type)
assert.Equal(t, "20000", closedOrders[0].Price.String(), "limit order price should not be changed")
assert.Equal(t, "20990", trades[0].Price.String())
assert.Equal(t, "20990", engine.LastPrice.String())
assert.Equal(t, "20990", engine.lastPrice.String())
// place a stop limit sell order with a higher price than the current price
stopOrder2 := types.SubmitOrder{
@ -395,7 +395,7 @@ func TestSimplePriceMatching_StopLimitOrderSell(t *testing.T) {
assert.Equal(t, types.OrderStatusFilled, closedOrders[0].Status)
assert.Equal(t, types.OrderTypeLimit, closedOrders[0].Type)
assert.Equal(t, "21000", trades[0].Price.String(), "trade price should be the kline price not the order price")
assert.Equal(t, "21000", engine.LastPrice.String(), "engine last price should be updated correctly")
assert.Equal(t, "21000", engine.lastPrice.String(), "engine last price should be updated correctly")
}
}
@ -403,10 +403,10 @@ func TestSimplePriceMatching_StopMarketOrderSell(t *testing.T) {
account := getTestAccount()
market := getTestMarket()
engine := &SimplePriceMatching{
Account: account,
account: account,
Market: market,
closedOrders: make(map[uint64]types.Order),
LastPrice: fixedpoint.NewFromFloat(22000.0),
lastPrice: fixedpoint.NewFromFloat(22000.0),
}
stopOrder := types.SubmitOrder{
@ -440,7 +440,7 @@ func TestSimplePriceMatching_PlaceLimitOrder(t *testing.T) {
account := getTestAccount()
market := getTestMarket()
engine := &SimplePriceMatching{
Account: account,
account: account,
Market: market,
closedOrders: make(map[uint64]types.Order),
}
@ -540,10 +540,10 @@ func TestSimplePriceMatching_LimitTakerOrder(t *testing.T) {
account := getTestAccount()
market := getTestMarket()
engine := &SimplePriceMatching{
Account: account,
account: account,
Market: market,
closedOrders: make(map[uint64]types.Order),
LastPrice: fixedpoint.NewFromFloat(20000.0),
lastPrice: fixedpoint.NewFromFloat(20000.0),
}
closedOrder, trade, err := engine.PlaceOrder(newLimitOrder("BTCUSDT", types.SideTypeBuy, 21000.0, 1.0))