CCI Put Call Ratio Divergence
The CCI Put Call Ratio Divergence strategy is built around CCI Put Call Ratio Divergence.
Testing indicates an average annual return of about 133%. It performs best in the crypto market.
Signals trigger when Divergence confirms divergence setups on intraday (5m) data. This makes the method suitable for active traders.
Stops rely on ATR multiples and factors like CciPeriod, AtrMultiplier. Adjust these defaults to balance risk and reward.
Details
- Entry Criteria: see implementation for indicator conditions.
- Long/Short: Both directions.
- Exit Criteria: opposite signal or stop logic.
- Stops: Yes, using indicator-based calculations.
- Default Values:
CciPeriod = 20AtrMultiplier = 2mCandleType = TimeSpan.FromMinutes(5).TimeFrame()
- Filters:
- Category: Trend following
- Direction: Both
- Indicators: Divergence
- Stops: Yes
- Complexity: Intermediate
- Timeframe: Intraday (5m)
- Seasonality: No
- Neural Networks: No
- Divergence: Yes
- Risk Level: Medium
using System;
using System.Collections.Generic;
using Ecng.Common;
using Ecng.Serialization;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// CCI reversal strategy filtered by deterministic put/call ratio divergence.
/// </summary>
public class CciPutCallRatioDivergenceStrategy : Strategy
{
private readonly StrategyParam<int> _cciPeriod;
private readonly StrategyParam<decimal> _atrMultiplier;
private readonly StrategyParam<int> _cooldownBars;
private readonly StrategyParam<DataType> _candleType;
private CommodityChannelIndex _cci = null!;
private AverageTrueRange _atr = null!;
private decimal _prevPcr;
private decimal _currentPcr;
private decimal _prevPrice;
private decimal? _prevCci;
private int _cooldownRemaining;
/// <summary>
/// CCI period.
/// </summary>
public int CciPeriod
{
get => _cciPeriod.Value;
set => _cciPeriod.Value = value;
}
/// <summary>
/// ATR multiplier for stop loss.
/// </summary>
public decimal AtrMultiplier
{
get => _atrMultiplier.Value;
set => _atrMultiplier.Value = value;
}
/// <summary>
/// Closed candles to wait between position changes.
/// </summary>
public int CooldownBars
{
get => _cooldownBars.Value;
set => _cooldownBars.Value = value;
}
/// <summary>
/// Candle type for strategy calculation.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Initialize strategy.
/// </summary>
public CciPutCallRatioDivergenceStrategy()
{
_cciPeriod = Param(nameof(CciPeriod), 20)
.SetRange(10, 50)
.SetDisplay("CCI Period", "Period for CCI calculation", "Indicators");
_atrMultiplier = Param(nameof(AtrMultiplier), 2m)
.SetRange(1m, 5m)
.SetDisplay("ATR Multiplier", "Multiplier for ATR-based stop loss", "Risk Management");
_cooldownBars = Param(nameof(CooldownBars), 24)
.SetNotNegative()
.SetDisplay("Cooldown Bars", "Closed candles to wait before another position change", "General");
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
.SetDisplay("Candle Type", "Type of candles to use", "General");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_cci?.Reset();
_atr?.Reset();
_cci = null!;
_atr = null!;
_prevPcr = 0m;
_currentPcr = 0m;
_prevPrice = 0m;
_prevCci = null;
_cooldownRemaining = 0;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_cci = new CommodityChannelIndex
{
Length = CciPeriod
};
_atr = new AverageTrueRange
{
Length = 14
};
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(_cci, _atr, ProcessCandle)
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawIndicator(area, _cci);
DrawIndicator(area, _atr);
DrawOwnTrades(area);
}
StartProtection(
new Unit(2, UnitTypes.Percent),
new Unit(2, UnitTypes.Percent)
);
}
private void ProcessCandle(ICandleMessage candle, decimal cci, decimal atr)
{
if (candle.State != CandleStates.Finished)
return;
var price = candle.ClosePrice;
UpdatePutCallRatio(candle);
if (_prevPrice == 0m)
{
_prevPrice = price;
_prevPcr = _currentPcr;
_prevCci = cci;
return;
}
if (!IsFormedAndOnlineAndAllowTrading())
{
_prevPrice = price;
_prevPcr = _currentPcr;
_prevCci = cci;
return;
}
if (_cooldownRemaining > 0)
_cooldownRemaining--;
var bullishDivergence = price < _prevPrice && _currentPcr > _prevPcr;
var bearishDivergence = price > _prevPrice && _currentPcr < _prevPcr;
var oversoldCross = _prevCci is decimal previousCci && previousCci >= -100m && cci < -100m;
var overboughtCross = _prevCci is decimal previousCci2 && previousCci2 <= 100m && cci > 100m;
if (_cooldownRemaining == 0 && oversoldCross && bullishDivergence && Position <= 0)
{
BuyMarket(Volume + (Position < 0 ? Math.Abs(Position) : 0m));
_cooldownRemaining = CooldownBars;
}
else if (_cooldownRemaining == 0 && overboughtCross && bearishDivergence && Position >= 0)
{
SellMarket(Volume + (Position > 0 ? Math.Abs(Position) : 0m));
_cooldownRemaining = CooldownBars;
}
else if (Position > 0 && cci >= 20m)
{
SellMarket(Position);
_cooldownRemaining = CooldownBars;
}
else if (Position < 0 && cci <= -20m)
{
BuyMarket(Math.Abs(Position));
_cooldownRemaining = CooldownBars;
}
_prevPrice = price;
_prevPcr = _currentPcr;
_prevCci = cci;
}
private void UpdatePutCallRatio(ICandleMessage candle)
{
var priceChange = (candle.ClosePrice - candle.OpenPrice) / Math.Max(candle.OpenPrice, 1m);
var range = (candle.HighPrice - candle.LowPrice) / Math.Max(candle.OpenPrice, 1m);
var skew = Math.Min(0.2m, range * 5m);
if (priceChange >= 0)
_currentPcr = 0.8m - priceChange + skew;
else
_currentPcr = 1.1m + Math.Abs(priceChange) + skew;
_currentPcr = Math.Max(0.5m, Math.Min(2.0m, _currentPcr));
}
}
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, Unit, UnitTypes
from StockSharp.Algo.Indicators import CommodityChannelIndex, AverageTrueRange
from StockSharp.Algo.Strategies import Strategy
class cci_put_call_ratio_divergence_strategy(Strategy):
"""CCI reversal strategy filtered by deterministic put/call ratio divergence."""
def __init__(self):
super(cci_put_call_ratio_divergence_strategy, self).__init__()
self._cci_period = self.Param("CciPeriod", 20) \
.SetRange(10, 50) \
.SetDisplay("CCI Period", "Period for CCI calculation", "Indicators")
self._atr_multiplier = self.Param("AtrMultiplier", 2.0) \
.SetRange(1.0, 5.0) \
.SetDisplay("ATR Multiplier", "Multiplier for ATR-based stop loss", "Risk Management")
self._cooldown_bars = self.Param("CooldownBars", 24) \
.SetNotNegative() \
.SetDisplay("Cooldown Bars", "Closed candles to wait before another position change", "General")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(4))) \
.SetDisplay("Candle Type", "Type of candles to use", "General")
self._cci = None
self._atr = None
self._prev_pcr = 0.0
self._current_pcr = 0.0
self._prev_price = 0.0
self._prev_cci = None
self._cooldown_remaining = 0
@property
def candle_type(self):
return self._candle_type.Value
def GetWorkingSecurities(self):
return [(self.Security, self.candle_type)]
def OnReseted(self):
super(cci_put_call_ratio_divergence_strategy, self).OnReseted()
self._cci = None
self._atr = None
self._prev_pcr = 0.0
self._current_pcr = 0.0
self._prev_price = 0.0
self._prev_cci = None
self._cooldown_remaining = 0
def OnStarted2(self, time):
super(cci_put_call_ratio_divergence_strategy, self).OnStarted2(time)
self._cci = CommodityChannelIndex()
self._cci.Length = int(self._cci_period.Value)
self._atr = AverageTrueRange()
self._atr.Length = 14
subscription = self.SubscribeCandles(self.candle_type)
subscription.Bind(self._cci, self._atr, self.ProcessCandle).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawIndicator(area, self._cci)
self.DrawIndicator(area, self._atr)
self.DrawOwnTrades(area)
self.StartProtection(
Unit(2, UnitTypes.Percent),
Unit(2, UnitTypes.Percent)
)
def ProcessCandle(self, candle, cci, atr):
if candle.State != CandleStates.Finished:
return
price = float(candle.ClosePrice)
cci_val = float(cci)
self.UpdatePutCallRatio(candle)
if self._prev_price == 0.0:
self._prev_price = price
self._prev_pcr = self._current_pcr
self._prev_cci = cci_val
return
if not self.IsFormedAndOnlineAndAllowTrading():
self._prev_price = price
self._prev_pcr = self._current_pcr
self._prev_cci = cci_val
return
if self._cooldown_remaining > 0:
self._cooldown_remaining -= 1
cooldown = int(self._cooldown_bars.Value)
bullish_divergence = price < self._prev_price and self._current_pcr > self._prev_pcr
bearish_divergence = price > self._prev_price and self._current_pcr < self._prev_pcr
oversold_cross = self._prev_cci is not None and self._prev_cci >= -100.0 and cci_val < -100.0
overbought_cross = self._prev_cci is not None and self._prev_cci <= 100.0 and cci_val > 100.0
if self._cooldown_remaining == 0 and oversold_cross and bullish_divergence and self.Position <= 0:
vol = self.Volume
if self.Position < 0:
vol = self.Volume + Math.Abs(self.Position)
self.BuyMarket(vol)
self._cooldown_remaining = cooldown
elif self._cooldown_remaining == 0 and overbought_cross and bearish_divergence and self.Position >= 0:
vol = self.Volume
if self.Position > 0:
vol = self.Volume + Math.Abs(self.Position)
self.SellMarket(vol)
self._cooldown_remaining = cooldown
elif self.Position > 0 and cci_val >= 20.0:
self.SellMarket(self.Position)
self._cooldown_remaining = cooldown
elif self.Position < 0 and cci_val <= -20.0:
self.BuyMarket(Math.Abs(self.Position))
self._cooldown_remaining = cooldown
self._prev_price = price
self._prev_pcr = self._current_pcr
self._prev_cci = cci_val
def UpdatePutCallRatio(self, candle):
price_change = float((candle.ClosePrice - candle.OpenPrice) / max(float(candle.OpenPrice), 1.0))
range_val = float((candle.HighPrice - candle.LowPrice) / max(float(candle.OpenPrice), 1.0))
skew = min(0.2, range_val * 5.0)
if price_change >= 0:
self._current_pcr = 0.8 - price_change + skew
else:
self._current_pcr = 1.1 + abs(price_change) + skew
self._current_pcr = max(0.5, min(2.0, self._current_pcr))
def CreateClone(self):
return cci_put_call_ratio_divergence_strategy()