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.
225 lines
6.6 KiB
225 lines
6.6 KiB
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 |
|
}
|
|
|