From 8958daf877fa667069837a1adaed308972b15bd3 Mon Sep 17 00:00:00 2001 From: Onur Cinar Date: Mon, 14 Oct 2024 18:12:26 -0700 Subject: [PATCH] Money Flow Index Strategy (#234) # Describe Request Money Flow Index Strategy is added. # Change Type Volume strategy. ## Summary by CodeRabbit - **New Features** - Introduced a new version (v2) of the Indicator Go module with enhanced features including improved testability, configurability, and support for generics. - Added new `Envelope` and `Money Flow Index` strategies, expanding the library's trading capabilities. - Enhanced documentation for clarity on usage, installation, and contributing guidelines. - New `Envelope` type and associated functions for calculating envelope indicators. - **Bug Fixes** - Updated function signatures for better flexibility in strategy initialization. - **Documentation** - Comprehensive updates to README files across various packages to reflect new features and usage instructions. - **Tests** - Added unit tests for the new strategies to validate functionality and ensure reliability. --------- Signed-off-by: Onur Cinar --- README.md | 3 +- strategy/volume/README.md | 123 +++++++++ strategy/volume/money_flow_index_strategy.go | 137 ++++++++++ .../volume/money_flow_index_strategy_test.go | 55 ++++ strategy/volume/testdata/brk-b.csv | 1 + .../testdata/money_flow_index_strategy.csv | 252 ++++++++++++++++++ strategy/volume/volume.go | 28 ++ trend/envelope.go | 8 +- 8 files changed, 602 insertions(+), 5 deletions(-) create mode 100644 strategy/volume/README.md create mode 100644 strategy/volume/money_flow_index_strategy.go create mode 100644 strategy/volume/money_flow_index_strategy_test.go create mode 120000 strategy/volume/testdata/brk-b.csv create mode 100644 strategy/volume/testdata/money_flow_index_strategy.csv create mode 100644 strategy/volume/volume.go diff --git a/README.md b/README.md index 72cc8e8..addf1c3 100644 --- a/README.md +++ b/README.md @@ -111,6 +111,7 @@ The following list of strategies are currently supported by this package: - Chande Forecast Oscillator Strategy - [Community Channel Index (CCI) Strategy](strategy/trend/README.md#type-ccistrategy) - [Double Exponential Moving Average (DEMA) Strategy](strategy/trend/README.md#type-demastrategy) +- [Envelope Strategy](strategy/trend/README.md#type-envelope) - [Golden Cross Strategy](strategy/trend/README.md#type-goldencrossstrategy) - [Kaufman's Adaptive Moving Average (KAMA) Strategy](strategy/trend/README.md#type-kamastrategy) - [Moving Average Convergence Divergence (MACD) Strategy](strategy/trend/README.md#type-macdstrategy) @@ -139,7 +140,7 @@ The following list of strategies are currently supported by this package: - Chaikin Money Flow Strategy - Ease of Movement Strategy - Force Index Strategy -- Money Flow Index Strategy +- [Money Flow Index Strategy](strategy/volume/README.md#type-moneyflowindexstrategy) - Negative Volume Index Strategy - Volume Weighted Average Price Strategy diff --git a/strategy/volume/README.md b/strategy/volume/README.md new file mode 100644 index 0000000..b010cc0 --- /dev/null +++ b/strategy/volume/README.md @@ -0,0 +1,123 @@ + + +# volume + +```go +import "github.com/cinar/indicator/v2/strategy/volume" +``` + +Package volume contains the volume strategy functions. + +This package belongs to the Indicator project. Indicator is a Golang module that supplies a variety of technical indicators, strategies, and a backtesting framework for analysis. + +### License + +``` +Copyright (c) 2021-2024 Onur Cinar. +The source code is provided under GNU AGPLv3 License. +https://github.com/cinar/indicator +``` + +### Disclaimer + +The information provided on this project is strictly for informational purposes and is not to be construed as advice or solicitation to buy or sell any security. + +## Index + +- [Constants](<#constants>) +- [func AllStrategies\(\) \[\]strategy.Strategy](<#AllStrategies>) +- [type MoneyFlowIndexStrategy](<#MoneyFlowIndexStrategy>) + - [func NewMoneyFlowIndexStrategy\(\) \*MoneyFlowIndexStrategy](<#NewMoneyFlowIndexStrategy>) + - [func NewMoneyFlowIndexStrategyWith\(sellAt, buyAt float64\) \*MoneyFlowIndexStrategy](<#NewMoneyFlowIndexStrategyWith>) + - [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>) + + +## Constants + + + +```go +const ( + // DefaultMoneyFlowIndexStrategySellAt is the default sell at of 80. + DefaultMoneyFlowIndexStrategySellAt = 80 + + // DefaultMoneyFlowIndexStrategyBuyAt is the default buy at of 20. + DefaultMoneyFlowIndexStrategyBuyAt = 20 +) +``` + + +## func [AllStrategies]() + +```go +func AllStrategies() []strategy.Strategy +``` + +AllStrategies returns a slice containing references to all available volume strategies. + + +## type [MoneyFlowIndexStrategy]() + +MoneyFlowIndexStrategy represents the configuration parameters for calculating the Money Flow Index strategy. Recommends a Sell action when it crosses over 80, and recommends a Buy action when it crosses below 20. + +```go +type MoneyFlowIndexStrategy struct { + // MoneyFlowIndex is the Money Flow Index indicator instance. + MoneyFlowIndex *volume.Mfi[float64] + + // SellAt is the sell at value. + SellAt float64 + + // BuyAt is the buy at value. + BuyAt float64 +} +``` + + +### func [NewMoneyFlowIndexStrategy]() + +```go +func NewMoneyFlowIndexStrategy() *MoneyFlowIndexStrategy +``` + +NewMoneyFlowIndexStrategy function initializes a new Money Flow Index strategy instance with the default parameters. + + +### func [NewMoneyFlowIndexStrategyWith]() + +```go +func NewMoneyFlowIndexStrategyWith(sellAt, buyAt float64) *MoneyFlowIndexStrategy +``` + +NewMoneyFlowIndexStrategyWith function initializes a new Money Flow Index strategy instance with the given parameters. + + +### func \(\*MoneyFlowIndexStrategy\) [Compute]() + +```go +func (m *MoneyFlowIndexStrategy) Compute(snapshots <-chan *asset.Snapshot) <-chan strategy.Action +``` + +Compute processes the provided asset snapshots and generates a stream of actionable recommendations. + + +### func \(\*MoneyFlowIndexStrategy\) [Name]() + +```go +func (m *MoneyFlowIndexStrategy) Name() string +``` + +Name returns the name of the strategy. + + +### func \(\*MoneyFlowIndexStrategy\) [Report]() + +```go +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. + +Generated by [gomarkdoc]() diff --git a/strategy/volume/money_flow_index_strategy.go b/strategy/volume/money_flow_index_strategy.go new file mode 100644 index 0000000..9e292d1 --- /dev/null +++ b/strategy/volume/money_flow_index_strategy.go @@ -0,0 +1,137 @@ +// 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" +) + +const ( + // DefaultMoneyFlowIndexStrategySellAt is the default sell at of 80. + DefaultMoneyFlowIndexStrategySellAt = 80 + + // DefaultMoneyFlowIndexStrategyBuyAt is the default buy at of 20. + DefaultMoneyFlowIndexStrategyBuyAt = 20 +) + +// MoneyFlowIndexStrategy represents the configuration parameters for calculating the Money Flow Index strategy. +// Recommends a Sell action when it crosses over 80, and recommends a Buy action when it crosses below 20. +type MoneyFlowIndexStrategy struct { + // MoneyFlowIndex is the Money Flow Index indicator instance. + MoneyFlowIndex *volume.Mfi[float64] + + // SellAt is the sell at value. + SellAt float64 + + // BuyAt is the buy at value. + BuyAt float64 +} + +// NewMoneyFlowIndexStrategy function initializes a new Money Flow Index strategy instance with the default parameters. +func NewMoneyFlowIndexStrategy() *MoneyFlowIndexStrategy { + return NewMoneyFlowIndexStrategyWith( + DefaultMoneyFlowIndexStrategySellAt, + DefaultMoneyFlowIndexStrategyBuyAt, + ) +} + +// NewMoneyFlowIndexStrategyWith function initializes a new Money Flow Index strategy instance with the +// given parameters. +func NewMoneyFlowIndexStrategyWith(sellAt, buyAt float64) *MoneyFlowIndexStrategy { + return &MoneyFlowIndexStrategy{ + MoneyFlowIndex: volume.NewMfi[float64](), + SellAt: sellAt, + BuyAt: buyAt, + } +} + +// Name returns the name of the strategy. +func (m *MoneyFlowIndexStrategy) Name() string { + return fmt.Sprintf("Money Flow Index Strategy (%f)", m.SellAt) +} + +// Compute processes the provided asset snapshots and generates a stream of actionable recommendations. +func (m *MoneyFlowIndexStrategy) Compute(snapshots <-chan *asset.Snapshot) <-chan strategy.Action { + snapshotsSplice := helper.Duplicate(snapshots, 4) + + highs := asset.SnapshotsAsHighs(snapshotsSplice[0]) + lows := asset.SnapshotsAsLows(snapshotsSplice[1]) + closings := asset.SnapshotsAsClosings(snapshotsSplice[2]) + volumes := asset.SnapshotsAsVolumes(snapshotsSplice[3]) + + mfis := m.MoneyFlowIndex.Compute(highs, lows, closings, volumes) + + actions := helper.Map(mfis, func(mfi float64) strategy.Action { + if mfi >= m.SellAt { + return strategy.Sell + } + + if mfi <= m.BuyAt { + return strategy.Buy + } + + return strategy.Hold + }) + + // Money Flow Index starts only after a full period. + actions = helper.Shift(actions, m.MoneyFlowIndex.IdlePeriod(), strategy.Hold) + + return actions +} + +// Report processes the provided asset snapshots and generates a report annotated with the recommended actions. +func (m *MoneyFlowIndexStrategy) Report(c <-chan *asset.Snapshot) *helper.Report { + // + // snapshots[0] -> dates + // snapshots[1] -> highs | + // snapshots[2] -> lows | + // snapshots[3] -> closings[0] -> closings + // closings[1] -> superTrend + // snapshots[4] -> volumes + // snapshots[5] -> actions -> annotations + // -> outcomes + // + snapshots := helper.Duplicate(c, 6) + + dates := helper.Skip( + asset.SnapshotsAsDates(snapshots[0]), + m.MoneyFlowIndex.IdlePeriod(), + ) + + highs := asset.SnapshotsAsHighs(snapshots[1]) + lows := asset.SnapshotsAsLows(snapshots[2]) + closingsSplice := helper.Duplicate( + asset.SnapshotsAsClosings(snapshots[3]), + 2, + ) + volumes := asset.SnapshotsAsVolumes(snapshots[4]) + + mfis := m.MoneyFlowIndex.Compute(highs, lows, closingsSplice[0], volumes) + closingsSplice[1] = helper.Skip(closingsSplice[1], m.MoneyFlowIndex.IdlePeriod()) + + actions, outcomes := strategy.ComputeWithOutcome(m, snapshots[5]) + actions = helper.Skip(actions, m.MoneyFlowIndex.IdlePeriod()) + outcomes = helper.Skip(outcomes, m.MoneyFlowIndex.IdlePeriod()) + + annotations := strategy.ActionsToAnnotations(actions) + outcomes = helper.MultiplyBy(outcomes, 100) + + report := helper.NewReport(m.Name(), dates) + report.AddChart() + report.AddChart() + + report.AddColumn(helper.NewNumericReportColumn("Close", closingsSplice[1])) + report.AddColumn(helper.NewNumericReportColumn("Money Flow Index", mfis), 1) + report.AddColumn(helper.NewAnnotationReportColumn(annotations), 0, 1) + + report.AddColumn(helper.NewNumericReportColumn("Outcome", outcomes), 2) + + return report +} diff --git a/strategy/volume/money_flow_index_strategy_test.go b/strategy/volume/money_flow_index_strategy_test.go new file mode 100644 index 0000000..598a8c5 --- /dev/null +++ b/strategy/volume/money_flow_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 TestMoneyFlowIndexStrategy(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/money_flow_index_strategy.csv", true) + if err != nil { + t.Fatal(err) + } + + expected := helper.Map(results, func(r *strategy.Result) strategy.Action { return r.Action }) + + mfis := volume.NewMoneyFlowIndexStrategy() + actual := mfis.Compute(snapshots) + + err = helper.CheckEquals(actual, expected) + if err != nil { + t.Fatal(err) + } +} + +func TestMoneyFlowIndexStrategyReport(t *testing.T) { + snapshots, err := helper.ReadFromCsvFile[asset.Snapshot]("testdata/brk-b.csv", true) + if err != nil { + t.Fatal(err) + } + + mfis := volume.NewMoneyFlowIndexStrategy() + report := mfis.Report(snapshots) + + fileName := "money_flow_index_strategy.html" + defer os.Remove(fileName) + + err = report.WriteToFile(fileName) + if err != nil { + t.Fatal(err) + } +} diff --git a/strategy/volume/testdata/brk-b.csv b/strategy/volume/testdata/brk-b.csv new file mode 120000 index 0000000..53db519 --- /dev/null +++ b/strategy/volume/testdata/brk-b.csv @@ -0,0 +1 @@ +../../../asset/testdata/repository/brk-b.csv \ No newline at end of file diff --git a/strategy/volume/testdata/money_flow_index_strategy.csv b/strategy/volume/testdata/money_flow_index_strategy.csv new file mode 100644 index 0000000..792409e --- /dev/null +++ b/strategy/volume/testdata/money_flow_index_strategy.csv @@ -0,0 +1,252 @@ +Action +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 diff --git a/strategy/volume/volume.go b/strategy/volume/volume.go new file mode 100644 index 0000000..e0fa6ee --- /dev/null +++ b/strategy/volume/volume.go @@ -0,0 +1,28 @@ +// Package volume contains the volume strategy functions. +// +// This package belongs to the Indicator project. Indicator is +// a Golang module that supplies a variety of technical +// indicators, strategies, and a backtesting framework +// for analysis. +// +// # License +// +// Copyright (c) 2021-2024 Onur Cinar. +// The source code is provided under GNU AGPLv3 License. +// https://github.com/cinar/indicator +// +// # Disclaimer +// +// The information provided on this project is strictly for +// informational purposes and is not to be construed as +// advice or solicitation to buy or sell any security. +package volume + +import ( + "github.com/cinar/indicator/v2/strategy" +) + +// AllStrategies returns a slice containing references to all available volume strategies. +func AllStrategies() []strategy.Strategy { + return []strategy.Strategy{} +} diff --git a/trend/envelope.go b/trend/envelope.go index 97ce533..b3f6ffe 100644 --- a/trend/envelope.go +++ b/trend/envelope.go @@ -39,7 +39,7 @@ func NewEnvelope[T helper.Number](ma Ma[T], percentage T) *Envelope[T] { func NewEnvelopeWithSma[T helper.Number]() *Envelope[T] { return NewEnvelope( NewSmaWithPeriod[T](DefaultEnvelopePeriod), - DefaultEnvelopePercentage, + T(DefaultEnvelopePercentage), ) } @@ -47,7 +47,7 @@ func NewEnvelopeWithSma[T helper.Number]() *Envelope[T] { func NewEnvelopeWithEma[T helper.Number]() *Envelope[T] { return NewEnvelope( NewEmaWithPeriod[T](DefaultEnvelopePeriod), - DefaultEnvelopePercentage, + T(DefaultEnvelopePercentage), ) } @@ -60,12 +60,12 @@ func (e *Envelope[T]) Compute(closings <-chan T) (<-chan T, <-chan T, <-chan T) upper := helper.MultiplyBy( middleSplice[0], - 1+(e.Percentage/100), + 1+(e.Percentage/100.0), ) lower := helper.MultiplyBy( middleSplice[2], - 1-(e.Percentage/100), + 1-(e.Percentage/100.0), ) return upper, middleSplice[1], lower