feature: add multiple exchange support in backtest

fix: change doc, since --exchange removed from backtest

fix: test for config changes
This commit is contained in:
zenix 2022-03-01 22:48:48 +09:00
parent d01fcb304d
commit 25b5eddc03
20 changed files with 237 additions and 185 deletions

View File

@ -52,11 +52,12 @@ backtest:
symbols: symbols:
- BTCUSDT - BTCUSDT
account: account:
makerCommission: 15 max:
takerCommission: 15 makerCommission: 15
balances: takerCommission: 15
BTC: 0.0 balances:
USDT: 10000.0 BTC: 0.0
USDT: 10000.0
exchangeStrategies: exchangeStrategies:
- on: max - on: max

View File

@ -5,8 +5,13 @@ persistence:
port: 6379 port: 6379
db: 0 db: 0
sessions:
binance:
exchange: binance
envVarPrefix: BINANCE
# example command: # 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: backtest:
# for testing max draw down (MDD) at 03-12 # for testing max draw down (MDD) at 03-12
# see here for more details # see here for more details
@ -16,9 +21,10 @@ backtest:
symbols: symbols:
- ETHUSDT - ETHUSDT
account: account:
balances: binance:
ETH: 1.0 balances:
USDT: 20_000.0 ETH: 1.0
USDT: 20_000.0
exchangeStrategies: exchangeStrategies:

View File

@ -30,12 +30,13 @@ backtest:
symbols: symbols:
- USDTTWD - USDTTWD
account: account:
makerCommission: 15 max:
takerCommission: 15 makerCommission: 15
balances: takerCommission: 15
BTC: 0.0 balances:
USDT: 10_000.0 BTC: 0.0
TWD: 100_000.0 USDT: 10_000.0
TWD: 100_000.0
exchangeStrategies: exchangeStrategies:
- on: max - on: max

View File

@ -4,9 +4,9 @@ sessions:
exchange: binance exchange: binance
envVarPrefix: binance envVarPrefix: binance
max: #max:
exchange: max # exchange: max
envVarPrefix: max # envVarPrefix: max
riskControls: riskControls:
# This is the session-based risk controller, which let you configure different risk controller by session. # This is the session-based risk controller, which let you configure different risk controller by session.
@ -26,23 +26,24 @@ riskControls:
maxOrderAmount: 1000.0 maxOrderAmount: 1000.0
# example command: # 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: backtest:
# for testing max draw down (MDD) at 03-12 # for testing max draw down (MDD) at 03-12
# see here for more details # see here for more details
# https://www.investopedia.com/terms/m/maximum-drawdown-mdd.asp # https://www.investopedia.com/terms/m/maximum-drawdown-mdd.asp
startTime: "2021-01-10" startTime: "2022-01-10"
endTime: "2021-01-21" endTime: "2022-01-11"
symbols: symbols:
- BTCUSDT - BTCUSDT
account: account:
balances: binance:
BTC: 0.0 balances:
USDT: 10000.0 BTC: 0.0
USDT: 10000.0
exchangeStrategies: exchangeStrategies:
- on: max - on: binance
grid: grid:
symbol: BTCUSDT symbol: BTCUSDT
quantity: 0.001 quantity: 0.001

View File

@ -40,18 +40,19 @@ backtest:
# for testing max draw down (MDD) at 03-12 # for testing max draw down (MDD) at 03-12
# see here for more details # see here for more details
# https://www.investopedia.com/terms/m/maximum-drawdown-mdd.asp # https://www.investopedia.com/terms/m/maximum-drawdown-mdd.asp
startTime: "2020-01-01" startTime: "2022-01-01"
endTime: "2020-01-15" endTime: "2022-01-15"
symbols: symbols:
- BTCUSDT - BTCUSDT
account: account:
makerCommission: 15 binance:
takerCommission: 15 makerCommission: 15
buyerCommission: 0 takerCommission: 15
sellerCommission: 0 buyerCommission: 0
balances: sellerCommission: 0
BTC: 0.1 balances:
USDT: 10000.0 BTC: 0.1
USDT: 10000.0
exchangeStrategies: exchangeStrategies:
- on: binance - on: binance

View File

@ -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: backtest:
startTime: "2021-08-01" startTime: "2021-08-01"
endTime: "2021-08-07" endTime: "2021-08-07"
symbols: symbols:
- ETHUSDT - ETHUSDT
account: account:
balances: binance:
ETH: 1.0 balances:
USDT: 20_000.0 ETH: 1.0
USDT: 20_000.0
riskControls: riskControls:
# This is the session-based risk controller, which let you configure different risk controller by session. # This is the session-based risk controller, which let you configure different risk controller by session.

View File

@ -16,6 +16,7 @@ backtest:
symbols: symbols:
- BNBBUSD - BNBBUSD
account: account:
balances: binance:
BNB: 0 balances:
BUSD: 10000 BNB: 0
BUSD: 10000

View File

@ -44,14 +44,14 @@ backtest:
startTime: "2020-09-04" startTime: "2020-09-04"
endTime: "2020-09-14" endTime: "2020-09-14"
symbols: symbols:
- BTCUSDT - LINKUSDT
- ETHUSDT
account: account:
makerCommission: 15 binance:
takerCommission: 15 makerCommission: 15
balances: takerCommission: 15
BTC: 0.0 balances:
USDT: 10000.0 LINK: 0.0
USDT: 10000.0
exchangeStrategies: exchangeStrategies:
@ -59,14 +59,14 @@ exchangeStrategies:
support: support:
symbol: LINKUSDT symbol: LINKUSDT
interval: 1m interval: 1m
minVolume: 1_000 minVolume: 2_000
marginOrderSideEffect: borrow marginOrderSideEffect: borrow
scaleQuantity: scaleQuantity:
byVolume: byVolume:
exp: exp:
domain: [ 1_000, 200_000 ] domain: [ 1_000, 200_000 ]
range: [ 0.5, 1.0 ] range: [ 3.0, 5.0 ]
maxBaseAssetBalance: 1000.0 maxBaseAssetBalance: 1000.0
minQuoteAssetBalance: 2000.0 minQuoteAssetBalance: 2000.0

View File

@ -44,9 +44,10 @@ backtest:
- BTCUSDT - BTCUSDT
- ETHUSDT - ETHUSDT
account: account:
balances: binance:
BTC: 0.0 balances:
USDT: 10000.0 BTC: 0.0
USDT: 10000.0
exchangeStrategies: exchangeStrategies:

View File

@ -47,7 +47,7 @@ bbgo [flags]
* [bbgo orderupdate](bbgo_orderupdate.md) - Listen to order update events * [bbgo orderupdate](bbgo_orderupdate.md) - Listen to order update events
* [bbgo pnl](bbgo_pnl.md) - pnl calculator * [bbgo pnl](bbgo_pnl.md) - pnl calculator
* [bbgo run](bbgo_run.md) - run strategies from config file * [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 sync](bbgo_sync.md) - sync trades and orders history
* [bbgo trades](bbgo_trades.md) - Query trading history * [bbgo trades](bbgo_trades.md) - Query trading history
* [bbgo tradeupdate](bbgo_tradeupdate.md) - Listen to trade update events * [bbgo tradeupdate](bbgo_tradeupdate.md) - Listen to trade update events

View File

@ -3,7 +3,7 @@
show user account details (ex: balance) show user account details (ex: balance)
``` ```
bbgo account [--session=[exchange_name]] [flags] bbgo account [--session SESSION] [flags]
``` ```
### Options ### Options

View File

@ -10,7 +10,6 @@ bbgo backtest [flags]
``` ```
--base-asset-baseline use base asset performance as the competitive baseline performance --base-asset-baseline use base asset performance as the competitive baseline performance
--exchange string target exchange
--force force execution without confirm --force force execution without confirm
-h, --help help for backtest -h, --help help for backtest
--output string the report output directory --output string the report output directory

View File

@ -3,7 +3,7 @@
Show user account balances Show user account balances
``` ```
bbgo balances --session SESSION [flags] bbgo balances [--session SESSION] [flags]
``` ```
### Options ### Options

View File

@ -1,6 +1,6 @@
## bbgo submit-order ## 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] bbgo submit-order --session SESSION --symbol SYMBOL --side SIDE --quantity QUANTITY [--price PRICE] [flags]

View File

@ -20,9 +20,10 @@ backtest:
account: account:
# the initial account balance you want to start with # the initial account balance you want to start with
balances: binance: # exchange name
BTC: 0.0 balances:
USDT: 10000.0 BTC: 0.0
USDT: 10000.0
``` ```
Note on date formats, the following date formats are supported: 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: And then, you can sync remote exchange k-lines (candle bars) data for back-testing:
```sh ```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. 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: Run back-test:
```sh ```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: If you're developing a strategy, you might want to start with a command like this:
```shell ```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 ## See Also

View File

@ -83,13 +83,15 @@ func NewExchange(sourceName types.ExchangeName, sourceExchange types.Exchange, s
endTime = time.Now() endTime = time.Now()
} }
configAccount := config.Account[sourceName.String()]
account := &types.Account{ account := &types.Account{
MakerFeeRate: config.Account.MakerFeeRate, MakerFeeRate: configAccount.MakerFeeRate,
TakerFeeRate: config.Account.TakerFeeRate, TakerFeeRate: configAccount.TakerFeeRate,
AccountType: types.AccountTypeSpot, AccountType: types.AccountTypeSpot,
} }
balances := config.Account.Balances.BalanceMap() balances := configAccount.Balances.BalanceMap()
account.UpdateBalances(balances) account.UpdateBalances(balances)
e := &Exchange{ e := &Exchange{
@ -288,7 +290,7 @@ func (e *Exchange) matchingBook(symbol string) (*SimplePriceMatching, bool) {
return m, ok return m, ok
} }
func (e *Exchange) FeedMarketData() error { func (e *Exchange) InitMarketData() {
e.userDataStream.OnTradeUpdate(func(trade types.Trade) { e.userDataStream.OnTradeUpdate(func(trade types.Trade) {
e.addTrade(trade) e.addTrade(trade)
}) })
@ -301,7 +303,9 @@ func (e *Exchange) FeedMarketData() error {
} }
e.matchingBooksMutex.Unlock() e.matchingBooksMutex.Unlock()
marketDataStream := e.marketDataStream }
func (e *Exchange) GetMarketData() (chan types.KLine, error) {
log.Infof("collecting backtest configurations...") log.Infof("collecting backtest configurations...")
loadedSymbols := map[string]struct{}{} loadedSymbols := map[string]struct{}{}
@ -310,8 +314,7 @@ func (e *Exchange) FeedMarketData() error {
types.Interval1m: {}, types.Interval1m: {},
types.Interval1d: {}, types.Interval1d: {},
} }
for _, sub := range e.marketDataStream.Subscriptions {
for _, sub := range marketDataStream.Subscriptions {
loadedSymbols[sub.Symbol] = struct{}{} loadedSymbols[sub.Symbol] = struct{}{}
switch sub.Channel { switch sub.Channel {
@ -319,7 +322,7 @@ func (e *Exchange) FeedMarketData() error {
loadedIntervals[types.Interval(sub.Options.Interval)] = struct{}{} loadedIntervals[types.Interval(sub.Options.Interval)] = struct{}{}
default: 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("using symbols: %v and intervals: %v for back-testing", symbols, intervals)
log.Infof("querying klines from database...") log.Infof("querying klines from database...")
klineC, errC := e.srv.QueryKLinesCh(e.startTime, e.endTime, e, symbols, intervals) klineC, errC := e.srv.QueryKLinesCh(e.startTime, e.endTime, e, symbols, intervals)
numKlines := 0 go func() {
for k := range klineC { if err := <-errC; err != nil {
if k.Interval == types.Interval1m { log.WithError(err).Error("backtest data feed error")
matching, ok := e.matchingBook(k.Symbol) }
if !ok { }()
log.Errorf("matching book of %s is not initialized", k.Symbol) return klineC, nil
continue }
}
// here we generate trades and order updates func (e *Exchange) ConsumeKLine(k types.KLine) {
matching.processKLine(k) if k.Interval == types.Interval1m {
numKlines++ 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 { e.marketDataStream.EmitKLineClosed(k)
log.WithError(err).Error("backtest data feed error") }
}
if numKlines == 0 { func (e *Exchange) CloseMarketData() error {
log.Error("kline data is empty, make sure you have sync the exchange market data") if err := e.marketDataStream.Close(); err != nil {
}
if err := marketDataStream.Close(); err != nil {
log.WithError(err).Error("stream close error") log.WithError(err).Error("stream close error")
return err return err
} }
return nil return nil
} }

View File

@ -98,10 +98,10 @@ type Backtest struct {
EndTime *types.LooseFormatTime `json:"endTime,omitempty" yaml:"endTime,omitempty"` 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 is an option, if set to true, back-testing should record the trades into database
RecordTrades bool `json:"recordTrades,omitempty" yaml:"recordTrades,omitempty"` RecordTrades bool `json:"recordTrades,omitempty" yaml:"recordTrades,omitempty"`
Account BacktestAccount `json:"account" yaml:"account"` Account map[string]BacktestAccount `json:"account" yaml:"account"`
Symbols []string `json:"symbols" yaml:"symbols"` Symbols []string `json:"symbols" yaml:"symbols"`
Session string `json:"session" yaml:"session"` Sessions []string `json:"sessions" yaml:"sessions"`
} }
type BacktestAccount struct { type BacktestAccount struct {

View File

@ -188,8 +188,8 @@ func TestLoadConfig(t *testing.T) {
assert.Len(t, config.ExchangeStrategies, 1) assert.Len(t, config.ExchangeStrategies, 1)
assert.NotNil(t, config.Backtest) assert.NotNil(t, config.Backtest)
assert.NotNil(t, config.Backtest.Account) assert.NotNil(t, config.Backtest.Account)
assert.NotNil(t, config.Backtest.Account.Balances) assert.NotNil(t, config.Backtest.Account["binance"].Balances)
assert.Len(t, config.Backtest.Account.Balances, 2) assert.Len(t, config.Backtest.Account["binance"].Balances, 2)
}, },
}, },
} }

View File

@ -14,13 +14,14 @@ backtest:
# https://www.investopedia.com/terms/m/maximum-drawdown-mdd.asp # https://www.investopedia.com/terms/m/maximum-drawdown-mdd.asp
startTime: "2020-01-01" startTime: "2020-01-01"
account: account:
makerCommission: 15 binance:
takerCommission: 15 makerCommission: 15
buyerCommission: 0 takerCommission: 15
sellerCommission: 0 buyerCommission: 0
balances: sellerCommission: 0
BTC: 1.0 balances:
USDT: 5000.0 BTC: 1.0
USDT: 5000.0
exchangeStrategies: exchangeStrategies:

View File

@ -36,7 +36,6 @@ type BackTestReport struct {
} }
func init() { func init() {
BacktestCmd.Flags().String("exchange", "", "target exchange")
BacktestCmd.Flags().Bool("sync", false, "sync backtest data") BacktestCmd.Flags().Bool("sync", false, "sync backtest data")
BacktestCmd.Flags().Bool("sync-only", false, "sync backtest data only, do not run backtest") 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") 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", Use: "backtest",
Short: "backtest your strategies", Short: "backtest your strategies",
SilenceUsage: true, SilenceUsage: true,
PreRunE: cobraInitRequired([]string{
"config",
}),
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
verboseCnt, err := cmd.Flags().GetCount("verbose") verboseCnt, err := cmd.Flags().GetCount("verbose")
if err != nil { if err != nil {
@ -110,11 +112,6 @@ var BacktestCmd = &cobra.Command{
return err return err
} }
exchangeNameStr, err := cmd.Flags().GetString("exchange")
if err != nil {
return err
}
userConfig, err := bbgo.Load(configFile, true) userConfig, err := bbgo.Load(configFile, true)
if err != nil { if err != nil {
return err return err
@ -129,45 +126,44 @@ var BacktestCmd = &cobra.Command{
log.SetLevel(log.ErrorLevel) log.SetLevel(log.ErrorLevel)
} }
// if it's declared in the cmd , use the cmd one first if userConfig.Backtest == nil {
if exchangeNameStr == "" { return errors.New("backtest config is not defined")
exchangeNameStr = userConfig.Backtest.Session
} }
var sourceExchange types.Exchange acceptAllSessions := false
var exchangeName types.ExchangeName 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 { for key, session := range userConfig.Sessions {
if exchangeNameStr == key { ok := acceptAllSessions
if !ok {
_, ok = whitelistedSessions[key]
}
if ok {
publicExchange, err := cmdutil.NewExchangePublic(session.ExchangeName) publicExchange, err := cmdutil.NewExchangePublic(session.ExchangeName)
if err != nil { if err != nil {
return err return err
} }
sourceExchange = publicExchange sourceExchanges[key] = 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
} }
} }
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
defer cancel() defer cancel()
if userConfig.Backtest == nil {
return errors.New("backtest config is not defined")
}
var now = time.Now() var now = time.Now()
var startTime, endTime time.Time var startTime, endTime time.Time
@ -183,10 +179,6 @@ var BacktestCmd = &cobra.Command{
} }
_ = endTime _ = 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)) log.Infof("starting backtest with startTime %s", startTime.Format(time.ANSIC))
environ := bbgo.NewEnvironment() environ := bbgo.NewEnvironment()
@ -223,43 +215,47 @@ var BacktestCmd = &cobra.Command{
log.Info("starting synchronization...") log.Info("starting synchronization...")
for _, symbol := range userConfig.Backtest.Symbols { 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 var supportIntervals map[types.Interval]int
if ok { if ok {
supportIntervals = exCustom.SupportedInterval() supportIntervals = exCustom.SupportedInterval()
} else { } else {
supportIntervals = types.SupportedIntervals 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")
} }
// if we don't have klines before the start time endpoint, the back-test will fail. for interval := range supportIntervals {
// because the last price will be missing. // if err := s.SyncKLineByInterval(ctx, exchange, symbol, interval, startTime, endTime); err != nil {
if firstKLine != nil { // return err
if err := backtestService.SyncExist(ctx, sourceExchange, symbol, syncFromTime, time.Now(), interval); 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 { // if we don't have klines before the start time endpoint, the back-test will fail.
return err // 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") log.Info("synchronization done")
if shouldVerify { for _, sourceExchange := range sourceExchanges {
err2, done := backtestService.Verify(userConfig.Backtest.Symbols, startTime, time.Now(), sourceExchange, verboseCnt) if shouldVerify {
if done { err2, done := backtestService.Verify(userConfig.Backtest.Symbols, startTime, time.Now(), sourceExchange, verboseCnt)
return err2 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) environ.SetStartTime(startTime)
// exchangeNameStr is the session name. // 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 { if err := environ.Init(ctx); err != nil {
return err return err
@ -314,7 +308,42 @@ var BacktestCmd = &cobra.Command{
return err 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...") log.Infof("shutting down trader...")
shutdownCtx, cancel := context.WithDeadline(ctx, time.Now().Add(10*time.Second)) 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 // put the logger back to print the pnl
log.SetLevel(log.InfoLevel) log.SetLevel(log.InfoLevel)
for _, session := range environ.Sessions() { for _, session := range environ.Sessions() {
backtestExchange := session.Exchange.(*backtest.Exchange)
exchangeName := session.Exchange.Name().String()
for symbol, trades := range session.Trades { for symbol, trades := range session.Trades {
market, ok := session.Market(symbol) market, ok := session.Market(symbol)
if !ok { if !ok {
return fmt.Errorf("market not found: %s", symbol) return fmt.Errorf("market not found: %s, %s", symbol, exchangeName)
} }
calculator := &pnl.AverageCostCalculator{ calculator := &pnl.AverageCostCalculator{
@ -337,21 +368,21 @@ var BacktestCmd = &cobra.Command{
startPrice, ok := session.StartPrice(symbol) startPrice, ok := session.StartPrice(symbol)
if !ok { 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) lastPrice, ok := session.LastPrice(symbol)
if !ok { 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) color.Green("%s %s PROFIT AND LOSS REPORT", strings.ToUpper(exchangeName), symbol)
log.Infof("===============================================") color.Green("===============================================")
report := calculator.Calculate(symbol, trades.Trades, lastPrice) report := calculator.Calculate(symbol, trades.Trades, lastPrice)
report.Print() report.Print()
initBalances := userConfig.Backtest.Account.Balances.BalanceMap() initBalances := userConfig.Backtest.Account[exchangeName].Balances.BalanceMap()
finalBalances := session.Account.Balances() finalBalances := session.Account.Balances()
log.Infof("INITIAL BALANCES:") log.Infof("INITIAL BALANCES:")