Skip to content

Commit

Permalink
Force Index Strategy added. (#238)
Browse files Browse the repository at this point in the history
# Describe Request

Force Index Strategy added.

# Change Type

New strategy.



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

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

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

<a name="ForceIndexStrategy"></a>
## type [ForceIndexStrategy](<https://github.com/cinar/indicator/blob/master/strategy/volume/force_index_strategy.go#L18-L21>)

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

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

```go
func NewForceIndexStrategy() *ForceIndexStrategy
```

NewForceIndexStrategy function initializes a new Force Index strategy instance with the default parameters.

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

```go
func NewForceIndexStrategyWith(period int) *ForceIndexStrategy
```

NewForceIndexStrategyWith function initializes a new Force Index strategy instance with the given parameters.

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

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

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

```go
func (f *ForceIndexStrategy) Name() string
```

Name returns the name of the strategy.

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

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

<a name="MoneyFlowIndexStrategy"></a>
## type [MoneyFlowIndexStrategy](<https://github.com/cinar/indicator/blob/master/strategy/volume/money_flow_index_strategy.go#L26-L35>)

Expand Down
111 changes: 111 additions & 0 deletions strategy/volume/force_index_strategy.go
Original file line number Diff line number Diff line change
@@ -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
}
55 changes: 55 additions & 0 deletions strategy/volume/force_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 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)
}
}
Loading

0 comments on commit 0ea963f

Please sign in to comment.