Skip to content

Commit 5c5bfbd

Browse files
whencanibecinar
andauthored
Coppock Curve Strategy added. Issue 344 (#388)
# Describe Request Implements the Coppock Curve Strategy as requested in issue #344. The Coppock Curve indicator already exists in the `momentum` package. This PR adds the strategy layer on top of it. Fixed #344 # Change Type New strategy. --------- Signed-off-by: Eonyak Cho <eonyakcho@gmail.com> Co-authored-by: Onur Cinar <onur.cinar@gmail.com>
1 parent 607513d commit 5c5bfbd

7 files changed

Lines changed: 509 additions & 2 deletions

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,7 @@ The following list of strategies are currently supported by this package:
168168
### 🚀 Momentum Strategies
169169

170170
- [Awesome Oscillator Strategy](strategy/momentum/README.md#AwesomeOscillatorStrategy)
171+
- [Coppock Curve Strategy](strategy/momentum/README.md#CoppockCurveStrategy)
171172
- [Elder Ray Strategy](strategy/momentum/README.md#ElderRayStrategy)
172173
- [Ichimoku Cloud Strategy](strategy/momentum/README.md#IchimokuCloudStrategy)
173174
- [RSI Strategy](strategy/momentum/README.md#RsiStrategy)

strategy/momentum/README.md

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,12 @@ The information provided on this project is strictly for informational purposes
3232
- [func \(a \*AwesomeOscillatorStrategy\) ComputeWithContext\(ctx context.Context, snapshots \<\-chan \*asset.Snapshot\) \<\-chan strategy.Action](<#AwesomeOscillatorStrategy.ComputeWithContext>)
3333
- [func \(\*AwesomeOscillatorStrategy\) Name\(\) string](<#AwesomeOscillatorStrategy.Name>)
3434
- [func \(a \*AwesomeOscillatorStrategy\) Report\(c \<\-chan \*asset.Snapshot\) \*helper.Report](<#AwesomeOscillatorStrategy.Report>)
35+
- [type CoppockCurveStrategy](<#CoppockCurveStrategy>)
36+
- [func NewCoppockCurveStrategy\(\) \*CoppockCurveStrategy](<#NewCoppockCurveStrategy>)
37+
- [func \(c \*CoppockCurveStrategy\) Compute\(snapshots \<\-chan \*asset.Snapshot\) \<\-chan strategy.Action](<#CoppockCurveStrategy.Compute>)
38+
- [func \(c \*CoppockCurveStrategy\) ComputeWithContext\(ctx context.Context, snapshots \<\-chan \*asset.Snapshot\) \<\-chan strategy.Action](<#CoppockCurveStrategy.ComputeWithContext>)
39+
- [func \(\*CoppockCurveStrategy\) Name\(\) string](<#CoppockCurveStrategy.Name>)
40+
- [func \(c \*CoppockCurveStrategy\) Report\(cr \<\-chan \*asset.Snapshot\) \*helper.Report](<#CoppockCurveStrategy.Report>)
3541
- [type ElderRayStrategy](<#ElderRayStrategy>)
3642
- [func NewElderRayStrategy\(\) \*ElderRayStrategy](<#NewElderRayStrategy>)
3743
- [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
224230

225231
Report processes the provided asset snapshots and generates a report annotated with the recommended actions.
226232

233+
<a name="CoppockCurveStrategy"></a>
234+
## type [CoppockCurveStrategy](<https://github.com/cinar/indicator/blob/master/strategy/momentum/coppock_curve_strategy.go#L18-L21>)
235+
236+
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.
237+
238+
```go
239+
type CoppockCurveStrategy struct {
240+
// CoppockCurve represents the configuration parameters for calculating the Coppock Curve.
241+
CoppockCurve *momentum.CoppockCurve[float64]
242+
}
243+
```
244+
245+
<a name="NewCoppockCurveStrategy"></a>
246+
### func [NewCoppockCurveStrategy](<https://github.com/cinar/indicator/blob/master/strategy/momentum/coppock_curve_strategy.go#L24>)
247+
248+
```go
249+
func NewCoppockCurveStrategy() *CoppockCurveStrategy
250+
```
251+
252+
NewCoppockCurveStrategy function initializes a new Coppock Curve strategy instance with the default parameters.
253+
254+
<a name="CoppockCurveStrategy.Compute"></a>
255+
### func \(\*CoppockCurveStrategy\) [Compute](<https://github.com/cinar/indicator/blob/master/strategy/momentum/coppock_curve_strategy.go#L94>)
256+
257+
```go
258+
func (c *CoppockCurveStrategy) Compute(snapshots <-chan *asset.Snapshot) <-chan strategy.Action
259+
```
260+
261+
Compute wraps ComputeWithContext for backwards compatibility.
262+
263+
Deprecated: Use ComputeWithContext instead.
264+
265+
<a name="CoppockCurveStrategy.ComputeWithContext"></a>
266+
### func \(\*CoppockCurveStrategy\) [ComputeWithContext](<https://github.com/cinar/indicator/blob/master/strategy/momentum/coppock_curve_strategy.go#L36>)
267+
268+
```go
269+
func (c *CoppockCurveStrategy) ComputeWithContext(ctx context.Context, snapshots <-chan *asset.Snapshot) <-chan strategy.Action
270+
```
271+
272+
ComputeWithContext processes the provided asset snapshots and generates a stream of actionable recommendations.
273+
274+
<a name="CoppockCurveStrategy.Name"></a>
275+
### func \(\*CoppockCurveStrategy\) [Name](<https://github.com/cinar/indicator/blob/master/strategy/momentum/coppock_curve_strategy.go#L31>)
276+
277+
```go
278+
func (*CoppockCurveStrategy) Name() string
279+
```
280+
281+
Name returns the name of the strategy.
282+
283+
<a name="CoppockCurveStrategy.Report"></a>
284+
### func \(\*CoppockCurveStrategy\) [Report](<https://github.com/cinar/indicator/blob/master/strategy/momentum/coppock_curve_strategy.go#L59>)
285+
286+
```go
287+
func (c *CoppockCurveStrategy) Report(cr <-chan *asset.Snapshot) *helper.Report
288+
```
289+
290+
Report processes the provided asset snapshots and generates a report annotated with the recommended actions.
291+
227292
<a name="ElderRayStrategy"></a>
228293
## type [ElderRayStrategy](<https://github.com/cinar/indicator/blob/master/strategy/momentum/elder_ray_strategy.go#L20-L23>)
229294

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
// Copyright (c) 2021-2026 Onur Cinar.
2+
// The source code is provided under GNU AGPLv3 License.
3+
// https://github.com/cinar/indicator
4+
5+
package momentum
6+
7+
import (
8+
"context"
9+
10+
"github.com/cinar/indicator/v2/asset"
11+
"github.com/cinar/indicator/v2/helper"
12+
"github.com/cinar/indicator/v2/momentum"
13+
"github.com/cinar/indicator/v2/strategy"
14+
)
15+
16+
// CoppockCurveStrategy represents the configuration parameters for calculating the Coppock Curve strategy.
17+
// A positive Coppock Curve value suggests a Buy signal, while a negative value suggests a Sell signal.
18+
type CoppockCurveStrategy struct {
19+
// CoppockCurve represents the configuration parameters for calculating the Coppock Curve.
20+
CoppockCurve *momentum.CoppockCurve[float64]
21+
}
22+
23+
// NewCoppockCurveStrategy function initializes a new Coppock Curve strategy instance with the default parameters.
24+
func NewCoppockCurveStrategy() *CoppockCurveStrategy {
25+
return &CoppockCurveStrategy{
26+
CoppockCurve: momentum.NewCoppockCurve[float64](),
27+
}
28+
}
29+
30+
// Name returns the name of the strategy.
31+
func (*CoppockCurveStrategy) Name() string {
32+
return "Coppock Curve Strategy"
33+
}
34+
35+
// ComputeWithContext processes the provided asset snapshots and generates a stream of actionable recommendations.
36+
func (c *CoppockCurveStrategy) ComputeWithContext(ctx context.Context, snapshots <-chan *asset.Snapshot) <-chan strategy.Action {
37+
closings := asset.SnapshotsAsClosingsWithContext(ctx, snapshots)
38+
39+
coppock := c.CoppockCurve.ComputeWithContext(ctx, closings)
40+
41+
actions := helper.MapWithContext(ctx, coppock, func(value float64) strategy.Action {
42+
if value > 0 {
43+
return strategy.Buy
44+
}
45+
46+
if value < 0 {
47+
return strategy.Sell
48+
}
49+
50+
return strategy.Hold
51+
})
52+
53+
actions = helper.ShiftWithContext(ctx, actions, c.CoppockCurve.IdlePeriod(), strategy.Hold)
54+
55+
return actions
56+
}
57+
58+
// Report processes the provided asset snapshots and generates a report annotated with the recommended actions.
59+
func (c *CoppockCurveStrategy) Report(cr <-chan *asset.Snapshot) *helper.Report {
60+
//
61+
// snapshots[0] -> dates
62+
// snapshots[1] -> Compute -> actions -> annotations
63+
// snapshots[2] -> closings[0] -> close
64+
// closings[1] -> CoppockCurve.Compute -> coppock
65+
//
66+
snapshots := helper.Duplicate(cr, 3)
67+
68+
dates := asset.SnapshotsAsDates(snapshots[0])
69+
70+
closings := helper.Duplicate(asset.SnapshotsAsClosings(snapshots[2]), 2)
71+
72+
coppock := helper.Shift(c.CoppockCurve.Compute(closings[0]), c.CoppockCurve.IdlePeriod(), 0)
73+
74+
actions, outcomes := strategy.ComputeWithOutcome(c, snapshots[1])
75+
annotations := strategy.ActionsToAnnotations(actions)
76+
outcomes = helper.MultiplyBy(outcomes, 100)
77+
78+
report := helper.NewReport(c.Name(), dates)
79+
report.AddChart()
80+
report.AddChart()
81+
82+
report.AddColumn(helper.NewNumericReportColumn("Close", closings[1]))
83+
report.AddColumn(helper.NewNumericReportColumn("Coppock Curve", coppock), 1)
84+
report.AddColumn(helper.NewAnnotationReportColumn(annotations), 0, 1)
85+
86+
report.AddColumn(helper.NewNumericReportColumn("Outcome", outcomes), 2)
87+
88+
return report
89+
}
90+
91+
// Compute wraps ComputeWithContext for backwards compatibility.
92+
//
93+
// Deprecated: Use ComputeWithContext instead.
94+
func (c *CoppockCurveStrategy) Compute(snapshots <-chan *asset.Snapshot) <-chan strategy.Action {
95+
return c.ComputeWithContext(context.Background(), snapshots)
96+
}
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
// Copyright (c) 2021-2026 Onur Cinar.
2+
// The source code is provided under GNU AGPLv3 License.
3+
// https://github.com/cinar/indicator
4+
5+
package momentum_test
6+
7+
import (
8+
"context"
9+
"testing"
10+
11+
"github.com/cinar/indicator/v2/asset"
12+
"github.com/cinar/indicator/v2/helper"
13+
indMomentum "github.com/cinar/indicator/v2/momentum"
14+
"github.com/cinar/indicator/v2/strategy"
15+
"github.com/cinar/indicator/v2/strategy/momentum"
16+
)
17+
18+
func TestCoppockCurveStrategy(t *testing.T) {
19+
snapshots, err := helper.ReadFromCsvFile[asset.Snapshot]("testdata/brk-b.csv")
20+
if err != nil {
21+
t.Fatal(err)
22+
}
23+
24+
results, err := helper.ReadFromCsvFile[strategy.Result]("testdata/coppock_curve_strategy.csv")
25+
if err != nil {
26+
t.Fatal(err)
27+
}
28+
29+
expected := helper.Map(results, func(r *strategy.Result) strategy.Action { return r.Action })
30+
31+
cc := momentum.NewCoppockCurveStrategy()
32+
actual := cc.Compute(snapshots)
33+
34+
err = helper.CheckEquals(actual, expected)
35+
if err != nil {
36+
t.Fatal(err)
37+
}
38+
}
39+
40+
func TestCoppockCurveStrategyReport(t *testing.T) {
41+
snapshots, err := helper.ReadFromCsvFile[asset.Snapshot]("testdata/brk-b.csv")
42+
if err != nil {
43+
t.Fatal(err)
44+
}
45+
46+
cc := momentum.NewCoppockCurveStrategy()
47+
48+
report := cc.Report(snapshots)
49+
50+
fileName := "coppock_curve_strategy.html"
51+
defer helper.Remove(t, fileName)
52+
53+
err = report.WriteToFile(fileName)
54+
if err != nil {
55+
t.Fatal(err)
56+
}
57+
}
58+
59+
func TestCoppockCurveStrategyZeroAndContext(t *testing.T) {
60+
ctx, cancel := context.WithCancel(context.Background())
61+
defer cancel()
62+
63+
cc := momentum.NewCoppockCurveStrategy()
64+
cc.CoppockCurve = indMomentum.NewCoppockCurveWithPeriods[float64](1, 1, 1)
65+
66+
snapshots := []*asset.Snapshot{
67+
{Close: 10},
68+
{Close: 11},
69+
{Close: 11},
70+
{Close: 10},
71+
}
72+
73+
actions := cc.ComputeWithContext(ctx, helper.SliceToChan(snapshots))
74+
actual := helper.ChanToSlice(actions)
75+
76+
expected := []strategy.Action{
77+
strategy.Hold, // Idle period (index 0)
78+
strategy.Buy, // Coppock > 0 (index 1)
79+
strategy.Hold, // Coppock == 0 (index 2)
80+
strategy.Sell, // Coppock < 0 (index 3)
81+
}
82+
83+
if len(actual) != len(expected) {
84+
t.Fatalf("expected length %d, got %d", len(expected), len(actual))
85+
}
86+
87+
for i, v := range actual {
88+
if v != expected[i] {
89+
t.Errorf("at index %d: expected %v, got %v", i, expected[i], v)
90+
}
91+
}
92+
}

strategy/momentum/momentum.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ func AllStrategies() []strategy.Strategy {
2525
return []strategy.Strategy{
2626
NewAwesomeOscillatorStrategy(),
2727
NewElderRayStrategy(),
28+
NewCoppockCurveStrategy(),
2829
NewIchimokuCloudStrategy(),
2930
NewRsiStrategy(),
3031
NewStochasticOscillatorStrategy(),

strategy/momentum/momentum_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import (
1212

1313
func TestAllStrategies(t *testing.T) {
1414
strategies := momentum.AllStrategies()
15-
if len(strategies) != 8 {
16-
t.Fatalf("expected 8 strategies, got %d", len(strategies))
15+
if len(strategies) != 9 {
16+
t.Fatalf("expected 9 strategies, got %d", len(strategies))
1717
}
1818
}

0 commit comments

Comments
 (0)