From 25b5eddc03ea2bd4ffff904143e63e708849a050 Mon Sep 17 00:00:00 2001 From: zenix Date: Tue, 1 Mar 2022 22:48:48 +0900 Subject: [PATCH] feature: add multiple exchange support in backtest fix: change doc, since --exchange removed from backtest fix: test for config changes --- config/bollgrid.yaml | 11 +- config/bollmaker.yaml | 14 ++- config/grid-usdttwd.yaml | 13 ++- config/grid.yaml | 23 ++-- config/pricedrop.yaml | 19 +-- config/schedule-ethusdt.yaml | 15 ++- config/skeleton.yaml | 7 +- config/support-margin.yaml | 18 +-- config/support.yaml | 7 +- doc/commands/bbgo.md | 2 +- doc/commands/bbgo_account.md | 2 +- doc/commands/bbgo_backtest.md | 1 - doc/commands/bbgo_balances.md | 2 +- doc/commands/bbgo_submit-order.md | 2 +- doc/topics/back-testing.md | 13 ++- pkg/backtest/exchange.go | 59 +++++----- pkg/bbgo/config.go | 8 +- pkg/bbgo/config_test.go | 4 +- pkg/bbgo/testdata/backtest.yaml | 15 +-- pkg/cmd/backtest.go | 187 +++++++++++++++++------------- 20 files changed, 237 insertions(+), 185 deletions(-) diff --git a/config/bollgrid.yaml b/config/bollgrid.yaml index 9ecf86156..53e686641 100644 --- a/config/bollgrid.yaml +++ b/config/bollgrid.yaml @@ -52,11 +52,12 @@ backtest: symbols: - BTCUSDT account: - makerCommission: 15 - takerCommission: 15 - balances: - BTC: 0.0 - USDT: 10000.0 + max: + makerCommission: 15 + takerCommission: 15 + balances: + BTC: 0.0 + USDT: 10000.0 exchangeStrategies: - on: max diff --git a/config/bollmaker.yaml b/config/bollmaker.yaml index cc1b490a0..0f01725e2 100644 --- a/config/bollmaker.yaml +++ b/config/bollmaker.yaml @@ -5,8 +5,13 @@ persistence: port: 6379 db: 0 +sessions: + binance: + exchange: binance + envVarPrefix: BINANCE + # example command: -# godotenv -f .env.local -- go run ./cmd/bbgo backtest --exchange max --sync-from 2020-11-01 --config config/grid.yaml --base-asset-baseline +# godotenv -f .env.local -- go run ./cmd/bbgo backtest --sync-from 2020-11-01 --config config/grid.yaml --base-asset-baseline backtest: # for testing max draw down (MDD) at 03-12 # see here for more details @@ -16,9 +21,10 @@ backtest: symbols: - ETHUSDT account: - balances: - ETH: 1.0 - USDT: 20_000.0 + binance: + balances: + ETH: 1.0 + USDT: 20_000.0 exchangeStrategies: diff --git a/config/grid-usdttwd.yaml b/config/grid-usdttwd.yaml index f557cc743..55e0dd5f5 100644 --- a/config/grid-usdttwd.yaml +++ b/config/grid-usdttwd.yaml @@ -30,12 +30,13 @@ backtest: symbols: - USDTTWD account: - makerCommission: 15 - takerCommission: 15 - balances: - BTC: 0.0 - USDT: 10_000.0 - TWD: 100_000.0 + max: + makerCommission: 15 + takerCommission: 15 + balances: + BTC: 0.0 + USDT: 10_000.0 + TWD: 100_000.0 exchangeStrategies: - on: max diff --git a/config/grid.yaml b/config/grid.yaml index aab419591..9430e8555 100644 --- a/config/grid.yaml +++ b/config/grid.yaml @@ -4,9 +4,9 @@ sessions: exchange: binance envVarPrefix: binance - max: - exchange: max - envVarPrefix: max + #max: + # exchange: max + # envVarPrefix: max riskControls: # This is the session-based risk controller, which let you configure different risk controller by session. @@ -26,23 +26,24 @@ riskControls: maxOrderAmount: 1000.0 # example command: -# godotenv -f .env.local -- go run ./cmd/bbgo backtest --exchange max --sync-from 2020-11-01 --config config/grid.yaml --base-asset-baseline +# godotenv -f .env.local -- go run ./cmd/bbgo backtest --sync-from 2020-11-01 --config config/grid.yaml --base-asset-baseline backtest: # for testing max draw down (MDD) at 03-12 # see here for more details # https://www.investopedia.com/terms/m/maximum-drawdown-mdd.asp - startTime: "2021-01-10" - endTime: "2021-01-21" + startTime: "2022-01-10" + endTime: "2022-01-11" symbols: - - BTCUSDT + - BTCUSDT account: - balances: - BTC: 0.0 - USDT: 10000.0 + binance: + balances: + BTC: 0.0 + USDT: 10000.0 exchangeStrategies: -- on: max +- on: binance grid: symbol: BTCUSDT quantity: 0.001 diff --git a/config/pricedrop.yaml b/config/pricedrop.yaml index 2d7efe228..ff4680617 100644 --- a/config/pricedrop.yaml +++ b/config/pricedrop.yaml @@ -40,18 +40,19 @@ backtest: # for testing max draw down (MDD) at 03-12 # see here for more details # https://www.investopedia.com/terms/m/maximum-drawdown-mdd.asp - startTime: "2020-01-01" - endTime: "2020-01-15" + startTime: "2022-01-01" + endTime: "2022-01-15" symbols: - BTCUSDT account: - makerCommission: 15 - takerCommission: 15 - buyerCommission: 0 - sellerCommission: 0 - balances: - BTC: 0.1 - USDT: 10000.0 + binance: + makerCommission: 15 + takerCommission: 15 + buyerCommission: 0 + sellerCommission: 0 + balances: + BTC: 0.1 + USDT: 10000.0 exchangeStrategies: - on: binance diff --git a/config/schedule-ethusdt.yaml b/config/schedule-ethusdt.yaml index 25d08e16a..ae40ba68e 100644 --- a/config/schedule-ethusdt.yaml +++ b/config/schedule-ethusdt.yaml @@ -1,14 +1,21 @@ --- -# time godotenv -f .env.local -- go run ./cmd/bbgo backtest --exchange binance --base-asset-baseline --config config/schedule-ethusdt.yaml -v + +sessions: + binance: + exchange: binance + envVarPrefix: binance + +# time godotenv -f .env.local -- go run ./cmd/bbgo backtest --base-asset-baseline --config config/schedule-ethusdt.yaml -v backtest: startTime: "2021-08-01" endTime: "2021-08-07" symbols: - ETHUSDT account: - balances: - ETH: 1.0 - USDT: 20_000.0 + binance: + balances: + ETH: 1.0 + USDT: 20_000.0 riskControls: # This is the session-based risk controller, which let you configure different risk controller by session. diff --git a/config/skeleton.yaml b/config/skeleton.yaml index a78be9432..f0db31245 100644 --- a/config/skeleton.yaml +++ b/config/skeleton.yaml @@ -16,6 +16,7 @@ backtest: symbols: - BNBBUSD account: - balances: - BNB: 0 - BUSD: 10000 + binance: + balances: + BNB: 0 + BUSD: 10000 diff --git a/config/support-margin.yaml b/config/support-margin.yaml index 58459e47e..808865477 100644 --- a/config/support-margin.yaml +++ b/config/support-margin.yaml @@ -44,14 +44,14 @@ backtest: startTime: "2020-09-04" endTime: "2020-09-14" symbols: - - BTCUSDT - - ETHUSDT + - LINKUSDT account: - makerCommission: 15 - takerCommission: 15 - balances: - BTC: 0.0 - USDT: 10000.0 + binance: + makerCommission: 15 + takerCommission: 15 + balances: + LINK: 0.0 + USDT: 10000.0 exchangeStrategies: @@ -59,14 +59,14 @@ exchangeStrategies: support: symbol: LINKUSDT interval: 1m - minVolume: 1_000 + minVolume: 2_000 marginOrderSideEffect: borrow scaleQuantity: byVolume: exp: domain: [ 1_000, 200_000 ] - range: [ 0.5, 1.0 ] + range: [ 3.0, 5.0 ] maxBaseAssetBalance: 1000.0 minQuoteAssetBalance: 2000.0 diff --git a/config/support.yaml b/config/support.yaml index 1c8e22ff7..744c70ccf 100644 --- a/config/support.yaml +++ b/config/support.yaml @@ -44,9 +44,10 @@ backtest: - BTCUSDT - ETHUSDT account: - balances: - BTC: 0.0 - USDT: 10000.0 + binance: + balances: + BTC: 0.0 + USDT: 10000.0 exchangeStrategies: diff --git a/doc/commands/bbgo.md b/doc/commands/bbgo.md index 4452439dc..15bd0ae07 100644 --- a/doc/commands/bbgo.md +++ b/doc/commands/bbgo.md @@ -47,7 +47,7 @@ bbgo [flags] * [bbgo orderupdate](bbgo_orderupdate.md) - Listen to order update events * [bbgo pnl](bbgo_pnl.md) - pnl calculator * [bbgo run](bbgo_run.md) - run strategies from config file -* [bbgo submit-order](bbgo_submit-order.md) - place limit order to the exchange +* [bbgo submit-order](bbgo_submit-order.md) - place order to the exchange * [bbgo sync](bbgo_sync.md) - sync trades and orders history * [bbgo trades](bbgo_trades.md) - Query trading history * [bbgo tradeupdate](bbgo_tradeupdate.md) - Listen to trade update events diff --git a/doc/commands/bbgo_account.md b/doc/commands/bbgo_account.md index fd75bf3bb..e14219411 100644 --- a/doc/commands/bbgo_account.md +++ b/doc/commands/bbgo_account.md @@ -3,7 +3,7 @@ show user account details (ex: balance) ``` -bbgo account [--session=[exchange_name]] [flags] +bbgo account [--session SESSION] [flags] ``` ### Options diff --git a/doc/commands/bbgo_backtest.md b/doc/commands/bbgo_backtest.md index a3d31bbe2..e7e594b80 100644 --- a/doc/commands/bbgo_backtest.md +++ b/doc/commands/bbgo_backtest.md @@ -10,7 +10,6 @@ bbgo backtest [flags] ``` --base-asset-baseline use base asset performance as the competitive baseline performance - --exchange string target exchange --force force execution without confirm -h, --help help for backtest --output string the report output directory diff --git a/doc/commands/bbgo_balances.md b/doc/commands/bbgo_balances.md index c318fa520..ff501bf41 100644 --- a/doc/commands/bbgo_balances.md +++ b/doc/commands/bbgo_balances.md @@ -3,7 +3,7 @@ Show user account balances ``` -bbgo balances --session SESSION [flags] +bbgo balances [--session SESSION] [flags] ``` ### Options diff --git a/doc/commands/bbgo_submit-order.md b/doc/commands/bbgo_submit-order.md index c6ded9639..7a7f4dc6b 100644 --- a/doc/commands/bbgo_submit-order.md +++ b/doc/commands/bbgo_submit-order.md @@ -1,6 +1,6 @@ ## bbgo submit-order -place limit order to the exchange +place order to the exchange ``` bbgo submit-order --session SESSION --symbol SYMBOL --side SIDE --quantity QUANTITY [--price PRICE] [flags] diff --git a/doc/topics/back-testing.md b/doc/topics/back-testing.md index e9d93a23e..5cff0164d 100644 --- a/doc/topics/back-testing.md +++ b/doc/topics/back-testing.md @@ -20,9 +20,10 @@ backtest: account: # the initial account balance you want to start with - balances: - BTC: 0.0 - USDT: 10000.0 + binance: # exchange name + balances: + BTC: 0.0 + USDT: 10000.0 ``` Note on date formats, the following date formats are supported: @@ -33,7 +34,7 @@ Note on date formats, the following date formats are supported: And then, you can sync remote exchange k-lines (candle bars) data for back-testing: ```sh -bbgo backtest --exchange binance -v --sync --sync-only --sync-from 2020-11-01 --config config/grid.yaml +bbgo backtest -v --sync --sync-only --sync-from 2020-11-01 --config config/grid.yaml ``` Note that, you should sync from an earlier date before your startTime because some indicator like EMA needs more data to calculate the current EMA value. @@ -48,13 +49,13 @@ Here we sync one month before `2021-01-10`. Run back-test: ```sh -bbgo backtest --exchange binance --base-asset-baseline --config config/grid.yaml +bbgo backtest --base-asset-baseline --config config/grid.yaml ``` If you're developing a strategy, you might want to start with a command like this: ```shell -godotenv -f .env.local -- go run ./cmd/bbgo backtest --exchange binance --config config/grid.yaml --base-asset-baseline +godotenv -f .env.local -- go run ./cmd/bbgo backtest --config config/grid.yaml --base-asset-baseline ``` ## See Also diff --git a/pkg/backtest/exchange.go b/pkg/backtest/exchange.go index 67f104d75..f5dfac234 100644 --- a/pkg/backtest/exchange.go +++ b/pkg/backtest/exchange.go @@ -83,13 +83,15 @@ func NewExchange(sourceName types.ExchangeName, sourceExchange types.Exchange, s endTime = time.Now() } + configAccount := config.Account[sourceName.String()] + account := &types.Account{ - MakerFeeRate: config.Account.MakerFeeRate, - TakerFeeRate: config.Account.TakerFeeRate, + MakerFeeRate: configAccount.MakerFeeRate, + TakerFeeRate: configAccount.TakerFeeRate, AccountType: types.AccountTypeSpot, } - balances := config.Account.Balances.BalanceMap() + balances := configAccount.Balances.BalanceMap() account.UpdateBalances(balances) e := &Exchange{ @@ -288,7 +290,7 @@ func (e *Exchange) matchingBook(symbol string) (*SimplePriceMatching, bool) { return m, ok } -func (e *Exchange) FeedMarketData() error { +func (e *Exchange) InitMarketData() { e.userDataStream.OnTradeUpdate(func(trade types.Trade) { e.addTrade(trade) }) @@ -301,7 +303,9 @@ func (e *Exchange) FeedMarketData() error { } e.matchingBooksMutex.Unlock() - marketDataStream := e.marketDataStream +} + +func (e *Exchange) GetMarketData() (chan types.KLine, error) { log.Infof("collecting backtest configurations...") loadedSymbols := map[string]struct{}{} @@ -310,8 +314,7 @@ func (e *Exchange) FeedMarketData() error { types.Interval1m: {}, types.Interval1d: {}, } - - for _, sub := range marketDataStream.Subscriptions { + for _, sub := range e.marketDataStream.Subscriptions { loadedSymbols[sub.Symbol] = struct{}{} switch sub.Channel { @@ -319,7 +322,7 @@ func (e *Exchange) FeedMarketData() error { loadedIntervals[types.Interval(sub.Options.Interval)] = struct{}{} default: - return fmt.Errorf("stream channel %s is not supported in backtest", sub.Channel) + return nil, fmt.Errorf("stream channel %s is not supported in backtest", sub.Channel) } } @@ -336,35 +339,33 @@ func (e *Exchange) FeedMarketData() error { log.Infof("using symbols: %v and intervals: %v for back-testing", symbols, intervals) log.Infof("querying klines from database...") klineC, errC := e.srv.QueryKLinesCh(e.startTime, e.endTime, e, symbols, intervals) - numKlines := 0 - for k := range klineC { - if k.Interval == types.Interval1m { - matching, ok := e.matchingBook(k.Symbol) - if !ok { - log.Errorf("matching book of %s is not initialized", k.Symbol) - continue - } + go func() { + if err := <-errC; err != nil { + log.WithError(err).Error("backtest data feed error") + } + }() + return klineC, nil +} - // here we generate trades and order updates - matching.processKLine(k) - numKlines++ +func (e *Exchange) ConsumeKLine(k types.KLine) { + if k.Interval == types.Interval1m { + matching, ok := e.matchingBook(k.Symbol) + if !ok { + log.Errorf("matching book of %s is not initialized", k.Symbol) + return } - marketDataStream.EmitKLineClosed(k) + // here we generate trades and order updates + matching.processKLine(k) } - if err := <-errC; err != nil { - log.WithError(err).Error("backtest data feed error") - } + e.marketDataStream.EmitKLineClosed(k) +} - if numKlines == 0 { - log.Error("kline data is empty, make sure you have sync the exchange market data") - } - - if err := marketDataStream.Close(); err != nil { +func (e *Exchange) CloseMarketData() error { + if err := e.marketDataStream.Close(); err != nil { log.WithError(err).Error("stream close error") return err } - return nil } diff --git a/pkg/bbgo/config.go b/pkg/bbgo/config.go index 4d8326975..01464fc8b 100644 --- a/pkg/bbgo/config.go +++ b/pkg/bbgo/config.go @@ -98,10 +98,10 @@ type Backtest struct { EndTime *types.LooseFormatTime `json:"endTime,omitempty" yaml:"endTime,omitempty"` // RecordTrades is an option, if set to true, back-testing should record the trades into database - RecordTrades bool `json:"recordTrades,omitempty" yaml:"recordTrades,omitempty"` - Account BacktestAccount `json:"account" yaml:"account"` - Symbols []string `json:"symbols" yaml:"symbols"` - Session string `json:"session" yaml:"session"` + RecordTrades bool `json:"recordTrades,omitempty" yaml:"recordTrades,omitempty"` + Account map[string]BacktestAccount `json:"account" yaml:"account"` + Symbols []string `json:"symbols" yaml:"symbols"` + Sessions []string `json:"sessions" yaml:"sessions"` } type BacktestAccount struct { diff --git a/pkg/bbgo/config_test.go b/pkg/bbgo/config_test.go index 683b7ab7e..f8603808f 100644 --- a/pkg/bbgo/config_test.go +++ b/pkg/bbgo/config_test.go @@ -188,8 +188,8 @@ func TestLoadConfig(t *testing.T) { assert.Len(t, config.ExchangeStrategies, 1) assert.NotNil(t, config.Backtest) assert.NotNil(t, config.Backtest.Account) - assert.NotNil(t, config.Backtest.Account.Balances) - assert.Len(t, config.Backtest.Account.Balances, 2) + assert.NotNil(t, config.Backtest.Account["binance"].Balances) + assert.Len(t, config.Backtest.Account["binance"].Balances, 2) }, }, } diff --git a/pkg/bbgo/testdata/backtest.yaml b/pkg/bbgo/testdata/backtest.yaml index 344169b0c..655331681 100644 --- a/pkg/bbgo/testdata/backtest.yaml +++ b/pkg/bbgo/testdata/backtest.yaml @@ -14,13 +14,14 @@ backtest: # https://www.investopedia.com/terms/m/maximum-drawdown-mdd.asp startTime: "2020-01-01" account: - makerCommission: 15 - takerCommission: 15 - buyerCommission: 0 - sellerCommission: 0 - balances: - BTC: 1.0 - USDT: 5000.0 + binance: + makerCommission: 15 + takerCommission: 15 + buyerCommission: 0 + sellerCommission: 0 + balances: + BTC: 1.0 + USDT: 5000.0 exchangeStrategies: diff --git a/pkg/cmd/backtest.go b/pkg/cmd/backtest.go index 984de3850..e04d6f7ae 100644 --- a/pkg/cmd/backtest.go +++ b/pkg/cmd/backtest.go @@ -36,7 +36,6 @@ type BackTestReport struct { } func init() { - BacktestCmd.Flags().String("exchange", "", "target exchange") BacktestCmd.Flags().Bool("sync", false, "sync backtest data") BacktestCmd.Flags().Bool("sync-only", false, "sync backtest data only, do not run backtest") BacktestCmd.Flags().String("sync-from", "", "sync backtest data from the given time, which will override the time range in the backtest config") @@ -54,6 +53,9 @@ var BacktestCmd = &cobra.Command{ Use: "backtest", Short: "backtest your strategies", SilenceUsage: true, + PreRunE: cobraInitRequired([]string{ + "config", + }), RunE: func(cmd *cobra.Command, args []string) error { verboseCnt, err := cmd.Flags().GetCount("verbose") if err != nil { @@ -110,11 +112,6 @@ var BacktestCmd = &cobra.Command{ return err } - exchangeNameStr, err := cmd.Flags().GetString("exchange") - if err != nil { - return err - } - userConfig, err := bbgo.Load(configFile, true) if err != nil { return err @@ -129,45 +126,44 @@ var BacktestCmd = &cobra.Command{ log.SetLevel(log.ErrorLevel) } - // if it's declared in the cmd , use the cmd one first - if exchangeNameStr == "" { - exchangeNameStr = userConfig.Backtest.Session + if userConfig.Backtest == nil { + return errors.New("backtest config is not defined") } - var sourceExchange types.Exchange - var exchangeName types.ExchangeName + acceptAllSessions := false + var whitelistedSessions map[string]struct{} + if len(userConfig.Backtest.Sessions) == 0 { + acceptAllSessions = true + } else { + for _, name := range userConfig.Backtest.Sessions { + _, err := types.ValidExchangeName(name) + if err != nil { + return err + } + whitelistedSessions[name] = struct{}{} + } + } + + sourceExchanges := make(map[string]types.Exchange) for key, session := range userConfig.Sessions { - if exchangeNameStr == key { + ok := acceptAllSessions + if !ok { + _, ok = whitelistedSessions[key] + } + if ok { publicExchange, err := cmdutil.NewExchangePublic(session.ExchangeName) if err != nil { return err } - sourceExchange = publicExchange - exchangeName = session.ExchangeName - } - } - - if sourceExchange == nil { - exchangeName, err = types.ValidExchangeName(exchangeNameStr) - if err != nil { - return err - } - - sourceExchange, err = cmdutil.NewExchangePublic(exchangeName) - if err != nil { - return err + sourceExchanges[key] = publicExchange } } ctx, cancel := context.WithCancel(context.Background()) defer cancel() - if userConfig.Backtest == nil { - return errors.New("backtest config is not defined") - } - var now = time.Now() var startTime, endTime time.Time @@ -183,10 +179,6 @@ var BacktestCmd = &cobra.Command{ } _ = endTime - if len(userConfig.CrossExchangeStrategies) > 0 { - log.Warnf("backtest does not support CrossExchangeStrategy, strategies won't be added.") - } - log.Infof("starting backtest with startTime %s", startTime.Format(time.ANSIC)) environ := bbgo.NewEnvironment() @@ -223,43 +215,47 @@ var BacktestCmd = &cobra.Command{ log.Info("starting synchronization...") for _, symbol := range userConfig.Backtest.Symbols { - exCustom, ok := sourceExchange.(types.CustomIntervalProvider) + for _, sourceExchange := range sourceExchanges { + exCustom, ok := sourceExchange.(types.CustomIntervalProvider) - var supportIntervals map[types.Interval]int - if ok { - supportIntervals = exCustom.SupportedInterval() - } else { - supportIntervals = types.SupportedIntervals - } - - for interval := range supportIntervals { - // if err := s.SyncKLineByInterval(ctx, exchange, symbol, interval, startTime, endTime); err != nil { - // return err - // } - firstKLine, err := backtestService.QueryFirstKLine(sourceExchange.Name(), symbol, interval) - if err != nil { - return errors.Wrapf(err, "failed to query backtest kline") + var supportIntervals map[types.Interval]int + if ok { + supportIntervals = exCustom.SupportedInterval() + } else { + supportIntervals = types.SupportedIntervals } - // if we don't have klines before the start time endpoint, the back-test will fail. - // because the last price will be missing. - if firstKLine != nil { - if err := backtestService.SyncExist(ctx, sourceExchange, symbol, syncFromTime, time.Now(), interval); err != nil { - return err + for interval := range supportIntervals { + // if err := s.SyncKLineByInterval(ctx, exchange, symbol, interval, startTime, endTime); err != nil { + // return err + // } + firstKLine, err := backtestService.QueryFirstKLine(sourceExchange.Name(), symbol, interval) + if err != nil { + return errors.Wrapf(err, "failed to query backtest kline") } - } else { - if err := backtestService.Sync(ctx, sourceExchange, symbol, syncFromTime, time.Now(), interval); err != nil { - return err + + // if we don't have klines before the start time endpoint, the back-test will fail. + // because the last price will be missing. + if firstKLine != nil { + if err := backtestService.SyncExist(ctx, sourceExchange, symbol, syncFromTime, time.Now(), interval); err != nil { + return err + } + } else { + if err := backtestService.Sync(ctx, sourceExchange, symbol, syncFromTime, time.Now(), interval); err != nil { + return err + } } } } } log.Info("synchronization done") - if shouldVerify { - err2, done := backtestService.Verify(userConfig.Backtest.Symbols, startTime, time.Now(), sourceExchange, verboseCnt) - if done { - return err2 + for _, sourceExchange := range sourceExchanges { + if shouldVerify { + err2, done := backtestService.Verify(userConfig.Backtest.Symbols, startTime, time.Now(), sourceExchange, verboseCnt) + if done { + return err2 + } } } @@ -283,19 +279,17 @@ var BacktestCmd = &cobra.Command{ } } - if userConfig.Backtest == nil { - return errors.New("backtest config can not be nil") - } - - backtestExchange, err := backtest.NewExchange(exchangeName, sourceExchange, backtestService, userConfig.Backtest) - if err != nil { - return errors.Wrap(err, "failed to create backtest exchange") - } - environ.SetStartTime(startTime) // exchangeNameStr is the session name. - environ.AddExchange(exchangeNameStr, backtestExchange) + for name, sourceExchange := range sourceExchanges { + + backtestExchange, err := backtest.NewExchange(sourceExchange.Name(), sourceExchange, backtestService, userConfig.Backtest) + if err != nil { + return errors.Wrap(err, "failed to create backtest exchange") + } + environ.AddExchange(name, backtestExchange) + } if err := environ.Init(ctx); err != nil { return err @@ -314,7 +308,42 @@ var BacktestCmd = &cobra.Command{ return err } - backtestExchange.FeedMarketData() + type KChanEx struct { + KChan chan types.KLine + Exchange *backtest.Exchange + } + for _, session := range environ.Sessions() { + backtestExchange := session.Exchange.(*backtest.Exchange) + backtestExchange.InitMarketData() + } + + var klineChans []KChanEx + for _, session := range environ.Sessions() { + exchange := session.Exchange.(*backtest.Exchange) + c, err := exchange.GetMarketData() + if err != nil { + return err + } + klineChans = append(klineChans, KChanEx{KChan: c, Exchange: exchange}) + } + + for { + count := len(klineChans) + for _, kchanex := range klineChans { + kLine, more := <-kchanex.KChan + if more { + kchanex.Exchange.ConsumeKLine(kLine) + } else { + if err := kchanex.Exchange.CloseMarketData(); err != nil { + return err + } + count-- + } + } + if count == 0 { + break + } + } log.Infof("shutting down trader...") shutdownCtx, cancel := context.WithDeadline(ctx, time.Now().Add(10*time.Second)) @@ -324,10 +353,12 @@ var BacktestCmd = &cobra.Command{ // put the logger back to print the pnl log.SetLevel(log.InfoLevel) for _, session := range environ.Sessions() { + backtestExchange := session.Exchange.(*backtest.Exchange) + exchangeName := session.Exchange.Name().String() for symbol, trades := range session.Trades { market, ok := session.Market(symbol) if !ok { - return fmt.Errorf("market not found: %s", symbol) + return fmt.Errorf("market not found: %s, %s", symbol, exchangeName) } calculator := &pnl.AverageCostCalculator{ @@ -337,21 +368,21 @@ var BacktestCmd = &cobra.Command{ startPrice, ok := session.StartPrice(symbol) if !ok { - return fmt.Errorf("start price not found: %s", symbol) + return fmt.Errorf("start price not found: %s, %s", symbol, exchangeName) } lastPrice, ok := session.LastPrice(symbol) if !ok { - return fmt.Errorf("last price not found: %s", symbol) + return fmt.Errorf("last price not found: %s, %s", symbol, exchangeName) } - log.Infof("%s PROFIT AND LOSS REPORT", symbol) - log.Infof("===============================================") + color.Green("%s %s PROFIT AND LOSS REPORT", strings.ToUpper(exchangeName), symbol) + color.Green("===============================================") report := calculator.Calculate(symbol, trades.Trades, lastPrice) report.Print() - initBalances := userConfig.Backtest.Account.Balances.BalanceMap() + initBalances := userConfig.Backtest.Account[exchangeName].Balances.BalanceMap() finalBalances := session.Account.Balances() log.Infof("INITIAL BALANCES:")