CCIダイバージェンス戦略
商品チャンネル指数(CCI)のダイバージェンスは、価格がインジケーターと反対方向に動くときにトレンド転換を予告することがあります。この戦略は価格のスイング高値・安値とCCIのそれを比較して、隠れた強さや弱さを識別します。
テストでは年平均リターン約91%を示しています。株式市場で最もよく機能します。
各ロウソクでシステムは直近の価格とCCI値を更新し、価格が新安値を付けながらCCIがより高い安値を形成すると強気ダイバージェンスにフラグを立てます。弱気ダイバージェンスはその逆です。ダイバージェンスが売られすぎまたは買われすぎのレベルと一致すると、ボラティリティストップとともにトレードが開かれます。
CCIがゼロラインを再び越えるとエグジットが発生し、インパルスが出し切られたことを示します。ダイバージェンスは持続することがあるため、古いシグナルを避けるために固定本数後にルールもリセットされます。
詳細
- エントリー条件: 価格/CCIダイバージェンス、ロングはCCIが-100以下、ショートはCCIが+100以上。
- ロング/ショート: 両方。
- エグジット条件: CCIがゼロを越えるかストップロス。
- ストップ: はい、パーセントベース。
- デフォルト値:
CciPeriod= 20DivergencePeriod= 5OverboughtLevel= 100OversoldLevel= -100CandleType= 15 minuteStopLossPercent= 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()