Estratégia de Divergência CCI
As divergências do Índice de Canal de Commodities (CCI) podem prenunciar reversões de tendência quando o preço se move na direção oposta ao indicador. Esta estratégia compara as máximas e mínimas de swing no preço com as do CCI para identificar força ou fraqueza ocultas.
Os testes indicam um retorno anual médio de aproximadamente 91%. Tem melhor desempenho no mercado de ações.
Em cada vela, o sistema atualiza os valores recentes de preço e CCI, sinalizando uma divergência altista quando o preço faz uma nova mínima enquanto o CCI forma uma mínima mais alta. A divergência baixista é o oposto. Quando uma divergência se alinha com níveis de sobrevenda ou sobrecompra, uma operação é aberta com um stop de volatilidade.
As saídas ocorrem quando o CCI cruza de volta pela linha zero, sinalizando que o impulso se esgotou. Como as divergências podem persistir, as regras também são redefinidas após um número fixo de barras para evitar sinais obsoletos.
Detalhes
- Critérios de entrada: Divergência preço/CCI com CCI abaixo de -100 para comprados ou acima de +100 para vendidos.
- Comprado/Vendido: Ambos.
- Critérios de saída: CCI cruzando o zero ou stop-loss.
- Stops: Sim, baseado em percentual.
- Valores padrão:
CciPeriod= 20DivergencePeriod= 5OverboughtLevel= 100OversoldLevel= -100CandleType= 15 minuteStopLossPercent= 2
- Filtros:
- Categoria: Divergência
- Direção: Ambos
- Indicadores: CCI
- Stops: Sim
- Complexidade: Avançado
- Período: Intradiário
- Sazonalidade: Não
- Redes neurais: Não
- Divergência: Sim
- Nível de risco: Médio
using System;
using System.Collections.Generic;
using System.Linq;
using Ecng.Common;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// Trades confirmed price/CCI pivot divergence in the CCI extreme zone.
/// A detected divergence stays valid for DivergencePeriod bars.
/// Exits on the favorable zero crossing or actual-fill percent protection.
/// </summary>
public class CciDivergenceStrategy : Strategy
{
private readonly StrategyParam<int> _cciPeriod;
private readonly StrategyParam<int> _divergencePeriod;
private readonly StrategyParam<decimal> _overboughtLevel;
private readonly StrategyParam<decimal> _oversoldLevel;
private readonly StrategyParam<DataType> _candleType;
private readonly StrategyParam<decimal> _stopLossPercent;
private readonly List<(decimal High, decimal Low, decimal Cci)> _window = new();
private (decimal Price, decimal Cci)? _lastLow;
private (decimal Price, decimal Cci)? _lastHigh;
private decimal? _previousCci;
private Sides? _signal;
private int _signalAge;
private Order _pendingOrder;
private CommodityChannelIndex _cci;
public int CciPeriod { get => _cciPeriod.Value; set => _cciPeriod.Value = value; }
public int DivergencePeriod { get => _divergencePeriod.Value; set => _divergencePeriod.Value = value; }
public decimal OverboughtLevel { get => _overboughtLevel.Value; set => _overboughtLevel.Value = value; }
public decimal OversoldLevel { get => _oversoldLevel.Value; set => _oversoldLevel.Value = value; }
public DataType CandleType { get => _candleType.Value; set => _candleType.Value = value; }
public decimal StopLossPercent { get => _stopLossPercent.Value; set => _stopLossPercent.Value = value; }
public CciDivergenceStrategy()
{
_cciPeriod = Param(nameof(CciPeriod), 20).SetGreaterThanZero()
.SetDisplay("CCI Period", "Typical-price CCI length", "Indicators");
_divergencePeriod = Param(nameof(DivergencePeriod), 5).SetRange(3, 31)
.SetDisplay("Divergence Period", "Odd-width confirmed price pivot window and bars a detected divergence stays valid", "Pattern");
_overboughtLevel = Param(nameof(OverboughtLevel), 100m)
.SetDisplay("Overbought Level", "CCI at the later high must exceed this level", "Pattern");
_oversoldLevel = Param(nameof(OversoldLevel), -100m)
.SetDisplay("Oversold Level", "CCI at the later low must be below this level", "Pattern");
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(15).TimeFrame())
.SetDisplay("Candle Type", "CCI and price pivot timeframe", "General");
_stopLossPercent = Param(nameof(StopLossPercent), 2m).SetNotNegative()
.SetDisplay("Stop Loss (%)", "Actual-fill percent stop; zero disables it", "Protection");
OrderRegistering += order => _pendingOrder = order;
}
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
=> [(Security, CandleType), (Security, DataType.Level1)];
protected override void OnReseted()
{
base.OnReseted();
ClearState();
}
private void ClearState()
{
_window.Clear();
_lastLow = _lastHigh = null;
_previousCci = null;
_signal = null;
_signalAge = 0;
_pendingOrder = null;
_cci = null;
}
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
if (DivergencePeriod % 2 != 1 || OverboughtLevel <= 0m || OversoldLevel >= 0m)
throw new InvalidOperationException("DivergencePeriod must be odd, OverboughtLevel positive, and OversoldLevel negative.");
ClearState();
StartProtection(new Unit(), new Unit(StopLossPercent, UnitTypes.Percent), useMarketOrders: true, isLocalStop: true);
foreach (var field in new[] { Level1Fields.BestBidPrice, Level1Fields.BestAskPrice })
{
var quotes = new Subscription(DataType.Level1, Security);
quotes.MarketData.BuildField = field;
SubscribeLevel1(quotes).Bind(ObserveProtectionQuote).Start();
}
_cci = new CommodityChannelIndex { Length = CciPeriod };
var candles = SubscribeCandles(CandleType);
candles.Bind(_cci, ProcessCandle).Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, candles);
DrawIndicator(area, _cci);
DrawOwnTrades(area);
}
}
private void ObserveProtectionQuote(Level1ChangeMessage quote)
{
// Native actual-fill protection evaluates executable quotes between signal candles.
}
private void ProcessCandle(ICandleMessage candle, decimal cciValue)
{
if (candle.State != CandleStates.Finished || !_cci.IsFormed)
return;
var crossedUp = _previousCci is decimal oldUp && oldUp <= 0m && cciValue > 0m;
var crossedDown = _previousCci is decimal oldDown && oldDown >= 0m && cciValue < 0m;
_previousCci = cciValue;
if (_signal is not null && ++_signalAge > DivergencePeriod)
_signal = null;
_window.Add((candle.HighPrice, candle.LowPrice, cciValue));
if (_window.Count == DivergencePeriod)
{
var middle = DivergencePeriod / 2;
var pivot = _window[middle];
if (_window.Where((_, index) => index != middle).All(bar => pivot.Low < bar.Low))
{
if (_lastLow is { } prior && pivot.Low < prior.Price &&
pivot.Cci > prior.Cci && pivot.Cci < OversoldLevel)
{
_signal = Sides.Buy;
_signalAge = 0;
}
_lastLow = (pivot.Low, pivot.Cci);
}
if (_window.Where((_, index) => index != middle).All(bar => pivot.High > bar.High))
{
if (_lastHigh is { } prior && pivot.High > prior.Price &&
pivot.Cci < prior.Cci && pivot.Cci > OverboughtLevel)
{
_signal = Sides.Sell;
_signalAge = 0;
}
_lastHigh = (pivot.High, pivot.Cci);
}
_window.RemoveAt(0);
}
if (!IsFormedAndOnlineAndAllowTrading() ||
_pendingOrder is not null && _pendingOrder.State is not (OrderStates.Done or OrderStates.Failed))
return;
if (Position > 0m && crossedUp)
SellMarket(Position);
else if (Position < 0m && crossedDown)
BuyMarket(Math.Abs(Position));
else if (Position == 0m && _signal is Sides side)
{
if (side == Sides.Buy)
BuyMarket(Volume);
else
SellMarket(Volume);
_signal = null;
}
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
clr.AddReference("StockSharp.BusinessEntities")
from System import TimeSpan, Decimal, Math
from StockSharp.Messages import DataType, CandleStates, Unit, UnitTypes, OrderStates, Level1Fields, Sides
from StockSharp.BusinessEntities import Subscription
from StockSharp.Algo.Indicators import CommodityChannelIndex
from StockSharp.Algo.Strategies import Strategy
class cci_divergence_strategy(Strategy):
"""Trade confirmed price/CCI pivot divergence in the CCI extreme zone.
A detected divergence stays valid for DivergencePeriod bars.
"""
def __init__(self):
super(cci_divergence_strategy, self).__init__()
self._cci_period = self.Param("CciPeriod", 20).SetGreaterThanZero().SetDisplay("CCI Period", "Typical-price CCI length", "Indicators")
self._divergence_period = self.Param("DivergencePeriod", 5).SetRange(3, 31).SetDisplay("Divergence Period", "Odd-width confirmed price pivot window and bars a detected divergence stays valid", "Pattern")
self._overbought_level = self.Param("OverboughtLevel", 100.0).SetDisplay("Overbought Level", "CCI at the later high must exceed this level", "Pattern")
self._oversold_level = self.Param("OversoldLevel", -100.0).SetDisplay("Oversold Level", "CCI at the later low must be below this level", "Pattern")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(15))).SetDisplay("Candle Type", "CCI and price pivot timeframe", "General")
self._stop_loss_percent = self.Param("StopLossPercent", 2.0).SetNotNegative().SetDisplay("Stop Loss (%)", "Actual-fill percent stop; zero disables it", "Protection")
self._clear_state()
self.OrderRegistering += self._track_pending
@property
def candle_type(self):
return self._candle_type.Value
def GetWorkingSecurities(self):
return [(self.Security, self.candle_type), (self.Security, DataType.Level1)]
def _track_pending(self, order):
self._pending_order = order
def _clear_state(self):
self._window = []
self._last_low = None
self._last_high = None
self._previous_cci = None
self._signal = None
self._signal_age = 0
self._pending_order = None
self._cci = None
def OnReseted(self):
super(cci_divergence_strategy, self).OnReseted()
self._clear_state()
def OnStarted2(self, time):
super(cci_divergence_strategy, self).OnStarted2(time)
if self._divergence_period.Value % 2 != 1 or self._overbought_level.Value <= 0 or self._oversold_level.Value >= 0:
raise ValueError("DivergencePeriod must be odd, OverboughtLevel positive, and OversoldLevel negative.")
self._clear_state()
self.StartProtection(Unit(), Unit(Decimal(self._stop_loss_percent.Value), UnitTypes.Percent), useMarketOrders=True, isLocalStop=True)
for field in (Level1Fields.BestBidPrice, Level1Fields.BestAskPrice):
quotes = Subscription(DataType.Level1, self.Security)
quotes.MarketData.BuildField = field
self.SubscribeLevel1(quotes).Bind(self._observe_protection_quote).Start()
self._cci = CommodityChannelIndex()
self._cci.Length = self._cci_period.Value
candles = self.SubscribeCandles(self.candle_type)
candles.Bind(self._cci, self._process_candle).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, candles)
self.DrawIndicator(area, self._cci)
self.DrawOwnTrades(area)
def _observe_protection_quote(self, quote):
# Native actual-fill protection evaluates executable quotes between signal candles.
pass
def _process_candle(self, candle, cci_value):
if candle.State != CandleStates.Finished or not self._cci.IsFormed:
return
up_cross = self._previous_cci is not None and self._previous_cci <= 0 and cci_value > 0
down_cross = self._previous_cci is not None and self._previous_cci >= 0 and cci_value < 0
self._previous_cci = cci_value
width = self._divergence_period.Value
if self._signal is not None:
self._signal_age += 1
if self._signal_age > width:
self._signal = None
self._window.append((candle.HighPrice, candle.LowPrice, cci_value))
if len(self._window) == width:
middle = width // 2
pivot = self._window[middle]
if all(pivot[1] < bar[1] for i, bar in enumerate(self._window) if i != middle):
if (self._last_low is not None and pivot[1] < self._last_low[0] and
pivot[2] > self._last_low[1] and pivot[2] < self._oversold_level.Value):
self._signal = Sides.Buy
self._signal_age = 0
self._last_low = (pivot[1], pivot[2])
if all(pivot[0] > bar[0] for i, bar in enumerate(self._window) if i != middle):
if (self._last_high is not None and pivot[0] > self._last_high[0] and
pivot[2] < self._last_high[1] and pivot[2] > self._overbought_level.Value):
self._signal = Sides.Sell
self._signal_age = 0
self._last_high = (pivot[0], pivot[2])
self._window.pop(0)
if not self.IsFormedAndOnlineAndAllowTrading():
return
if self._pending_order is not None and self._pending_order.State not in (OrderStates.Done, OrderStates.Failed):
return
if self.Position > 0 and up_cross:
self.SellMarket(self.Position)
elif self.Position < 0 and down_cross:
self.BuyMarket(Math.Abs(self.Position))
elif self.Position == 0 and self._signal is not None:
if self._signal == Sides.Buy:
self.BuyMarket(self.Volume)
else:
self.SellMarket(self.Volume)
self._signal = None
def CreateClone(self):
return cci_divergence_strategy()