diff --git a/README.md b/README.md
index dc392540..88d41661 100644
--- a/README.md
+++ b/README.md
@@ -168,6 +168,7 @@ The following list of strategies are currently supported by this package:
### 🚀 Momentum Strategies
- [Awesome Oscillator Strategy](strategy/momentum/README.md#AwesomeOscillatorStrategy)
+- [Coppock Curve Strategy](strategy/momentum/README.md#CoppockCurveStrategy)
- [Elder Ray Strategy](strategy/momentum/README.md#ElderRayStrategy)
- [Ichimoku Cloud Strategy](strategy/momentum/README.md#IchimokuCloudStrategy)
- [RSI Strategy](strategy/momentum/README.md#RsiStrategy)
diff --git a/strategy/momentum/README.md b/strategy/momentum/README.md
index eed072c2..a034a342 100644
--- a/strategy/momentum/README.md
+++ b/strategy/momentum/README.md
@@ -32,6 +32,12 @@ The information provided on this project is strictly for informational purposes
- [func \(a \*AwesomeOscillatorStrategy\) ComputeWithContext\(ctx context.Context, snapshots \<\-chan \*asset.Snapshot\) \<\-chan strategy.Action](<#AwesomeOscillatorStrategy.ComputeWithContext>)
- [func \(\*AwesomeOscillatorStrategy\) Name\(\) string](<#AwesomeOscillatorStrategy.Name>)
- [func \(a \*AwesomeOscillatorStrategy\) Report\(c \<\-chan \*asset.Snapshot\) \*helper.Report](<#AwesomeOscillatorStrategy.Report>)
+- [type CoppockCurveStrategy](<#CoppockCurveStrategy>)
+ - [func NewCoppockCurveStrategy\(\) \*CoppockCurveStrategy](<#NewCoppockCurveStrategy>)
+ - [func \(c \*CoppockCurveStrategy\) Compute\(snapshots \<\-chan \*asset.Snapshot\) \<\-chan strategy.Action](<#CoppockCurveStrategy.Compute>)
+ - [func \(c \*CoppockCurveStrategy\) ComputeWithContext\(ctx context.Context, snapshots \<\-chan \*asset.Snapshot\) \<\-chan strategy.Action](<#CoppockCurveStrategy.ComputeWithContext>)
+ - [func \(\*CoppockCurveStrategy\) Name\(\) string](<#CoppockCurveStrategy.Name>)
+ - [func \(c \*CoppockCurveStrategy\) Report\(cr \<\-chan \*asset.Snapshot\) \*helper.Report](<#CoppockCurveStrategy.Report>)
- [type ElderRayStrategy](<#ElderRayStrategy>)
- [func NewElderRayStrategy\(\) \*ElderRayStrategy](<#NewElderRayStrategy>)
- [func \(e \*ElderRayStrategy\) Compute\(snapshots \<\-chan \*asset.Snapshot\) \<\-chan strategy.Action](<#ElderRayStrategy.Compute>)
@@ -224,6 +230,65 @@ func (a *AwesomeOscillatorStrategy) Report(c <-chan *asset.Snapshot) *helper.Rep
Report processes the provided asset snapshots and generates a report annotated with the recommended actions.
+
+## type [CoppockCurveStrategy]()
+
+CoppockCurveStrategy represents the configuration parameters for calculating the Coppock Curve strategy. A positive Coppock Curve value suggests a Buy signal, while a negative value suggests a Sell signal.
+
+```go
+type CoppockCurveStrategy struct {
+ // CoppockCurve represents the configuration parameters for calculating the Coppock Curve.
+ CoppockCurve *momentum.CoppockCurve[float64]
+}
+```
+
+
+### func [NewCoppockCurveStrategy]()
+
+```go
+func NewCoppockCurveStrategy() *CoppockCurveStrategy
+```
+
+NewCoppockCurveStrategy function initializes a new Coppock Curve strategy instance with the default parameters.
+
+
+### func \(\*CoppockCurveStrategy\) [Compute]()
+
+```go
+func (c *CoppockCurveStrategy) Compute(snapshots <-chan *asset.Snapshot) <-chan strategy.Action
+```
+
+Compute wraps ComputeWithContext for backwards compatibility.
+
+Deprecated: Use ComputeWithContext instead.
+
+
+### func \(\*CoppockCurveStrategy\) [ComputeWithContext]()
+
+```go
+func (c *CoppockCurveStrategy) ComputeWithContext(ctx context.Context, snapshots <-chan *asset.Snapshot) <-chan strategy.Action
+```
+
+ComputeWithContext processes the provided asset snapshots and generates a stream of actionable recommendations.
+
+
+### func \(\*CoppockCurveStrategy\) [Name]()
+
+```go
+func (*CoppockCurveStrategy) Name() string
+```
+
+Name returns the name of the strategy.
+
+
+### func \(\*CoppockCurveStrategy\) [Report]()
+
+```go
+func (c *CoppockCurveStrategy) Report(cr <-chan *asset.Snapshot) *helper.Report
+```
+
+Report processes the provided asset snapshots and generates a report annotated with the recommended actions.
+
## type [ElderRayStrategy]()
diff --git a/strategy/momentum/coppock_curve_strategy.go b/strategy/momentum/coppock_curve_strategy.go
new file mode 100644
index 00000000..1828ed97
--- /dev/null
+++ b/strategy/momentum/coppock_curve_strategy.go
@@ -0,0 +1,96 @@
+// Copyright (c) 2021-2026 Onur Cinar.
+// The source code is provided under GNU AGPLv3 License.
+// https://github.com/cinar/indicator
+
+package momentum
+
+import (
+ "context"
+
+ "github.com/cinar/indicator/v2/asset"
+ "github.com/cinar/indicator/v2/helper"
+ "github.com/cinar/indicator/v2/momentum"
+ "github.com/cinar/indicator/v2/strategy"
+)
+
+// CoppockCurveStrategy represents the configuration parameters for calculating the Coppock Curve strategy.
+// A positive Coppock Curve value suggests a Buy signal, while a negative value suggests a Sell signal.
+type CoppockCurveStrategy struct {
+ // CoppockCurve represents the configuration parameters for calculating the Coppock Curve.
+ CoppockCurve *momentum.CoppockCurve[float64]
+}
+
+// NewCoppockCurveStrategy function initializes a new Coppock Curve strategy instance with the default parameters.
+func NewCoppockCurveStrategy() *CoppockCurveStrategy {
+ return &CoppockCurveStrategy{
+ CoppockCurve: momentum.NewCoppockCurve[float64](),
+ }
+}
+
+// Name returns the name of the strategy.
+func (*CoppockCurveStrategy) Name() string {
+ return "Coppock Curve Strategy"
+}
+
+// ComputeWithContext processes the provided asset snapshots and generates a stream of actionable recommendations.
+func (c *CoppockCurveStrategy) ComputeWithContext(ctx context.Context, snapshots <-chan *asset.Snapshot) <-chan strategy.Action {
+ closings := asset.SnapshotsAsClosingsWithContext(ctx, snapshots)
+
+ coppock := c.CoppockCurve.ComputeWithContext(ctx, closings)
+
+ actions := helper.MapWithContext(ctx, coppock, func(value float64) strategy.Action {
+ if value > 0 {
+ return strategy.Buy
+ }
+
+ if value < 0 {
+ return strategy.Sell
+ }
+
+ return strategy.Hold
+ })
+
+ actions = helper.ShiftWithContext(ctx, actions, c.CoppockCurve.IdlePeriod(), strategy.Hold)
+
+ return actions
+}
+
+// Report processes the provided asset snapshots and generates a report annotated with the recommended actions.
+func (c *CoppockCurveStrategy) Report(cr <-chan *asset.Snapshot) *helper.Report {
+ //
+ // snapshots[0] -> dates
+ // snapshots[1] -> Compute -> actions -> annotations
+ // snapshots[2] -> closings[0] -> close
+ // closings[1] -> CoppockCurve.Compute -> coppock
+ //
+ snapshots := helper.Duplicate(cr, 3)
+
+ dates := asset.SnapshotsAsDates(snapshots[0])
+
+ closings := helper.Duplicate(asset.SnapshotsAsClosings(snapshots[2]), 2)
+
+ coppock := helper.Shift(c.CoppockCurve.Compute(closings[0]), c.CoppockCurve.IdlePeriod(), 0)
+
+ actions, outcomes := strategy.ComputeWithOutcome(c, snapshots[1])
+ annotations := strategy.ActionsToAnnotations(actions)
+ outcomes = helper.MultiplyBy(outcomes, 100)
+
+ report := helper.NewReport(c.Name(), dates)
+ report.AddChart()
+ report.AddChart()
+
+ report.AddColumn(helper.NewNumericReportColumn("Close", closings[1]))
+ report.AddColumn(helper.NewNumericReportColumn("Coppock Curve", coppock), 1)
+ report.AddColumn(helper.NewAnnotationReportColumn(annotations), 0, 1)
+
+ report.AddColumn(helper.NewNumericReportColumn("Outcome", outcomes), 2)
+
+ return report
+}
+
+// Compute wraps ComputeWithContext for backwards compatibility.
+//
+// Deprecated: Use ComputeWithContext instead.
+func (c *CoppockCurveStrategy) Compute(snapshots <-chan *asset.Snapshot) <-chan strategy.Action {
+ return c.ComputeWithContext(context.Background(), snapshots)
+}
diff --git a/strategy/momentum/coppock_curve_strategy_test.go b/strategy/momentum/coppock_curve_strategy_test.go
new file mode 100644
index 00000000..0a3ed10d
--- /dev/null
+++ b/strategy/momentum/coppock_curve_strategy_test.go
@@ -0,0 +1,92 @@
+// Copyright (c) 2021-2026 Onur Cinar.
+// The source code is provided under GNU AGPLv3 License.
+// https://github.com/cinar/indicator
+
+package momentum_test
+
+import (
+ "context"
+ "testing"
+
+ "github.com/cinar/indicator/v2/asset"
+ "github.com/cinar/indicator/v2/helper"
+ indMomentum "github.com/cinar/indicator/v2/momentum"
+ "github.com/cinar/indicator/v2/strategy"
+ "github.com/cinar/indicator/v2/strategy/momentum"
+)
+
+func TestCoppockCurveStrategy(t *testing.T) {
+ snapshots, err := helper.ReadFromCsvFile[asset.Snapshot]("testdata/brk-b.csv")
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ results, err := helper.ReadFromCsvFile[strategy.Result]("testdata/coppock_curve_strategy.csv")
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ expected := helper.Map(results, func(r *strategy.Result) strategy.Action { return r.Action })
+
+ cc := momentum.NewCoppockCurveStrategy()
+ actual := cc.Compute(snapshots)
+
+ err = helper.CheckEquals(actual, expected)
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestCoppockCurveStrategyReport(t *testing.T) {
+ snapshots, err := helper.ReadFromCsvFile[asset.Snapshot]("testdata/brk-b.csv")
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ cc := momentum.NewCoppockCurveStrategy()
+
+ report := cc.Report(snapshots)
+
+ fileName := "coppock_curve_strategy.html"
+ defer helper.Remove(t, fileName)
+
+ err = report.WriteToFile(fileName)
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestCoppockCurveStrategyZeroAndContext(t *testing.T) {
+ ctx, cancel := context.WithCancel(context.Background())
+ defer cancel()
+
+ cc := momentum.NewCoppockCurveStrategy()
+ cc.CoppockCurve = indMomentum.NewCoppockCurveWithPeriods[float64](1, 1, 1)
+
+ snapshots := []*asset.Snapshot{
+ {Close: 10},
+ {Close: 11},
+ {Close: 11},
+ {Close: 10},
+ }
+
+ actions := cc.ComputeWithContext(ctx, helper.SliceToChan(snapshots))
+ actual := helper.ChanToSlice(actions)
+
+ expected := []strategy.Action{
+ strategy.Hold, // Idle period (index 0)
+ strategy.Buy, // Coppock > 0 (index 1)
+ strategy.Hold, // Coppock == 0 (index 2)
+ strategy.Sell, // Coppock < 0 (index 3)
+ }
+
+ if len(actual) != len(expected) {
+ t.Fatalf("expected length %d, got %d", len(expected), len(actual))
+ }
+
+ for i, v := range actual {
+ if v != expected[i] {
+ t.Errorf("at index %d: expected %v, got %v", i, expected[i], v)
+ }
+ }
+}
diff --git a/strategy/momentum/momentum.go b/strategy/momentum/momentum.go
index 08f74667..72e321f9 100644
--- a/strategy/momentum/momentum.go
+++ b/strategy/momentum/momentum.go
@@ -25,6 +25,7 @@ func AllStrategies() []strategy.Strategy {
return []strategy.Strategy{
NewAwesomeOscillatorStrategy(),
NewElderRayStrategy(),
+ NewCoppockCurveStrategy(),
NewIchimokuCloudStrategy(),
NewRsiStrategy(),
NewStochasticOscillatorStrategy(),
diff --git a/strategy/momentum/momentum_test.go b/strategy/momentum/momentum_test.go
index b265cd80..a3931867 100644
--- a/strategy/momentum/momentum_test.go
+++ b/strategy/momentum/momentum_test.go
@@ -12,7 +12,7 @@ import (
func TestAllStrategies(t *testing.T) {
strategies := momentum.AllStrategies()
- if len(strategies) != 8 {
- t.Fatalf("expected 8 strategies, got %d", len(strategies))
+ if len(strategies) != 9 {
+ t.Fatalf("expected 9 strategies, got %d", len(strategies))
}
}
diff --git a/strategy/momentum/testdata/coppock_curve_strategy.csv b/strategy/momentum/testdata/coppock_curve_strategy.csv
new file mode 100644
index 00000000..5d833bd0
--- /dev/null
+++ b/strategy/momentum/testdata/coppock_curve_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
+-1
+-1
+-1
+1
+1
+1
+1
+1
+1
+1
+1
+1
+1
+1
+1
+1
+1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+1
+1
+1
+1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+1
+1
+1
+1
+1
+1
+1
+1
+1
+1
+1
+1
+1
+1
+1
+1
+1
+1
+1
+1
+1
+1
+1
+1
+1
+1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+1
+1
+1
+1
+1
+-1
+-1
+-1
+-1
+1
+1
+1
+1
+1
+1
+1
+1
+1
+1
+1
+1
+1
+1
+1
+1
+1
+1
+1
+1
+1
+1
+1
+1
+1
+1
+1
+1
+1
+1
+1
+1
+1
+1
+1
+1
+1
+1
+1
+1
+1
+1
+1
+1
+1
+1
+1
+1
+1
+1
+1
+1
+1
+1
+1
+-1
+-1
+-1
+-1
+-1
+-1
+1
+1
+1
+1
+1
+1
+1
+1
+1
+1
+1
+1
+1
+1
+1
+1
+1
+1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+-1
+1
+1
+1
+1
+1
+1
+1
+1
+1
+1
+1
+1
+1