-
-
Notifications
You must be signed in to change notification settings - Fork 104
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
- Loading branch information
There are no files selected for viewing
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 | ||
} | ||
Comment on lines
+42
to
+67
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add error handling for potential channel issues in In the |
||
|
||
// 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Refactor the The |
||
} |
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) | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add input validation for
period
parameter inNewForceIndexStrategyWith
The constructor
NewForceIndexStrategyWith
allows custom period specification but does not validate the input parameter. Consider adding validation to ensure that theperiod
is a positive integer greater than zero to prevent potential runtime errors.Apply this diff to add input validation:
📝 Committable suggestion