Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Force Index Strategy added. #238

Merged
merged 1 commit into from
Oct 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Force Index Strategy added.
  • Loading branch information
cinar committed Oct 18, 2024
commit 1f3c3614bfe9c04e776114746cdefcc118f84692
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,
)
}
Comment on lines +23 to +28
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Add input validation for period parameter in NewForceIndexStrategyWith

The constructor NewForceIndexStrategyWith allows custom period specification but does not validate the input parameter. Consider adding validation to ensure that the period is a positive integer greater than zero to prevent potential runtime errors.

Apply this diff to add input validation:

 func NewForceIndexStrategyWith(period int) *ForceIndexStrategy {
+	if period <= 0 {
+		panic("period must be a positive integer greater than zero")
+	}
 	return &ForceIndexStrategy{
 		ForceIndex: volume.NewFiWithPeriod[float64](period),
 	}
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// NewForceIndexStrategy function initializes a new Force Index strategy instance with the default parameters.
func NewForceIndexStrategy() *ForceIndexStrategy {
return NewForceIndexStrategyWith(
volume.DefaultFiPeriod,
)
}
// NewForceIndexStrategy function initializes a new Force Index strategy instance with the default parameters.
func NewForceIndexStrategy() *ForceIndexStrategy {
return NewForceIndexStrategyWith(
volume.DefaultFiPeriod,
)
}
func NewForceIndexStrategyWith(period int) *ForceIndexStrategy {
if period <= 0 {
panic("period must be a positive integer greater than zero")
}
return &ForceIndexStrategy{
ForceIndex: volume.NewFiWithPeriod[float64](period),
}
}


// 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
}
Comment on lines +42 to +67
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Add error handling for potential channel issues in Compute

In the Compute method, there is no error handling for potential issues with the snapshots channel, such as premature closure or receiving nil values. Consider implementing error checks to handle such cases gracefully and prevent runtime panics.


// 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
Comment on lines +70 to +110
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Refactor the Report method for improved readability

The Report method contains multiple duplications and skips, which might affect readability and maintainability. Consider refactoring to simplify the data flow, possibly by extracting common patterns into helper functions or restructuring the method for clarity.

}
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
Loading