From df96b66b9edc68c01cd81a836d38f9d578087654 Mon Sep 17 00:00:00 2001
From: Onur Cinar <onur.cinar@gmail.com>
Date: Sat, 19 Oct 2024 19:03:49 -0700
Subject: [PATCH] Ease of Movement Strategy is added.

---
 README.md                                     |   2 +-
 strategy/volume/README.md                     |  63 +++++
 strategy/volume/ease_of_movement_strategy.go  | 119 +++++++++
 .../volume/ease_of_movement_strategy_test.go  |  55 ++++
 .../testdata/ease_of_movement_strategy.csv    | 252 ++++++++++++++++++
 strategy/volume/volume.go                     |   1 +
 6 files changed, 491 insertions(+), 1 deletion(-)
 create mode 100644 strategy/volume/ease_of_movement_strategy.go
 create mode 100644 strategy/volume/ease_of_movement_strategy_test.go
 create mode 100644 strategy/volume/testdata/ease_of_movement_strategy.csv

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.
 
+<a name="EaseOfMovementStrategy"></a>
+## type [EaseOfMovementStrategy](<https://github.com/cinar/indicator/blob/master/strategy/volume/ease_of_movement_strategy.go#L18-L21>)
+
+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]
+}
+```
+
+<a name="NewEaseOfMovementStrategy"></a>
+### func [NewEaseOfMovementStrategy](<https://github.com/cinar/indicator/blob/master/strategy/volume/ease_of_movement_strategy.go#L25>)
+
+```go
+func NewEaseOfMovementStrategy() *EaseOfMovementStrategy
+```
+
+NewEaseOfMovementStrategy function initializes a new Ease of Movement strategy instance with the default parameters.
+
+<a name="NewEaseOfMovementStrategyWith"></a>
+### func [NewEaseOfMovementStrategyWith](<https://github.com/cinar/indicator/blob/master/strategy/volume/ease_of_movement_strategy.go#L33>)
+
+```go
+func NewEaseOfMovementStrategyWith(period int) *EaseOfMovementStrategy
+```
+
+NewEaseOfMovementStrategyWith function initializes a new Ease of Movement strategy instance with the given parameters.
+
+<a name="EaseOfMovementStrategy.Compute"></a>
+### func \(\*EaseOfMovementStrategy\) [Compute](<https://github.com/cinar/indicator/blob/master/strategy/volume/ease_of_movement_strategy.go#L45>)
+
+```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.
+
+<a name="EaseOfMovementStrategy.Name"></a>
+### func \(\*EaseOfMovementStrategy\) [Name](<https://github.com/cinar/indicator/blob/master/strategy/volume/ease_of_movement_strategy.go#L40>)
+
+```go
+func (e *EaseOfMovementStrategy) Name() string
+```
+
+Name function returns the name of the strategy.
+
+<a name="EaseOfMovementStrategy.Report"></a>
+### func \(\*EaseOfMovementStrategy\) [Report](<https://github.com/cinar/indicator/blob/master/strategy/volume/ease_of_movement_strategy.go#L73>)
+
+```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.
+
 <a name="ForceIndexStrategy"></a>
 ## type [ForceIndexStrategy](<https://github.com/cinar/indicator/blob/master/strategy/volume/force_index_strategy.go#L18-L21>)
 
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(),