Browse Source

backtest optimize

main
strange 7 months ago
parent
commit
4abcc754f3
  1. 2
      config/exchange.toml
  2. 77
      internal/trading/backtest/backtest_stats.go
  3. 21
      internal/trading/backtest/sig_strategy_backtester.go
  4. 15
      internal/trading/backtest/trading_plan_backtester.go
  5. 12
      pkg/indicator/indicator_plot.go
  6. 8
      pkg/indicator/macd.go
  7. 6
      pkg/indicator/super_trend.go
  8. 4
      pkg/trade/types.go

2
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交易地址如下:

77
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

21
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():

15
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
}

12
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"
)

8
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}},
},
}
}

6
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}},
}},
},
}

4
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:"-"` // 回测交易单
}

Loading…
Cancel
Save