CCI 背离策略
当价格走势与商品通道指数(CCI)方向相反时,可能预示趋势即将反转。本策略比较价格和 CCI 的波峰波谷,以识别隐藏的强弱。
测试表明年均收益约为 91%,该策略在股票市场表现最佳。
系统在每根蜡烛更新最近的价格与 CCI 值。当价格创出新低而 CCI 形成更高的低点时,标记为看涨背离;反之则为看跌背离。当背离同时处于超卖或超买区域时,在波动性止损的配合下开仓。
平仓条件为 CCI 重新穿越零线,表明动能已完成。由于背离可能持续,规则还会在固定的根数后重置,以避免过时信号。
细节
- 入场条件:价格与 CCI 背离,且做多时 CCI 低于 -100,做空时高于 +100。
- 多/空:双向。
- 退出条件:CCI 穿越零线或止损。
- 止损:是,按百分比计算。
- 默认值:
CciPeriod= 20DivergencePeriod= 5OverboughtLevel= 100OversoldLevel= -100CandleType= 15 分钟StopLossPercent= 2
- 过滤条件:
- 类别: 背离
- 方向: 双向
- 指标: CCI
- 止损: 有
- 复杂度: 高级
- 时间框架: 日内
- 季节性: 无
- 神经网络: 无
- 背离: 是
- 风险级别: 中等
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()