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.
237 lines
6.6 KiB
237 lines
6.6 KiB
package backtest |
|
|
|
import ( |
|
"context" |
|
"errors" |
|
"fmt" |
|
"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/utils/collect" |
|
"sig-pub/pkg/zlog" |
|
"time" |
|
|
|
"github.com/bytedance/sonic" |
|
) |
|
|
|
// todo 将backtest独立成单独服务横向扩展 |
|
type TradingPlanBacktester struct { |
|
indicatorReg *indicator.IndicatorRegistry |
|
sigStrategyReg *strategy.SigStrategyRegistry |
|
exchangeClient pb.ExchangeServiceClient |
|
|
|
plan entity.TradePlan |
|
account *BacktestAccount |
|
sigStrategyType strategy.SigStrategyType |
|
sigStrategy strategy.ISigStrategy |
|
sigStrategyInput types.Input |
|
closeStrategy *trade.CloseStrategy |
|
riskStrategy *trade.RiskStrategy |
|
tradeStrategy *trade.TradeStrategy |
|
} |
|
|
|
func NewTradingPlanBacktester(indicatorReg *indicator.IndicatorRegistry, sigStrategyReg *strategy.SigStrategyRegistry, exchangeClient pb.ExchangeServiceClient) *TradingPlanBacktester { |
|
return &TradingPlanBacktester{ |
|
indicatorReg: indicatorReg, |
|
sigStrategyReg: sigStrategyReg, |
|
exchangeClient: exchangeClient, |
|
} |
|
} |
|
|
|
func (b *TradingPlanBacktester) Init(cash float64, plan entity.TradePlan) (err error) { |
|
b.plan = plan |
|
// trade account |
|
simulator := NewTradeSimulator(0.0005, 0.0008) |
|
b.account = NewBacktestAccount(cash, simulator) |
|
// sig strategy |
|
ok := false |
|
b.sigStrategyType, b.sigStrategy, ok = b.sigStrategyReg.NewSigStrategy(plan.SigStrategy) |
|
if !ok { |
|
err = fmt.Errorf("sig strategy not exists %s", plan.SigStrategy) |
|
return |
|
} |
|
if err = sonic.UnmarshalString(plan.SigStrategyParam, &b.sigStrategyInput); err != nil { |
|
return |
|
} |
|
if err = b.sigStrategy.Init(b.sigStrategyInput); 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 |
|
} |
|
// 平仓策略 |
|
b.closeStrategy, err = trade.NewCloseStrategy(*closeStrategyParam) |
|
if err != nil { |
|
return |
|
} |
|
// 风险管理策略 |
|
b.riskStrategy, err = trade.NewRiskStrategy(*riskStrategyParam) |
|
if err != nil { |
|
return |
|
} |
|
// 交易策略 |
|
b.tradeStrategy, err = trade.NewTradeStrategy(*tradeStrategyParam) |
|
if err != nil { |
|
return |
|
} |
|
return |
|
} |
|
|
|
// 核心引擎,模拟交易、持仓跟踪、费用计算 |
|
func (b *TradingPlanBacktester) Backtest(ctx context.Context, sr *pb.SeriesRange) (test *BacktestTradingPlan, err error) { |
|
test = &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: sr.Before, |
|
SeriesAfter: 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(interval types.Interval, k types.Kline) (err error) { |
|
// 检查仓位平仓 |
|
return b.closeByKlineInterval1m(k) |
|
}) |
|
|
|
intervalSeries := types.NewIntervalState[*sig.KlineSeries]() |
|
err = sigStrategyBacktester.Backtest(ctx, b.sigStrategyInput, sr, intervalSeries, func(sigSide types.Side, k types.Kline) (err error) { |
|
test.Singals++ |
|
// 根据交易信号检查仓位平仓 |
|
if err = b.closeBySigSingal(sigSide, k); err != nil { |
|
return |
|
} |
|
// 交易下单 |
|
return b.onSideSingal(sigSide, k) |
|
}) |
|
if err != nil { |
|
return |
|
} |
|
|
|
// 读最新的k线 |
|
kSeries := intervalSeries.Get(types.Interval(sr.Interval)) |
|
lastCandle, err := kSeries.Get(0) |
|
if err != nil { |
|
return |
|
} |
|
// 关闭所有未平仓仓位 |
|
b.forceCloseAllHoldingPosition(lastCandle) |
|
|
|
// 回测结果 |
|
test.Etime = time.Now().UnixMilli() |
|
test.EndCash = b.account.cash |
|
test.Profit = b.account.profit |
|
test.TotalTrades = len(b.account.trades) |
|
test.WinningTrades = b.account.winningTrades |
|
test.LosingTrades = b.account.losingTrades |
|
test.Fee = b.account.fee |
|
// trades |
|
var trades []*Trade |
|
for _, trade := range b.account.trades { |
|
trade.BacktestId = test.Id |
|
trade.Ctime = test.Ctime |
|
trades = append(trades, trade) |
|
} |
|
collect.SortAsc(trades, func(t *Trade) int64 { return t.Id }) |
|
test.Trades = trades |
|
// 最大回撤 |
|
drawdown := b.account.maxDrawdown |
|
test.MaxDrawdown = max((drawdown[0]-drawdown[1])/drawdown[0], (drawdown[2]-drawdown[3])/drawdown[2]) |
|
return |
|
} |
|
|
|
// forceCloseAllHoldingPosition 关闭所有未平仓仓位 |
|
func (b *TradingPlanBacktester) forceCloseAllHoldingPosition(k types.Kline) (err error) { |
|
for _, pos := range b.account.positions { |
|
err = b.account.ClosePosition(pos, k, trade.CauseCloseForced) |
|
if err != nil { |
|
return |
|
} |
|
} |
|
return |
|
} |
|
|
|
// closeByKlineInterval1m k线更新时检查平仓 |
|
func (b *TradingPlanBacktester) closeByKlineInterval1m(k types.Kline) (err error) { |
|
var posErrs []error |
|
positions := b.account.OpenPositions() |
|
for _, pos := range positions { |
|
closePos, cause := b.closeStrategy.OnKline(k, pos) |
|
if closePos { |
|
errc := b.account.ClosePosition(pos, k, cause) |
|
if errc != nil { |
|
posErrs = append(posErrs, errc) |
|
zlog.Errorf("close position error: k=%#v, err=%v", k, errc) |
|
} |
|
} |
|
} |
|
if len(posErrs) > 0 { |
|
err = errors.Join(posErrs...) |
|
return |
|
} |
|
return |
|
} |
|
|
|
// closeBySigSingal 交易信号出现时检查平仓 |
|
func (b *TradingPlanBacktester) closeBySigSingal(sigSide types.Side, kline types.Kline) (err error) { |
|
var posErrs []error |
|
positions := b.account.OpenPositions() |
|
for _, pos := range positions { |
|
closePos, cause := b.closeStrategy.OnSigStrategySingal(sigSide, pos) |
|
if closePos { |
|
errc := b.account.ClosePosition(pos, kline, cause) |
|
if errc != nil { |
|
posErrs = append(posErrs, errc) |
|
zlog.Error("close position error: ", errc) |
|
} |
|
} |
|
} |
|
if len(posErrs) > 0 { |
|
err = errors.Join(posErrs...) |
|
return |
|
} |
|
return |
|
} |
|
|
|
// onSideSingal 出现买卖信号 |
|
func (b *TradingPlanBacktester) onSideSingal(sigSide types.Side, k types.Kline) (err error) { |
|
// 买卖信号交易风险分析 |
|
doTrade, causes, err := b.riskStrategy.SigRiskAnalyze(sigSide) |
|
if err != nil { |
|
return |
|
} |
|
if !doTrade { |
|
_ = causes // todo 记录信号不交易原因分析 log db analyze |
|
return |
|
} |
|
tradeArg, err := b.tradeStrategy.SigTrade(sigSide, k) |
|
if err != nil { |
|
return |
|
} |
|
ok, cause, err := b.account.TradeOrder(tradeArg) |
|
if err != nil { |
|
return |
|
} |
|
if !ok { |
|
_ = cause // todo 记录不交易原因 |
|
} |
|
return |
|
}
|
|
|