diff --git a/backtest/backtest.go b/backtest/backtest.go index fb67659..1ab2c71 100644 --- a/backtest/backtest.go +++ b/backtest/backtest.go @@ -154,7 +154,7 @@ func (b *Backtest) worker(names <-chan string, wg *sync.WaitGroup) { actions, outcomes := strategy.ComputeWithOutcome(currentStrategy, snapshotsSplice[0]) err = b.report.Write(name, currentStrategy, snapshotsSplice[1], actions, outcomes) if err != nil { - log.Printf("Unable to report write for %s: %v", name, err) + log.Printf("Unable to write report for %s: %v", name, err) } } diff --git a/backtest/html_report.go b/backtest/html_report.go index 6ea4dc2..ba9b6b8 100644 --- a/backtest/html_report.go +++ b/backtest/html_report.go @@ -32,7 +32,7 @@ var htmlReportTmpl string //go:embed "html_asset_report.tmpl" var htmlAssetReportTmpl string -// HTMLReport is the backtest HTML report interface. +// HTMLReport is the backtest HTML report. type HTMLReport struct { Report diff --git a/backtest/report_factory.go b/backtest/report_factory.go new file mode 100644 index 0000000..2ce558f --- /dev/null +++ b/backtest/report_factory.go @@ -0,0 +1,42 @@ +// Copyright (c) 2021-2024 Onur Cinar. +// The source code is provided under GNU AGPLv3 License. +// https://github.com/cinar/indicator + +package backtest + +import ( + "fmt" +) + +const ( + // HTMLReportBuilderName is the name for the HTML report builder. + HTMLReportBuilderName = "html" +) + +// ReportBuilderFunc defines a function to build a new report using the given configuration parameter. +type ReportBuilderFunc func(config string) (Report, error) + +// reportBuilders provides mapping for the report builders. +var reportBuilders = map[string]ReportBuilderFunc{ + HTMLReportBuilderName: htmlReportBuilder, +} + +// RegisterReportBuilder registers the given builder. +func RegisterReportBuilder(name string, builder ReportBuilderFunc) { + reportBuilders[name] = builder +} + +// NewReport builds a new report by the given name type and the configuration. +func NewReport(name, config string) (Report, error) { + builder, ok := reportBuilders[name] + if !ok { + return nil, fmt.Errorf("unknown report: %s", name) + } + + return builder(config) +} + +// htmlReportBuilder builds a new HTML report instance. +func htmlReportBuilder(config string) (Report, error) { + return NewHTMLReport(config), nil +} diff --git a/backtest/report_factory_test.go b/backtest/report_factory_test.go new file mode 100644 index 0000000..e292b16 --- /dev/null +++ b/backtest/report_factory_test.go @@ -0,0 +1,53 @@ +// Copyright (c) 2021-2024 Onur Cinar. +// The source code is provided under GNU AGPLv3 License. +// https://github.com/cinar/indicator + +package backtest_test + +import ( + "testing" + + "github.com/cinar/indicator/v2/backtest" +) + +func TestNewReportUnknown(t *testing.T) { + report, err := backtest.NewReport("unknown", "") + if err == nil { + t.Fatalf("unknown report: %T", report) + } +} + +func TestRegisterReportBuilder(t *testing.T) { + builderName := "testbuilder" + + report, err := backtest.NewReport(builderName, "") + if err == nil { + t.Fatalf("testbuilder is: %T", report) + } + + backtest.RegisterReportBuilder(builderName, func(config string) (backtest.Report, error) { + return backtest.NewHTMLReport(config), nil + }) + + report, err = backtest.NewReport(builderName, "") + if err != nil { + t.Fatalf("testbuilder is not found: %v", err) + } + + _, ok := report.(*backtest.HTMLReport) + if !ok { + t.Fatalf("testbuilder is: %T", report) + } +} + +func TestNewReportMemory(t *testing.T) { + report, err := backtest.NewReport(backtest.HTMLReportBuilderName, "") + if err != nil { + t.Fatal(err) + } + + _, ok := report.(*backtest.HTMLReport) + if !ok { + t.Fatalf("report not correct type: %T", report) + } +} diff --git a/cmd/indicator-backtest/main.go b/cmd/indicator-backtest/main.go index 84b18bc..a02292b 100644 --- a/cmd/indicator-backtest/main.go +++ b/cmd/indicator-backtest/main.go @@ -13,7 +13,6 @@ import ( "github.com/cinar/indicator/v2/asset" "github.com/cinar/indicator/v2/backtest" - "github.com/cinar/indicator/v2/helper" "github.com/cinar/indicator/v2/strategy" "github.com/cinar/indicator/v2/strategy/compound" "github.com/cinar/indicator/v2/strategy/momentum" @@ -22,15 +21,14 @@ import ( ) func main() { - var sourceName string - var sourceConfig string - var outputDir string + var repositoryName string + var repositoryConfig string + var reportName string + var reportConfig string var workers int var lastDays int - var writeStrategyRerpots bool var addSplits bool var addAnds bool - var dateFormat string fmt.Fprintln(os.Stderr, "Indicator Backtest") fmt.Fprintln(os.Stderr, "Copyright (c) 2021-2024 Onur Cinar.") @@ -38,45 +36,45 @@ func main() { fmt.Fprintln(os.Stderr, "https://github.com/cinar/indicator") fmt.Fprintln(os.Stderr) - flag.StringVar(&sourceName, "source-name", "filesystem", "source repository type") - flag.StringVar(&sourceConfig, "source-config", "", "source repository config") - flag.StringVar(&outputDir, "output", ".", "output directory") + flag.StringVar(&repositoryName, "repository-name", "filesystem", "repository name") + flag.StringVar(&repositoryConfig, "repository-config", "", "repository config") + flag.StringVar(&reportName, "report-name", "html", "report name") + flag.StringVar(&reportConfig, "report-config", ".", "report type") flag.IntVar(&workers, "workers", backtest.DefaultBacktestWorkers, "number of concurrent workers") flag.IntVar(&lastDays, "last", backtest.DefaultLastDays, "number of days to do backtest") - flag.BoolVar(&writeStrategyRerpots, "write-strategy-reports", backtest.DefaultWriteStrategyReports, "write individual strategy reports") flag.BoolVar(&addSplits, "splits", false, "add the split strategies") flag.BoolVar(&addAnds, "ands", false, "add the and strategies") - flag.StringVar(&dateFormat, "date-format", helper.DefaultReportDateFormat, "date format to use") flag.Parse() - source, err := asset.NewRepository(sourceName, sourceConfig) + source, err := asset.NewRepository(repositoryName, repositoryConfig) if err != nil { log.Fatalf("unable to initialize source: %v", err) } - htmlReport := backtest.NewHTMLReport(outputDir) - htmlReport.WriteStrategyReports = writeStrategyRerpots - htmlReport.DateFormat = dateFormat + report, err := backtest.NewReport(repositoryName, repositoryConfig) + if err != nil { + log.Fatalf("unable to initialize report: %v", err) + } - backtest := backtest.NewBacktest(source, htmlReport) - backtest.Workers = workers - backtest.LastDays = lastDays - backtest.Names = append(backtest.Names, flag.Args()...) - backtest.Strategies = append(backtest.Strategies, compound.AllStrategies()...) - backtest.Strategies = append(backtest.Strategies, momentum.AllStrategies()...) - backtest.Strategies = append(backtest.Strategies, strategy.AllStrategies()...) - backtest.Strategies = append(backtest.Strategies, trend.AllStrategies()...) - backtest.Strategies = append(backtest.Strategies, volatility.AllStrategies()...) + backtester := backtest.NewBacktest(source, report) + backtester.Workers = workers + backtester.LastDays = lastDays + backtester.Names = append(backtester.Names, flag.Args()...) + backtester.Strategies = append(backtester.Strategies, compound.AllStrategies()...) + backtester.Strategies = append(backtester.Strategies, momentum.AllStrategies()...) + backtester.Strategies = append(backtester.Strategies, strategy.AllStrategies()...) + backtester.Strategies = append(backtester.Strategies, trend.AllStrategies()...) + backtester.Strategies = append(backtester.Strategies, volatility.AllStrategies()...) if addSplits { - backtest.Strategies = append(backtest.Strategies, strategy.AllSplitStrategies(backtest.Strategies)...) + backtester.Strategies = append(backtester.Strategies, strategy.AllSplitStrategies(backtester.Strategies)...) } if addAnds { - backtest.Strategies = append(backtest.Strategies, strategy.AllAndStrategies(backtest.Strategies)...) + backtester.Strategies = append(backtester.Strategies, strategy.AllAndStrategies(backtester.Strategies)...) } - err = backtest.Run() + err = backtester.Run() if err != nil { log.Fatalf("unable to run backtest: %v", err) }