From 7aaea257dff9abb3ca27d3185183915d8e3ff8e5 Mon Sep 17 00:00:00 2001 From: zenix Date: Thu, 10 Nov 2022 18:17:35 +0900 Subject: [PATCH] feature: optimizer add profitFactor optimization. Optimization value use float64 instead to save memory and boost performance --- pkg/backtest/report.go | 2 ++ pkg/cmd/backtest.go | 27 ++++++++++++++-------- pkg/optimizer/config.go | 2 +- pkg/optimizer/grid.go | 44 ++++++++++++++++++++++++------------ pkg/optimizer/hpoptimizer.go | 12 +++++++--- 5 files changed, 58 insertions(+), 29 deletions(-) diff --git a/pkg/backtest/report.go b/pkg/backtest/report.go index 7af0340c3..bce827699 100644 --- a/pkg/backtest/report.go +++ b/pkg/backtest/report.go @@ -81,6 +81,8 @@ type SessionSymbolReport struct { Manifests Manifests `json:"manifests,omitempty"` Sharpe fixedpoint.Value `json:"sharpeRatio"` Sortino fixedpoint.Value `json:"sortinoRatio"` + ProfitFactor fixedpoint.Value `json:"profitFactor"` + WinningRatio fixedpoint.Value `json:"winningRatio"` } func (r *SessionSymbolReport) InitialEquityValue() fixedpoint.Value { diff --git a/pkg/cmd/backtest.go b/pkg/cmd/backtest.go index 192261e8b..3e2c24a42 100644 --- a/pkg/cmd/backtest.go +++ b/pkg/cmd/backtest.go @@ -4,11 +4,6 @@ import ( "bufio" "context" "fmt" - "github.com/c9s/bbgo/pkg/cmd/cmdutil" - "github.com/c9s/bbgo/pkg/data/tsv" - "github.com/c9s/bbgo/pkg/util" - "github.com/fatih/color" - "github.com/google/uuid" "os" "path/filepath" "sort" @@ -16,6 +11,12 @@ import ( "syscall" "time" + "github.com/c9s/bbgo/pkg/cmd/cmdutil" + "github.com/c9s/bbgo/pkg/data/tsv" + "github.com/c9s/bbgo/pkg/util" + "github.com/fatih/color" + "github.com/google/uuid" + "github.com/pkg/errors" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" @@ -523,8 +524,11 @@ var BacktestCmd = &cobra.Command{ for _, session := range environ.Sessions() { for symbol, trades := range session.Trades { - intervalProfits := sessionTradeStats[session.Name][symbol].IntervalProfits[types.Interval1d] - symbolReport, err := createSymbolReport(userConfig, session, symbol, trades.Trades, intervalProfits) + tradeState := sessionTradeStats[session.Name][symbol] + profitFactor := tradeState.ProfitFactor + winningRatio := tradeState.WinningRatio + intervalProfits := tradeState.IntervalProfits[types.Interval1d] + symbolReport, err := createSymbolReport(userConfig, session, symbol, trades.Trades, intervalProfits, profitFactor, winningRatio) if err != nil { return err } @@ -615,7 +619,8 @@ func collectSubscriptionIntervals(environ *bbgo.Environment) (allKLineIntervals return allKLineIntervals, requiredInterval, backTestIntervals } -func createSymbolReport(userConfig *bbgo.Config, session *bbgo.ExchangeSession, symbol string, trades []types.Trade, intervalProfit *types.IntervalProfitCollector) ( +func createSymbolReport(userConfig *bbgo.Config, session *bbgo.ExchangeSession, symbol string, trades []types.Trade, intervalProfit *types.IntervalProfitCollector, + profitFactor, winningRatio fixedpoint.Value) ( *backtest.SessionSymbolReport, error, ) { @@ -661,8 +666,10 @@ func createSymbolReport(userConfig *bbgo.Config, session *bbgo.ExchangeSession, InitialBalances: initBalances, FinalBalances: finalBalances, // Manifests: manifests, - Sharpe: sharpeRatio, - Sortino: sortinoRatio, + Sharpe: sharpeRatio, + Sortino: sortinoRatio, + ProfitFactor: profitFactor, + WinningRatio: winningRatio, } for _, s := range session.Subscriptions { diff --git a/pkg/optimizer/config.go b/pkg/optimizer/config.go index a9e03b378..4d5e0515b 100644 --- a/pkg/optimizer/config.go +++ b/pkg/optimizer/config.go @@ -79,7 +79,7 @@ func LoadConfig(yamlConfigFileName string) (*Config, error) { switch objective := strings.ToLower(optConfig.Objective); objective { case "", "default": optConfig.Objective = HpOptimizerObjectiveEquity - case HpOptimizerObjectiveEquity, HpOptimizerObjectiveProfit, HpOptimizerObjectiveVolume: + case HpOptimizerObjectiveEquity, HpOptimizerObjectiveProfit, HpOptimizerObjectiveVolume, HpOptimizerObjectiveProfitFactor: optConfig.Objective = objective default: return nil, fmt.Errorf(`unknown objective "%s"`, optConfig.Objective) diff --git a/pkg/optimizer/grid.go b/pkg/optimizer/grid.go index eab8352f7..0d40d7fdd 100644 --- a/pkg/optimizer/grid.go +++ b/pkg/optimizer/grid.go @@ -14,30 +14,43 @@ import ( "github.com/c9s/bbgo/pkg/fixedpoint" ) -type MetricValueFunc func(summaryReport *backtest.SummaryReport) fixedpoint.Value +type MetricValueFunc func(summaryReport *backtest.SummaryReport) float64 -var TotalProfitMetricValueFunc = func(summaryReport *backtest.SummaryReport) fixedpoint.Value { - return summaryReport.TotalProfit +var TotalProfitMetricValueFunc = func(summaryReport *backtest.SummaryReport) float64 { + return summaryReport.TotalProfit.Float64() } -var TotalVolume = func(summaryReport *backtest.SummaryReport) fixedpoint.Value { +var TotalVolume = func(summaryReport *backtest.SummaryReport) float64 { if len(summaryReport.SymbolReports) == 0 { - return fixedpoint.Zero + return 0 } - buyVolume := summaryReport.SymbolReports[0].PnL.BuyVolume - sellVolume := summaryReport.SymbolReports[0].PnL.SellVolume - return buyVolume.Add(sellVolume) + buyVolume := summaryReport.SymbolReports[0].PnL.BuyVolume.Float64() + sellVolume := summaryReport.SymbolReports[0].PnL.SellVolume.Float64() + return buyVolume + sellVolume } -var TotalEquityDiff = func(summaryReport *backtest.SummaryReport) fixedpoint.Value { +var TotalEquityDiff = func(summaryReport *backtest.SummaryReport) float64 { if len(summaryReport.SymbolReports) == 0 { - return fixedpoint.Zero + return 0 } - initEquity := summaryReport.InitialEquityValue - finalEquity := summaryReport.FinalEquityValue - return finalEquity.Sub(initEquity) + initEquity := summaryReport.InitialEquityValue.Float64() + finalEquity := summaryReport.FinalEquityValue.Float64() + return finalEquity - initEquity +} + +var ProfitFactorMetricValueFunc = func(summaryReport *backtest.SummaryReport) float64 { + if len(summaryReport.SymbolReports) == 0 { + return 0 + } + if len(summaryReport.SymbolReports) > 1 { + panic("multiple symbols' profitfactor optimization not supported") + } + report := summaryReport.SymbolReports[0] + pf := report.ProfitFactor.Float64() + win := report.WinningRatio.Float64() + return pf*0.9 + win*0.1 } type Metric struct { @@ -51,7 +64,7 @@ type Metric struct { Key string `json:"key"` // Value is the metric value of the metric - Value fixedpoint.Value `json:"value,omitempty"` + Value float64 `json:"value,omitempty"` } func copyParams(params []interface{}) []interface{} { @@ -199,6 +212,7 @@ func (o *GridOptimizer) Run(executor Executor, configJson []byte) (map[string][] "totalProfit": TotalProfitMetricValueFunc, "totalVolume": TotalVolume, "totalEquityDiff": TotalEquityDiff, + "profitFactor": ProfitFactorMetricValueFunc, } var metrics = map[string][]Metric{} @@ -287,7 +301,7 @@ func (o *GridOptimizer) Run(executor Executor, configJson []byte) (map[string][] sort.Slice(metrics[n], func(i, j int) bool { a := metrics[n][i].Value b := metrics[n][j].Value - return a.Compare(b) > 0 + return a > b }) } diff --git a/pkg/optimizer/hpoptimizer.go b/pkg/optimizer/hpoptimizer.go index ecf806ac4..6f8236da0 100644 --- a/pkg/optimizer/hpoptimizer.go +++ b/pkg/optimizer/hpoptimizer.go @@ -3,6 +3,9 @@ package optimizer import ( "context" "fmt" + "math" + "sync" + "github.com/c-bata/goptuna" goptunaCMAES "github.com/c-bata/goptuna/cmaes" goptunaSOBOL "github.com/c-bata/goptuna/sobol" @@ -11,10 +14,9 @@ import ( "github.com/cheggaaa/pb/v3" "github.com/sirupsen/logrus" "golang.org/x/sync/errgroup" - "math" - "sync" ) +// WARNING: the text here could only be lower cases const ( // HpOptimizerObjectiveEquity optimize the parameters to maximize equity gain HpOptimizerObjectiveEquity = "equity" @@ -22,6 +24,8 @@ const ( HpOptimizerObjectiveProfit = "profit" // HpOptimizerObjectiveVolume optimize the parameters to maximize trading volume HpOptimizerObjectiveVolume = "volume" + // HpOptimizerObjectiveProfitFactor optimize the parameters to maximize profit factor + HpOptimizerObjectiveProfitFactor = "profitfactor" ) const ( @@ -198,6 +202,8 @@ func (o *HyperparameterOptimizer) buildObjective(executor Executor, configJson [ metricValueFunc = TotalVolume case HpOptimizerObjectiveEquity: metricValueFunc = TotalEquityDiff + case HpOptimizerObjectiveProfitFactor: + metricValueFunc = ProfitFactorMetricValueFunc } return func(trial goptuna.Trial) (float64, error) { @@ -225,7 +231,7 @@ func (o *HyperparameterOptimizer) buildObjective(executor Executor, configJson [ return 0.0, err } // By config, the Goptuna optimize the parameters by maximize the objective output. - return metricValueFunc(summary).Float64(), nil + return metricValueFunc(summary), nil } }