Skip to content
Merged
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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
65 changes: 65 additions & 0 deletions strategy/momentum/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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>)
Expand Down Expand Up @@ -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.

<a name="CoppockCurveStrategy"></a>
## type [CoppockCurveStrategy](<https://github.com/cinar/indicator/blob/master/strategy/momentum/coppock_curve_strategy.go#L18-L21>)

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]
}
```

<a name="NewCoppockCurveStrategy"></a>
### func [NewCoppockCurveStrategy](<https://github.com/cinar/indicator/blob/master/strategy/momentum/coppock_curve_strategy.go#L24>)

```go
func NewCoppockCurveStrategy() *CoppockCurveStrategy
```

NewCoppockCurveStrategy function initializes a new Coppock Curve strategy instance with the default parameters.

<a name="CoppockCurveStrategy.Compute"></a>
### func \(\*CoppockCurveStrategy\) [Compute](<https://github.com/cinar/indicator/blob/master/strategy/momentum/coppock_curve_strategy.go#L94>)

```go
func (c *CoppockCurveStrategy) Compute(snapshots <-chan *asset.Snapshot) <-chan strategy.Action
```

Compute wraps ComputeWithContext for backwards compatibility.

Deprecated: Use ComputeWithContext instead.

<a name="CoppockCurveStrategy.ComputeWithContext"></a>
### func \(\*CoppockCurveStrategy\) [ComputeWithContext](<https://github.com/cinar/indicator/blob/master/strategy/momentum/coppock_curve_strategy.go#L36>)

```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.

<a name="CoppockCurveStrategy.Name"></a>
### func \(\*CoppockCurveStrategy\) [Name](<https://github.com/cinar/indicator/blob/master/strategy/momentum/coppock_curve_strategy.go#L31>)

```go
func (*CoppockCurveStrategy) Name() string
```

Name returns the name of the strategy.

<a name="CoppockCurveStrategy.Report"></a>
### func \(\*CoppockCurveStrategy\) [Report](<https://github.com/cinar/indicator/blob/master/strategy/momentum/coppock_curve_strategy.go#L59>)

```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.

<a name="ElderRayStrategy"></a>
## type [ElderRayStrategy](<https://github.com/cinar/indicator/blob/master/strategy/momentum/elder_ray_strategy.go#L20-L23>)

Expand Down
96 changes: 96 additions & 0 deletions strategy/momentum/coppock_curve_strategy.go
Original file line number Diff line number Diff line change
@@ -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)
}
92 changes: 92 additions & 0 deletions strategy/momentum/coppock_curve_strategy_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
}
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