The Color Coppock Strategy implements a trading system based on a modified Coppock oscillator. The oscillator sums two Rate of Change (ROC) values and smooths the result with a moving average. Rising momentum generates long signals, while falling momentum generates short signals.
How It Works
Calculate two ROC values with different periods.
Sum both ROC values and apply a Simple Moving Average for smoothing.
Compare the current oscillator value with the previous two values:
If the oscillator turns upward after declining, the strategy enters a long position or closes an existing short.
If the oscillator turns downward after rising, the strategy enters a short position or closes an existing long.
Position volume is taken from the strategy's Volume property.
Parameters
Name
Description
Roc1Period
Period for the first ROC calculation.
Roc2Period
Period for the second ROC calculation.
SmoothingPeriod
SMA period applied to the sum of both ROC values.
CandleType
Candle type used for indicator calculations.
Usage
Attach the strategy to a security and set the desired parameters.
The strategy subscribes to the specified candles and processes only finished candles.
Trades are executed with market orders using the default volume.
Notes
The strategy uses only high-level API calls such as SubscribeCandles and market order helpers.
All comments inside the code are written in English.
using System;
using System.Linq;
using System.Collections.Generic;
using Ecng.Common;
using Ecng.Collections;
using Ecng.Serialization;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// Strategy based on the Coppock Curve oscillator.
/// Uses ROC sum smoothed by SMA.
/// </summary>
public class ColorCoppockStrategy : Strategy
{
private readonly StrategyParam<int> _roc1Period;
private readonly StrategyParam<int> _roc2Period;
private readonly StrategyParam<int> _smoothingPeriod;
private readonly StrategyParam<DataType> _candleType;
private readonly List<decimal> _closes = new();
private readonly List<decimal> _coppockValues = new();
private decimal? _prevCoppock;
private decimal? _prevPrevCoppock;
public int Roc1Period { get => _roc1Period.Value; set => _roc1Period.Value = value; }
public int Roc2Period { get => _roc2Period.Value; set => _roc2Period.Value = value; }
public int SmoothingPeriod { get => _smoothingPeriod.Value; set => _smoothingPeriod.Value = value; }
public DataType CandleType { get => _candleType.Value; set => _candleType.Value = value; }
public ColorCoppockStrategy()
{
_roc1Period = Param(nameof(Roc1Period), 14)
.SetDisplay("ROC1 Period", "First ROC calculation period", "Parameters");
_roc2Period = Param(nameof(Roc2Period), 10)
.SetDisplay("ROC2 Period", "Second ROC calculation period", "Parameters");
_smoothingPeriod = Param(nameof(SmoothingPeriod), 10)
.SetDisplay("Smoothing Period", "SMA period for ROC sum", "Parameters");
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
.SetDisplay("Candle Type", "Time frame for processing", "General");
}
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
=> [(Security, CandleType)];
protected override void OnReseted()
{
base.OnReseted();
_closes.Clear();
_coppockValues.Clear();
_prevCoppock = _prevPrevCoppock = null;
}
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
var sma = new ExponentialMovingAverage { Length = Roc1Period };
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(sma, ProcessCandle)
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawOwnTrades(area);
}
}
private void ProcessCandle(ICandleMessage candle, decimal smaValue)
{
if (candle.State != CandleStates.Finished)
return;
_closes.Add(candle.ClosePrice);
var maxPeriod = Math.Max(Roc1Period, Roc2Period);
if (_closes.Count > maxPeriod + SmoothingPeriod + 5)
_closes.RemoveAt(0);
if (!IsFormedAndOnlineAndAllowTrading())
return;
if (_closes.Count <= maxPeriod)
return;
// Calculate ROC values
var idx = _closes.Count - 1;
decimal roc1 = 0, roc2 = 0;
if (idx >= Roc1Period && _closes[idx - Roc1Period] != 0)
roc1 = (_closes[idx] - _closes[idx - Roc1Period]) / _closes[idx - Roc1Period] * 100m;
if (idx >= Roc2Period && _closes[idx - Roc2Period] != 0)
roc2 = (_closes[idx] - _closes[idx - Roc2Period]) / _closes[idx - Roc2Period] * 100m;
_coppockValues.Add(roc1 + roc2);
if (_coppockValues.Count > SmoothingPeriod + 5)
_coppockValues.RemoveAt(0);
if (_coppockValues.Count < SmoothingPeriod)
return;
// SMA of ROC sum
var coppock = _coppockValues.Skip(_coppockValues.Count - SmoothingPeriod).Average();
if (_prevCoppock is decimal prev && _prevPrevCoppock is decimal prevPrev)
{
// Buy when Coppock turns up from a bottom
if (prev < prevPrev && coppock > prev && Position <= 0)
BuyMarket();
// Sell when Coppock turns down from a top
else if (prev > prevPrev && coppock < prev && Position >= 0)
SellMarket();
}
_prevPrevCoppock = _prevCoppock;
_prevCoppock = coppock;
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan, Math
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Indicators import ExponentialMovingAverage
from StockSharp.Algo.Strategies import Strategy
class color_coppock_strategy(Strategy):
def __init__(self):
super(color_coppock_strategy, self).__init__()
self._roc1_period = self.Param("Roc1Period", 14) \
.SetDisplay("ROC1 Period", "First ROC calculation period", "Parameters")
self._roc2_period = self.Param("Roc2Period", 10) \
.SetDisplay("ROC2 Period", "Second ROC calculation period", "Parameters")
self._smoothing_period = self.Param("SmoothingPeriod", 10) \
.SetDisplay("Smoothing Period", "SMA period for ROC sum", "Parameters")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(4))) \
.SetDisplay("Candle Type", "Time frame for processing", "General")
self._closes = []
self._coppock_values = []
self._prev_coppock = None
self._prev_prev_coppock = None
@property
def roc1_period(self):
return self._roc1_period.Value
@property
def roc2_period(self):
return self._roc2_period.Value
@property
def smoothing_period(self):
return self._smoothing_period.Value
@property
def candle_type(self):
return self._candle_type.Value
def OnReseted(self):
super(color_coppock_strategy, self).OnReseted()
self._closes = []
self._coppock_values = []
self._prev_coppock = None
self._prev_prev_coppock = None
def OnStarted2(self, time):
super(color_coppock_strategy, self).OnStarted2(time)
sma = ExponentialMovingAverage()
sma.Length = int(self.roc1_period)
subscription = self.SubscribeCandles(self.candle_type)
subscription.Bind(sma, self.process_candle).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawOwnTrades(area)
def process_candle(self, candle, sma_value):
if candle.State != CandleStates.Finished:
return
close = float(candle.ClosePrice)
self._closes.append(close)
r1p = int(self.roc1_period)
r2p = int(self.roc2_period)
sp = int(self.smoothing_period)
max_period = max(r1p, r2p)
if len(self._closes) > max_period + sp + 5:
self._closes.pop(0)
if len(self._closes) <= max_period:
return
idx = len(self._closes) - 1
roc1 = 0.0
roc2 = 0.0
if idx >= r1p and self._closes[idx - r1p] != 0.0:
roc1 = (self._closes[idx] - self._closes[idx - r1p]) / self._closes[idx - r1p] * 100.0
if idx >= r2p and self._closes[idx - r2p] != 0.0:
roc2 = (self._closes[idx] - self._closes[idx - r2p]) / self._closes[idx - r2p] * 100.0
self._coppock_values.append(roc1 + roc2)
if len(self._coppock_values) > sp + 5:
self._coppock_values.pop(0)
if len(self._coppock_values) < sp:
return
# SMA of ROC sum
coppock = sum(self._coppock_values[-sp:]) / sp
if self._prev_coppock is not None and self._prev_prev_coppock is not None:
# Buy when Coppock turns up from bottom
if self._prev_coppock < self._prev_prev_coppock and coppock > self._prev_coppock and self.Position <= 0:
self.BuyMarket()
# Sell when Coppock turns down from top
elif self._prev_coppock > self._prev_prev_coppock and coppock < self._prev_coppock and self.Position >= 0:
self.SellMarket()
self._prev_prev_coppock = self._prev_coppock
self._prev_coppock = coppock
def CreateClone(self):
return color_coppock_strategy()