diff --git a/README.md b/README.md index 987febd..6fd32a4 100644 --- a/README.md +++ b/README.md @@ -138,7 +138,7 @@ The following list of strategies are currently supported by this package: ### 📢 Volume Strategies - [Chaikin Money Flow Strategy](strategy/volume/README.md#type-chaikinmoneyflowstrategy) -- Ease of Movement Strategy +- [Ease of Movement Strategy](strategy/volume/README.md#type-easeofmovementstrategy) - [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) diff --git a/strategy/volume/README.md b/strategy/volume/README.md index a504dad..3c97fc5 100644 --- a/strategy/volume/README.md +++ b/strategy/volume/README.md @@ -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 EaseOfMovementStrategy](<#EaseOfMovementStrategy>) + - [func NewEaseOfMovementStrategy\(\) \*EaseOfMovementStrategy](<#NewEaseOfMovementStrategy>) + - [func NewEaseOfMovementStrategyWith\(period int\) \*EaseOfMovementStrategy](<#NewEaseOfMovementStrategyWith>) + - [func \(e \*EaseOfMovementStrategy\) Compute\(snapshots \<\-chan \*asset.Snapshot\) \<\-chan strategy.Action](<#EaseOfMovementStrategy.Compute>) + - [func \(e \*EaseOfMovementStrategy\) Name\(\) string](<#EaseOfMovementStrategy.Name>) + - [func \(e \*EaseOfMovementStrategy\) Report\(snapshots \<\-chan \*asset.Snapshot\) \*helper.Report](<#EaseOfMovementStrategy.Report>) - [type ForceIndexStrategy](<#ForceIndexStrategy>) - [func NewForceIndexStrategy\(\) \*ForceIndexStrategy](<#NewForceIndexStrategy>) - [func NewForceIndexStrategyWith\(period int\) \*ForceIndexStrategy](<#NewForceIndexStrategyWith>) @@ -147,6 +153,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. + +## type [EaseOfMovementStrategy]() + +EaseOfMovementStrategy represents the configuration parameters for calculating the Ease of Movement strategy. Recommends a Buy action when it crosses above 0, and recommends a Sell action when it crosses below 0. + +```go +type EaseOfMovementStrategy struct { + // EaseOfMovement is the Ease of Movement indicator instance. + EaseOfMovement *volume.Emv[float64] +} +``` + + +### func [NewEaseOfMovementStrategy]() + +```go +func NewEaseOfMovementStrategy() *EaseOfMovementStrategy +``` + +NewEaseOfMovementStrategy function initializes a new Ease of Movement strategy instance with the default parameters. + + +### func [NewEaseOfMovementStrategyWith]() + +```go +func NewEaseOfMovementStrategyWith(period int) *EaseOfMovementStrategy +``` + +NewEaseOfMovementStrategyWith function initializes a new Ease of Movement strategy instance with the given parameters. + + +### func \(\*EaseOfMovementStrategy\) [Compute]() + +```go +func (e *EaseOfMovementStrategy) Compute(snapshots <-chan *asset.Snapshot) <-chan strategy.Action +``` + +Compute function processes the provided asset snapshots and generates a stream of actionable recommendations. + + +### func \(\*EaseOfMovementStrategy\) [Name]() + +```go +func (e *EaseOfMovementStrategy) Name() string +``` + +Name function returns the name of the strategy. + + +### func \(\*EaseOfMovementStrategy\) [Report]() + +```go +func (e *EaseOfMovementStrategy) Report(snapshots <-chan *asset.Snapshot) *helper.Report +``` + +Report function processes the provided asset snapshots and generates a report annotated with the recommended actions. + ## type [ForceIndexStrategy]() diff --git a/strategy/volume/ease_of_movement_strategy.go b/strategy/volume/ease_of_movement_strategy.go new file mode 100644 index 0000000..2db1454 --- /dev/null +++ b/strategy/volume/ease_of_movement_strategy.go @@ -0,0 +1,119 @@ +// 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" +) + +// EaseOfMovementStrategy represents the configuration parameters for calculating the Ease of Movement strategy. +// Recommends a Buy action when it crosses above 0, and recommends a Sell action when it crosses below 0. +type EaseOfMovementStrategy struct { + // EaseOfMovement is the Ease of Movement indicator instance. + EaseOfMovement *volume.Emv[float64] +} + +// NewEaseOfMovementStrategy function initializes a new Ease of Movement strategy instance with the +// default parameters. +func NewEaseOfMovementStrategy() *EaseOfMovementStrategy { + return NewEaseOfMovementStrategyWith( + volume.DefaultEmvPeriod, + ) +} + +// NewEaseOfMovementStrategyWith function initializes a new Ease of Movement strategy instance with the +// given parameters. +func NewEaseOfMovementStrategyWith(period int) *EaseOfMovementStrategy { + return &EaseOfMovementStrategy{ + EaseOfMovement: volume.NewEmvWithPeriod[float64](period), + } +} + +// Name function returns the name of the strategy. +func (e *EaseOfMovementStrategy) Name() string { + return fmt.Sprintf("Ease of Movement Strategy (%d)", e.EaseOfMovement.IdlePeriod()+1) +} + +// Compute function processes the provided asset snapshots and generates a stream of actionable recommendations. +func (e *EaseOfMovementStrategy) Compute(snapshots <-chan *asset.Snapshot) <-chan strategy.Action { + snapshotsSplice := helper.Duplicate(snapshots, 3) + + highs := asset.SnapshotsAsHighs(snapshotsSplice[0]) + lows := asset.SnapshotsAsLows(snapshotsSplice[1]) + volumes := asset.SnapshotsAsVolumes(snapshotsSplice[2]) + + emvs := e.EaseOfMovement.Compute(highs, lows, volumes) + + actions := helper.Map(emvs, func(emv float64) strategy.Action { + if emv > 0 { + return strategy.Buy + } + + if emv < 0 { + return strategy.Sell + } + + return strategy.Hold + }) + + // Ease of Movement starts only after a full period. + actions = helper.Shift(actions, e.EaseOfMovement.IdlePeriod(), strategy.Hold) + + return actions +} + +// Report function processes the provided asset snapshots and generates a report annotated with the recommended actions. +func (e *EaseOfMovementStrategy) Report(snapshots <-chan *asset.Snapshot) *helper.Report { + // + // snapshots[0] -> dates + // snapshots[1] -> highs | + // snapshots[2] -> lows | + // snapshots[3] -> volumes -> emv + // snapshots[4] -> closings + // snapshots[5] -> actions -> annotations + // -> outcomes + // + snapshotsSplice := helper.Duplicate(snapshots, 6) + + dates := helper.Skip( + asset.SnapshotsAsDates(snapshotsSplice[0]), + e.EaseOfMovement.IdlePeriod(), + ) + + highs := asset.SnapshotsAsHighs(snapshotsSplice[1]) + lows := asset.SnapshotsAsLows(snapshotsSplice[2]) + volumes := asset.SnapshotsAsVolumes(snapshotsSplice[3]) + + closings := helper.Skip( + asset.SnapshotsAsClosings(snapshotsSplice[4]), + e.EaseOfMovement.IdlePeriod(), + ) + + emvs := e.EaseOfMovement.Compute(highs, lows, volumes) + + actions, outcomes := strategy.ComputeWithOutcome(e, snapshotsSplice[5]) + actions = helper.Skip(actions, e.EaseOfMovement.IdlePeriod()) + outcomes = helper.Skip(outcomes, e.EaseOfMovement.IdlePeriod()) + + annotations := strategy.ActionsToAnnotations(actions) + outcomes = helper.MultiplyBy(outcomes, 100) + + report := helper.NewReport(e.Name(), dates) + report.AddChart() + report.AddChart() + + report.AddColumn(helper.NewNumericReportColumn("Close", closings)) + report.AddColumn(helper.NewNumericReportColumn("Ease of Movement", emvs), 1) + report.AddColumn(helper.NewAnnotationReportColumn(annotations), 0, 1) + + report.AddColumn(helper.NewNumericReportColumn("Outcome", outcomes), 2) + + return report +} diff --git a/strategy/volume/ease_of_movement_strategy_test.go b/strategy/volume/ease_of_movement_strategy_test.go new file mode 100644 index 0000000..f3a2bf3 --- /dev/null +++ b/strategy/volume/ease_of_movement_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 TestEaseOfMovementStrategy(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/ease_of_movement_strategy.csv", true) + if err != nil { + t.Fatal(err) + } + + expected := helper.Map(results, func(r *strategy.Result) strategy.Action { return r.Action }) + + emvs := volume.NewEaseOfMovementStrategy() + actual := emvs.Compute(snapshots) + + err = helper.CheckEquals(actual, expected) + if err != nil { + t.Fatal(err) + } +} + +func TestEaseOfMovementStrategyReport(t *testing.T) { + snapshots, err := helper.ReadFromCsvFile[asset.Snapshot]("testdata/brk-b.csv", true) + if err != nil { + t.Fatal(err) + } + + emvs := volume.NewEaseOfMovementStrategy() + report := emvs.Report(snapshots) + + fileName := "ease_of_movement_strategy.html" + defer os.Remove(fileName) + + err = report.WriteToFile(fileName) + if err != nil { + t.Fatal(err) + } +} diff --git a/strategy/volume/testdata/ease_of_movement_strategy.csv b/strategy/volume/testdata/ease_of_movement_strategy.csv new file mode 100644 index 0000000..daf9b63 --- /dev/null +++ b/strategy/volume/testdata/ease_of_movement_strategy.csv @@ -0,0 +1,252 @@ +Action +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +-1 +-1 +-1 +-1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +-1 +-1 +-1 +-1 +-1 +-1 +-1 +-1 +-1 +-1 +-1 +-1 +-1 +-1 +1 +1 +-1 +1 +1 +-1 +-1 +-1 +-1 +-1 +-1 +-1 +-1 +-1 +-1 +-1 +1 +1 +1 +1 +1 +-1 +-1 +1 +-1 +-1 +-1 +-1 +-1 +-1 +-1 +-1 +-1 +-1 +-1 +-1 +-1 +-1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +-1 +-1 +-1 +-1 +-1 +-1 +-1 +-1 +-1 +-1 +-1 +-1 +-1 +-1 +-1 +-1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +-1 +-1 +-1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +-1 +-1 +-1 +-1 +-1 +-1 +-1 +-1 +-1 +-1 +-1 +-1 +-1 +-1 +-1 +-1 +-1 +-1 +-1 +-1 +-1 +-1 +-1 +-1 +-1 +-1 +-1 +-1 +-1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 diff --git a/strategy/volume/volume.go b/strategy/volume/volume.go index 97cc359..3ff9251 100644 --- a/strategy/volume/volume.go +++ b/strategy/volume/volume.go @@ -26,6 +26,7 @@ import ( func AllStrategies() []strategy.Strategy { return []strategy.Strategy{ NewChaikinMoneyFlowStrategy(), + NewEaseOfMovementStrategy(), NewForceIndexStrategy(), NewMoneyFlowIndexStrategy(), NewNegativeVolumeIndexStrategy(),