2020-11-06 13:40:48 +00:00
|
|
|
package service
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
|
|
|
"strings"
|
|
|
|
"time"
|
|
|
|
|
|
|
|
"github.com/jmoiron/sqlx"
|
|
|
|
"github.com/pkg/errors"
|
|
|
|
log "github.com/sirupsen/logrus"
|
|
|
|
|
|
|
|
"github.com/c9s/bbgo/pkg/types"
|
|
|
|
)
|
|
|
|
|
|
|
|
type BacktestService struct {
|
|
|
|
DB *sqlx.DB
|
|
|
|
}
|
|
|
|
|
|
|
|
func (s *BacktestService) Sync(ctx context.Context, exchange types.Exchange, symbol string, startTime time.Time) error {
|
2020-11-06 16:49:17 +00:00
|
|
|
now := time.Now()
|
2020-11-06 13:40:48 +00:00
|
|
|
for interval := range types.SupportedIntervals {
|
|
|
|
log.Infof("synchronizing lastKLine for interval %s from exchange %s", interval, exchange.Name())
|
|
|
|
|
2020-11-06 16:49:17 +00:00
|
|
|
lastKLine, err := s.QueryLast(exchange.Name(), symbol, interval)
|
2020-11-06 13:40:48 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2020-11-06 16:49:17 +00:00
|
|
|
if lastKLine != nil {
|
|
|
|
log.Infof("found last checkpoint %s", lastKLine.EndTime)
|
|
|
|
startTime = lastKLine.StartTime.Add(time.Minute)
|
|
|
|
}
|
|
|
|
|
|
|
|
batch := &types.ExchangeBatchProcessor{Exchange: exchange}
|
|
|
|
|
|
|
|
// should use channel here
|
|
|
|
klineC, errC := batch.BatchQueryKLines(ctx, symbol, interval, startTime, now)
|
|
|
|
// var previousKLine types.KLine
|
|
|
|
for k := range klineC {
|
2020-11-06 13:40:48 +00:00
|
|
|
if err := s.Insert(k); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
2020-11-06 16:49:17 +00:00
|
|
|
|
|
|
|
if err := <-errC; err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2020-11-06 13:40:48 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// QueryLast queries the last order from the database
|
2020-11-06 16:49:17 +00:00
|
|
|
func (s *BacktestService) QueryLast(ex types.ExchangeName, symbol string, interval types.Interval) (*types.KLine, error) {
|
2020-11-06 13:40:48 +00:00
|
|
|
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.
|
2020-11-06 16:49:17 +00:00
|
|
|
sql := "SELECT * FROM binance_klines WHERE `symbol` = :symbol AND `interval` = :interval ORDER BY end_time DESC LIMIT 1"
|
2020-11-06 13:40:48 +00:00
|
|
|
sql = strings.ReplaceAll(sql, "binance_klines", ex.String()+"_klines")
|
|
|
|
|
|
|
|
rows, err := s.DB.NamedQuery(sql, map[string]interface{}{
|
|
|
|
"exchange": ex,
|
|
|
|
"interval": interval,
|
|
|
|
"symbol": symbol,
|
|
|
|
})
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
return nil, errors.Wrap(err, "query last order error")
|
|
|
|
}
|
|
|
|
|
|
|
|
if rows.Err() != nil {
|
|
|
|
return nil, rows.Err()
|
|
|
|
}
|
|
|
|
|
|
|
|
defer rows.Close()
|
|
|
|
|
|
|
|
if rows.Next() {
|
|
|
|
var kline types.KLine
|
|
|
|
err = rows.StructScan(&kline)
|
|
|
|
return &kline, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil, rows.Err()
|
|
|
|
}
|
|
|
|
|
2020-11-08 04:13:34 +00:00
|
|
|
func (s *BacktestService) QueryKLinesForward(exchange types.ExchangeName, symbol string, interval types.Interval, startTime time.Time) ([]types.KLine, error) {
|
|
|
|
sql := "SELECT * FROM `binance_klines` WHERE `end_time` >= :startTime AND `symbol` = :symbol AND `interval` = :interval ORDER BY end_time ASC"
|
|
|
|
sql = strings.ReplaceAll(sql, "binance_klines", exchange.String()+"_klines")
|
|
|
|
|
|
|
|
rows, err := s.DB.NamedQuery(sql, map[string]interface{}{
|
|
|
|
"startTime": startTime,
|
|
|
|
"symbol": symbol,
|
|
|
|
"interval": interval,
|
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return s.scanRows(rows)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (s *BacktestService) QueryKLinesBackward(exchange types.ExchangeName, symbol string, interval types.Interval, endTime time.Time) ([]types.KLine, error) {
|
|
|
|
sql := "SELECT * FROM `binance_klines` WHERE `end_time` <= :endTime AND `symbol` = :symbol AND `interval` = :interval ORDER BY end_time ASC"
|
|
|
|
sql = strings.ReplaceAll(sql, "binance_klines", exchange.String()+"_klines")
|
|
|
|
|
|
|
|
rows, err := s.DB.NamedQuery(sql, map[string]interface{}{
|
|
|
|
"endTime": endTime,
|
|
|
|
"symbol": symbol,
|
|
|
|
"interval": interval,
|
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return s.scanRows(rows)
|
|
|
|
}
|
|
|
|
|
2020-11-06 18:57:50 +00:00
|
|
|
func (s *BacktestService) QueryKLinesCh(since time.Time, exchange types.Exchange, symbols []string, intervals []types.Interval) (chan types.KLine, chan error) {
|
|
|
|
sql := "SELECT * FROM `binance_klines` WHERE `end_time` >= :since AND `symbol` IN (:symbols) AND `interval` IN (:intervals) ORDER BY end_time ASC"
|
2020-11-06 16:49:17 +00:00
|
|
|
sql = strings.ReplaceAll(sql, "binance_klines", exchange.Name().String()+"_klines")
|
2020-11-06 13:40:48 +00:00
|
|
|
|
2020-11-06 16:49:17 +00:00
|
|
|
sql, args, err := sqlx.Named(sql, map[string]interface{}{
|
2020-11-06 13:40:48 +00:00
|
|
|
"since": since,
|
2020-11-07 12:11:07 +00:00
|
|
|
"symbols": symbols,
|
2020-11-06 16:49:17 +00:00
|
|
|
"intervals": types.IntervalSlice(intervals),
|
2020-11-06 13:40:48 +00:00
|
|
|
})
|
2020-11-06 16:49:17 +00:00
|
|
|
sql, args, err = sqlx.In(sql, args...)
|
|
|
|
sql = s.DB.Rebind(sql)
|
|
|
|
|
|
|
|
rows, err := s.DB.Queryx(sql, args...)
|
2020-11-06 13:40:48 +00:00
|
|
|
if err != nil {
|
2020-11-06 16:49:17 +00:00
|
|
|
log.WithError(err).Error("query error")
|
|
|
|
|
|
|
|
errC := make(chan error, 1)
|
|
|
|
|
|
|
|
// avoid blocking
|
|
|
|
go func() {
|
|
|
|
errC <- err
|
|
|
|
close(errC)
|
|
|
|
}()
|
|
|
|
return nil, errC
|
2020-11-06 13:40:48 +00:00
|
|
|
}
|
|
|
|
|
2020-11-06 16:49:17 +00:00
|
|
|
return s.scanRowsCh(rows)
|
2020-11-06 13:40:48 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// scanRowsCh scan rows into channel
|
2020-11-06 16:49:17 +00:00
|
|
|
func (s *BacktestService) scanRowsCh(rows *sqlx.Rows) (chan types.KLine, chan error) {
|
2020-11-07 12:11:07 +00:00
|
|
|
ch := make(chan types.KLine, 500)
|
2020-11-06 16:49:17 +00:00
|
|
|
errC := make(chan error, 1)
|
2020-11-06 13:40:48 +00:00
|
|
|
|
|
|
|
go func() {
|
2020-11-06 16:49:17 +00:00
|
|
|
defer close(errC)
|
2020-11-07 12:11:07 +00:00
|
|
|
defer close(ch)
|
2020-11-06 13:40:48 +00:00
|
|
|
defer rows.Close()
|
|
|
|
|
|
|
|
for rows.Next() {
|
|
|
|
var kline types.KLine
|
|
|
|
if err := rows.StructScan(&kline); err != nil {
|
2020-11-06 16:49:17 +00:00
|
|
|
errC <- err
|
|
|
|
return
|
2020-11-06 13:40:48 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
ch <- kline
|
|
|
|
}
|
|
|
|
|
|
|
|
if err := rows.Err(); err != nil {
|
2020-11-06 16:49:17 +00:00
|
|
|
errC <- err
|
|
|
|
return
|
2020-11-06 13:40:48 +00:00
|
|
|
}
|
2020-11-07 12:11:07 +00:00
|
|
|
|
2020-11-06 13:40:48 +00:00
|
|
|
}()
|
|
|
|
|
2020-11-06 16:49:17 +00:00
|
|
|
return ch, errC
|
2020-11-06 13:40:48 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func (s *BacktestService) scanRows(rows *sqlx.Rows) (klines []types.KLine, err error) {
|
|
|
|
for rows.Next() {
|
|
|
|
var kline types.KLine
|
|
|
|
if err := rows.StructScan(&kline); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
klines = append(klines, kline)
|
|
|
|
}
|
|
|
|
|
|
|
|
return klines, rows.Err()
|
|
|
|
}
|
|
|
|
|
|
|
|
func (s *BacktestService) Insert(kline types.KLine) error {
|
|
|
|
if len(kline.Exchange) == 0 {
|
|
|
|
return errors.New("kline.Exchange field should not be empty")
|
|
|
|
}
|
|
|
|
|
2020-11-08 04:13:34 +00:00
|
|
|
sql := "INSERT INTO `binance_klines` (`exchange`, `start_time`, `end_time`, `symbol`, `interval`, `open`, `high`, `low`, `close`, `closed`, `volume`)" +
|
|
|
|
"VALUES (:exchange, :start_time, :end_time, :symbol, :interval, :open, :high, :low, :close, :closed, :volume)"
|
2020-11-06 13:40:48 +00:00
|
|
|
sql = strings.ReplaceAll(sql, "binance_klines", kline.Exchange+"_klines")
|
|
|
|
|
|
|
|
_, err := s.DB.NamedExec(sql, kline)
|
|
|
|
return err
|
|
|
|
}
|