bbgo_origin/pkg/strategy/grid2/strategy_test.go

495 lines
15 KiB
Go
Raw Normal View History

2022-11-25 10:07:02 +00:00
//go:build !dnum
package grid2
import (
"context"
"os"
"testing"
"github.com/sirupsen/logrus"
"github.com/stretchr/testify/assert"
"github.com/c9s/bbgo/pkg/backtest"
"github.com/c9s/bbgo/pkg/bbgo"
"github.com/c9s/bbgo/pkg/exchange"
"github.com/c9s/bbgo/pkg/fixedpoint"
"github.com/c9s/bbgo/pkg/service"
"github.com/c9s/bbgo/pkg/types"
)
func TestStrategy_checkRequiredInvestmentByQuantity(t *testing.T) {
s := &Strategy{
logger: logrus.NewEntry(logrus.New()),
Market: types.Market{
BaseCurrency: "BTC",
QuoteCurrency: "USDT",
},
}
t.Run("quote to base balance conversion check", func(t *testing.T) {
_, requiredQuote, err := s.checkRequiredInvestmentByQuantity(number(0.0), number(10_000.0), number(0.1), number(13_500.0), []Pin{
Pin(number(10_000.0)), // 0.1 * 10_000 = 1000 USD (buy)
Pin(number(11_000.0)), // 0.1 * 11_000 = 1100 USD (buy)
Pin(number(12_000.0)), // 0.1 * 12_000 = 1200 USD (buy)
Pin(number(13_000.0)), // 0.1 * 13_000 = 1300 USD (buy)
Pin(number(14_000.0)), // 0.1 * 14_000 = 1400 USD (buy)
Pin(number(15_000.0)), // 0.1 * 15_000 = 1500 USD
})
assert.NoError(t, err)
assert.Equal(t, number(6000.0), requiredQuote)
})
t.Run("quote to base balance conversion not enough", func(t *testing.T) {
_, requiredQuote, err := s.checkRequiredInvestmentByQuantity(number(0.0), number(5_000.0), number(0.1), number(13_500.0), []Pin{
Pin(number(10_000.0)), // 0.1 * 10_000 = 1000 USD (buy)
Pin(number(11_000.0)), // 0.1 * 11_000 = 1100 USD (buy)
Pin(number(12_000.0)), // 0.1 * 12_000 = 1200 USD (buy)
Pin(number(13_000.0)), // 0.1 * 13_000 = 1300 USD (buy)
Pin(number(14_000.0)), // 0.1 * 14_000 = 1400 USD (buy)
Pin(number(15_000.0)), // 0.1 * 15_000 = 1500 USD
})
assert.EqualError(t, err, "quote balance (5000.000000 USDT) is not enough, required = quote 6000.000000")
assert.Equal(t, number(6000.0), requiredQuote)
})
}
type PriceSideAssert struct {
Price fixedpoint.Value
Side types.SideType
}
func assertPriceSide(t *testing.T, priceSideAsserts []PriceSideAssert, orders []types.SubmitOrder) {
for i, a := range priceSideAsserts {
assert.Equalf(t, a.Price, orders[i].Price, "order #%d price should be %f", i+1, a.Price.Float64())
assert.Equalf(t, a.Side, orders[i].Side, "order at price %f should be %s", a.Price.Float64(), a.Side)
}
}
func TestStrategy_generateGridOrders(t *testing.T) {
t.Run("quote only", func(t *testing.T) {
s := newTestStrategy()
s.grid = NewGrid(s.LowerPrice, s.UpperPrice, fixedpoint.NewFromInt(s.GridNum), s.Market.TickSize)
s.grid.CalculateArithmeticPins()
s.QuantityOrAmount.Quantity = number(0.01)
lastPrice := number(15300)
orders, err := s.generateGridOrders(number(10000.0), number(0), lastPrice)
assert.NoError(t, err)
if !assert.Equal(t, 10, len(orders)) {
for _, o := range orders {
t.Logf("- %s %s", o.Price.String(), o.Side)
}
}
assertPriceSide(t, []PriceSideAssert{
{number(19000.0), types.SideTypeBuy},
{number(18000.0), types.SideTypeBuy},
{number(17000.0), types.SideTypeBuy},
{number(16000.0), types.SideTypeBuy},
{number(15000.0), types.SideTypeBuy},
{number(14000.0), types.SideTypeBuy},
{number(13000.0), types.SideTypeBuy},
{number(12000.0), types.SideTypeBuy},
{number(11000.0), types.SideTypeBuy},
{number(10000.0), types.SideTypeBuy},
}, orders)
})
t.Run("base + quote", func(t *testing.T) {
s := newTestStrategy()
s.grid = NewGrid(s.LowerPrice, s.UpperPrice, fixedpoint.NewFromInt(s.GridNum), s.Market.TickSize)
s.grid.CalculateArithmeticPins()
s.QuantityOrAmount.Quantity = number(0.01)
lastPrice := number(15300)
orders, err := s.generateGridOrders(number(10000.0), number(0.021), lastPrice)
assert.NoError(t, err)
if !assert.Equal(t, 10, len(orders)) {
for _, o := range orders {
t.Logf("- %s %s", o.Price.String(), o.Side)
}
}
assertPriceSide(t, []PriceSideAssert{
{number(20000.0), types.SideTypeSell},
{number(19000.0), types.SideTypeSell},
{number(17000.0), types.SideTypeBuy},
{number(16000.0), types.SideTypeBuy},
{number(15000.0), types.SideTypeBuy},
{number(14000.0), types.SideTypeBuy},
{number(13000.0), types.SideTypeBuy},
{number(12000.0), types.SideTypeBuy},
{number(11000.0), types.SideTypeBuy},
{number(10000.0), types.SideTypeBuy},
}, orders)
})
t.Run("enough base + quote", func(t *testing.T) {
s := newTestStrategy()
s.grid = NewGrid(s.LowerPrice, s.UpperPrice, fixedpoint.NewFromInt(s.GridNum), s.Market.TickSize)
s.grid.CalculateArithmeticPins()
s.QuantityOrAmount.Quantity = number(0.01)
lastPrice := number(15300)
orders, err := s.generateGridOrders(number(10000.0), number(1.0), lastPrice)
assert.NoError(t, err)
if !assert.Equal(t, 10, len(orders)) {
for _, o := range orders {
t.Logf("- %s %s", o.Price.String(), o.Side)
}
}
assertPriceSide(t, []PriceSideAssert{
{number(20000.0), types.SideTypeSell},
{number(19000.0), types.SideTypeSell},
{number(18000.0), types.SideTypeSell},
{number(17000.0), types.SideTypeSell},
{number(16000.0), types.SideTypeSell},
{number(14000.0), types.SideTypeBuy},
{number(13000.0), types.SideTypeBuy},
{number(12000.0), types.SideTypeBuy},
{number(11000.0), types.SideTypeBuy},
{number(10000.0), types.SideTypeBuy},
}, orders)
})
t.Run("enough base + quote + profitSpread", func(t *testing.T) {
s := newTestStrategy()
s.ProfitSpread = number(1_000)
s.grid = NewGrid(s.LowerPrice, s.UpperPrice, fixedpoint.NewFromInt(s.GridNum), s.Market.TickSize)
s.grid.CalculateArithmeticPins()
s.QuantityOrAmount.Quantity = number(0.01)
lastPrice := number(15300)
orders, err := s.generateGridOrders(number(10000.0), number(1.0), lastPrice)
assert.NoError(t, err)
if !assert.Equal(t, 11, len(orders)) {
for _, o := range orders {
t.Logf("- %s %s", o.Price.String(), o.Side)
}
}
assertPriceSide(t, []PriceSideAssert{
{number(21000.0), types.SideTypeSell},
{number(20000.0), types.SideTypeSell},
{number(19000.0), types.SideTypeSell},
{number(18000.0), types.SideTypeSell},
{number(17000.0), types.SideTypeSell},
{number(15000.0), types.SideTypeBuy},
{number(14000.0), types.SideTypeBuy},
{number(13000.0), types.SideTypeBuy},
{number(12000.0), types.SideTypeBuy},
{number(11000.0), types.SideTypeBuy},
{number(10000.0), types.SideTypeBuy},
}, orders)
})
}
func TestStrategy_checkRequiredInvestmentByAmount(t *testing.T) {
s := &Strategy{
logger: logrus.NewEntry(logrus.New()),
Market: types.Market{
BaseCurrency: "BTC",
QuoteCurrency: "USDT",
},
}
t.Run("quote to base balance conversion", func(t *testing.T) {
2022-11-25 07:42:45 +00:00
_, requiredQuote, err := s.checkRequiredInvestmentByAmount(
number(0.0), number(3_000.0),
number(1000.0),
number(13_500.0), []Pin{
Pin(number(10_000.0)),
Pin(number(11_000.0)),
Pin(number(12_000.0)),
Pin(number(13_000.0)),
Pin(number(14_000.0)),
Pin(number(15_000.0)),
})
assert.EqualError(t, err, "quote balance (3000.000000 USDT) is not enough, required = quote 4999.999890")
assert.InDelta(t, 4999.999890, requiredQuote.Float64(), number(0.001).Float64())
})
}
func TestStrategy_calculateQuoteInvestmentQuantity(t *testing.T) {
t.Run("quote quantity", func(t *testing.T) {
// quoteInvestment = (10,000 + 11,000 + 12,000 + 13,000 + 14,000) * q
// q = quoteInvestment / (10,000 + 11,000 + 12,000 + 13,000 + 14,000)
// q = 12_000 / (10,000 + 11,000 + 12,000 + 13,000 + 14,000)
// q = 0.2
s := newTestStrategy()
lastPrice := number(13_500.0)
quantity, err := s.calculateQuoteInvestmentQuantity(number(12_000.0), lastPrice, []Pin{
Pin(number(10_000.0)), // buy
Pin(number(11_000.0)), // buy
Pin(number(12_000.0)), // buy
Pin(number(13_000.0)), // buy
Pin(number(14_000.0)), // buy
Pin(number(15_000.0)),
})
assert.NoError(t, err)
assert.Equal(t, number(0.2).String(), quantity.String())
})
t.Run("profit spread", func(t *testing.T) {
// quoteInvestment = (10,000 + 11,000 + 12,000 + 13,000 + 14,000 + 15,000) * q
// q = quoteInvestment / (10,000 + 11,000 + 12,000 + 13,000 + 14,000 + 15,000)
// q = 7500 / (10,000 + 11,000 + 12,000 + 13,000 + 14,000 + 15,000)
// q = 0.1
s := newTestStrategy()
s.ProfitSpread = number(2000.0)
lastPrice := number(13_500.0)
quantity, err := s.calculateQuoteInvestmentQuantity(number(7500.0), lastPrice, []Pin{
Pin(number(10_000.0)), // sell order @ 12_000
Pin(number(11_000.0)), // sell order @ 13_000
Pin(number(12_000.0)), // sell order @ 14_000
Pin(number(13_000.0)), // sell order @ 15_000
Pin(number(14_000.0)), // sell order @ 16_000
Pin(number(15_000.0)), // sell order @ 17_000
})
assert.NoError(t, err)
assert.Equal(t, number(0.1).String(), quantity.String())
})
}
func newTestStrategy() *Strategy {
market := types.Market{
BaseCurrency: "BTC",
QuoteCurrency: "USDT",
TickSize: number(0.01),
PricePrecision: 2,
VolumePrecision: 8,
}
s := &Strategy{
logger: logrus.NewEntry(logrus.New()),
Market: market,
GridProfitStats: newGridProfitStats(market),
UpperPrice: number(20_000),
LowerPrice: number(10_000),
GridNum: 10,
// QuoteInvestment: number(9000.0),
}
return s
}
func TestStrategy_calculateProfit(t *testing.T) {
t.Run("earn quote without compound", func(t *testing.T) {
s := newTestStrategy()
profit := s.calculateProfit(types.Order{
SubmitOrder: types.SubmitOrder{
Price: number(13_000),
Quantity: number(1.0),
},
}, number(12_000), number(1.0))
assert.NotNil(t, profit)
assert.Equal(t, "USDT", profit.Currency)
assert.InDelta(t, 1000.0, profit.Profit.Float64(), 0.1)
})
t.Run("earn quote with compound", func(t *testing.T) {
s := newTestStrategy()
s.Compound = true
profit := s.calculateProfit(types.Order{
SubmitOrder: types.SubmitOrder{
Price: number(13_000),
Quantity: number(1.0),
},
}, number(12_000), number(1.0))
assert.NotNil(t, profit)
assert.Equal(t, "USDT", profit.Currency)
assert.InDelta(t, 1000.0, profit.Profit.Float64(), 0.1)
})
t.Run("earn base without compound", func(t *testing.T) {
s := newTestStrategy()
s.EarnBase = true
s.Compound = false
quoteQuantity := number(12_000).Mul(number(1.0))
sellQuantity := quoteQuantity.Div(number(13_000.0))
buyOrder := types.SubmitOrder{
Price: number(12_000.0),
Quantity: number(1.0),
}
profit := s.calculateProfit(types.Order{
SubmitOrder: types.SubmitOrder{
Price: number(13_000.0),
Quantity: sellQuantity,
},
}, buyOrder.Price, buyOrder.Quantity)
assert.NotNil(t, profit)
assert.Equal(t, "BTC", profit.Currency)
assert.InDelta(t, sellQuantity.Float64()-buyOrder.Quantity.Float64(), profit.Profit.Float64(), 0.001)
})
}
func TestBacktestStrategy(t *testing.T) {
market := types.Market{
BaseCurrency: "BTC",
QuoteCurrency: "USDT",
TickSize: number(0.01),
PricePrecision: 2,
VolumePrecision: 8,
}
strategy := &Strategy{
logger: logrus.NewEntry(logrus.New()),
Symbol: "BTCUSDT",
Market: market,
GridProfitStats: newGridProfitStats(market),
UpperPrice: number(60_000),
LowerPrice: number(28_000),
GridNum: 100,
QuoteInvestment: number(9000.0),
}
RunBacktest(t, strategy)
}
func RunBacktest(t *testing.T, strategy bbgo.SingleExchangeStrategy) {
// TEMPLATE {{{ start backtest
const sqliteDbFile = "../../../data/bbgo_test.sqlite3"
const backtestExchangeName = "binance"
const backtestStartTime = "2022-06-01"
const backtestEndTime = "2022-06-30"
startTime, err := types.ParseLooseFormatTime(backtestStartTime)
assert.NoError(t, err)
endTime, err := types.ParseLooseFormatTime(backtestEndTime)
assert.NoError(t, err)
backtestConfig := &bbgo.Backtest{
StartTime: startTime,
EndTime: &endTime,
RecordTrades: false,
FeeMode: bbgo.BacktestFeeModeToken,
Accounts: map[string]bbgo.BacktestAccount{
backtestExchangeName: {
MakerFeeRate: number(0.075 * 0.01),
TakerFeeRate: number(0.075 * 0.01),
Balances: bbgo.BacktestAccountBalanceMap{
"USDT": number(10_000.0),
"BTC": number(1.0),
},
},
},
Symbols: []string{"BTCUSDT"},
Sessions: []string{backtestExchangeName},
SyncSecKLines: false,
}
t.Logf("backtestConfig: %+v", backtestConfig)
ctx := context.Background()
environ := bbgo.NewEnvironment()
environ.SetStartTime(startTime.Time())
info, err := os.Stat(sqliteDbFile)
assert.NoError(t, err)
t.Logf("sqlite: %+v", info)
err = environ.ConfigureDatabaseDriver(ctx, "sqlite3", sqliteDbFile)
if !assert.NoError(t, err) {
return
}
backtestService := &service.BacktestService{DB: environ.DatabaseService.DB}
defer func() {
err := environ.DatabaseService.DB.Close()
assert.NoError(t, err)
}()
environ.BacktestService = backtestService
bbgo.SetBackTesting(backtestService)
defer bbgo.SetBackTesting(nil)
exName, err := types.ValidExchangeName(backtestExchangeName)
if !assert.NoError(t, err) {
return
}
t.Logf("using exchange source: %s", exName)
publicExchange, err := exchange.NewPublic(exName)
if !assert.NoError(t, err) {
return
}
backtestExchange, err := backtest.NewExchange(exName, publicExchange, backtestService, backtestConfig)
if !assert.NoError(t, err) {
return
}
session := environ.AddExchange(backtestExchangeName, backtestExchange)
assert.NotNil(t, session)
err = environ.Init(ctx)
assert.NoError(t, err)
for _, ses := range environ.Sessions() {
userDataStream := ses.UserDataStream.(types.StandardStreamEmitter)
backtestEx := ses.Exchange.(*backtest.Exchange)
backtestEx.MarketDataStream = ses.MarketDataStream.(types.StandardStreamEmitter)
backtestEx.BindUserData(userDataStream)
}
trader := bbgo.NewTrader(environ)
if assert.NotNil(t, trader) {
trader.DisableLogging()
}
userConfig := &bbgo.Config{
Backtest: backtestConfig,
ExchangeStrategies: []bbgo.ExchangeStrategyMount{
{
Mounts: []string{backtestExchangeName},
Strategy: strategy,
},
},
}
err = trader.Configure(userConfig)
assert.NoError(t, err)
err = trader.Run(ctx)
assert.NoError(t, err)
2022-12-05 18:37:55 +00:00
allKLineIntervals, requiredInterval, backTestIntervals := backtest.CollectSubscriptionIntervals(environ)
t.Logf("requiredInterval: %s backTestIntervals: %v", requiredInterval, backTestIntervals)
_ = allKLineIntervals
exchangeSources, err := backtest.InitializeExchangeSources(environ.Sessions(), startTime.Time(), endTime.Time(), requiredInterval, backTestIntervals...)
if !assert.NoError(t, err) {
return
}
doneC := make(chan struct{})
go func() {
count := 0
exSource := exchangeSources[0]
for k := range exSource.C {
exSource.Exchange.ConsumeKLine(k, requiredInterval)
count++
}
err = exSource.Exchange.CloseMarketData()
assert.NoError(t, err)
assert.Greater(t, count, 0, "kLines count must be greater than 0, please check your backtest date range and symbol settings")
close(doneC)
}()
2022-12-05 18:37:55 +00:00
<-doneC
// }}}
}