From 195dc1a51fa879f9944f943c4b6669c47b0c7610 Mon Sep 17 00:00:00 2001 From: Onur Cinar Date: Wed, 16 Oct 2024 17:52:36 -0700 Subject: [PATCH 1/2] Negative Volume Index Strategy is added. --- README.md | 2 +- strategy/volume/README.md | 75 +++++ .../volume/negative_volume_index_strategy.go | 144 ++++++++++ .../negative_volume_index_strategy_test.go | 55 ++++ .../negative_volume_index_strategy.csv | 256 ++++++++++++++++++ strategy/volume/volume.go | 1 + 6 files changed, 532 insertions(+), 1 deletion(-) create mode 100644 strategy/volume/negative_volume_index_strategy.go create mode 100644 strategy/volume/negative_volume_index_strategy_test.go create mode 100644 strategy/volume/testdata/negative_volume_index_strategy.csv diff --git a/README.md b/README.md index a61d158..7a6265c 100644 --- a/README.md +++ b/README.md @@ -141,7 +141,7 @@ The following list of strategies are currently supported by this package: - Ease of Movement Strategy - Force Index Strategy - [Money Flow Index Strategy](strategy/volume/README.md#type-moneyflowindexstrategy) -- Negative Volume Index Strategy +- [Negative Volume Index Strategy](strategy/volume/README.md#type-negativevolumeindexstrategy) - Volume Weighted Average Price Strategy ### 🧪 Compound Strategies diff --git a/strategy/volume/README.md b/strategy/volume/README.md index da3f28a..4dfeb14 100644 --- a/strategy/volume/README.md +++ b/strategy/volume/README.md @@ -38,6 +38,12 @@ The information provided on this project is strictly for informational purposes - [func \(m \*MoneyFlowIndexStrategy\) Compute\(snapshots \<\-chan \*asset.Snapshot\) \<\-chan strategy.Action](<#MoneyFlowIndexStrategy.Compute>) - [func \(m \*MoneyFlowIndexStrategy\) Name\(\) string](<#MoneyFlowIndexStrategy.Name>) - [func \(m \*MoneyFlowIndexStrategy\) Report\(c \<\-chan \*asset.Snapshot\) \*helper.Report](<#MoneyFlowIndexStrategy.Report>) +- [type NegativeVolumeIndexStrategy](<#NegativeVolumeIndexStrategy>) + - [func NewNegativeVolumeIndexStrategy\(\) \*NegativeVolumeIndexStrategy](<#NewNegativeVolumeIndexStrategy>) + - [func NewNegativeVolumeIndexStrategyWith\(emaPeriod int\) \*NegativeVolumeIndexStrategy](<#NewNegativeVolumeIndexStrategyWith>) + - [func \(n \*NegativeVolumeIndexStrategy\) Compute\(snapshots \<\-chan \*asset.Snapshot\) \<\-chan strategy.Action](<#NegativeVolumeIndexStrategy.Compute>) + - [func \(n \*NegativeVolumeIndexStrategy\) Name\(\) string](<#NegativeVolumeIndexStrategy.Name>) + - [func \(n \*NegativeVolumeIndexStrategy\) Report\(c \<\-chan \*asset.Snapshot\) \*helper.Report](<#NegativeVolumeIndexStrategy.Report>) ## Constants @@ -54,6 +60,15 @@ const ( ) ``` + + +```go +const ( + // DefaultNegativeVolumeIndexStrategyEmaPeriod is the default EMA period of 255. + DefaultNegativeVolumeIndexStrategyEmaPeriod = 255 +) +``` + ## func [AllStrategies]() @@ -183,4 +198,64 @@ func (m *MoneyFlowIndexStrategy) Report(c <-chan *asset.Snapshot) *helper.Report Report processes the provided asset snapshots and generates a report annotated with the recommended actions. + +## type [NegativeVolumeIndexStrategy]() + +NegativeVolumeIndexStrategy represents the configuration parameters for calculating the Negative Volume Index strategy. Recommends a Buy action when it crosses below its EMA, recommends a Sell action when it crosses above its EMA, and recommends a Hold action otherwise. + +```go +type NegativeVolumeIndexStrategy struct { + // NegativeVolumeIndex is the Negative Volume Index indicator instance. + NegativeVolumeIndex *volume.Nvi[float64] + + // NegativeVolumeIndexEma is the Negative Volume Index EMA instance. + NegativeVolumeIndexEma *trend.Ema[float64] +} +``` + + +### func [NewNegativeVolumeIndexStrategy]() + +```go +func NewNegativeVolumeIndexStrategy() *NegativeVolumeIndexStrategy +``` + +NewNegativeVolumeIndexStrategy function initializes a new Negative Volume Index strategy instance with the default parameters. + + +### func [NewNegativeVolumeIndexStrategyWith]() + +```go +func NewNegativeVolumeIndexStrategyWith(emaPeriod int) *NegativeVolumeIndexStrategy +``` + +NewNegativeVolumeIndexStrategyWith function initializes a new Negative Volume Index strategy instance with the given parameters. + + +### func \(\*NegativeVolumeIndexStrategy\) [Compute]() + +```go +func (n *NegativeVolumeIndexStrategy) Compute(snapshots <-chan *asset.Snapshot) <-chan strategy.Action +``` + +Compute processes the provided asset snapshots and generates a stream of actionable recommendations. + + +### func \(\*NegativeVolumeIndexStrategy\) [Name]() + +```go +func (n *NegativeVolumeIndexStrategy) Name() string +``` + +Name returns the name of the strategy. + + +### func \(\*NegativeVolumeIndexStrategy\) [Report]() + +```go +func (n *NegativeVolumeIndexStrategy) Report(c <-chan *asset.Snapshot) *helper.Report +``` + +Report processes the provided asset snapshots and generates a report annotated with the recommended actions. + Generated by [gomarkdoc]() diff --git a/strategy/volume/negative_volume_index_strategy.go b/strategy/volume/negative_volume_index_strategy.go new file mode 100644 index 0000000..224004f --- /dev/null +++ b/strategy/volume/negative_volume_index_strategy.go @@ -0,0 +1,144 @@ +// 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/trend" + "github.com/cinar/indicator/v2/volume" +) + +const ( + // DefaultNegativeVolumeIndexStrategyEmaPeriod is the default EMA period of 255. + DefaultNegativeVolumeIndexStrategyEmaPeriod = 255 +) + +// NegativeVolumeIndexStrategy represents the configuration parameters for calculating the Negative Volume Index +// strategy. Recommends a Buy action when it crosses below its EMA, recommends a Sell action when it crosses +// above its EMA, and recommends a Hold action otherwise. +type NegativeVolumeIndexStrategy struct { + // NegativeVolumeIndex is the Negative Volume Index indicator instance. + NegativeVolumeIndex *volume.Nvi[float64] + + // NegativeVolumeIndexEma is the Negative Volume Index EMA instance. + NegativeVolumeIndexEma *trend.Ema[float64] +} + +// NewNegativeVolumeIndexStrategy function initializes a new Negative Volume Index strategy instance with the +// default parameters. +func NewNegativeVolumeIndexStrategy() *NegativeVolumeIndexStrategy { + return NewNegativeVolumeIndexStrategyWith( + DefaultNegativeVolumeIndexStrategyEmaPeriod, + ) +} + +// NewNegativeVolumeIndexStrategyWith function initializes a new Negative Volume Index strategy instance with the +// given parameters. +func NewNegativeVolumeIndexStrategyWith(emaPeriod int) *NegativeVolumeIndexStrategy { + return &NegativeVolumeIndexStrategy{ + NegativeVolumeIndex: volume.NewNvi[float64](), + NegativeVolumeIndexEma: trend.NewEmaWithPeriod[float64](emaPeriod), + } +} + +// Name returns the name of the strategy. +func (n *NegativeVolumeIndexStrategy) Name() string { + return fmt.Sprintf("Negative Volume Index Strategy (%d)", n.NegativeVolumeIndexEma.Period) +} + +// Compute processes the provided asset snapshots and generates a stream of actionable recommendations. +func (n *NegativeVolumeIndexStrategy) Compute(snapshots <-chan *asset.Snapshot) <-chan strategy.Action { + snapshotsSplice := helper.Duplicate(snapshots, 2) + + closings := asset.SnapshotsAsClosings(snapshotsSplice[0]) + volumes := asset.SnapshotsAsVolumes(snapshotsSplice[1]) + + nvisSplice := helper.Duplicate( + n.NegativeVolumeIndex.Compute(closings, volumes), + 2, + ) + + nvisSplice[0] = helper.Skip(nvisSplice[0], n.NegativeVolumeIndexEma.IdlePeriod()) + nviEmas := n.NegativeVolumeIndexEma.Compute(nvisSplice[1]) + + actions := helper.Operate(nvisSplice[0], nviEmas, func(nvi, nviEma float64) strategy.Action { + if nvi < nviEma { + return strategy.Buy + } + + if nvi > nviEma { + return strategy.Sell + } + + return strategy.Hold + }) + + // Negative Volume Index starts only after a full period. + actions = helper.Shift( + actions, + n.NegativeVolumeIndex.IdlePeriod()+n.NegativeVolumeIndexEma.IdlePeriod(), + strategy.Hold, + ) + + return actions +} + +// Report processes the provided asset snapshots and generates a report annotated with the recommended actions. +func (n *NegativeVolumeIndexStrategy) Report(c <-chan *asset.Snapshot) *helper.Report { + // + // snapshots[0] -> dates + // snapshots[1] -> closings[0] -> closings + // closings[1] -> negative volume index[0] -> negative volume index + // negative volume index[1] -> negative volume index ema + // snapshots[2] -> volumes + // snapshots[3] -> actions -> annotations + // -> outcomes + // + snapshots := helper.Duplicate(c, 4) + + period := n.NegativeVolumeIndex.IdlePeriod() + n.NegativeVolumeIndexEma.IdlePeriod() + + dates := helper.Skip(asset.SnapshotsAsDates(snapshots[0]), period) + + closingsSplice := helper.Duplicate( + asset.SnapshotsAsClosings(snapshots[1]), + 2, + ) + volumes := asset.SnapshotsAsVolumes(snapshots[2]) + + nvisSplice := helper.Duplicate( + n.NegativeVolumeIndex.Compute(closingsSplice[0], volumes), + 2, + ) + + nvisSplice[0] = helper.Skip(nvisSplice[0], n.NegativeVolumeIndexEma.IdlePeriod()) + nviEmas := n.NegativeVolumeIndexEma.Compute(nvisSplice[1]) + + closingsSplice[1] = helper.Skip(closingsSplice[1], period) + + actions, outcomes := strategy.ComputeWithOutcome(n, snapshots[3]) + actions = helper.Skip(actions, period) + outcomes = helper.Skip(outcomes, period) + + annotations := strategy.ActionsToAnnotations(actions) + outcomes = helper.MultiplyBy(outcomes, 100) + + report := helper.NewReport(n.Name(), dates) + report.AddChart() + report.AddChart() + + report.AddColumn(helper.NewNumericReportColumn("Close", closingsSplice[1])) + report.AddColumn(helper.NewNumericReportColumn("NVI", nvisSplice[0]), 1) + report.AddColumn(helper.NewNumericReportColumn("NVI EMA", nviEmas), 1) + report.AddColumn(helper.NewAnnotationReportColumn(annotations), 0, 1) + + report.AddColumn(helper.NewNumericReportColumn("Outcome", outcomes), 2) + + return report +} diff --git a/strategy/volume/negative_volume_index_strategy_test.go b/strategy/volume/negative_volume_index_strategy_test.go new file mode 100644 index 0000000..5410c47 --- /dev/null +++ b/strategy/volume/negative_volume_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 TestNegativeVolumeIndexStrategy(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/negative_volume_index_strategy.csv", true) + if err != nil { + t.Fatal(err) + } + + expected := helper.Map(results, func(r *strategy.Result) strategy.Action { return r.Action }) + + nvis := volume.NewNegativeVolumeIndexStrategy() + actual := nvis.Compute(snapshots) + + err = helper.CheckEquals(actual, expected) + if err != nil { + t.Fatal(err) + } +} + +func TestNegativeVolumeIndexStrategyReport(t *testing.T) { + snapshots, err := helper.ReadFromCsvFile[asset.Snapshot]("testdata/brk-b.csv", true) + if err != nil { + t.Fatal(err) + } + + nvis := volume.NewNegativeVolumeIndexStrategy() + report := nvis.Report(snapshots) + + fileName := "negative_volume_index_strategy.html" + defer os.Remove(fileName) + + err = report.WriteToFile(fileName) + if err != nil { + t.Fatal(err) + } +} diff --git a/strategy/volume/testdata/negative_volume_index_strategy.csv b/strategy/volume/testdata/negative_volume_index_strategy.csv new file mode 100644 index 0000000..2927c29 --- /dev/null +++ b/strategy/volume/testdata/negative_volume_index_strategy.csv @@ -0,0 +1,256 @@ +Action +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 diff --git a/strategy/volume/volume.go b/strategy/volume/volume.go index 48fcbd3..55698f6 100644 --- a/strategy/volume/volume.go +++ b/strategy/volume/volume.go @@ -27,5 +27,6 @@ func AllStrategies() []strategy.Strategy { return []strategy.Strategy{ NewChaikinMoneyFlowStrategy(), NewMoneyFlowIndexStrategy(), + NewNegativeVolumeIndexStrategy(), } } From 5340f092119bc19dfda946bd3acbeb4f842afe32 Mon Sep 17 00:00:00 2001 From: Onur Cinar Date: Wed, 16 Oct 2024 18:03:43 -0700 Subject: [PATCH 2/2] Increased testing. --- .../negative_volume_index_strategy_test.go | 2 +- .../negative_volume_index_strategy.csv | 482 +++++++++--------- 2 files changed, 240 insertions(+), 244 deletions(-) diff --git a/strategy/volume/negative_volume_index_strategy_test.go b/strategy/volume/negative_volume_index_strategy_test.go index 5410c47..1468008 100644 --- a/strategy/volume/negative_volume_index_strategy_test.go +++ b/strategy/volume/negative_volume_index_strategy_test.go @@ -27,7 +27,7 @@ func TestNegativeVolumeIndexStrategy(t *testing.T) { expected := helper.Map(results, func(r *strategy.Result) strategy.Action { return r.Action }) - nvis := volume.NewNegativeVolumeIndexStrategy() + nvis := volume.NewNegativeVolumeIndexStrategyWith(12) actual := nvis.Compute(snapshots) err = helper.CheckEquals(actual, expected) diff --git a/strategy/volume/testdata/negative_volume_index_strategy.csv b/strategy/volume/testdata/negative_volume_index_strategy.csv index 2927c29..3f55c03 100644 --- a/strategy/volume/testdata/negative_volume_index_strategy.csv +++ b/strategy/volume/testdata/negative_volume_index_strategy.csv @@ -11,246 +11,242 @@ Action 0 0 0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -0 -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 +-1