Estrategia de Divergencia CCI
Las divergencias del Índice de Canal de Materias Primas (CCI) pueden presagiar reversiones de tendencia cuando el precio se mueve en dirección opuesta al indicador. Esta estrategia compara los máximos y mínimos del precio con los del CCI para identificar fortaleza o debilidad ocultas.
Las pruebas indican un rendimiento anual promedio de aproximadamente el 91%. Funciona mejor en el mercado de acciones.
En cada vela, el sistema actualiza los valores recientes de precio y CCI, marcando una divergencia alcista cuando el precio hace un nuevo mínimo mientras el CCI forma un mínimo más alto. La divergencia bajista es la situación opuesta. Cuando una divergencia se alinea con niveles de sobreventa o sobrecompra, se abre una operación con un stop de volatilidad.
Las salidas ocurren cuando el CCI vuelve a cruzar la línea cero, señalando que el impulso se ha agotado. Dado que las divergencias pueden persistir, las reglas también se reinician después de un número fijo de barras para evitar señales obsoletas.
Detalles
- Criterios de entrada: Divergencia precio/CCI con CCI por debajo de -100 para largos o por encima de +100 para cortos.
- Largo/Corto: Ambos.
- Criterios de salida: CCI cruzando el cero o stop-loss.
- Stops: Sí, basado en porcentaje.
- Valores predeterminados:
CciPeriod= 20DivergencePeriod= 5OverboughtLevel= 100OversoldLevel= -100CandleType= 15 minuteStopLossPercent= 2
- Filtros:
- Categoría: Divergencia
- Dirección: Ambos
- Indicadores: CCI
- Stops: Sí
- Complejidad: Avanzado
- Marco temporal: Intradía
- Estacionalidad: No
- Redes neuronales: No
- Divergencia: Sí
- Nivel de riesgo: Medio
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()