mirror of
https://github.com/c9s/bbgo.git
synced 2024-11-22 14:55:16 +00:00
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:
parent
d01fcb304d
commit
25b5eddc03
|
@ -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
|
||||||
|
|
|
@ -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:
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -16,6 +16,7 @@ backtest:
|
||||||
symbols:
|
symbols:
|
||||||
- BNBBUSD
|
- BNBBUSD
|
||||||
account:
|
account:
|
||||||
balances:
|
binance:
|
||||||
BNB: 0
|
balances:
|
||||||
BUSD: 10000
|
BNB: 0
|
||||||
|
BUSD: 10000
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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:
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
Show user account balances
|
Show user account balances
|
||||||
|
|
||||||
```
|
```
|
||||||
bbgo balances --session SESSION [flags]
|
bbgo balances [--session SESSION] [flags]
|
||||||
```
|
```
|
||||||
|
|
||||||
### Options
|
### Options
|
||||||
|
|
|
@ -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]
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
15
pkg/bbgo/testdata/backtest.yaml
vendored
15
pkg/bbgo/testdata/backtest.yaml
vendored
|
@ -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:
|
||||||
|
|
|
@ -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:")
|
||||||
|
|
Loading…
Reference in New Issue
Block a user