From 144fd8fb003e6fa9db0b86498f5e36244a2522d5 Mon Sep 17 00:00:00 2001 From: Eonyak Cho Date: Wed, 22 Apr 2026 14:25:42 +0100 Subject: [PATCH] Coppock Curve Strategy added. Issue 344 Signed-off-by: Eonyak Cho --- strategy/momentum/coppock_curve_strategy.go | 87 ++++++ .../momentum/coppock_curve_strategy_test.go | 55 ++++ strategy/momentum/momentum.go | 1 + strategy/momentum/momentum_test.go | 4 +- .../testdata/coppock_curve_strategy.csv | 252 ++++++++++++++++++ 5 files changed, 397 insertions(+), 2 deletions(-) create mode 100644 strategy/momentum/coppock_curve_strategy.go create mode 100644 strategy/momentum/coppock_curve_strategy_test.go create mode 100644 strategy/momentum/testdata/coppock_curve_strategy.csv diff --git a/strategy/momentum/coppock_curve_strategy.go b/strategy/momentum/coppock_curve_strategy.go new file mode 100644 index 0000000..ace8f3a --- /dev/null +++ b/strategy/momentum/coppock_curve_strategy.go @@ -0,0 +1,87 @@ +// Copyright (c) 2021-2026 Onur Cinar. +// The source code is provided under GNU AGPLv3 License. +// https://github.com/cinar/indicator + +package momentum + +import ( + "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" +} + +// Compute processes the provided asset snapshots and generates a stream of actionable recommendations. +func (c *CoppockCurveStrategy) Compute(snapshots <-chan *asset.Snapshot) <-chan strategy.Action { + closings := asset.SnapshotsAsClosings(snapshots) + + coppock := c.CoppockCurve.Compute(closings) + + actions := helper.Map(coppock, func(value float64) strategy.Action { + if value > 0 { + return strategy.Buy + } + + if value < 0 { + return strategy.Sell + } + + return strategy.Hold + }) + + actions = helper.Shift(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 +} diff --git a/strategy/momentum/coppock_curve_strategy_test.go b/strategy/momentum/coppock_curve_strategy_test.go new file mode 100644 index 0000000..0ef2bd9 --- /dev/null +++ b/strategy/momentum/coppock_curve_strategy_test.go @@ -0,0 +1,55 @@ +// Copyright (c) 2021-2026 Onur Cinar. +// The source code is provided under GNU AGPLv3 License. +// https://github.com/cinar/indicator + +package momentum_test + +import ( + "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/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) + } +} diff --git a/strategy/momentum/momentum.go b/strategy/momentum/momentum.go index 08f7466..72e321f 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 b265cd8..a393186 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 0000000..5d833bd --- /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