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.
 
 

220 lines
6.5 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)
err = sigStrategyBacktester.Backtest(ctx, sr, func(sigSide types.Side, k types.Kline) (err error) {
closeManager.OnKline(k, account)
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
}