diff --git a/config/exchange.toml b/config/exchange.toml index de70f8d..caa8ef0 100644 --- a/config/exchange.toml +++ b/config/exchange.toml @@ -19,7 +19,7 @@ marketSubscribeLimit = 16 consumeBatch = 1024 consumeLater = 2000 # 时间到达later或者数据累计到batch触发consume # httpProxy = "" -# httpProxy = "http://192.168.1.8:7890" +# httpProxy = "http://192.168.1.5:7890" httpProxy = "http://10.255.183.209:7890" # 模拟盘API交易地址如下: diff --git a/internal/trading/backtest/backtest_stats.go b/internal/trading/backtest/backtest_stats.go index bee4876..9bc84b7 100644 --- a/internal/trading/backtest/backtest_stats.go +++ b/internal/trading/backtest/backtest_stats.go @@ -115,6 +115,83 @@ func sharpeFromEquitySnapshots(snapshots []*EquitySnapshot, rfAnnual float64) fl return meanEx / sd * math.Sqrt(periodsPerYear) } +// sortinoFromEquitySnapshots computes Sortino Ratio based on equity time series snapshots. +func sortinoFromEquitySnapshots(snapshots []*EquitySnapshot, rfAnnual float64) float64 { + if len(snapshots) < 2 { + return 0 + } + // ensure sorted by timestamp + sort.Slice(snapshots, func(i, j int) bool { return snapshots[i].Ts < snapshots[j].Ts }) + + const secsYear = 365.0 * 24.0 * 3600.0 + var returns []float64 + var dts []float64 + for i := 1; i < len(snapshots); i++ { + prev := snapshots[i-1].Equity + cur := snapshots[i].Equity + if prev <= 0 { + continue + } + returns = append(returns, cur/prev-1) + dt := float64(snapshots[i].Ts-snapshots[i-1].Ts) / 1000.0 + if dt <= 0 { + dt = 1.0 + } + dts = append(dts, dt) + } + if len(returns) <= 1 { + return 0 + } + sum := 0.0 + for _, d := range dts { + sum += d + } + avgDt := sum / float64(len(dts)) + periodsPerYear := secsYear / avgDt + rfPeriod := rfAnnual / periodsPerYear + + excess := make([]float64, len(returns)) + downsideSum := 0.0 + for i := range returns { + excess[i] = returns[i] - rfPeriod + if excess[i] < 0 { + downsideSum += excess[i] * excess[i] + } + } + meanEx := ta.Avg(excess) + downsideDev := math.Sqrt(downsideSum / float64(len(returns))) + if downsideDev == 0 { + return 0 + } + return meanEx / downsideDev * math.Sqrt(periodsPerYear) +} + +func calmarRatio(annualReturn float64, maxDrawdown float64) float64 { + if maxDrawdown == 0 { + return 0 + } + return annualReturn / maxDrawdown +} + +func profitFactor(orders []*trade.TradeOrder) float64 { + grossProfit := 0.0 + grossLoss := 0.0 + for _, o := range orders { + if o.Profit > 0 { + grossProfit += o.Profit + } else { + grossLoss += math.Abs(o.Profit) + } + } + if grossLoss == 0 { + if grossProfit == 0 { + return 0 + } + return 999.0 // Infinite + } + return grossProfit / grossLoss +} + func stddev(x []float64) float64 { if len(x) <= 1 { return 0 diff --git a/internal/trading/backtest/sig_strategy_backtester.go b/internal/trading/backtest/sig_strategy_backtester.go index d294ca9..1272e26 100644 --- a/internal/trading/backtest/sig_strategy_backtester.go +++ b/internal/trading/backtest/sig_strategy_backtester.go @@ -266,6 +266,13 @@ func (b *SigStrategyBacktester) multiInstanceIntervalSeries(ctx context.Context, syncCh := make(chan int64) syncChans = append(syncChans, syncCh) go func(isr *pb.SeriesRange, kSeries *types.KlineSeries, syncCh chan int64) { + defer func() { + if e := recover(); e != nil { + zlog.Errorf("fetch interval history panic: inst=%s(%s) interval=%s, err=%v", sr.InstId, sr.Exchange, isr.Interval, e) + err = fmt.Errorf("panic: %v", e) + close(stopCh) + } + }() interval := types.Interval(isr.Interval) intervalAdder := types.SupportedIntervals[interval] driverTs := int64(0) @@ -311,7 +318,7 @@ func (b *SigStrategyBacktester) multiInstanceIntervalSeries(ctx context.Context, }) zlog.Debugf("other sr finish with: %s(%s), %v, last=%d", isr.InstId, isr.Interval, err1, intervalAdder(kSeries.MustGet(0).Ts, 1)) if err1 == nil { - syncCh <- -1 // 通知更新完毕, 后续不再更新 + close(syncCh) // k线数据拉取完毕, 后续不再更新 } else if err1 != errStop { zlog.Errorf("fetch interval history error: inst=%s(%s) interval=%s, err=%v", sr.InstId, sr.Exchange, interval, err1) err = err1 @@ -332,15 +339,19 @@ func (b *SigStrategyBacktester) multiInstanceIntervalSeries(ctx context.Context, return } driverTS := driverIntervalAdder(k.Ts, 1) - for i, syncCh := range syncChans { + for _, syncCh := range syncChans { if syncCh == nil { continue } syncCh <- driverTS // 通知其他周期更新到主周期时间 + } + for i, syncCh := range syncChans { + if syncCh == nil { + continue + } select { - case sig := <-syncCh: // 等待该周期更新完毕 - if sig == -1 { - // 后续不再更新 + case _, ok := <-syncCh: // 等待其他周期更新完毕 + if !ok { syncChans[i] = nil } case <-ctx.Done(): diff --git a/internal/trading/backtest/trading_plan_backtester.go b/internal/trading/backtest/trading_plan_backtester.go index e9412eb..f30c5ab 100644 --- a/internal/trading/backtest/trading_plan_backtester.go +++ b/internal/trading/backtest/trading_plan_backtester.go @@ -181,6 +181,21 @@ func (b *TradingPlanBacktester) Backtest(ctx context.Context) (test *trade.Backt 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 } diff --git a/pkg/indicator/indicator_plot.go b/pkg/indicator/indicator_plot.go index 1c9abf3..c72b243 100644 --- a/pkg/indicator/indicator_plot.go +++ b/pkg/indicator/indicator_plot.go @@ -27,9 +27,11 @@ const ( // Series 绘图颜色 const ( - ColorRed string = "red" - ColorGreen string = "green" - ColorYellow string = "yellow" - ColorBlue string = "blue" - ColorPurple string = "purple" + ColorRed string = "#f23645" + ColorGreen string = "#089981" + ColorRed2 string = "#f7a9a7" + ColorGreen2 string = "#92d2cc" + ColorYellow string = "#cdcf36" + ColorBlue string = "#556cd6" + ColorPurple string = "#e48dce" ) diff --git a/pkg/indicator/macd.go b/pkg/indicator/macd.go index 177af7f..6dcd743 100644 --- a/pkg/indicator/macd.go +++ b/pkg/indicator/macd.go @@ -18,12 +18,12 @@ func (c *MACD) Meta() IndicatorMeta { }, State: []string{"dif", "dea"}, Plots: []Plot{ - {State: "vector", Type: PlotHistogram, Props: PlotProps{"color": ColorGreen}, Exps: []PlotExp{ - {Exp: "vector < 0", Props: PlotProps{"color": ColorRed}}, - {Exp: "vector >= 0", Props: PlotProps{"color": ColorGreen}}, + {State: "vector", Type: PlotHistogram, Props: PlotProps{"color": ColorGreen2}, Exps: []PlotExp{ + {Exp: "vector < 0", Props: PlotProps{"color": ColorRed2}}, + {Exp: "vector >= 0", Props: PlotProps{"color": ColorGreen2}}, }}, {State: "dif", Type: PlotLine, Props: PlotProps{"color": ColorYellow}}, - {State: "dea", Type: PlotLine, Props: PlotProps{"color": ColorBlue}}, + {State: "dea", Type: PlotLine, Props: PlotProps{"color": ColorRed}}, }, } } diff --git a/pkg/indicator/super_trend.go b/pkg/indicator/super_trend.go index e706d8c..c221066 100644 --- a/pkg/indicator/super_trend.go +++ b/pkg/indicator/super_trend.go @@ -15,9 +15,9 @@ func (c SuperTrend) Meta() IndicatorMeta { }, State: []string{"direction"}, Plots: []Plot{ - {State: "vector", Type: PlotLine, Props: PlotProps{"color": ColorGreen}, Exps: []PlotExp{ - {Exp: "direction == -1", Props: PlotProps{"color": ColorRed}}, - {Exp: "direction == 1", Props: PlotProps{"color": ColorGreen}}, + {State: "vector", Type: PlotLine, Props: PlotProps{"color": ColorGreen, "lineWidth": 2}, Exps: []PlotExp{ + {Exp: "direction == -1", Props: PlotProps{"color": ColorRed2}}, + {Exp: "direction == 1", Props: PlotProps{"color": ColorGreen2}}, }}, }, } diff --git a/pkg/trade/types.go b/pkg/trade/types.go index dbac545..4e1ae43 100644 --- a/pkg/trade/types.go +++ b/pkg/trade/types.go @@ -77,6 +77,10 @@ type BacktestTradingPlan struct { Fee float64 `json:"fee" gorm:"column:fee"` // 总手续费 MaxDrawdown float64 `json:"maxDrawdown" gorm:"column:max_drawdown"` // 最大回撤 SharpeRatio float64 `json:"sharpeRatio" gorm:"column:sharpe_ratio"` // 夏普比率 + SortinoRatio float64 `json:"sortinoRatio" gorm:"column:sortino_ratio"` // 索提诺比率 + CalmarRatio float64 `json:"calmarRatio" gorm:"column:calmar_ratio"` // 卡尔玛比率 + ProfitFactor float64 `json:"profitFactor" gorm:"column:profit_factor"` // 盈利因子 + WinRate float64 `json:"winRate" gorm:"column:win_rate"` // 胜率 Trades []*TradeOrder `json:"-" gorm:"-"` // 回测交易单 }