You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
138 lines
3.8 KiB
138 lines
3.8 KiB
package backtest |
|
|
|
import ( |
|
"context" |
|
"fmt" |
|
"io" |
|
"sig-pub/api/pb" |
|
"sig-pub/internal/trading/sig" |
|
"sig-pub/pkg/indicator" |
|
"sig-pub/pkg/strategy" |
|
"sig-pub/pkg/trade" |
|
"sig-pub/pkg/types" |
|
"sig-pub/pkg/types/decimals" |
|
"sig-pub/pkg/utils/collect" |
|
"sig-pub/pkg/utils/times" |
|
"sig-pub/pkg/zlog" |
|
|
|
"google.golang.org/grpc" |
|
) |
|
|
|
type Backtest struct { |
|
exchangeClient pb.ExchangeServiceClient |
|
indReg *indicator.IndicatorRegistry |
|
|
|
riskStrategy *trade.RiskStrategy |
|
} |
|
|
|
func NewBacktest(exchangeClient pb.ExchangeServiceClient, indReg *indicator.IndicatorRegistry, sigStrategyReg *strategy.SigStrategyRegistry) *Backtest { |
|
return &Backtest{ |
|
exchangeClient: exchangeClient, |
|
indReg: indReg, |
|
riskStrategy: trade.NewRiskStrategy(), |
|
} |
|
} |
|
|
|
func (b *Backtest) RunTradingPlan(ctx context.Context, tradingPlan *sig.TradingPlan, stime, etime int64, sigKlineSeries *sig.KlineSeries) (err error) { |
|
plan := tradingPlan.Plan |
|
exchange := pb.ExchangeType(plan.Exchange) |
|
interval := types.Interval(plan.Interval) |
|
instId := plan.InstId |
|
|
|
sigStrategy := tradingPlan.GetSigStrategy() |
|
maxWindow := sigStrategy.MaxWindow() |
|
if maxWindow < 0 || maxWindow > indicator.MaxWindow { |
|
err = fmt.Errorf("invalid window %d 0-%d, planId=%d", maxWindow, indicator.MaxWindow, plan.Id) |
|
return |
|
} |
|
|
|
// 回测账户 |
|
account := NewAccount(10000, NewSimulator(0.0005, 0.0008)) |
|
// 平仓管理器 |
|
closeManager := NewCloseManager(0.02, 0) |
|
closeManager.SetDynamicParams(0.1, 0.02, 0) |
|
|
|
seriesRange := &pb.SeriesRange{ |
|
Exchange: exchange, |
|
InstId: instId, |
|
Interval: string(interval), |
|
Before: stime, |
|
After: etime, |
|
Open: false, |
|
Live: false, |
|
Desc: false, |
|
Window: uint32(maxWindow), |
|
} |
|
// fetch history klines via stream |
|
req := &pb.ReqHistoryKlineStream{Series: seriesRange} |
|
stream, err := b.exchangeClient.HistoryKlineStream(ctx, req, grpc.UseCompressor("snappy")) |
|
if err != nil { |
|
return |
|
} |
|
|
|
watch := times.NewWatch() |
|
recvTimes, recvTotal := 0, 0 |
|
var lastK *types.Kline |
|
var msg *pb.RspHistoryKlineStream |
|
for { |
|
select { |
|
case <-ctx.Done(): |
|
err = ctx.Err() |
|
return |
|
default: |
|
} |
|
msg, err = stream.Recv() |
|
if err == io.EOF { |
|
break |
|
} |
|
if err != nil { |
|
return |
|
} |
|
recvTimes++ |
|
recvTotal += len(msg.Klines) |
|
for _, k := range msg.Klines { |
|
kline := new(types.Kline) |
|
kline.ParsePBKline(seriesRange.Exchange, k) |
|
lastK = kline |
|
if lastTs, serial := sigKlineSeries.Update(kline); !serial { |
|
err = fmt.Errorf("kline not series: %s(%s), interval=%s, lastTs=%d", instId, exchange, interval, lastTs) |
|
return |
|
} |
|
length := sigKlineSeries.Length() |
|
if length <= maxWindow { |
|
continue |
|
} |
|
|
|
// 平仓策略 |
|
closeManager.OnKline(*kline, account) |
|
|
|
sigSide := tradingPlan.Update(strategy.StrategyTypeSig) |
|
if sigSide.IsValid() { |
|
b.onSigSideSignalWithAccount(sigSide, *kline, account, closeManager) |
|
} |
|
} |
|
} |
|
|
|
_ = lastK |
|
zlog.Debugf("recv=%d, total=%d, use %s", recvTimes, recvTotal, watch.ElapsedFmt(".")) |
|
collect.SortDesc(account.Trades, func(t *Trade) float64 { return t.Pnl }) |
|
exposure := account.Cash + account.PositionCost() |
|
_ = exposure |
|
return |
|
} |
|
|
|
// onSigSideSignalWithAccount 交易策略发出交易信号(使用指定的账户和平仓管理器) |
|
func (b *Backtest) onSigSideSignalWithAccount(sigSide types.Side, k types.Kline, account *Account, closeManager *CloseManager) { |
|
// risk check before executing |
|
side := b.riskStrategy.SideAssess(sigSide) |
|
if !side.IsValid() { |
|
zlog.Debugf("risk strategy filter sig side: %s", sigSide.String()) |
|
return |
|
} |
|
// zlog.Debugf("apply market order: ts=%d, side=%s", k.Ts, side.String()) |
|
price := decimals.MustToFloat64(k.Close) |
|
account.ApplyMarketOrder(side, 0.01, price, k.Ts) |
|
|
|
// 根据信号方向平掉相反方向的仓位:如果信号是买入,平掉所有卖出仓位;如果信号是卖出,平掉所有买入仓位 |
|
closeManager.CloseBySignal(sigSide, account, k) |
|
}
|
|
|