Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 87 additions & 0 deletions strategy/momentum/coppock_curve_strategy.go
Original file line number Diff line number Diff line change
@@ -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
}
55 changes: 55 additions & 0 deletions strategy/momentum/coppock_curve_strategy_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
1 change: 1 addition & 0 deletions strategy/momentum/momentum.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ func AllStrategies() []strategy.Strategy {
return []strategy.Strategy{
NewAwesomeOscillatorStrategy(),
NewElderRayStrategy(),
NewCoppockCurveStrategy(),
NewIchimokuCloudStrategy(),
NewRsiStrategy(),
NewStochasticOscillatorStrategy(),
Expand Down
4 changes: 2 additions & 2 deletions strategy/momentum/momentum_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))
}
}
Loading
Loading