package backtest import ( "context" "fmt" "io" "sig-pub/api/pb" "sig-pub/internal/trading/sig" "sig-pub/pkg/data/entity" "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" "github.com/bytedance/sonic" "google.golang.org/grpc" ) type BacktestStat struct { // total_return 年化收益 // sharpe_ratio 夏普比率 // max_drawdown 最大回撤 // num_trades 单数 // win_rate 胜率 } type TradeAccount struct { trade.ITradeAccount cash float64 } func NewTradeAccount(cash float64) *TradeAccount { return &TradeAccount{ cash: cash, } } type TradingPlanBacktester struct { exchangeClient pb.ExchangeServiceClient indicatorReg *indicator.IndicatorRegistry sigStrategyReg *strategy.SigStrategyRegistry } func NewTradingPlanBacktester(exchangeClient pb.ExchangeServiceClient, indicatorReg *indicator.IndicatorRegistry, sigStrategyReg *strategy.SigStrategyRegistry) *TradingPlanBacktester { return &TradingPlanBacktester{ exchangeClient: exchangeClient, indicatorReg: indicatorReg, sigStrategyReg: sigStrategyReg, } } // 核心引擎,模拟交易、持仓跟踪、费用计算 func (b *TradingPlanBacktester) Backtest(ctx context.Context, cash float64, plan entity.TradePlan, sr *pb.SeriesRange) (err error) { // sig strategy sigStrategyType, sigStrategy, ok := b.sigStrategyReg.NewSigStrategy(plan.SigStrategy) if !ok { err = fmt.Errorf("sig strategy not exists %s", plan.SigStrategy) return } sigStrategyParam := make(strategy.StrategyParam) if err = sonic.UnmarshalString(plan.SigStrategyParam, &sigStrategyParam); err != nil { return } if err = sigStrategy.Init(sigStrategyParam); err != nil { return } closeStrategyParam, tradeStrategyParam, riskStrategyParam := new(trade.CloseStrategyParam), new(trade.TradeStrategyParam), new(trade.RiskStrategyParam) if err = sonic.UnmarshalString(plan.CloseStrategyParam, closeStrategyParam); err != nil { return } if err = sonic.UnmarshalString(plan.TradeStrategyParam, tradeStrategyParam); err != nil { return } if err = sonic.UnmarshalString(plan.RiskStrategyParam, riskStrategyParam); err != nil { return } // 平仓策略 closeStrategy, err := trade.NewCloseStrategy(*closeStrategyParam) if err != nil { return } // 风险管理策略 riskStrategy, err := trade.NewRiskStrategy(*riskStrategyParam) if err != nil { return } tradeAccount := NewTradeAccount(cash) _ = tradeAccount // 平仓管理器 closeManager := NewCloseManager(0.02, 0) closeManager.SetDynamicParams(0.1, 0.02, 0) // 回测账户 account := NewAccount(10000, NewSimulator(0.0005, 0.0008)) sigStrategyBacktester := NewSigStrategyBacktester(sigStrategyType, sigStrategy, b.indicatorReg, b.exchangeClient) // 平仓策略订阅1分钟曲线 sigStrategyBacktester.SubKline(types.Interval1m, func(interval types.Interval, k *types.Kline) (err error) { closeManager.OnKline(*k, account) return }) // 下单 err = sigStrategyBacktester.Backtest(ctx, sr, func(sigSide types.Side, k types.Kline) (err error) { b.onSigSideSignalWithAccount(sigSide, k, account, closeManager, riskStrategy) return b.onSideSingal(sigSide, k, closeStrategy) }) return } // onSideSingal 出现买卖信号 func (b *TradingPlanBacktester) onSideSingal(sigSide types.Side, k types.Kline, closeStrategy *trade.CloseStrategy) (err error) { return } // onSigSideSignalWithAccount 交易策略发出交易信号(使用指定的账户和平仓管理器) func (b *TradingPlanBacktester) onSigSideSignalWithAccount(sigSide types.Side, k types.Kline, account *Account, closeManager *CloseManager, riskStrategy *trade.RiskStrategy) { // risk check before executing side := 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) } func (b *TradingPlanBacktester) 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 := int(sigStrategy.(strategy.ISingleSigStrategy).RequiredSeries()) 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, WindowExtra: 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, nil) } } } _ = 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 }