diff --git a/backtest/README.md b/backtest/README.md
new file mode 100644
index 0000000..fffa95a
--- /dev/null
+++ b/backtest/README.md
@@ -0,0 +1,201 @@
+
+
+# backtest
+
+```go
+import "github.com/cinar/indicator/v2/backtest"
+```
+
+Package backtest contains the backtest functions.
+
+This package belongs to the Indicator project. Indicator is a Golang module that supplies a variety of technical indicators, strategies, and a backtesting framework for analysis.
+
+### License
+
+```
+Copyright (c) 2021-2024 Onur Cinar.
+The source code is provided under GNU AGPLv3 License.
+https://github.com/cinar/indicator
+```
+
+### Disclaimer
+
+The information provided on this project is strictly for informational purposes and is not to be construed as advice or solicitation to buy or sell any security.
+
+## Index
+
+- [Constants](<#constants>)
+- [type Backtest](<#Backtest>)
+ - [func NewBacktest\(repository asset.Repository, report Report\) \*Backtest](<#NewBacktest>)
+ - [func \(b \*Backtest\) Run\(\) error](<#Backtest.Run>)
+- [type HTMLReport](<#HTMLReport>)
+ - [func NewHTMLReport\(outputDir string\) \*HTMLReport](<#NewHTMLReport>)
+ - [func \(h \*HTMLReport\) AssetBegin\(name string, strategies \[\]strategy.Strategy\) error](<#HTMLReport.AssetBegin>)
+ - [func \(h \*HTMLReport\) AssetEnd\(name string\) error](<#HTMLReport.AssetEnd>)
+ - [func \(h \*HTMLReport\) Begin\(assetNames \[\]string, \_ \[\]strategy.Strategy\) error](<#HTMLReport.Begin>)
+ - [func \(h \*HTMLReport\) End\(\) error](<#HTMLReport.End>)
+ - [func \(h \*HTMLReport\) Write\(assetName string, currentStrategy strategy.Strategy, snapshots \<\-chan \*asset.Snapshot, actions \<\-chan strategy.Action, outcomes \<\-chan float64\) error](<#HTMLReport.Write>)
+- [type Report](<#Report>)
+
+
+## Constants
+
+
+
+```go
+const (
+ // DefaultBacktestWorkers is the default number of backtest workers.
+ DefaultBacktestWorkers = 1
+
+ // DefaultLastDays is the default number of days backtest should go back.
+ DefaultLastDays = 365
+)
+```
+
+
+
+```go
+const (
+ // DefaultWriteStrategyReports is the default state of writing individual strategy reports.
+ DefaultWriteStrategyReports = true
+)
+```
+
+
+## type [Backtest]()
+
+Backtest function rigorously evaluates the potential performance of the specified strategies applied to a defined set of assets. It generates comprehensive visual representations for each strategy\-asset pairing.
+
+```go
+type Backtest struct {
+
+ // Names is the names of the assets to backtest.
+ Names []string
+
+ // Strategies is the list of strategies to apply.
+ Strategies []strategy.Strategy
+
+ // Workers is the number of concurrent workers.
+ Workers int
+
+ // LastDays is the number of days backtest should go back.
+ LastDays int
+ // contains filtered or unexported fields
+}
+```
+
+
+### func [NewBacktest]()
+
+```go
+func NewBacktest(repository asset.Repository, report Report) *Backtest
+```
+
+NewBacktest function initializes a new backtest instance.
+
+
+### func \(\*Backtest\) [Run]()
+
+```go
+func (b *Backtest) Run() error
+```
+
+Run executes a comprehensive performance evaluation of the designated strategies, applied to a specified collection of assets. In the absence of explicitly defined assets, encompasses all assets within the repository. Likewise, in the absence of explicitly defined strategies, encompasses all the registered strategies.
+
+
+## type [HTMLReport]()
+
+HTMLReport is the backtest HTML report interface.
+
+```go
+type HTMLReport struct {
+ Report
+
+ // WriteStrategyReports indicates whether the individual strategy reports should be generated.
+ WriteStrategyReports bool
+
+ // DateFormat is the date format that is used in the reports.
+ DateFormat string
+ // contains filtered or unexported fields
+}
+```
+
+
+### func [NewHTMLReport]()
+
+```go
+func NewHTMLReport(outputDir string) *HTMLReport
+```
+
+NewHTMLReport initializes a new HTML report instance.
+
+
+### func \(\*HTMLReport\) [AssetBegin]()
+
+```go
+func (h *HTMLReport) AssetBegin(name string, strategies []strategy.Strategy) error
+```
+
+AssetBegin is called when backtesting for the given asset begins.
+
+
+### func \(\*HTMLReport\) [AssetEnd]()
+
+```go
+func (h *HTMLReport) AssetEnd(name string) error
+```
+
+AssetEnd is called when backtesting for the given asset ends.
+
+
+### func \(\*HTMLReport\) [Begin]()
+
+```go
+func (h *HTMLReport) Begin(assetNames []string, _ []strategy.Strategy) error
+```
+
+Begin is called when the backtest starts.
+
+
+### func \(\*HTMLReport\) [End]()
+
+```go
+func (h *HTMLReport) End() error
+```
+
+End is called when the backtest ends.
+
+
+### func \(\*HTMLReport\) [Write]()
+
+```go
+func (h *HTMLReport) Write(assetName string, currentStrategy strategy.Strategy, snapshots <-chan *asset.Snapshot, actions <-chan strategy.Action, outcomes <-chan float64) error
+```
+
+Write writes the given strategy actions and outomes to the report.
+
+
+## type [Report]()
+
+Report is the backtest report interface.
+
+```go
+type Report interface {
+ // Begin is called when the backtest begins.
+ Begin(assetNames []string, strategies []strategy.Strategy) error
+
+ // AssetBegin is called when backtesting for the given asset begins.
+ AssetBegin(name string, strategies []strategy.Strategy) error
+
+ // Write writes the given strategy actions and outomes to the report.
+ Write(assetName string, currentStrategy strategy.Strategy, snapshots <-chan *asset.Snapshot, actions <-chan strategy.Action, outcomes <-chan float64) error
+
+ // AssetEnd is called when backtesting for the given asset ends.
+ AssetEnd(name string) error
+
+ // End is called when the backtest ends.
+ End() error
+}
+```
+
+Generated by [gomarkdoc]()
diff --git a/backtest/backtest.go b/backtest/backtest.go
new file mode 100644
index 0000000..fb67659
--- /dev/null
+++ b/backtest/backtest.go
@@ -0,0 +1,167 @@
+// Package backtest contains the backtest functions.
+//
+// This package belongs to the Indicator project. Indicator is
+// a Golang module that supplies a variety of technical
+// indicators, strategies, and a backtesting framework
+// for analysis.
+//
+// # License
+//
+// Copyright (c) 2021-2024 Onur Cinar.
+// The source code is provided under GNU AGPLv3 License.
+// https://github.com/cinar/indicator
+//
+// # Disclaimer
+//
+// The information provided on this project is strictly for
+// informational purposes and is not to be construed as
+// advice or solicitation to buy or sell any security.
+package backtest
+
+import (
+ "fmt"
+ "log"
+ "sync"
+ "time"
+
+ "github.com/cinar/indicator/v2/asset"
+ "github.com/cinar/indicator/v2/helper"
+ "github.com/cinar/indicator/v2/strategy"
+)
+
+const (
+ // DefaultBacktestWorkers is the default number of backtest workers.
+ DefaultBacktestWorkers = 1
+
+ // DefaultLastDays is the default number of days backtest should go back.
+ DefaultLastDays = 365
+)
+
+// Backtest function rigorously evaluates the potential performance of the
+// specified strategies applied to a defined set of assets. It generates
+// comprehensive visual representations for each strategy-asset pairing.
+type Backtest struct {
+ // repository is the repository to retrieve the assets from.
+ repository asset.Repository
+
+ // report is the report writer for the backtest.
+ report Report
+
+ // Names is the names of the assets to backtest.
+ Names []string
+
+ // Strategies is the list of strategies to apply.
+ Strategies []strategy.Strategy
+
+ // Workers is the number of concurrent workers.
+ Workers int
+
+ // LastDays is the number of days backtest should go back.
+ LastDays int
+}
+
+// NewBacktest function initializes a new backtest instance.
+func NewBacktest(repository asset.Repository, report Report) *Backtest {
+ return &Backtest{
+ repository: repository,
+ report: report,
+ Names: []string{},
+ Strategies: []strategy.Strategy{},
+ Workers: DefaultBacktestWorkers,
+ LastDays: DefaultLastDays,
+ }
+}
+
+// Run executes a comprehensive performance evaluation of the designated strategies,
+// applied to a specified collection of assets. In the absence of explicitly defined
+// assets, encompasses all assets within the repository. Likewise, in the absence of
+// explicitly defined strategies, encompasses all the registered strategies.
+func (b *Backtest) Run() error {
+ // When asset names are absent, considers all assets within the provided repository for evaluation.
+ if len(b.Names) == 0 {
+ assets, err := b.repository.Assets()
+ if err != nil {
+ return err
+ }
+
+ b.Names = assets
+ }
+
+ // When strategies are absent, considers all strategies.
+ if len(b.Strategies) == 0 {
+ b.Strategies = []strategy.Strategy{
+ strategy.NewBuyAndHoldStrategy(),
+ }
+ }
+
+ // Begin report.
+ err := b.report.Begin(b.Names, b.Strategies)
+ if err != nil {
+ return fmt.Errorf("unable to begin report: %w", err)
+ }
+
+ // Run the backtest workers.
+ names := helper.SliceToChan(b.Names)
+ wg := &sync.WaitGroup{}
+
+ for i := 0; i < b.Workers; i++ {
+ wg.Add(1)
+ go b.worker(names, wg)
+ }
+
+ // Wait for all workers to finish.
+ wg.Wait()
+
+ // End report.
+ err = b.report.End()
+ if err != nil {
+ return fmt.Errorf("unable to end report: %w", err)
+ }
+
+ return nil
+}
+
+// worker is a backtesting worker that concurrently executes backtests for individual
+// assets. It receives asset names from the provided channel, and performs backtests
+// using the given strategies.
+func (b *Backtest) worker(names <-chan string, wg *sync.WaitGroup) {
+ defer wg.Done()
+
+ since := time.Now().AddDate(0, 0, -b.LastDays)
+
+ for name := range names {
+ log.Printf("Backtesting %s...", name)
+ snapshots, err := b.repository.GetSince(name, since)
+ if err != nil {
+ log.Printf("Unable to retrieve the snapshots for %s: %v", name, err)
+ continue
+ }
+
+ // We don't expect the snapshots to be a stream during backtesting.
+ snapshotsSlice := helper.ChanToSlice(snapshots)
+
+ // Backtesting asset has begun.
+ err = b.report.AssetBegin(name, b.Strategies)
+ if err != nil {
+ log.Printf("Unable to asset begin for %s: %v", name, err)
+ continue
+ }
+
+ // Backtest strategies on the given asset.
+ for _, currentStrategy := range b.Strategies {
+ snapshotsSplice := helper.Duplicate(helper.SliceToChan(snapshotsSlice), 2)
+
+ 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)
+ }
+ }
+
+ // Backtesting asset had ended
+ err = b.report.AssetEnd(name)
+ if err != nil {
+ log.Printf("Unable to asset end for %s: %v", name, err)
+ }
+ }
+}
diff --git a/strategy/backtest_test.go b/backtest/backtest_test.go
similarity index 77%
rename from strategy/backtest_test.go
rename to backtest/backtest_test.go
index d99e8af..33ccd9b 100644
--- a/strategy/backtest_test.go
+++ b/backtest/backtest_test.go
@@ -2,14 +2,14 @@
// The source code is provided under GNU AGPLv3 License.
// https://github.com/cinar/indicator
-package strategy_test
+package backtest_test
import (
"os"
"testing"
"github.com/cinar/indicator/v2/asset"
- "github.com/cinar/indicator/v2/strategy"
+ "github.com/cinar/indicator/v2/backtest"
"github.com/cinar/indicator/v2/strategy/trend"
)
@@ -23,7 +23,8 @@ func TestBacktest(t *testing.T) {
defer os.RemoveAll(outputDir)
- backtest := strategy.NewBacktest(repository, outputDir)
+ htmlReport := backtest.NewHTMLReport(outputDir)
+ backtest := backtest.NewBacktest(repository, htmlReport)
backtest.Names = append(backtest.Names, "brk-b")
backtest.Strategies = append(backtest.Strategies, trend.NewApoStrategy())
@@ -43,7 +44,8 @@ func TestBacktestAllAssetsAndStrategies(t *testing.T) {
defer os.RemoveAll(outputDir)
- backtest := strategy.NewBacktest(repository, outputDir)
+ htmlReport := backtest.NewHTMLReport(outputDir)
+ backtest := backtest.NewBacktest(repository, htmlReport)
err = backtest.Run()
if err != nil {
@@ -61,7 +63,8 @@ func TestBacktestNonExistingAsset(t *testing.T) {
defer os.RemoveAll(outputDir)
- backtest := strategy.NewBacktest(repository, outputDir)
+ htmlReport := backtest.NewHTMLReport(outputDir)
+ backtest := backtest.NewBacktest(repository, htmlReport)
backtest.Names = append(backtest.Names, "non_existing")
err = backtest.Run()
diff --git a/strategy/backtest_asset_report.tmpl b/backtest/html_asset_report.tmpl
similarity index 100%
rename from strategy/backtest_asset_report.tmpl
rename to backtest/html_asset_report.tmpl
diff --git a/backtest/html_report.go b/backtest/html_report.go
new file mode 100644
index 0000000..6ea4dc2
--- /dev/null
+++ b/backtest/html_report.go
@@ -0,0 +1,262 @@
+// Copyright (c) 2021-2024 Onur Cinar.
+// The source code is provided under GNU AGPLv3 License.
+// https://github.com/cinar/indicator
+
+package backtest
+
+import (
+ // Go embed report template.
+ _ "embed"
+ "fmt"
+ "log"
+ "os"
+ "path"
+ "path/filepath"
+ "slices"
+ "text/template"
+ "time"
+
+ "github.com/cinar/indicator/v2/asset"
+ "github.com/cinar/indicator/v2/helper"
+ "github.com/cinar/indicator/v2/strategy"
+)
+
+const (
+ // DefaultWriteStrategyReports is the default state of writing individual strategy reports.
+ DefaultWriteStrategyReports = true
+)
+
+//go:embed "html_report.tmpl"
+var htmlReportTmpl string
+
+//go:embed "html_asset_report.tmpl"
+var htmlAssetReportTmpl string
+
+// HTMLReport is the backtest HTML report interface.
+type HTMLReport struct {
+ Report
+
+ // outputDir is the output directory for the generated reports.
+ outputDir string
+
+ // assetResults is the mapping from the asset name to strategy results.
+ assetResults map[string][]*htmlReportResult
+
+ // bestResults is the best results for each asset.
+ bestResults []*htmlReportResult
+
+ // WriteStrategyReports indicates whether the individual strategy reports should be generated.
+ WriteStrategyReports bool
+
+ // DateFormat is the date format that is used in the reports.
+ DateFormat string
+}
+
+// htmlReportResult encapsulates the outcome of running a strategy.
+type htmlReportResult struct {
+ // AssetName is the name of the asset.
+ AssetName string
+
+ // StrategyName is the name of the strategy.
+ StrategyName string
+
+ // Action is the last recommended action by the strategy.
+ Action strategy.Action
+
+ // Since indicates how long the current action recommendation has been in effect.
+ Since int
+
+ // Outcome is the effectiveness of applying the recommended actions.
+ Outcome float64
+
+ // Transactions is the number of transactions made by the strategy.
+ Transactions int
+}
+
+// NewHTMLReport initializes a new HTML report instance.
+func NewHTMLReport(outputDir string) *HTMLReport {
+ return &HTMLReport{
+ outputDir: outputDir,
+ assetResults: make(map[string][]*htmlReportResult),
+ WriteStrategyReports: DefaultWriteStrategyReports,
+ DateFormat: helper.DefaultReportDateFormat,
+ }
+}
+
+// Begin is called when the backtest starts.
+func (h *HTMLReport) Begin(assetNames []string, _ []strategy.Strategy) error {
+ // Make sure that output directory exists.
+ err := os.MkdirAll(h.outputDir, 0o700)
+ if err != nil {
+ return fmt.Errorf("unable to make the output directory: %w", err)
+ }
+
+ h.bestResults = make([]*htmlReportResult, 0, len(assetNames))
+
+ return nil
+}
+
+// AssetBegin is called when backtesting for the given asset begins.
+func (h *HTMLReport) AssetBegin(name string, strategies []strategy.Strategy) error {
+ _, ok := h.assetResults[name]
+ if ok {
+ return fmt.Errorf("asset has already begun: %s", name)
+ }
+
+ h.assetResults[name] = make([]*htmlReportResult, 0, len(strategies))
+
+ return nil
+}
+
+// Write writes the given strategy actions and outomes to the report.
+func (h *HTMLReport) Write(assetName string, currentStrategy strategy.Strategy, snapshots <-chan *asset.Snapshot, actions <-chan strategy.Action, outcomes <-chan float64) error {
+ actionsSplice := helper.Duplicate(actions, 3)
+
+ actions = helper.Last(actionsSplice[0], 1)
+ sinces := helper.Last(helper.Since[strategy.Action, int](actionsSplice[1]), 1)
+ outcomes = helper.Last(outcomes, 1)
+ transactions := helper.Last(strategy.CountTransactions(actionsSplice[2]), 1)
+
+ // Generate inidividual strategy report.
+ if h.WriteStrategyReports {
+ report := currentStrategy.Report(snapshots)
+ report.DateFormat = h.DateFormat
+
+ reportFile := h.strategyReportFileName(assetName, currentStrategy.Name())
+
+ err := report.WriteToFile(path.Join(h.outputDir, reportFile))
+ if err != nil {
+ return fmt.Errorf("unable to write report for %s (%v)", assetName, err)
+ }
+ } else {
+ go helper.Drain(snapshots)
+ }
+
+ // Get asset strategy results.
+ results, ok := h.assetResults[assetName]
+ if !ok {
+ return fmt.Errorf("asset has not begun: %s", assetName)
+ }
+
+ // Append current strategy result for the asset.
+ h.assetResults[assetName] = append(results, &htmlReportResult{
+ AssetName: assetName,
+ StrategyName: currentStrategy.Name(),
+ Action: <-actions,
+ Since: <-sinces,
+ Outcome: <-outcomes * 100,
+ Transactions: <-transactions,
+ })
+
+ return nil
+}
+
+// AssetEnd is called when backtesting for the given asset ends.
+func (h *HTMLReport) AssetEnd(name string) error {
+ results, ok := h.assetResults[name]
+ if !ok {
+ return fmt.Errorf("asset has not begun: %s", name)
+ }
+
+ delete(h.assetResults, name)
+
+ // Sort the backtest results by the outcomes.
+ slices.SortFunc(results, func(a, b *htmlReportResult) int {
+ return int(b.Outcome - a.Outcome)
+ })
+
+ bestResult := results[0]
+
+ // Report the best result for the current asset.
+ log.Printf("Best outcome for %s is %.2f%% with %s.", name, bestResult.Outcome, bestResult.StrategyName)
+ h.bestResults = append(h.bestResults, bestResult)
+
+ // Write the asset report.
+ err := h.writeAssetReport(name, results)
+ if err != nil {
+ return fmt.Errorf("unable to write report for %s: %w", name, err)
+ }
+
+ return nil
+}
+
+// End is called when the backtest ends.
+func (h *HTMLReport) End() error {
+ // Sort the best results by the outcomes.
+ slices.SortFunc(h.bestResults, func(a, b *htmlReportResult) int {
+ return int(b.Outcome - a.Outcome)
+ })
+
+ return h.writeReport()
+}
+
+// strategyReportFileName defines the HTML report file name for the given asset and strategy.
+func (*HTMLReport) strategyReportFileName(assetName, strategyName string) string {
+ return fmt.Sprintf("%s - %s.html", assetName, strategyName)
+}
+
+// writeAssetReport generates a detailed report for the asset, summarizing the backtest results.
+func (h *HTMLReport) writeAssetReport(name string, results []*htmlReportResult) error {
+ type Model struct {
+ AssetName string
+ Results []*htmlReportResult
+ GeneratedOn string
+ }
+
+ model := Model{
+ AssetName: name,
+ Results: results,
+ GeneratedOn: time.Now().String(),
+ }
+
+ file, err := os.Create(filepath.Join(h.outputDir, fmt.Sprintf("%s.html", name)))
+ if err != nil {
+ return fmt.Errorf("unable to open asset report file for %s: %w", name, err)
+ }
+
+ defer helper.CloseAndLogError(file, "unable to close asset report file")
+
+ tmpl, err := template.New("report").Parse(htmlAssetReportTmpl)
+ if err != nil {
+ return fmt.Errorf("unable to initialize asset report template: %w", err)
+ }
+
+ err = tmpl.Execute(file, model)
+ if err != nil {
+ return fmt.Errorf("unable to execute asset report template: %w", err)
+ }
+
+ return nil
+}
+
+// writeReport generates a detailed report for the best results for all the assets.
+func (h *HTMLReport) writeReport() error {
+ type Model struct {
+ Results []*htmlReportResult
+ GeneratedOn string
+ }
+
+ model := Model{
+ Results: h.bestResults,
+ GeneratedOn: time.Now().String(),
+ }
+
+ file, err := os.Create(filepath.Join(h.outputDir, "index.html"))
+ if err != nil {
+ return fmt.Errorf("unable to open main report file: %w", err)
+ }
+
+ defer helper.CloseAndLogError(file, "unable to close main report file")
+
+ tmpl, err := template.New("report").Parse(htmlReportTmpl)
+ if err != nil {
+ return fmt.Errorf("unable to execute main report template: %w", err)
+ }
+
+ err = tmpl.Execute(file, model)
+ if err != nil {
+ return fmt.Errorf("unable to execute main report template: %w", err)
+ }
+
+ return nil
+}
diff --git a/strategy/backtest_report.tmpl b/backtest/html_report.tmpl
similarity index 100%
rename from strategy/backtest_report.tmpl
rename to backtest/html_report.tmpl
diff --git a/backtest/report.go b/backtest/report.go
new file mode 100644
index 0000000..26d25c5
--- /dev/null
+++ b/backtest/report.go
@@ -0,0 +1,28 @@
+// Copyright (c) 2021-2024 Onur Cinar.
+// The source code is provided under GNU AGPLv3 License.
+// https://github.com/cinar/indicator
+
+package backtest
+
+import (
+ "github.com/cinar/indicator/v2/asset"
+ "github.com/cinar/indicator/v2/strategy"
+)
+
+// Report is the backtest report interface.
+type Report interface {
+ // Begin is called when the backtest begins.
+ Begin(assetNames []string, strategies []strategy.Strategy) error
+
+ // AssetBegin is called when backtesting for the given asset begins.
+ AssetBegin(name string, strategies []strategy.Strategy) error
+
+ // Write writes the given strategy actions and outomes to the report.
+ Write(assetName string, currentStrategy strategy.Strategy, snapshots <-chan *asset.Snapshot, actions <-chan strategy.Action, outcomes <-chan float64) error
+
+ // AssetEnd is called when backtesting for the given asset ends.
+ AssetEnd(name string) error
+
+ // End is called when the backtest ends.
+ End() error
+}
diff --git a/backtest/testdata/repository/brk-b.csv b/backtest/testdata/repository/brk-b.csv
new file mode 120000
index 0000000..53db519
--- /dev/null
+++ b/backtest/testdata/repository/brk-b.csv
@@ -0,0 +1 @@
+../../../asset/testdata/repository/brk-b.csv
\ No newline at end of file
diff --git a/cmd/indicator-backtest/main.go b/cmd/indicator-backtest/main.go
index e8ae154..84b18bc 100644
--- a/cmd/indicator-backtest/main.go
+++ b/cmd/indicator-backtest/main.go
@@ -12,6 +12,7 @@ import (
"os"
"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"
@@ -40,9 +41,9 @@ func main() {
flag.StringVar(&sourceName, "source-name", "filesystem", "source repository type")
flag.StringVar(&sourceConfig, "source-config", "", "source repository config")
flag.StringVar(&outputDir, "output", ".", "output directory")
- flag.IntVar(&workers, "workers", strategy.DefaultBacktestWorkers, "number of concurrent workers")
- flag.IntVar(&lastDays, "last", strategy.DefaultLastDays, "number of days to do backtest")
- flag.BoolVar(&writeStrategyRerpots, "write-strategy-reports", strategy.DefaultWriteStrategyReports, "write individual strategy reports")
+ 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")
@@ -53,11 +54,13 @@ func main() {
log.Fatalf("unable to initialize source: %v", err)
}
- backtest := strategy.NewBacktest(source, outputDir)
+ htmlReport := backtest.NewHTMLReport(outputDir)
+ htmlReport.WriteStrategyReports = writeStrategyRerpots
+ htmlReport.DateFormat = dateFormat
+
+ backtest := backtest.NewBacktest(source, htmlReport)
backtest.Workers = workers
backtest.LastDays = lastDays
- backtest.WriteStrategyReports = writeStrategyRerpots
- backtest.DateFormat = dateFormat
backtest.Names = append(backtest.Names, flag.Args()...)
backtest.Strategies = append(backtest.Strategies, compound.AllStrategies()...)
backtest.Strategies = append(backtest.Strategies, momentum.AllStrategies()...)
diff --git a/helper/README.md b/helper/README.md
index 8720aa4..64e4eb2 100644
--- a/helper/README.md
+++ b/helper/README.md
@@ -36,6 +36,7 @@ The information provided on this project is strictly for informational purposes
- [func ChangePercent\[T Number\]\(c \<\-chan T, before int\) \<\-chan T](<#ChangePercent>)
- [func ChangeRatio\[T Number\]\(c \<\-chan T, before int\) \<\-chan T](<#ChangeRatio>)
- [func CheckEquals\[T comparable\]\(inputs ...\<\-chan T\) error](<#CheckEquals>)
+- [func CloseAndLogError\(closer io.Closer, message string\)](<#CloseAndLogError>)
- [func Count\[T Number, O any\]\(from T, other \<\-chan O\) \<\-chan T](<#Count>)
- [func DecrementBy\[T Number\]\(c \<\-chan T, d T\) \<\-chan T](<#DecrementBy>)
- [func Divide\[T Number\]\(ac, bc \<\-chan T\) \<\-chan T](<#Divide>)
@@ -309,6 +310,15 @@ func CheckEquals[T comparable](inputs ...<-chan T) error
CheckEquals determines whether the two channels are equal.
+
+## func [CloseAndLogError]()
+
+```go
+func CloseAndLogError(closer io.Closer, message string)
+```
+
+CloseAndLogError attempts to close the closer and logs any error.
+
## func [Count]()
diff --git a/helper/closer.go b/helper/closer.go
new file mode 100644
index 0000000..9e73ae8
--- /dev/null
+++ b/helper/closer.go
@@ -0,0 +1,18 @@
+// Copyright (c) 2021-2024 Onur Cinar.
+// The source code is provided under GNU AGPLv3 License.
+// https://github.com/cinar/indicator
+
+package helper
+
+import (
+ "io"
+ "log"
+)
+
+// CloseAndLogError attempts to close the closer and logs any error.
+func CloseAndLogError(closer io.Closer, message string) {
+ err := closer.Close()
+ if err != nil {
+ log.Printf("%s: %v", message, err)
+ }
+}
diff --git a/strategy/README.md b/strategy/README.md
index c4038bd..df7693a 100644
--- a/strategy/README.md
+++ b/strategy/README.md
@@ -24,7 +24,6 @@ The information provided on this project is strictly for informational purposes
## Index
-- [Constants](<#constants>)
- [func ActionSources\(strategies \[\]Strategy, snapshots \<\-chan \*asset.Snapshot\) \[\]\<\-chan Action](<#ActionSources>)
- [func ActionsToAnnotations\(ac \<\-chan Action\) \<\-chan string](<#ActionsToAnnotations>)
- [func ComputeWithOutcome\(s Strategy, c \<\-chan \*asset.Snapshot\) \(\<\-chan Action, \<\-chan float64\)](<#ComputeWithOutcome>)
@@ -40,9 +39,6 @@ The information provided on this project is strictly for informational purposes
- [func \(a \*AndStrategy\) Compute\(snapshots \<\-chan \*asset.Snapshot\) \<\-chan Action](<#AndStrategy.Compute>)
- [func \(a \*AndStrategy\) Name\(\) string](<#AndStrategy.Name>)
- [func \(a \*AndStrategy\) Report\(c \<\-chan \*asset.Snapshot\) \*helper.Report](<#AndStrategy.Report>)
-- [type Backtest](<#Backtest>)
- - [func NewBacktest\(repository asset.Repository, outputDir string\) \*Backtest](<#NewBacktest>)
- - [func \(b \*Backtest\) Run\(\) error](<#Backtest.Run>)
- [type BuyAndHoldStrategy](<#BuyAndHoldStrategy>)
- [func NewBuyAndHoldStrategy\(\) \*BuyAndHoldStrategy](<#NewBuyAndHoldStrategy>)
- [func \(\*BuyAndHoldStrategy\) Compute\(snapshots \<\-chan \*asset.Snapshot\) \<\-chan Action](<#BuyAndHoldStrategy.Compute>)
@@ -71,23 +67,6 @@ The information provided on this project is strictly for informational purposes
- [func AllStrategies\(\) \[\]Strategy](<#AllStrategies>)
-## Constants
-
-
-
-```go
-const (
- // DefaultBacktestWorkers is the default number of backtest workers.
- DefaultBacktestWorkers = 1
-
- // DefaultLastDays is the default number of days backtest should go back.
- DefaultLastDays = 30
-
- // DefaultWriteStrategyReports is the default state of writing individual strategy reports.
- DefaultWriteStrategyReports = true
-)
-```
-
## func [ActionSources]()
@@ -249,53 +228,6 @@ func (a *AndStrategy) Report(c <-chan *asset.Snapshot) *helper.Report
Report processes the provided asset snapshots and generates a report annotated with the recommended actions.
-
-## type [Backtest]()
-
-Backtest function rigorously evaluates the potential performance of the specified strategies applied to a defined set of assets. It generates comprehensive visual representations for each strategy\-asset pairing.
-
-```go
-type Backtest struct {
-
- // Names is the names of the assets to backtest.
- Names []string
-
- // Strategies is the list of strategies to apply.
- Strategies []Strategy
-
- // Workers is the number of concurrent workers.
- Workers int
-
- // LastDays is the number of days backtest should go back.
- LastDays int
-
- // WriteStrategyReports indicates whether the individual strategy reports should be generated.
- WriteStrategyReports bool
-
- // DateFormat is the date format that is used in the reports.
- DateFormat string
- // contains filtered or unexported fields
-}
-```
-
-
-### func [NewBacktest]()
-
-```go
-func NewBacktest(repository asset.Repository, outputDir string) *Backtest
-```
-
-NewBacktest function initializes a new backtest instance.
-
-
-### func \(\*Backtest\) [Run]()
-
-```go
-func (b *Backtest) Run() error
-```
-
-Run executes a comprehensive performance evaluation of the designated strategies, applied to a specified collection of assets. In the absence of explicitly defined assets, encompasses all assets within the repository. Likewise, in the absence of explicitly defined strategies, encompasses all the registered strategies.
-
## type [BuyAndHoldStrategy]()
diff --git a/strategy/backtest.go b/strategy/backtest.go
deleted file mode 100644
index 277661d..0000000
--- a/strategy/backtest.go
+++ /dev/null
@@ -1,307 +0,0 @@
-// Copyright (c) 2021-2024 Onur Cinar.
-// The source code is provided under GNU AGPLv3 License.
-// https://github.com/cinar/indicator
-
-package strategy
-
-import (
- // Go embed report template.
- _ "embed"
- "fmt"
- "log"
- "os"
- "path"
- "path/filepath"
- "slices"
- "sync"
- "text/template"
- "time"
-
- "github.com/cinar/indicator/v2/asset"
- "github.com/cinar/indicator/v2/helper"
-)
-
-const (
- // DefaultBacktestWorkers is the default number of backtest workers.
- DefaultBacktestWorkers = 1
-
- // DefaultLastDays is the default number of days backtest should go back.
- DefaultLastDays = 30
-
- // DefaultWriteStrategyReports is the default state of writing individual strategy reports.
- DefaultWriteStrategyReports = true
-)
-
-//go:embed "backtest_report.tmpl"
-var backtestReportTmpl string
-
-//go:embed "backtest_asset_report.tmpl"
-var backtestAssetReportTmpl string
-
-// Backtest function rigorously evaluates the potential performance of the
-// specified strategies applied to a defined set of assets. It generates
-// comprehensive visual representations for each strategy-asset pairing.
-type Backtest struct {
- // repository is the repository to retrieve the assets from.
- repository asset.Repository
-
- // outputDir is the output directory for the generated reports.
- outputDir string
-
- // Names is the names of the assets to backtest.
- Names []string
-
- // Strategies is the list of strategies to apply.
- Strategies []Strategy
-
- // Workers is the number of concurrent workers.
- Workers int
-
- // LastDays is the number of days backtest should go back.
- LastDays int
-
- // WriteStrategyReports indicates whether the individual strategy reports should be generated.
- WriteStrategyReports bool
-
- // DateFormat is the date format that is used in the reports.
- DateFormat string
-}
-
-// backtestResult encapsulates the outcome of running a strategy.
-type backtestResult struct {
- // AssetName is the name of the asset.
- AssetName string
-
- // StrategyName is the name of the strategy.
- StrategyName string
-
- // Action is the last recommended action by the strategy.
- Action Action
-
- // Since indicates how long the current action recommendation has been in effect.
- Since int
-
- // Outcome is the effectiveness of applying the recommended actions.
- Outcome float64
-
- // Transactions is the number of transactions made by the strategy.
- Transactions int
-}
-
-// NewBacktest function initializes a new backtest instance.
-func NewBacktest(repository asset.Repository, outputDir string) *Backtest {
- return &Backtest{
- repository: repository,
- outputDir: outputDir,
- Names: []string{},
- Strategies: []Strategy{},
- Workers: DefaultBacktestWorkers,
- LastDays: DefaultLastDays,
- WriteStrategyReports: DefaultWriteStrategyReports,
- DateFormat: helper.DefaultReportDateFormat,
- }
-}
-
-// Run executes a comprehensive performance evaluation of the designated strategies,
-// applied to a specified collection of assets. In the absence of explicitly defined
-// assets, encompasses all assets within the repository. Likewise, in the absence of
-// explicitly defined strategies, encompasses all the registered strategies.
-func (b *Backtest) Run() error {
- // When asset names are absent, considers all assets within the
- // provided repository for evaluation.
- if len(b.Names) == 0 {
- assets, err := b.repository.Assets()
- if err != nil {
- return err
- }
-
- b.Names = assets
- }
-
- // When strategies are absent, considers all strategies.
- if len(b.Strategies) == 0 {
- b.Strategies = b.allStrategies()
- }
-
- // Make sure that output directory exists.
- err := os.MkdirAll(b.outputDir, 0o700)
- if err != nil {
- return err
- }
-
- // Run the backtest workers and get the resutls.
- results := b.runWorkers()
-
- // Write the backtest report.
- return b.writeReport(results)
-}
-
-// allStrategies returns a slice containing references to all available strategies.
-func (*Backtest) allStrategies() []Strategy {
- return []Strategy{
- NewBuyAndHoldStrategy(),
- }
-}
-
-// runWorkers initiates and manages the execution of multiple backtest workers.
-func (b *Backtest) runWorkers() []*backtestResult {
- names := helper.SliceToChan(b.Names)
- results := make(chan *backtestResult)
- wg := &sync.WaitGroup{}
-
- for i := 0; i < b.Workers; i++ {
- wg.Add(1)
- go b.worker(names, results, wg)
- }
-
- // Wait for all workers to finish.
- go func() {
- wg.Wait()
- close(results)
- }()
-
- resultsSlice := helper.ChanToSlice(results)
-
- // Sort the backtest results by the outcomes.
- slices.SortFunc(resultsSlice, func(a, b *backtestResult) int {
- return int(b.Outcome - a.Outcome)
- })
-
- return resultsSlice
-}
-
-// strategyReportFileName
-func (*Backtest) strategyReportFileName(assetName, strategyName string) string {
- return fmt.Sprintf("%s - %s.html", assetName, strategyName)
-}
-
-// worker is a backtesting worker that concurrently executes backtests for individual
-// assets. It receives asset names from the provided channel, and performs backtests
-// using the given strategies.
-func (b *Backtest) worker(names <-chan string, bestResults chan<- *backtestResult, wg *sync.WaitGroup) {
- defer wg.Done()
-
- since := time.Now().AddDate(0, 0, -b.LastDays)
-
- for name := range names {
- log.Printf("Backtesting %s...", name)
- snapshots, err := b.repository.GetSince(name, since)
- if err != nil {
- log.Printf("Unable to retrieve the snapshots for %s (%v)", name, err)
- continue
- }
-
- // We don't expect the snapshots to be a stream during backtesting.
- snapshotsSlice := helper.ChanToSlice(snapshots)
-
- results := make([]*backtestResult, 0, len(b.Strategies))
-
- for _, st := range b.Strategies {
- actions, outcomes := ComputeWithOutcome(st, helper.SliceToChan(snapshotsSlice))
- actionsSplice := helper.Duplicate(actions, 3)
-
- actions = helper.Last(DenormalizeActions(actionsSplice[0]), 1)
- sinces := helper.Last(helper.Since[Action, int](actionsSplice[1]), 1)
- outcomes = helper.Last(outcomes, 1)
- transactions := helper.Last(CountTransactions(actionsSplice[2]), 1)
-
- // Generate inidividual strategy report.
- if b.WriteStrategyReports {
- report := st.Report(helper.SliceToChan(snapshotsSlice))
- report.DateFormat = b.DateFormat
-
- err := report.WriteToFile(path.Join(b.outputDir, b.strategyReportFileName(name, st.Name())))
- if err != nil {
- log.Printf("Unable to write report for %s (%v)", name, err)
- continue
- }
- }
-
- results = append(results, &backtestResult{
- AssetName: name,
- StrategyName: st.Name(),
- Action: <-actions,
- Since: <-sinces,
- Outcome: <-outcomes * 100,
- Transactions: <-transactions,
- })
- }
-
- // Sort the backtest results by the outcomes.
- slices.SortFunc(results, func(a, b *backtestResult) int {
- return int(b.Outcome - a.Outcome)
- })
-
- // Report the best result for the current asset.
- log.Printf("Best outcome for %s is %.2f%% with %s.", name, results[0].Outcome, results[0].StrategyName)
- bestResults <- results[0]
-
- // Write the asset report.
- err = b.writeAssetReport(name, results)
- if err != nil {
- log.Printf("Unable to write report with %v.", err)
- }
- }
-}
-
-// writeAssetReport generates a detailed report for the asset, summarizing the backtest results.
-func (b *Backtest) writeAssetReport(name string, results []*backtestResult) error {
- type Model struct {
- AssetName string
- Results []*backtestResult
- GeneratedOn string
- }
-
- model := Model{
- AssetName: name,
- Results: results,
- GeneratedOn: time.Now().String(),
- }
-
- file, err := os.Create(filepath.Join(b.outputDir, fmt.Sprintf("%s.html", name)))
- if err != nil {
- return err
- }
-
- tmpl, err := template.New("report").Parse(backtestAssetReportTmpl)
- if err != nil {
- return err
- }
-
- err = tmpl.Execute(file, model)
- if err != nil {
- return err
- }
-
- return file.Close()
-}
-
-// writeReport generates a detailed report for the backtest results.
-func (b *Backtest) writeReport(results []*backtestResult) error {
- type Model struct {
- Results []*backtestResult
- GeneratedOn string
- }
-
- model := Model{
- Results: results,
- GeneratedOn: time.Now().String(),
- }
-
- file, err := os.Create(filepath.Join(b.outputDir, "index.html"))
- if err != nil {
- return err
- }
-
- tmpl, err := template.New("report").Parse(backtestReportTmpl)
- if err != nil {
- return err
- }
-
- err = tmpl.Execute(file, model)
- if err != nil {
- return err
- }
-
- return file.Close()
-}