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.
298 lines
8.9 KiB
298 lines
8.9 KiB
package backtest |
|
|
|
import ( |
|
"context" |
|
"math" |
|
"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" |
|
"time" |
|
|
|
"github.com/bytedance/sonic" |
|
) |
|
|
|
// TradingPlanBacktester 交易计划回测 |
|
// todo 将backtest独立成单独服务横向扩展 |
|
type TradingPlanBacktester struct { |
|
sigStrategyType strategy.SigStrategyType |
|
sigStrategy strategy.ISigStrategy |
|
indicatorReg *indicator.IndicatorRegistry |
|
exchangeClient pb.ExchangeServiceClient |
|
|
|
plan entity.TradePlan |
|
sr *pb.SeriesRange |
|
account *BacktestTradeAccount |
|
sigStrategyInput types.Input |
|
|
|
tradeStrategyInput types.Input |
|
closeStrategy trade.ICloseStrategy |
|
riskStrategy trade.IRiskStrategy |
|
tradeStrategy trade.ITradeStrategy |
|
instanceIntervalSigStrategyContext strategy.IInstanceIntervalSigStrategyContext |
|
} |
|
|
|
func NewTradingPlanBacktester( |
|
sigType strategy.SigStrategyType, |
|
strategy strategy.ISigStrategy, |
|
indicatorReg *indicator.IndicatorRegistry, |
|
exchangeClient pb.ExchangeServiceClient, |
|
) *TradingPlanBacktester { |
|
return &TradingPlanBacktester{ |
|
sigStrategyType: sigType, |
|
sigStrategy: strategy, |
|
indicatorReg: indicatorReg, |
|
exchangeClient: exchangeClient, |
|
} |
|
} |
|
|
|
func (b *TradingPlanBacktester) Init(cash float64, plan entity.TradePlan, sr *pb.SeriesRange) (err error) { |
|
b.plan = plan |
|
b.sr = sr |
|
// trade account |
|
b.account = NewBacktestTradeAccount(cash) |
|
|
|
// sig strategy param |
|
if err = sonic.UnmarshalString(plan.SigStrategyParam, &b.sigStrategyInput); err != nil { |
|
return |
|
} |
|
if err = b.sigStrategy.Init(b.sigStrategyInput); err != nil { |
|
return |
|
} |
|
|
|
// 交易策略参数 |
|
var tradeStrategyInput, closeStrategyInput, riskStrategyInput types.Input |
|
if err = sonic.UnmarshalString(plan.TradeStrategyParam, &tradeStrategyInput); err != nil { |
|
return |
|
} |
|
if err = sonic.UnmarshalString(plan.CloseStrategyParam, &closeStrategyInput); err != nil { |
|
return |
|
} |
|
if err = sonic.UnmarshalString(plan.RiskStrategyParam, &riskStrategyInput); err != nil { |
|
return |
|
} |
|
b.tradeStrategyInput = make(types.Input) |
|
b.tradeStrategyInput.Assign(tradeStrategyInput, closeStrategyInput, riskStrategyInput) |
|
|
|
// 交易策略 |
|
tradeStrate := trade.NewSigTradeStrategy() |
|
if err = tradeStrate.Init(b.tradeStrategyInput); err != nil { |
|
return |
|
} |
|
// 平仓策略 |
|
b.closeStrategy = tradeStrate |
|
// 风险管理策略 |
|
b.riskStrategy = tradeStrate |
|
// 交易策略 |
|
b.tradeStrategy = tradeStrate |
|
return |
|
} |
|
|
|
// 核心引擎,模拟交易、持仓跟踪、费用计算 |
|
func (b *TradingPlanBacktester) Backtest(ctx context.Context) (test *trade.BacktestTradingPlan, err error) { |
|
test = &trade.BacktestTradingPlan{ |
|
Id: time.Now().Unix(), |
|
UserId: 10001, |
|
PlanId: b.plan.Id, |
|
InstId: b.plan.InstId, |
|
Exchange: pb.ExchangeType(b.plan.Exchange), |
|
Interval: b.plan.Interval, |
|
SeriesBefore: b.sr.Before, |
|
SeriesAfter: b.sr.After, |
|
Ctime: time.Now().UnixMilli(), |
|
Cash: b.account.cash, |
|
} |
|
// 交易信号回测器 |
|
sigStrategyBacktester := NewSigStrategyBacktester(b.sigStrategyType, b.sigStrategy, b.indicatorReg, b.exchangeClient) |
|
|
|
sigStrategyBacktester.SubKline(types.Interval1m, func(instId string, interval types.Interval, k types.Kline) (err error) { |
|
// 检查仓位平仓 |
|
return b.closeByKlineInterval1m(instId, k) |
|
}) |
|
|
|
iiks := types.NewInstanceIntervalKlineSeries() |
|
b.instanceIntervalSigStrategyContext = sig.NewInstanceIntervalSigStrategyContext(b.tradeStrategyInput, iiks, b.indicatorReg) |
|
|
|
err = sigStrategyBacktester.Backtest(ctx, b.sigStrategyInput, b.sr, iiks, nil, func(instId string, sigSide types.Side, k types.Kline) (err error) { |
|
test.Singals++ |
|
// 根据交易信号检查仓位平仓 |
|
if err = b.closeBySigSingal(instId, sigSide, k); err != nil { |
|
return |
|
} |
|
// 交易下单 |
|
return b.onSideSingal(instId, sigSide, k) |
|
}) |
|
if err != nil { |
|
return |
|
} |
|
|
|
// 关闭所有未平仓仓位 |
|
err = b.forceCloseAllHoldingPosition() |
|
if err != nil { |
|
return |
|
} |
|
|
|
// 统计回测结果 |
|
test.Etime = time.Now().UnixMilli() |
|
test.EndCash = b.account.cash |
|
var profit float64 |
|
var orders []*trade.TradeOrder |
|
var snapshots []*EquitySnapshot // collect equity snapshots once per timestamp |
|
for _, order := range b.account.orders { |
|
switch order.TradeType { |
|
case trade.TradeTypeOpen: |
|
test.TotalTrades++ |
|
case trade.TradeTypeClose: |
|
if order.Profit > 0 { |
|
test.WinningTrades += len(order.Trades) |
|
} else { |
|
test.LosingTrades += len(order.Trades) |
|
} |
|
snapshots = append(snapshots, &EquitySnapshot{TradeId: order.TradeId, Ts: order.Ctime, Equity: order.Equity}) |
|
} |
|
test.Profit += order.Profit |
|
test.Fee += order.Fee |
|
order.BacktestId = test.Id |
|
profit += order.Profit |
|
orders = append(orders, order) |
|
} |
|
collect.SortAsc(orders, func(t *trade.TradeOrder) int64 { return t.TradeId }) |
|
test.Trades = orders |
|
// 最大回撤 |
|
drawdown := b.account.maxDrawdown |
|
test.MaxDrawdown = max((drawdown[0]-drawdown[1])/drawdown[0], (drawdown[2]-drawdown[3])/drawdown[2]) |
|
// 夏普比率: 使用 equity-curve method (periodic equity snapshots) |
|
collect.SortAsc(snapshots, func(sna *EquitySnapshot) int64 { return sna.TradeId }) |
|
var snaps []*EquitySnapshot |
|
var prevTs int64 |
|
for _, snap := range snapshots { |
|
if snap.Ts != prevTs { |
|
prevTs = snap.Ts |
|
snaps = append(snaps, snap) |
|
} else if len(snaps) > 0 { |
|
snaps[len(snaps)-1] = snap |
|
} |
|
} |
|
sharpeRatio := sharpeFromEquitySnapshots(snaps, 0.01) |
|
pow := math.Pow(10, float64(6)) |
|
test.SharpeRatio = math.Round(sharpeRatio*pow) / pow |
|
|
|
// Advanced Metrics |
|
test.SortinoRatio = math.Round(sortinoFromEquitySnapshots(snaps, 0.01)*pow) / pow |
|
test.ProfitFactor = math.Round(profitFactor(orders)*pow) / pow |
|
if test.TotalTrades > 0 { |
|
test.WinRate = float64(test.WinningTrades) / float64(test.TotalTrades) |
|
} |
|
|
|
durationMs := float64(b.sr.After - b.sr.Before) |
|
if durationMs > 0 && test.Cash > 0 { |
|
totalReturn := (test.EndCash - test.Cash) / test.Cash |
|
annualReturn := totalReturn * (365.0 * 24.0 * 3600.0 * 1000.0 / durationMs) |
|
test.CalmarRatio = math.Round(calmarRatio(annualReturn, test.MaxDrawdown)*pow) / pow |
|
} |
|
|
|
return |
|
} |
|
|
|
// forceCloseAllHoldingPosition 关闭所有未平仓仓位 |
|
func (b *TradingPlanBacktester) forceCloseAllHoldingPosition() (err error) { |
|
var closeTickets []trade.TradeTicket |
|
for _, instId := range b.account.GetOpenTradeInsts() { |
|
k := b.instanceIntervalSigStrategyContext.Get(instId, trade.PriceDriverInterval, 0) |
|
price := decimals.MustToFloat64(k.Close) |
|
b.account.OnPrice(instId, price) |
|
trades := b.account.GetOpenTrades(instId) |
|
for _, trd := range trades { |
|
closeTickets = append(closeTickets, trade.TradeTicket{ |
|
TradeType: trade.TradeTypeClose, |
|
InstId: instId, |
|
TradesId: []int64{trd.TradeId}, |
|
Side: trd.Side.Opposite(), |
|
Price: price, |
|
Leverage: trd.Leverage, |
|
Qty: trd.Qty, |
|
Interval: string(k.Interval), |
|
Ktime: k.Interval.MustAddMul(k.Ts, 1), |
|
Ctime: time.Now().UnixMilli(), |
|
Cause: trade.CauseCloseForced, |
|
}) |
|
} |
|
} |
|
|
|
for _, ticket := range closeTickets { |
|
err = b.account.CloseTradeOrder(ticket) |
|
if err != nil { |
|
return |
|
} |
|
} |
|
return |
|
} |
|
|
|
// closeByKlineInterval1m k线更新时检查平仓 |
|
func (b *TradingPlanBacktester) closeByKlineInterval1m(instId string, k types.Kline) (err error) { |
|
price := decimals.MustToFloat64(k.Close) |
|
closeTickets, err := b.closeStrategy.CloseAssessOnPrice(b.instanceIntervalSigStrategyContext, b.account, instId, price) |
|
if err != nil { |
|
return |
|
} |
|
if len(closeTickets) == 0 { |
|
return |
|
} |
|
|
|
for _, ticket := range closeTickets { |
|
err = b.account.CloseTradeOrder(ticket) |
|
if err != nil { |
|
return |
|
} |
|
} |
|
return |
|
} |
|
|
|
// closeBySigSingal 交易信号出现时检查平仓 |
|
func (b *TradingPlanBacktester) closeBySigSingal(instId string, sigSide types.Side, _ types.Kline) (err error) { |
|
closeTickets, err := b.closeStrategy.CloseAssessOnSig(b.instanceIntervalSigStrategyContext, b.account, instId, sigSide) |
|
if err != nil { |
|
return |
|
} |
|
if len(closeTickets) == 0 { |
|
return |
|
} |
|
|
|
for _, ticket := range closeTickets { |
|
err = b.account.CloseTradeOrder(ticket) |
|
if err != nil { |
|
return |
|
} |
|
} |
|
|
|
return |
|
} |
|
|
|
// onSideSingal 出现买卖信号 |
|
func (b *TradingPlanBacktester) onSideSingal(instId string, sigSide types.Side, _ types.Kline) (err error) { |
|
// 买卖信号交易风险分析 |
|
doTrade, causes, err := b.riskStrategy.RishAssess(b.instanceIntervalSigStrategyContext, b.account, instId, sigSide) |
|
if err != nil { |
|
return |
|
} |
|
if !doTrade { |
|
_ = causes // todo 记录信号不交易原因分析 log db analyze |
|
return |
|
} |
|
tickets, err := b.tradeStrategy.TradeAssess(b.instanceIntervalSigStrategyContext, b.account, instId, sigSide) |
|
if err != nil || len(tickets) == 0 { |
|
return |
|
} |
|
for _, ticket := range tickets { |
|
_, err = b.account.MarketOrder(ticket) |
|
if err != nil { |
|
return |
|
} |
|
} |
|
return |
|
}
|
|
|