From 0ea963faffad426e1c7bbbc59c65df09454da124 Mon Sep 17 00:00:00 2001 From: Onur Cinar Date: Thu, 17 Oct 2024 18:07:01 -0700 Subject: [PATCH] Force Index Strategy added. (#238) # Describe Request Force Index Strategy added. # Change Type New strategy. ## Summary by CodeRabbit - **New Features** - Updated documentation to reflect enhancements in version 2 of the Indicator Go module. - Introduced the `ForceIndexStrategy`, expanding the library's volume strategies. - Added dedicated test data in CSV format for easier validation of indicators and strategies. - Streamlined data handling with support for Go channels and helper functions. - **Bug Fixes** - Enhanced overall code quality and testability with a minimum of 90% code coverage. - **Documentation** - Comprehensive updates to installation, usage instructions, and backtesting functionality. --- README.md | 2 +- strategy/volume/README.md | 63 +++++ strategy/volume/force_index_strategy.go | 111 ++++++++ strategy/volume/force_index_strategy_test.go | 55 ++++ .../volume/testdata/force_index_strategy.csv | 252 ++++++++++++++++++ strategy/volume/volume.go | 1 + 6 files changed, 483 insertions(+), 1 deletion(-) create mode 100644 strategy/volume/force_index_strategy.go create mode 100644 strategy/volume/force_index_strategy_test.go create mode 100644 strategy/volume/testdata/force_index_strategy.csv diff --git a/README.md b/README.md index 7a6265c..91c6484 100644 --- a/README.md +++ b/README.md @@ -139,7 +139,7 @@ The following list of strategies are currently supported by this package: - [Chaikin Money Flow Strategy](strategy/volume/README.md#type-chaikinmoneyflowstrategy) - Ease of Movement Strategy -- Force Index Strategy +- [Force Index Strategy](strategy/volume/README.md#type-forceindexstrategy) - [Money Flow Index Strategy](strategy/volume/README.md#type-moneyflowindexstrategy) - [Negative Volume Index Strategy](strategy/volume/README.md#type-negativevolumeindexstrategy) - Volume Weighted Average Price Strategy diff --git a/strategy/volume/README.md b/strategy/volume/README.md index 4dfeb14..302b9b5 100644 --- a/strategy/volume/README.md +++ b/strategy/volume/README.md @@ -32,6 +32,12 @@ The information provided on this project is strictly for informational purposes - [func \(c \*ChaikinMoneyFlowStrategy\) Compute\(snapshots \<\-chan \*asset.Snapshot\) \<\-chan strategy.Action](<#ChaikinMoneyFlowStrategy.Compute>) - [func \(c \*ChaikinMoneyFlowStrategy\) Name\(\) string](<#ChaikinMoneyFlowStrategy.Name>) - [func \(c \*ChaikinMoneyFlowStrategy\) Report\(snapshots \<\-chan \*asset.Snapshot\) \*helper.Report](<#ChaikinMoneyFlowStrategy.Report>) +- [type ForceIndexStrategy](<#ForceIndexStrategy>) + - [func NewForceIndexStrategy\(\) \*ForceIndexStrategy](<#NewForceIndexStrategy>) + - [func NewForceIndexStrategyWith\(period int\) \*ForceIndexStrategy](<#NewForceIndexStrategyWith>) + - [func \(f \*ForceIndexStrategy\) Compute\(snapshots \<\-chan \*asset.Snapshot\) \<\-chan strategy.Action](<#ForceIndexStrategy.Compute>) + - [func \(f \*ForceIndexStrategy\) Name\(\) string](<#ForceIndexStrategy.Name>) + - [func \(f \*ForceIndexStrategy\) Report\(c \<\-chan \*asset.Snapshot\) \*helper.Report](<#ForceIndexStrategy.Report>) - [type MoneyFlowIndexStrategy](<#MoneyFlowIndexStrategy>) - [func NewMoneyFlowIndexStrategy\(\) \*MoneyFlowIndexStrategy](<#NewMoneyFlowIndexStrategy>) - [func NewMoneyFlowIndexStrategyWith\(sellAt, buyAt float64\) \*MoneyFlowIndexStrategy](<#NewMoneyFlowIndexStrategyWith>) @@ -135,6 +141,63 @@ func (c *ChaikinMoneyFlowStrategy) Report(snapshots <-chan *asset.Snapshot) *hel Report function processes the provided asset snapshots and generates a report annotated with the recommended actions. + +## type [ForceIndexStrategy]() + +ForceIndexStrategy represents the configuration parameters for calculating the Force Index strategy. It recommends a Buy action when it crosses above zero, and a Sell action when it crosses below zero. + +```go +type ForceIndexStrategy struct { + // ForceIndex is the Force Index instance. + ForceIndex *volume.Fi[float64] +} +``` + + +### func [NewForceIndexStrategy]() + +```go +func NewForceIndexStrategy() *ForceIndexStrategy +``` + +NewForceIndexStrategy function initializes a new Force Index strategy instance with the default parameters. + + +### func [NewForceIndexStrategyWith]() + +```go +func NewForceIndexStrategyWith(period int) *ForceIndexStrategy +``` + +NewForceIndexStrategyWith function initializes a new Force Index strategy instance with the given parameters. + + +### func \(\*ForceIndexStrategy\) [Compute]() + +```go +func (f *ForceIndexStrategy) Compute(snapshots <-chan *asset.Snapshot) <-chan strategy.Action +``` + +Compute processes the provided asset snapshots and generates a stream of actionable recommendations. + + +### func \(\*ForceIndexStrategy\) [Name]() + +```go +func (f *ForceIndexStrategy) Name() string +``` + +Name returns the name of the strategy. + + +### func \(\*ForceIndexStrategy\) [Report]() + +```go +func (f *ForceIndexStrategy) Report(c <-chan *asset.Snapshot) *helper.Report +``` + +Report processes the provided asset snapshots and generates a report annotated with the recommended actions. + ## type [MoneyFlowIndexStrategy]() diff --git a/strategy/volume/force_index_strategy.go b/strategy/volume/force_index_strategy.go new file mode 100644 index 0000000..01b5a1a --- /dev/null +++ b/strategy/volume/force_index_strategy.go @@ -0,0 +1,111 @@ +// Copyright (c) 2021-2024 Onur Cinar. +// The source code is provided under GNU AGPLv3 License. +// https://github.com/cinar/indicator + +package volume + +import ( + "fmt" + + "github.com/cinar/indicator/v2/asset" + "github.com/cinar/indicator/v2/helper" + "github.com/cinar/indicator/v2/strategy" + "github.com/cinar/indicator/v2/volume" +) + +// ForceIndexStrategy represents the configuration parameters for calculating the Force Index strategy. +// It recommends a Buy action when it crosses above zero, and a Sell action when it crosses below zero. +type ForceIndexStrategy struct { + // ForceIndex is the Force Index instance. + ForceIndex *volume.Fi[float64] +} + +// NewForceIndexStrategy function initializes a new Force Index strategy instance with the default parameters. +func NewForceIndexStrategy() *ForceIndexStrategy { + return NewForceIndexStrategyWith( + volume.DefaultFiPeriod, + ) +} + +// NewForceIndexStrategyWith function initializes a new Force Index strategy instance with the given parameters. +func NewForceIndexStrategyWith(period int) *ForceIndexStrategy { + return &ForceIndexStrategy{ + ForceIndex: volume.NewFiWithPeriod[float64](period), + } +} + +// Name returns the name of the strategy. +func (f *ForceIndexStrategy) Name() string { + return fmt.Sprintf("Force Index Strategy (%d)", f.ForceIndex.IdlePeriod()+1) +} + +// Compute processes the provided asset snapshots and generates a stream of actionable recommendations. +func (f *ForceIndexStrategy) Compute(snapshots <-chan *asset.Snapshot) <-chan strategy.Action { + snapshotsSplice := helper.Duplicate(snapshots, 2) + + closings := asset.SnapshotsAsClosings(snapshotsSplice[0]) + volumes := asset.SnapshotsAsVolumes(snapshotsSplice[1]) + + fis := f.ForceIndex.Compute(closings, volumes) + + actions := helper.Map(fis, func(fi float64) strategy.Action { + if fi > 0 { + return strategy.Buy + } + + if fi < 0 { + return strategy.Sell + } + + return strategy.Hold + }) + + // Force Index starts only after a full period. + actions = helper.Shift(actions, f.ForceIndex.IdlePeriod(), strategy.Hold) + + return actions +} + +// Report processes the provided asset snapshots and generates a report annotated with the recommended actions. +func (f *ForceIndexStrategy) Report(c <-chan *asset.Snapshot) *helper.Report { + // + // snapshots[0] -> dates + // snapshots[1] -> closings[0] -> closings + // closings[1] -> force index + // snapshots[2] -> volumes + // snapshots[3] -> actions -> annotations + // -> outcomes + // + snapshots := helper.Duplicate(c, 4) + + dates := helper.Skip(asset.SnapshotsAsDates(snapshots[0]), f.ForceIndex.IdlePeriod()) + + closingsSplice := helper.Duplicate( + asset.SnapshotsAsClosings(snapshots[1]), + 2, + ) + volumes := asset.SnapshotsAsVolumes(snapshots[2]) + + fis := f.ForceIndex.Compute(closingsSplice[0], volumes) + + closingsSplice[1] = helper.Skip(closingsSplice[1], f.ForceIndex.IdlePeriod()) + + actions, outcomes := strategy.ComputeWithOutcome(f, snapshots[3]) + actions = helper.Skip(actions, f.ForceIndex.IdlePeriod()) + outcomes = helper.Skip(outcomes, f.ForceIndex.IdlePeriod()) + + annotations := strategy.ActionsToAnnotations(actions) + outcomes = helper.MultiplyBy(outcomes, 100) + + report := helper.NewReport(f.Name(), dates) + report.AddChart() + report.AddChart() + + report.AddColumn(helper.NewNumericReportColumn("Close", closingsSplice[1])) + report.AddColumn(helper.NewNumericReportColumn("Force Index", fis), 1) + report.AddColumn(helper.NewAnnotationReportColumn(annotations), 0, 1) + + report.AddColumn(helper.NewNumericReportColumn("Outcome", outcomes), 2) + + return report +} diff --git a/strategy/volume/force_index_strategy_test.go b/strategy/volume/force_index_strategy_test.go new file mode 100644 index 0000000..3aade49 --- /dev/null +++ b/strategy/volume/force_index_strategy_test.go @@ -0,0 +1,55 @@ +// Copyright (c) 2021-2024 Onur Cinar. +// The source code is provided under GNU AGPLv3 License. +// https://github.com/cinar/indicator + +package volume_test + +import ( + "os" + "testing" + + "github.com/cinar/indicator/v2/asset" + "github.com/cinar/indicator/v2/helper" + "github.com/cinar/indicator/v2/strategy" + "github.com/cinar/indicator/v2/strategy/volume" +) + +func TestForceIndexStrategy(t *testing.T) { + snapshots, err := helper.ReadFromCsvFile[asset.Snapshot]("testdata/brk-b.csv", true) + if err != nil { + t.Fatal(err) + } + + results, err := helper.ReadFromCsvFile[strategy.Result]("testdata/force_index_strategy.csv", true) + if err != nil { + t.Fatal(err) + } + + expected := helper.Map(results, func(r *strategy.Result) strategy.Action { return r.Action }) + + fis := volume.NewForceIndexStrategy() + actual := fis.Compute(snapshots) + + err = helper.CheckEquals(actual, expected) + if err != nil { + t.Fatal(err) + } +} + +func TestForceIndexStrategyReport(t *testing.T) { + snapshots, err := helper.ReadFromCsvFile[asset.Snapshot]("testdata/brk-b.csv", true) + if err != nil { + t.Fatal(err) + } + + fis := volume.NewForceIndexStrategy() + report := fis.Report(snapshots) + + fileName := "force_index_strategy.html" + defer os.Remove(fileName) + + err = report.WriteToFile(fileName) + if err != nil { + t.Fatal(err) + } +} diff --git a/strategy/volume/testdata/force_index_strategy.csv b/strategy/volume/testdata/force_index_strategy.csv new file mode 100644 index 0000000..a88dc23 --- /dev/null +++ b/strategy/volume/testdata/force_index_strategy.csv @@ -0,0 +1,252 @@ +Action +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +-1 +-1 +-1 +-1 +-1 +-1 +-1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +-1 +-1 +-1 +-1 +-1 +-1 +-1 +-1 +-1 +1 +1 +1 +-1 +-1 +1 +-1 +-1 +-1 +1 +-1 +-1 +-1 +-1 +-1 +-1 +-1 +-1 +-1 +-1 +-1 +1 +1 +1 +1 +1 +-1 +-1 +-1 +-1 +-1 +-1 +-1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +-1 +-1 +1 +1 +1 +1 +1 +-1 +1 +1 +1 +1 +1 +1 +-1 +-1 +-1 +-1 +-1 +-1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +-1 +1 +-1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +-1 +-1 +-1 +-1 +-1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +-1 +1 +-1 +-1 +-1 +-1 +-1 +-1 +-1 +-1 +-1 +-1 +-1 +-1 +-1 +-1 +-1 +-1 +-1 +-1 +-1 +-1 +-1 +-1 +-1 +-1 +-1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 diff --git a/strategy/volume/volume.go b/strategy/volume/volume.go index 55698f6..07d52aa 100644 --- a/strategy/volume/volume.go +++ b/strategy/volume/volume.go @@ -26,6 +26,7 @@ import ( func AllStrategies() []strategy.Strategy { return []strategy.Strategy{ NewChaikinMoneyFlowStrategy(), + NewForceIndexStrategy(), NewMoneyFlowIndexStrategy(), NewNegativeVolumeIndexStrategy(), }