Ver en GitHub

Estrategia iCCI iMA

Esta estrategia replica el asesor experto MetaTrader «iCCI iMA». Opera cruces del Commodity Channel Index (CCI) contra una media móvil exponencial (EMA) aplicada directamente al flujo del CCI. Un CCI secundario, calculado con su propio período, supervisa reversiones de sobrecompra/sobreventa alrededor de las bandas ±100. Las órdenes se dimensionan en lotes, opcionalmente escaladas por el balance de la cuenta, y cada operación está protegida por niveles configurables de stop-loss y take-profit expresados en pips.

Cómo funciona

  • Fuente de datos – Una serie de velas configurable (velas de 1 minuto por defecto) alimenta todos los cálculos de indicadores usando el precio típico de la vela (high + low + close) / 3.
  • Indicadores principales – El CCI primario mide el momentum con la longitud CciPeriod. Una EMA de ese CCI (longitud MaPeriod) suaviza el oscilador y actúa como línea de señal. El CCI secundario CciClosePeriod monitorea cruces de umbral.
  • Lógica de entrada – Una posición larga se abre cuando el CCI actual está por encima de su EMA mientras el valor de hace dos velas completadas estaba por debajo de la EMA, indicando un cruce ascendente. Una posición corta refleja esta lógica cuando el CCI cruza hacia abajo. El algoritmo solo opera después de que todos los indicadores estén completamente formados y dos barras históricas estén disponibles para reproducir el look-back original de la implementación MQL.
  • Lógica de salida – Los largos existentes se cierran cuando el CCI secundario cae de vuelta por debajo de +100 o cuando el CCI primario cae bajo la EMA después de haber estado por encima dos barras antes. Los cortos salen cuando el CCI secundario sube por encima de −100 o cuando el CCI vuelve a subir por encima de la EMA bajo la misma confirmación de dos barras. Los stops protectores monitorean cada vela finalizada: las posiciones largas se cierran si el precio baja a entry − stopLossPips * pipSize y toman beneficios en entry + takeProfitPips * pipSize; los cortos usan los niveles simétricos con entry + stopLoss y entry − takeProfit. El tamaño de pip se deriva del paso de precio del valor y se adapta a cotizaciones de 3 o 5 dígitos multiplicando el tamaño del tick por 10, coincidiendo con la conversión de MetaTrader.
  • Dimensionamiento de posición – El tamaño de lote base (LotSize) se valida contra los valores VolumeStep, MinVolume y MaxVolume del instrumento para que las órdenes respeten las restricciones del exchange. Si la gestión de dinero está habilitada, la estrategia multiplica el tamaño de lote por un factor entero igual al balance de la cuenta dividido por DepositPerLot, limitado a 20, y actualizado en cada barra, reproduciendo el escalado entero del experto original.

Parámetros

  • Tipo de Vela – Serie de datos usada para los cálculos de indicadores.
  • Período CCI – Longitud del CCI primario que impulsa las señales de cruce.
  • Período CCI Cierre – Longitud del CCI secundario usado para vigilar las reversiones ±100.
  • Período EMA CCI – Período de la EMA que suaviza los valores del CCI primario.
  • Tamaño de Lote – Volumen de trading base en lotes antes de cualquier escalado.
  • Habilitar Gestión de Dinero – Activa el escalado del tamaño de lote basado en balance.
  • Depósito Por Lote – Incremento de balance requerido para aumentar el multiplicador de lote en uno (activo solo cuando la gestión de dinero está activada).
  • Stop Loss (pips) – Distancia de stop protector en pips; establecer en cero para deshabilitar.
  • Take Profit (pips) – Distancia del objetivo de beneficio en pips; establecer en cero para deshabilitar.

El algoritmo requiere dos velas completamente terminadas antes de comenzar a operar para que las comparaciones de cruce de dos barras coincidan con la lógica MQL fuente. Las verificaciones de stop-loss y take-profit se evalúan en velas cerradas usando sus extremos de máximo/mínimo, lo que aproxima las órdenes protectoras del lado del servidor de MetaTrader dentro de la API StockSharp de alto nivel.

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;
	}
}