Стратегия iCCI iMA
Эта стратегия является переносом советника MetaTrader «iCCI iMA». Алгоритм торгует пересечения индикатора Commodity Channel Index (CCI) с экспоненциальной скользящей средней (EMA), рассчитанной непосредственно по значениям CCI. Дополнительный CCI контролирует возвраты от зон перекупленности/перепроданности около уровней ±100. Объём заявок задаётся в лотах, при необходимости масштабируется от баланса счёта, а каждая сделка защищена стоп-лоссом и тейк-профитом, задаваемыми в пунктах.
Логика работы
- Источник данных. Все индикаторы получают значения из настраиваемой серии свечей (по умолчанию — минутные), используя типичную цену
(high + low + close) / 3. - Индикаторы. Основной CCI с периодом
CciPeriodизмеряет импульс. EMA длинойMaPeriod, рассчитанная по потоку CCI, формирует сигнальную линию. Второй CCI с периодомCciClosePeriodотслеживает выходы из зон ±100. - Вход в позицию. Длинная позиция открывается, когда текущий CCI находится выше своей EMA, а значение два завершённых бара назад было ниже EMA (вверх пересечение). Короткая позиция открывается при обратном пересечении вниз. Торговля начинается только после формирования всех индикаторов и накопления двух завершённых свечей, что полностью повторяет сдвиги в исходном MQL-коде.
- Выход из позиции. Лонг закрывается, если вспомогательный CCI опускается ниже +100 либо основной CCI пересекает EMA сверху вниз при условии, что два бара назад он был выше. Шорт закрывается при подъёме CCIclose выше −100 либо при пересечении CCI и EMA снизу вверх. Дополнительно каждую закрытую свечу проверяются уровни защиты: для лонга позиция закрывается при достижении
entry − stopLossPips * pipSizeи фиксируется прибыль приentry + takeProfitPips * pipSize; для шорта используются симметричные уровни. Размер пункта вычисляется по шагу цены инструмента и для инструментов с 3 или 5 знаками умножается на 10, как и в оригинальном советнике. - Управление объёмом. Базовый лот (
LotSize) проверяется на соответствиеVolumeStep,MinVolumeиMaxVolumeинструмента. При включённой функции money-management объём умножается на целый коэффициент, равный частному от деления баланса наDepositPerLot, ограниченному значением 20. Коэффициент пересчитывается на каждом баре и полностью повторяет логику MQL.
Параметры
- Candle Type — тип свечей, используемых в расчётах.
- CCI Period — период основного CCI, формирующего сигналы пересечения.
- CCI Close Period — период вспомогательного CCI для контроля уровней ±100.
- CCI EMA Period — период EMA, сглаживающей поток значений CCI.
- Lot Size — базовый торговый объём в лотах.
- Enable Money Management — включает масштабирование объёма по балансу.
- Deposit Per Lot — прибавка к балансу, необходимая для увеличения коэффициента объёма на единицу (работает только при включённом money-management).
- Stop Loss (pips) — расстояние стоп-лосса в пунктах, ноль отключает защиту.
- Take Profit (pips) — расстояние тейк-профита в пунктах, ноль отключает цель.
Стратегия начинает торговать только после появления двух завершённых свечей, чтобы условия сравнения сдвига на два бара соответствовали оригиналу. Проверка стоп-лосса и тейк-профита выполняется по максимумам/минимумам закрывшейся свечи, что приближает серверные защитные ордера MetaTrader в рамках высокоуровневого API StockSharp.
using System;
using System.Linq;
using System.Collections.Generic;
using Ecng.Common;
using Ecng.Collections;
using Ecng.Serialization;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// CCI and EMA crossover strategy converted from the MetaTrader iCCI iMA expert.
/// The strategy trades when the Commodity Channel Index crosses its exponential moving average.
/// </summary>
public class IcciImaStrategy : Strategy
{
private readonly StrategyParam<int> _cciPeriod;
private readonly StrategyParam<int> _cciClosePeriod;
private readonly StrategyParam<int> _maPeriod;
private readonly StrategyParam<decimal> _stopLossPips;
private readonly StrategyParam<decimal> _takeProfitPips;
private readonly StrategyParam<bool> _useMoneyManagement;
private readonly StrategyParam<decimal> _depositPerLot;
private readonly StrategyParam<decimal> _lotSize;
private readonly StrategyParam<DataType> _candleType;
private CommodityChannelIndex _cci = null!;
private CommodityChannelIndex _cciClose = null!;
private ExponentialMovingAverage _cciMa = null!;
private decimal _pipSize;
private decimal _lotMultiplier = 1m;
private decimal? _entryPrice;
private decimal? _prevCci;
private decimal? _prev2Cci;
private decimal? _prevCciClose;
private decimal? _prev2CciClose;
private decimal? _prevMa;
private decimal? _prev2Ma;
private int _historyCount;
/// <summary>
/// Constructor.
/// </summary>
public IcciImaStrategy()
{
_cciPeriod = Param(nameof(CciPeriod), 14)
.SetGreaterThanZero()
.SetDisplay("CCI Period", "Length of the main CCI indicator", "Indicators")
.SetOptimize(5, 100, 1);
_cciClosePeriod = Param(nameof(CciClosePeriod), 14)
.SetGreaterThanZero()
.SetDisplay("CCI Close Period", "Length of the CCI used for overbought and oversold exits", "Indicators")
.SetOptimize(5, 100, 1);
_maPeriod = Param(nameof(MaPeriod), 15)
.SetGreaterThanZero()
.SetDisplay("CCI EMA Period", "Length of the EMA applied to the CCI values", "Indicators")
.SetOptimize(5, 100, 1);
_stopLossPips = Param(nameof(StopLossPips), 50m)
.SetDisplay("Stop Loss (pips)", "Protective stop distance in pips", "Risk");
_takeProfitPips = Param(nameof(TakeProfitPips), 40m)
.SetDisplay("Take Profit (pips)", "Profit target distance in pips", "Risk");
_useMoneyManagement = Param(nameof(UseMoneyManagement), false)
.SetDisplay("Enable Money Management", "Scale position size by account balance", "Money Management");
_depositPerLot = Param(nameof(DepositPerLot), 1000m)
.SetGreaterThanZero()
.SetDisplay("Deposit Per Lot", "Balance required to increase the lot multiplier", "Money Management");
_lotSize = Param(nameof(LotSize), 0.1m)
.SetGreaterThanZero()
.SetDisplay("Lot Size", "Base trading volume in lots", "Trading")
.SetOptimize(0.01m, 1m, 0.01m);
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
.SetDisplay("Candle Type", "Data series used for calculations", "General");
}
/// <summary>
/// Length of the primary CCI indicator.
/// </summary>
public int CciPeriod
{
get => _cciPeriod.Value;
set => _cciPeriod.Value = value;
}
/// <summary>
/// Length of the CCI used for exit signals around ±100.
/// </summary>
public int CciClosePeriod
{
get => _cciClosePeriod.Value;
set => _cciClosePeriod.Value = value;
}
/// <summary>
/// Exponential moving average period applied to the CCI values.
/// </summary>
public int MaPeriod
{
get => _maPeriod.Value;
set => _maPeriod.Value = value;
}
/// <summary>
/// Stop-loss distance expressed in pips.
/// </summary>
public decimal StopLossPips
{
get => _stopLossPips.Value;
set => _stopLossPips.Value = value;
}
/// <summary>
/// Take-profit distance expressed in pips.
/// </summary>
public decimal TakeProfitPips
{
get => _takeProfitPips.Value;
set => _takeProfitPips.Value = value;
}
/// <summary>
/// Enable adaptive money management.
/// </summary>
public bool UseMoneyManagement
{
get => _useMoneyManagement.Value;
set => _useMoneyManagement.Value = value;
}
/// <summary>
/// Deposit amount required to increase the lot multiplier by one.
/// </summary>
public decimal DepositPerLot
{
get => _depositPerLot.Value;
set => _depositPerLot.Value = value;
}
/// <summary>
/// Base trading volume in lots.
/// </summary>
public decimal LotSize
{
get => _lotSize.Value;
set => _lotSize.Value = value;
}
/// <summary>
/// Candle type used for indicator calculations.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
ResetState();
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
ResetState();
_cci = new CommodityChannelIndex
{
Length = CciPeriod
};
_cciClose = new CommodityChannelIndex
{
Length = CciClosePeriod
};
_cciMa = new EMA
{
Length = MaPeriod,
};
_pipSize = CalculatePipSize();
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(_cci, _cciClose, ProcessCandle)
.Start();
// protection not needed
}
private void ProcessCandle(ICandleMessage candle, decimal cciValue, decimal cciCloseValue)
{
if (candle.State != CandleStates.Finished)
return;
var maValue = _cciMa.Process(new DecimalIndicatorValue(_cciMa, cciValue, candle.OpenTime) { IsFinal = true }).ToDecimal();
if (!_cci.IsFormed || !_cciClose.IsFormed || !_cciMa.IsFormed)
{
UpdateHistory(cciValue, cciCloseValue, maValue);
return;
}
// Update the lot multiplier according to the current balance settings.
UpdateMoneyManagement();
if (_historyCount < 2)
{
UpdateHistory(cciValue, cciCloseValue, maValue);
return;
}
// Check whether stop-loss or take-profit levels were touched on the latest candle.
HandleStops(candle);
// indicators formed check already done above
var cciTwoBarsAgo = _prev2Cci ?? 0m;
var maTwoBarsAgo = _prev2Ma ?? 0m;
var cciCloseTwoBarsAgo = _prev2CciClose ?? 0m;
// Determine exit conditions from the secondary CCI and the smoothed crossover.
var shouldCloseLong = (cciCloseTwoBarsAgo > 100m && cciCloseValue <= 100m) || (cciValue < maValue && cciTwoBarsAgo >= maTwoBarsAgo);
var shouldCloseShort = (cciCloseTwoBarsAgo < -100m && cciCloseValue >= -100m) || (cciValue > maValue && cciTwoBarsAgo <= maTwoBarsAgo);
if (Position > 0 && shouldCloseLong)
{
SellMarket();
_entryPrice = null;
}
else if (Position < 0 && shouldCloseShort)
{
BuyMarket();
_entryPrice = null;
}
// Validate the requested lot size against security constraints.
var volume = AdjustVolume(LotSize * _lotMultiplier);
if (volume > 0m)
{
if (cciValue > maValue && cciTwoBarsAgo < maTwoBarsAgo && Position <= 0)
{
var totalVolume = volume + Math.Abs(Position);
if (totalVolume > 0m)
{
BuyMarket();
_entryPrice = candle.ClosePrice;
}
}
else if (cciValue < maValue && cciTwoBarsAgo > maTwoBarsAgo && Position >= 0)
{
var totalVolume = volume + Math.Abs(Position);
if (totalVolume > 0m)
{
SellMarket();
_entryPrice = candle.ClosePrice;
}
}
}
if (Position == 0)
_entryPrice = null;
UpdateHistory(cciValue, cciCloseValue, maValue);
}
private void HandleStops(ICandleMessage candle)
{
if (_entryPrice == null)
return;
var priceStep = _pipSize > 0m ? _pipSize : Security?.PriceStep ?? 0m;
if (priceStep <= 0m)
return;
// Convert the configured pip distances into absolute price offsets.
var stopLossDistance = StopLossPips > 0m ? StopLossPips * priceStep : 0m;
var takeProfitDistance = TakeProfitPips > 0m ? TakeProfitPips * priceStep : 0m;
if (Position > 0)
{
var entry = _entryPrice.Value;
if (stopLossDistance > 0m && candle.LowPrice <= entry - stopLossDistance)
{
SellMarket();
_entryPrice = null;
return;
}
if (takeProfitDistance > 0m && candle.HighPrice >= entry + takeProfitDistance)
{
SellMarket();
_entryPrice = null;
}
}
else if (Position < 0)
{
var entry = _entryPrice.Value;
var absPosition = Math.Abs(Position);
if (stopLossDistance > 0m && candle.HighPrice >= entry + stopLossDistance)
{
BuyMarket();
_entryPrice = null;
return;
}
if (takeProfitDistance > 0m && candle.LowPrice <= entry - takeProfitDistance)
{
BuyMarket();
_entryPrice = null;
}
}
}
private void UpdateMoneyManagement()
{
if (!UseMoneyManagement)
{
_lotMultiplier = 1m;
return;
}
if (DepositPerLot <= 0m)
return;
var balance = Portfolio?.CurrentValue;
if (balance == null || balance <= 0m)
return;
var ratio = (int)(balance.Value / DepositPerLot);
if (ratio < 2)
return;
// Cap the multiplier at twenty lots, replicating the MQL expert behaviour.
_lotMultiplier = Math.Min(20, ratio);
}
private void UpdateHistory(decimal cciValue, decimal cciCloseValue, decimal maValue)
{
// Shift cached values so the strategy can access readings from two completed candles ago.
_prev2Cci = _prevCci;
_prevCci = cciValue;
_prev2CciClose = _prevCciClose;
_prevCciClose = cciCloseValue;
_prev2Ma = _prevMa;
_prevMa = maValue;
if (_historyCount < 2)
_historyCount++;
}
private decimal AdjustVolume(decimal volume)
{
if (volume <= 0m)
return 0m;
var security = Security;
if (security == null)
return volume;
var step = security.VolumeStep ?? 0m;
if (step > 0m)
{
// Align the order size with the instrument volume step.
var steps = Math.Floor(volume / step);
volume = steps * step;
}
var minVolume = security.MinVolume ?? 0m;
if (volume < minVolume)
return 0m;
var maxVolume = security.MaxVolume;
if (maxVolume != null && volume > maxVolume.Value)
volume = maxVolume.Value;
return volume;
}
private decimal CalculatePipSize()
{
var priceStep = Security?.PriceStep ?? 0m;
if (priceStep <= 0m)
return 0m;
var bits = decimal.GetBits(priceStep);
var scale = (bits[3] >> 16) & 0xFF;
// Symbols with three or five digits require a tenfold pip multiplier.
var multiplier = scale == 3 || scale == 5 ? 10m : 1m;
return priceStep * multiplier;
}
private void ResetState()
{
// Restore cached values and multipliers before a new backtest/run.
_pipSize = 0m;
_lotMultiplier = 1m;
_entryPrice = null;
_prevCci = null;
_prev2Cci = null;
_prevCciClose = null;
_prev2CciClose = null;
_prevMa = null;
_prev2Ma = null;
_historyCount = 0;
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.BusinessEntities")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan, Decimal, Math
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Strategies import Strategy
from StockSharp.Algo.Indicators import (
CommodityChannelIndex, ExponentialMovingAverage
)
from indicator_extensions import *
class icci_ima_strategy(Strategy):
"""CCI and EMA crossover strategy: trades when CCI crosses its smoothed EMA."""
def __init__(self):
super(icci_ima_strategy, self).__init__()
self._cci_period = self.Param("CciPeriod", 14) \
.SetGreaterThanZero() \
.SetDisplay("CCI Period", "Length of the main CCI indicator", "Indicators")
self._cci_close_period = self.Param("CciClosePeriod", 14) \
.SetGreaterThanZero() \
.SetDisplay("CCI Close Period", "Length of the CCI used for overbought/oversold exits", "Indicators")
self._ma_period = self.Param("MaPeriod", 15) \
.SetGreaterThanZero() \
.SetDisplay("CCI EMA Period", "Length of the EMA applied to the CCI values", "Indicators")
self._stop_loss_pips = self.Param("StopLossPips", Decimal(50)) \
.SetDisplay("Stop Loss (pips)", "Protective stop distance in pips", "Risk")
self._take_profit_pips = self.Param("TakeProfitPips", Decimal(40)) \
.SetDisplay("Take Profit (pips)", "Profit target distance in pips", "Risk")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(4))) \
.SetDisplay("Candle Type", "Data series used for calculations", "General")
self._pip_size = Decimal(0)
self._entry_price = None
self._prev_cci = None
self._prev2_cci = None
self._prev_cci_close = None
self._prev2_cci_close = None
self._prev_ma = None
self._prev2_ma = None
self._history_count = 0
@property
def CciPeriod(self):
return self._cci_period.Value
@CciPeriod.setter
def CciPeriod(self, value):
self._cci_period.Value = value
@property
def CciClosePeriod(self):
return self._cci_close_period.Value
@CciClosePeriod.setter
def CciClosePeriod(self, value):
self._cci_close_period.Value = value
@property
def MaPeriod(self):
return self._ma_period.Value
@MaPeriod.setter
def MaPeriod(self, value):
self._ma_period.Value = value
@property
def StopLossPips(self):
return self._stop_loss_pips.Value
@StopLossPips.setter
def StopLossPips(self, value):
self._stop_loss_pips.Value = value
@property
def TakeProfitPips(self):
return self._take_profit_pips.Value
@TakeProfitPips.setter
def TakeProfitPips(self, value):
self._take_profit_pips.Value = value
@property
def CandleType(self):
return self._candle_type.Value
@CandleType.setter
def CandleType(self, value):
self._candle_type.Value = value
def _calc_pip_size(self):
sec = self.Security
if sec is None or sec.PriceStep is None:
return Decimal(0)
step = sec.PriceStep
if step <= Decimal(0):
return Decimal(0)
bits = Decimal.GetBits(step)
scale = (bits[3] >> 16) & 0xFF
if scale == 3 or scale == 5:
multiplier = Decimal(10)
else:
multiplier = Decimal(1)
return Decimal.Multiply(step, multiplier)
def OnStarted2(self, time):
super(icci_ima_strategy, self).OnStarted2(time)
self._reset_state()
self._cci = CommodityChannelIndex()
self._cci.Length = self.CciPeriod
self._cci_close = CommodityChannelIndex()
self._cci_close.Length = self.CciClosePeriod
self._cci_ma = ExponentialMovingAverage()
self._cci_ma.Length = self.MaPeriod
self._pip_size = self._calc_pip_size()
subscription = self.SubscribeCandles(self.CandleType)
subscription.Bind(self._cci, self._cci_close, self._process_candle).Start()
def _process_candle(self, candle, cci_value, cci_close_value):
if candle.State != CandleStates.Finished:
return
cci_val = Decimal(float(cci_value))
cci_close_val = Decimal(float(cci_close_value))
ma_result = process_float(self._cci_ma, cci_val, candle.OpenTime, True)
ma_val = Decimal(float(ma_result))
if not self._cci.IsFormed or not self._cci_close.IsFormed or not self._cci_ma.IsFormed:
self._update_history(cci_val, cci_close_val, ma_val)
return
if self._history_count < 2:
self._update_history(cci_val, cci_close_val, ma_val)
return
self._handle_stops(candle)
cci_two_ago = self._prev2_cci if self._prev2_cci is not None else Decimal(0)
ma_two_ago = self._prev2_ma if self._prev2_ma is not None else Decimal(0)
cci_close_two_ago = self._prev2_cci_close if self._prev2_cci_close is not None else Decimal(0)
d100 = Decimal(100)
dm100 = Decimal(-100)
should_close_long = ((cci_close_two_ago > d100 and cci_close_val <= d100) or
(cci_val < ma_val and cci_two_ago >= ma_two_ago))
should_close_short = ((cci_close_two_ago < dm100 and cci_close_val >= dm100) or
(cci_val > ma_val and cci_two_ago <= ma_two_ago))
if self.Position > 0 and should_close_long:
self.SellMarket()
self._entry_price = None
elif self.Position < 0 and should_close_short:
self.BuyMarket()
self._entry_price = None
if cci_val > ma_val and cci_two_ago < ma_two_ago and self.Position <= 0:
self.BuyMarket()
self._entry_price = candle.ClosePrice
elif cci_val < ma_val and cci_two_ago > ma_two_ago and self.Position >= 0:
self.SellMarket()
self._entry_price = candle.ClosePrice
if self.Position == 0:
self._entry_price = None
self._update_history(cci_val, cci_close_val, ma_val)
def _handle_stops(self, candle):
if self._entry_price is None:
return
if self._pip_size > Decimal(0):
price_step = self._pip_size
else:
sec = self.Security
price_step = sec.PriceStep if sec is not None and sec.PriceStep is not None else Decimal(0)
if price_step <= Decimal(0):
return
sl_pips = self.StopLossPips
tp_pips = self.TakeProfitPips
sl_dist = Decimal.Multiply(sl_pips, price_step) if sl_pips > Decimal(0) else Decimal(0)
tp_dist = Decimal.Multiply(tp_pips, price_step) if tp_pips > Decimal(0) else Decimal(0)
if self.Position > 0:
entry = self._entry_price
if sl_dist > Decimal(0) and candle.LowPrice <= Decimal.Subtract(entry, sl_dist):
self.SellMarket()
self._entry_price = None
return
if tp_dist > Decimal(0) and candle.HighPrice >= Decimal.Add(entry, tp_dist):
self.SellMarket()
self._entry_price = None
elif self.Position < 0:
entry = self._entry_price
if sl_dist > Decimal(0) and candle.HighPrice >= Decimal.Add(entry, sl_dist):
self.BuyMarket()
self._entry_price = None
return
if tp_dist > Decimal(0) and candle.LowPrice <= Decimal.Subtract(entry, tp_dist):
self.BuyMarket()
self._entry_price = None
def _update_history(self, cci_val, cci_close_val, ma_val):
self._prev2_cci = self._prev_cci
self._prev_cci = cci_val
self._prev2_cci_close = self._prev_cci_close
self._prev_cci_close = cci_close_val
self._prev2_ma = self._prev_ma
self._prev_ma = ma_val
if self._history_count < 2:
self._history_count += 1
def _reset_state(self):
self._pip_size = Decimal(0)
self._entry_price = None
self._prev_cci = None
self._prev2_cci = None
self._prev_cci_close = None
self._prev2_cci_close = None
self._prev_ma = None
self._prev2_ma = None
self._history_count = 0
def OnReseted(self):
super(icci_ima_strategy, self).OnReseted()
self._reset_state()
def CreateClone(self):
return icci_ima_strategy()