Merge pull request #1019 from c9s/feature/grid2

strategy: grid2: profit spread, prune historical trades . etc
This commit is contained in:
Yo-An Lin 2022-12-06 03:04:12 +08:00 committed by GitHub
commit 4b0f03227e
8 changed files with 363 additions and 51 deletions

View File

@ -26,6 +26,9 @@ jobs:
steps:
- uses: actions/checkout@v2
with:
lfs: 'true'
ssh-key: ${{ secrets.git_ssh_key }}
- uses: actions/cache@v2
with:
@ -66,6 +69,12 @@ jobs:
- name: Build
run: go build -v ./cmd/bbgo
- name: Link Market Data
run: |
mkdir -p ~/.bbgo/cache
ln -s $PWD/data/binance-markets.json ~/.bbgo/cache/binance-markets.json
touch data/binance-markets.json
- name: Test
run: |
go test -race -coverprofile coverage.txt -covermode atomic ./pkg/...

View File

@ -16,8 +16,8 @@ sessions:
# example command:
# go run ./cmd/bbgo backtest --config config/grid2.yaml --base-asset-baseline
backtest:
startTime: "2022-06-01"
endTime: "2022-06-30"
startTime: "2021-06-01"
endTime: "2021-06-30"
symbols:
- BTCUSDT
sessions: [binance]
@ -25,16 +25,16 @@ backtest:
binance:
balances:
BTC: 0.0
USDT: 10000.0
USDT: 21_000.0
exchangeStrategies:
- on: binance
grid2:
symbol: BTCUSDT
upperPrice: 15_000.0
lowerPrice: 10_000.0
gridNumber: 10
upperPrice: 60_000.0
lowerPrice: 28_000.0
gridNumber: 1000
## compound is used for buying more inventory when the profit is made by the filled SELL order.
## when compound is disabled, fixed quantity is used for each grid order.
@ -45,14 +45,14 @@ exchangeStrategies:
## meaning that earn BTC instead of USDT when trading in the BTCUSDT pair.
# earnBase: true
## triggerPrice is used for opening your grid only when the last price touches your trigger price.
## triggerPrice (optional) is used for opening your grid only when the last price touches your trigger price.
## this is useful when you don't want to create a grid from a higher price.
## for example, when the last price hit 17_000.0 then open a grid with the price range 13_000 to 20_000
triggerPrice: 17_000.0
# triggerPrice: 17_000.0
## triggerPrice is used for closing your grid only when the last price touches your stop loss price.
## triggerPrice (optional) is used for closing your grid only when the last price touches your stop loss price.
## for example, when the price drops to 17_000.0 then close the grid and sell all base inventory.
stopLossPrice: 10_000.0
# stopLossPrice: 10_000.0
## profitSpread is the profit spread of the arbitrage order (sell order)
## greater the profitSpread, greater the profit you make when the sell order is filled.
@ -73,12 +73,20 @@ exchangeStrategies:
## 3) quoteInvestment and baseInvestment: when using quoteInvestment, the strategy will automatically calculate your best quantity for the whole grid.
## quoteInvestment is required, and baseInvestment is optional (could be zero)
## if you have existing BTC position and want to reuse it you can set the baseInvestment.
quoteInvestment: 10_000
quoteInvestment: 20_000
## baseInvestment is optional
## baseInvestment (optional) can be useful when you have existing inventory, maybe bought at much lower price
baseInvestment: 0.0
## closeWhenCancelOrder (optional)
## default to false
closeWhenCancelOrder: true
## resetPositionWhenStart (optional)
## default to false
resetPositionWhenStart: false
## clearOpenOrdersWhenStart (optional)
## default to false
clearOpenOrdersWhenStart: false
keepOrdersWhenShutdown: false

File diff suppressed because one or more lines are too long

View File

@ -43,7 +43,7 @@ var BackTestService *service.BacktestService
func SetBackTesting(s *service.BacktestService) {
BackTestService = s
IsBackTesting = true
IsBackTesting = s != nil
}
var LoadedExchangeStrategies = make(map[string]SingleExchangeStrategy)

View File

@ -2,15 +2,22 @@ package bbgo
import (
"sync"
"time"
"github.com/c9s/bbgo/pkg/types"
)
const TradeExpiryTime = 24 * time.Hour
const PruneTriggerNumOfTrades = 10_000
type TradeStore struct {
// any created trades for tracking trades
sync.Mutex
trades map[uint64]types.Trade
EnablePrune bool
trades map[uint64]types.Trade
lastTradeTime time.Time
}
func NewTradeStore() *TradeStore {
@ -93,11 +100,54 @@ func (s *TradeStore) Add(trades ...types.Trade) {
for _, trade := range trades {
s.trades[trade.ID] = trade
s.touchLastTradeTime(trade)
}
}
func (s *TradeStore) touchLastTradeTime(trade types.Trade) {
if trade.Time.Time().After(s.lastTradeTime) {
s.lastTradeTime = trade.Time.Time()
}
}
// pruneExpiredTrades prunes trades that are older than the expiry time
// see TradeExpiryTime
func (s *TradeStore) pruneExpiredTrades(curTime time.Time) {
s.Lock()
defer s.Unlock()
var trades = make(map[uint64]types.Trade)
var cutOffTime = curTime.Add(-TradeExpiryTime)
for _, trade := range s.trades {
if trade.Time.Before(cutOffTime) {
continue
}
trades[trade.ID] = trade
}
s.trades = trades
}
func (s *TradeStore) Prune(curTime time.Time) {
s.pruneExpiredTrades(curTime)
}
func (s *TradeStore) isCoolTrade(trade types.Trade) bool {
// if the time of last trade is over 1 hour, we call it's cool trade
return s.lastTradeTime != (time.Time{}) && time.Time(trade.Time).Sub(s.lastTradeTime) > time.Hour
}
func (s *TradeStore) BindStream(stream types.Stream) {
stream.OnTradeUpdate(func(trade types.Trade) {
s.Add(trade)
})
if s.EnablePrune {
stream.OnTradeUpdate(func(trade types.Trade) {
if s.isCoolTrade(trade) {
s.Prune(time.Time(trade.Time))
}
})
}
}

View File

@ -0,0 +1,39 @@
package bbgo
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/c9s/bbgo/pkg/types"
)
func TestTradeStore_isCoolTrade(t *testing.T) {
now := time.Now()
store := NewTradeStore()
store.lastTradeTime = now.Add(-2 * time.Hour)
ok := store.isCoolTrade(types.Trade{
Time: types.Time(now),
})
assert.True(t, ok)
store.lastTradeTime = now.Add(-2 * time.Minute)
ok = store.isCoolTrade(types.Trade{
Time: types.Time(now),
})
assert.False(t, ok)
}
func TestTradeStore_Prune(t *testing.T) {
now := time.Now()
store := NewTradeStore()
store.Add(
types.Trade{ID: 1, Time: types.Time(now.Add(-25 * time.Hour))},
types.Trade{ID: 2, Time: types.Time(now.Add(-23 * time.Hour))},
types.Trade{ID: 3, Time: types.Time(now.Add(-2 * time.Minute))},
types.Trade{ID: 4, Time: types.Time(now.Add(-1 * time.Minute))},
)
store.Prune(now)
assert.Equal(t, 3, len(store.trades))
}

View File

@ -37,6 +37,11 @@ type Strategy struct {
Symbol string `json:"symbol"`
// ProfitSpread is the fixed profit spread you want to submit the sell order
// When ProfitSpread is enabled, the grid will shift up, e.g.,
// If you opened a grid with the price range 10_000 to 20_000
// With profit spread set to 3_000
// The sell orders will be placed in the range 13_000 to 23_000
// And the buy orders will be placed in the original price range 10_000 to 20_000
ProfitSpread fixedpoint.Value `json:"profitSpread"`
// GridNum is the grid number, how many orders you want to post on the orderbook.
@ -302,7 +307,7 @@ func (s *Strategy) handleOrderFilled(o types.Order) {
// use the profit to buy more inventory in the grid
if s.Compound || s.EarnBase {
newQuantity = orderQuoteQuantity.Div(newPrice)
newQuantity = fixedpoint.Max(orderQuoteQuantity.Div(newPrice), s.Market.MinQuantity)
}
// calculate profit
@ -322,7 +327,7 @@ func (s *Strategy) handleOrderFilled(o types.Order) {
}
if s.EarnBase {
newQuantity = orderQuoteQuantity.Div(newPrice).Sub(baseSellQuantityReduction)
newQuantity = fixedpoint.Max(orderQuoteQuantity.Div(newPrice).Sub(baseSellQuantityReduction), s.Market.MinQuantity)
}
}
@ -359,7 +364,7 @@ func (s *Strategy) checkRequiredInvestmentByQuantity(baseBalance, quoteBalance,
requiredQuote = fixedpoint.Zero
// when we need to place a buy-to-sell conversion order, we need to mark the price
si := len(pins) - 1
si := -1
for i := len(pins) - 1; i >= 0; i-- {
pin := pins[i]
price := fixedpoint.Value(pin)
@ -418,7 +423,7 @@ func (s *Strategy) checkRequiredInvestmentByAmount(baseBalance, quoteBalance, am
requiredQuote = fixedpoint.Zero
// when we need to place a buy-to-sell conversion order, we need to mark the price
si := len(pins) - 1
si := -1
for i := len(pins) - 1; i >= 0; i-- {
pin := pins[i]
price := fixedpoint.Value(pin)
@ -439,9 +444,10 @@ func (s *Strategy) checkRequiredInvestmentByAmount(baseBalance, quoteBalance, am
}
} else {
// for orders that buy
if i+1 == si {
if s.ProfitSpread.IsZero() && i+1 == si {
continue
}
requiredQuote = requiredQuote.Add(amount)
}
}
@ -478,7 +484,7 @@ func (s *Strategy) calculateQuoteInvestmentQuantity(quoteInvestment, lastPrice f
// quoteInvestment = (p1 + p2 + p3) * q
// q = quoteInvestment / (p1 + p2 + p3)
totalQuotePrice := fixedpoint.Zero
si := len(pins) - 1
si := -1
for i := len(pins) - 1; i >= 0; i-- {
pin := pins[i]
price := fixedpoint.Value(pin)
@ -488,16 +494,17 @@ func (s *Strategy) calculateQuoteInvestmentQuantity(quoteInvestment, lastPrice f
// for orders that sell
// if we still have the base balance
// quantity := amount.Div(lastPrice)
if i > 0 { // we do not want to sell at i == 0
if s.ProfitSpread.Sign() > 0 {
totalQuotePrice = totalQuotePrice.Add(price)
} else if i > 0 { // we do not want to sell at i == 0
// convert sell to buy quote and add to requiredQuote
nextLowerPin := pins[i-1]
nextLowerPrice := fixedpoint.Value(nextLowerPin)
// requiredQuote = requiredQuote.Add(quantity.Mul(nextLowerPrice))
totalQuotePrice = totalQuotePrice.Add(nextLowerPrice)
}
} else {
// for orders that buy
if i+1 == si {
if s.ProfitSpread.IsZero() && i+1 == si {
continue
}
@ -520,9 +527,15 @@ func (s *Strategy) calculateQuoteBaseInvestmentQuantity(quoteInvestment, baseInv
for i := len(pins) - 1; i >= 0; i-- {
pin := pins[i]
price := fixedpoint.Value(pin)
if price.Compare(lastPrice) < 0 {
sellPrice := price
if s.ProfitSpread.Sign() > 0 {
sellPrice = sellPrice.Add(s.ProfitSpread)
}
if sellPrice.Compare(lastPrice) < 0 {
break
}
numberOfSellOrders++
}
@ -540,31 +553,36 @@ func (s *Strategy) calculateQuoteBaseInvestmentQuantity(quoteInvestment, baseInv
s.logger.Infof("grid base investment quantity range: %f <=> %f", minBaseQuantity.Float64(), maxBaseQuantity.Float64())
}
buyPlacedPrice := fixedpoint.Zero
// calculate quantity with quote investment
totalQuotePrice := fixedpoint.Zero
// quoteInvestment = (p1 * q) + (p2 * q) + (p3 * q) + ....
// =>
// quoteInvestment = (p1 + p2 + p3) * q
// maxBuyQuantity = quoteInvestment / (p1 + p2 + p3)
for i := len(pins) - 1; i >= 0; i-- {
si := -1
for i := len(pins) - 1 - maxNumberOfSellOrders; i >= 0; i-- {
pin := pins[i]
price := fixedpoint.Value(pin)
// buy price greater than the last price will trigger taker order.
if price.Compare(lastPrice) >= 0 {
// for orders that sell
// if we still have the base balance
// quantity := amount.Div(lastPrice)
if i > 0 { // we do not want to sell at i == 0
// convert sell to buy quote and add to requiredQuote
si = i
// when profit spread is set, we count all the grid prices as buy prices
if s.ProfitSpread.Sign() > 0 {
totalQuotePrice = totalQuotePrice.Add(price)
} else if i > 0 {
// when profit spread is not set
// we do not want to place sell order at i == 0
// here we submit an order to convert a buy order into a sell order
nextLowerPin := pins[i-1]
nextLowerPrice := fixedpoint.Value(nextLowerPin)
// requiredQuote = requiredQuote.Add(quantity.Mul(nextLowerPrice))
totalQuotePrice = totalQuotePrice.Add(nextLowerPrice)
buyPlacedPrice = nextLowerPrice
}
} else {
// for orders that buy
if !buyPlacedPrice.IsZero() && price.Compare(buyPlacedPrice) == 0 {
if s.ProfitSpread.IsZero() && i+1 == si {
continue
}
@ -626,7 +644,7 @@ func (s *Strategy) newStopLossPriceHandler(ctx context.Context, session *bbgo.Ex
func (s *Strategy) newTakeProfitHandler(ctx context.Context, session *bbgo.ExchangeSession) types.KLineCallback {
return types.KLineWith(s.Symbol, types.Interval1m, func(k types.KLine) {
if s.TakeProfitPrice.Compare(k.High) < 0 {
if s.TakeProfitPrice.Compare(k.High) > 0 {
return
}
@ -786,6 +804,13 @@ func (s *Strategy) generateGridOrders(totalQuote, totalBase, lastPrice fixedpoin
for i := len(pins) - 1; i >= 0; i-- {
pin := pins[i]
price := fixedpoint.Value(pin)
sellPrice := price
// when profitSpread is set, the sell price is shift upper with the given spread
if s.ProfitSpread.Sign() > 0 {
sellPrice = sellPrice.Add(s.ProfitSpread)
}
quantity := s.QuantityOrAmount.Quantity
if quantity.IsZero() {
quantity = s.QuantityOrAmount.Amount.Div(price)
@ -799,7 +824,7 @@ func (s *Strategy) generateGridOrders(totalQuote, totalBase, lastPrice fixedpoin
Symbol: s.Symbol,
Type: types.OrderTypeLimit,
Side: types.SideTypeSell,
Price: price,
Price: sellPrice,
Quantity: quantity,
Market: s.Market,
TimeInForce: types.TimeInForceGTC,
@ -826,7 +851,7 @@ func (s *Strategy) generateGridOrders(totalQuote, totalBase, lastPrice fixedpoin
// skip i == 0
}
} else {
if i+1 == si {
if s.ProfitSpread.IsZero() && i+1 == si {
continue
}
@ -906,6 +931,10 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se
s.groupID = util.FNV32(instanceID)
s.logger.Infof("using group id %d from fnv(%s)", s.groupID, instanceID)
if s.ProfitSpread.Sign() > 0 {
s.ProfitSpread = s.Market.TruncatePrice(s.ProfitSpread)
}
if s.GridProfitStats == nil {
s.GridProfitStats = newGridProfitStats(s.Market)
}
@ -923,6 +952,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se
}
s.historicalTrades = bbgo.NewTradeStore()
s.historicalTrades.EnablePrune = true
s.historicalTrades.BindStream(session.UserDataStream)
s.orderExecutor = bbgo.NewGeneralOrderExecutor(session, s.Symbol, ID, instanceID, s.Position)

View File

@ -3,12 +3,17 @@
package grid2
import (
"context"
"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"
)
@ -56,8 +61,8 @@ type PriceSideAssert struct {
func assertPriceSide(t *testing.T, priceSideAsserts []PriceSideAssert, orders []types.SubmitOrder) {
for i, a := range priceSideAsserts {
assert.Equal(t, a.Side, orders[i].Side)
assert.Equal(t, a.Price, orders[i].Price)
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)
}
}
@ -149,6 +154,38 @@ func TestStrategy_generateGridOrders(t *testing.T) {
}, 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) {
@ -178,20 +215,14 @@ func TestStrategy_checkRequiredInvestmentByAmount(t *testing.T) {
}
func TestStrategy_calculateQuoteInvestmentQuantity(t *testing.T) {
s := &Strategy{
logger: logrus.NewEntry(logrus.New()),
Market: types.Market{
BaseCurrency: "BTC",
QuoteCurrency: "USDT",
},
}
t.Run("calculate quote quantity from quote investment", func(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
quantity, err := s.calculateQuoteInvestmentQuantity(number(12_000.0), number(13_500.0), []Pin{
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
@ -202,13 +233,36 @@ func TestStrategy_calculateQuoteInvestmentQuantity(t *testing.T) {
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),
BaseCurrency: "BTC",
QuoteCurrency: "USDT",
TickSize: number(0.01),
PricePrecision: 2,
VolumePrecision: 8,
}
s := &Strategy{
@ -218,6 +272,8 @@ func newTestStrategy() *Strategy {
UpperPrice: number(20_000),
LowerPrice: number(10_000),
GridNum: 10,
// QuoteInvestment: number(9000.0),
}
return s
}
@ -275,3 +331,122 @@ func TestStrategy_calculateProfit(t *testing.T) {
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),
}
// TEMPLATE {{{ start backtest
startTime, err := types.ParseLooseFormatTime("2021-06-01")
assert.NoError(t, err)
endTime, err := types.ParseLooseFormatTime("2021-06-30")
assert.NoError(t, err)
backtestConfig := &bbgo.Backtest{
StartTime: startTime,
EndTime: &endTime,
RecordTrades: false,
FeeMode: bbgo.BacktestFeeModeToken,
Accounts: map[string]bbgo.BacktestAccount{
"binance": {
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{"binance"},
SyncSecKLines: false,
}
t.Logf("backtestConfig: %+v", backtestConfig)
ctx := context.Background()
environ := bbgo.NewEnvironment()
environ.SetStartTime(startTime.Time())
err = environ.ConfigureDatabaseDriver(ctx, "sqlite3", "../../../data/bbgo_test.sqlite3")
assert.NoError(t, err)
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("binance")
if !assert.NoError(t, err) {
return
}
publicExchange, err := exchange.NewPublic(exName)
if !assert.NoError(t, err) {
return
}
backtestExchange, err := backtest.NewExchange(publicExchange.Name(), publicExchange, backtestService, backtestConfig)
if !assert.NoError(t, err) {
return
}
session := environ.AddExchange(exName.String(), 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()
}
// TODO: add grid2 to the user config and run backtest
userConfig := &bbgo.Config{
ExchangeStrategies: []bbgo.ExchangeStrategyMount{
{
Mounts: []string{"binance"},
Strategy: strategy,
},
},
}
err = trader.Configure(userConfig)
assert.NoError(t, err)
err = trader.Run(ctx)
assert.NoError(t, err)
// TODO: feed data
// }}}
}