Skip to content

Commit

Permalink
Negative Volume Index Strategy is added. (#237)
Browse files Browse the repository at this point in the history
# Describe Request

Negative Volume Index Stratey is added.

# Change Type

New strategy.



<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

## Release Notes

- **New Features**
- Introduced the `Negative Volume Index Strategy` for enhanced trading
decisions.
- Comprehensive updates to the documentation, including installation
instructions and a new section on backtesting functionality.
- Added support for fully configurable indicators and strategies,
enhancing user flexibility.

- **Bug Fixes**
- Improved clarity in licensing information and contributing guidelines.

- **Documentation**
- Expanded lists of indicators and strategies with detailed descriptions
and links.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
  • Loading branch information
cinar authored Oct 17, 2024
1 parent 3e0fce2 commit 467323d
Show file tree
Hide file tree
Showing 6 changed files with 528 additions and 1 deletion.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
75 changes: 75 additions & 0 deletions strategy/volume/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -54,6 +60,15 @@ const (
)
```

<a name="DefaultNegativeVolumeIndexStrategyEmaPeriod"></a>

```go
const (
// DefaultNegativeVolumeIndexStrategyEmaPeriod is the default EMA period of 255.
DefaultNegativeVolumeIndexStrategyEmaPeriod = 255
)
```

<a name="AllStrategies"></a>
## func [AllStrategies](<https://github.com/cinar/indicator/blob/master/strategy/volume/volume.go#L26>)

Expand Down Expand Up @@ -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.

<a name="NegativeVolumeIndexStrategy"></a>
## type [NegativeVolumeIndexStrategy](<https://github.com/cinar/indicator/blob/master/strategy/volume/negative_volume_index_strategy.go#L25-L31>)

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]
}
```

<a name="NewNegativeVolumeIndexStrategy"></a>
### func [NewNegativeVolumeIndexStrategy](<https://github.com/cinar/indicator/blob/master/strategy/volume/negative_volume_index_strategy.go#L35>)

```go
func NewNegativeVolumeIndexStrategy() *NegativeVolumeIndexStrategy
```

NewNegativeVolumeIndexStrategy function initializes a new Negative Volume Index strategy instance with the default parameters.

<a name="NewNegativeVolumeIndexStrategyWith"></a>
### func [NewNegativeVolumeIndexStrategyWith](<https://github.com/cinar/indicator/blob/master/strategy/volume/negative_volume_index_strategy.go#L43>)

```go
func NewNegativeVolumeIndexStrategyWith(emaPeriod int) *NegativeVolumeIndexStrategy
```

NewNegativeVolumeIndexStrategyWith function initializes a new Negative Volume Index strategy instance with the given parameters.

<a name="NegativeVolumeIndexStrategy.Compute"></a>
### func \(\*NegativeVolumeIndexStrategy\) [Compute](<https://github.com/cinar/indicator/blob/master/strategy/volume/negative_volume_index_strategy.go#L56>)

```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.

<a name="NegativeVolumeIndexStrategy.Name"></a>
### func \(\*NegativeVolumeIndexStrategy\) [Name](<https://github.com/cinar/indicator/blob/master/strategy/volume/negative_volume_index_strategy.go#L51>)

```go
func (n *NegativeVolumeIndexStrategy) Name() string
```

Name returns the name of the strategy.

<a name="NegativeVolumeIndexStrategy.Report"></a>
### func \(\*NegativeVolumeIndexStrategy\) [Report](<https://github.com/cinar/indicator/blob/master/strategy/volume/negative_volume_index_strategy.go#L93>)

```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](<https://github.com/princjef/gomarkdoc>)
144 changes: 144 additions & 0 deletions strategy/volume/negative_volume_index_strategy.go
Original file line number Diff line number Diff line change
@@ -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
}
55 changes: 55 additions & 0 deletions strategy/volume/negative_volume_index_strategy_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
Loading

0 comments on commit 467323d

Please sign in to comment.