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