add strict start time, sync time checking for preventing back-test failure

related to #311
This commit is contained in:
c9s 2021-12-07 15:21:37 +08:00
parent 132fe893e1
commit f1e3cc6049
2 changed files with 78 additions and 43 deletions

View File

@ -27,6 +27,8 @@ func init() {
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")
BacktestCmd.Flags().Bool("verify", false, "verify the kline back-test data")
BacktestCmd.Flags().Bool("base-asset-baseline", false, "use base asset performance as the competitive baseline performance")
BacktestCmd.Flags().CountP("verbose", "v", "verbose level")
BacktestCmd.Flags().String("config", "config/bbgo.yaml", "strategy config file")
@ -73,6 +75,7 @@ var BacktestCmd = &cobra.Command{
if err != nil {
return err
}
jsonOutputEnabled := len(outputDirectory) > 0
syncOnly, err := cmd.Flags().GetBool("sync-only")
@ -85,6 +88,11 @@ var BacktestCmd = &cobra.Command{
return err
}
shouldVerify, err := cmd.Flags().GetBool("verify")
if err != nil {
return err
}
exchangeNameStr, err := cmd.Flags().GetString("exchange")
if err != nil {
return err
@ -145,7 +153,7 @@ var BacktestCmd = &cobra.Command{
environ.BacktestService = backtestService
if wantSync {
var syncFromTime = startTime
var syncFromTime time.Time
// override the sync from time if the option is given
if len(syncFromDateStr) > 0 {
@ -159,61 +167,78 @@ var BacktestCmd = &cobra.Command{
}
} else {
// we need at least 1 month backward data for EMA and last prices
syncFromTime = syncFromTime.AddDate(0, -1, 0)
log.Infof("adjusted sync start time to %s for backward market data", syncFromTime)
syncFromTime = startTime.AddDate(0, -1, 0)
log.Infof("adjusted sync start time %s to %s for backward market data", startTime, syncFromTime)
}
log.Info("starting synchronization...")
for _, symbol := range userConfig.Backtest.Symbols {
firstKLine, err := backtestService.QueryFirstKLine(sourceExchange.Name(), symbol, types.Interval1m)
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.
// because the last price will be missing.
if syncFromTime.Before(firstKLine.EndTime) {
return fmt.Errorf("the sync-from-time you gave %s is earlier than the previous sync entry %s. "+
"re-syncing data from the earlier date before your first sync is not support,"+
"please clean up the kline table and restart a new sync",
syncFromTime,
firstKLine.EndTime)
}
if err := backtestService.Sync(ctx, sourceExchange, symbol, syncFromTime); err != nil {
return err
}
}
log.Info("synchronization done")
var corruptCnt = 0
for _, symbol := range userConfig.Backtest.Symbols {
log.Infof("verifying backtesting data...")
if shouldVerify {
var corruptCnt = 0
for _, symbol := range userConfig.Backtest.Symbols {
log.Infof("verifying backtesting data...")
for interval := range types.SupportedIntervals {
log.Infof("verifying %s %s kline data...", symbol, interval)
for interval := range types.SupportedIntervals {
log.Infof("verifying %s %s kline data...", symbol, interval)
klineC, errC := backtestService.QueryKLinesCh(startTime, time.Now(), sourceExchange, []string{symbol}, []types.Interval{interval})
var emptyKLine types.KLine
var prevKLine types.KLine
for k := range klineC {
if verboseCnt > 1 {
fmt.Fprint(os.Stderr, ".")
}
if prevKLine != emptyKLine {
if prevKLine.StartTime.Add(interval.Duration()) != k.StartTime {
corruptCnt++
log.Errorf("found kline data corrupted at time: %s kline: %+v", k.StartTime, k)
log.Errorf("between %d and %d",
prevKLine.StartTime.Unix(),
k.StartTime.Unix())
klineC, errC := backtestService.QueryKLinesCh(startTime, time.Now(), sourceExchange, []string{symbol}, []types.Interval{interval})
var emptyKLine types.KLine
var prevKLine types.KLine
for k := range klineC {
if verboseCnt > 1 {
fmt.Fprint(os.Stderr, ".")
}
if prevKLine != emptyKLine {
if prevKLine.StartTime.Add(interval.Duration()) != k.StartTime {
corruptCnt++
log.Errorf("found kline data corrupted at time: %s kline: %+v", k.StartTime, k)
log.Errorf("between %d and %d",
prevKLine.StartTime.Unix(),
k.StartTime.Unix())
}
}
prevKLine = k
}
prevKLine = k
}
if verboseCnt > 1 {
fmt.Fprintln(os.Stderr)
}
if verboseCnt > 1 {
fmt.Fprintln(os.Stderr)
}
if err := <-errC; err != nil {
return err
if err := <-errC; err != nil {
return err
}
}
}
}
log.Infof("backtest verification completed")
if corruptCnt > 0 {
log.Errorf("found %d corruptions", corruptCnt)
} else {
log.Infof("found %d corruptions", corruptCnt)
log.Infof("backtest verification completed")
if corruptCnt > 0 {
log.Errorf("found %d corruptions", corruptCnt)
} else {
log.Infof("found %d corruptions", corruptCnt)
}
}
if syncOnly {

View File

@ -2,6 +2,7 @@ package service
import (
"context"
"strconv"
"strings"
"time"
@ -20,13 +21,13 @@ type BacktestService struct {
func (s *BacktestService) SyncKLineByInterval(ctx context.Context, exchange types.Exchange, symbol string, interval types.Interval, startTime, endTime time.Time) error {
log.Infof("synchronizing lastKLine for interval %s from exchange %s", interval, exchange.Name())
lastKLine, err := s.QueryLast(exchange.Name(), symbol, interval)
lastKLine, err := s.QueryKLine(exchange.Name(), symbol, interval, "DESC", 1)
if err != nil {
return err
}
if lastKLine != nil {
log.Infof("found last checkpoint %s", lastKLine.EndTime)
log.Infof("found the last %s kline data checkpoint %s", symbol, lastKLine.EndTime)
startTime = lastKLine.StartTime.Add(time.Minute)
}
@ -59,12 +60,21 @@ func (s *BacktestService) Sync(ctx context.Context, exchange types.Exchange, sym
return nil
}
// QueryLast queries the last order from the database
func (s *BacktestService) QueryLast(ex types.ExchangeName, symbol string, interval types.Interval) (*types.KLine, error) {
func (s *BacktestService) QueryFirstKLine(ex types.ExchangeName, symbol string, interval types.Interval) (*types.KLine, error) {
return s.QueryKLine(ex, symbol, interval, "ASC", 1)
}
// QueryLastKLine queries the last kline from the database
func (s *BacktestService) QueryLastKLine(ex types.ExchangeName, symbol string, interval types.Interval) (*types.KLine, error) {
return s.QueryKLine(ex, symbol, interval, "DESC", 1)
}
// QueryKLine queries the klines from the database
func (s *BacktestService) QueryKLine(ex types.ExchangeName, symbol string, interval types.Interval, orderBy string, limit int) (*types.KLine, error) {
log.Infof("querying last kline exchange = %s AND symbol = %s AND interval = %s", ex, symbol, interval)
// make the SQL syntax IDE friendly, so that it can analyze it.
sql := "SELECT * FROM binance_klines WHERE `symbol` = :symbol AND `interval` = :interval ORDER BY end_time DESC LIMIT 1"
sql := "SELECT * FROM binance_klines WHERE `symbol` = :symbol AND `interval` = :interval ORDER BY end_time " + orderBy + " LIMIT " + strconv.Itoa(limit)
sql = strings.ReplaceAll(sql, "binance_klines", ex.String()+"_klines")
rows, err := s.DB.NamedQuery(sql, map[string]interface{}{
@ -74,7 +84,7 @@ func (s *BacktestService) QueryLast(ex types.ExchangeName, symbol string, interv
})
if err != nil {
return nil, errors.Wrap(err, "query last order error")
return nil, errors.Wrap(err, "query kline error")
}
if rows.Err() != nil {