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..1468008
--- /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.NewNegativeVolumeIndexStrategyWith(12)
+ 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..3f55c03
--- /dev/null
+++ b/strategy/volume/testdata/negative_volume_index_strategy.csv
@@ -0,0 +1,252 @@
+Action
+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
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(),
}
}