Ver en GitHub

Estrategia Ma2Cci EMA

Estrategia de cruce de dos medias móviles exponenciales confirmada por una ruptura de la línea cero del Índice del Canal de Materias Primas (CCI). El tamaño de la posición y la colocación del stop se derivan de la volatilidad del Rango Verdadero Promedio (ATR) y un porcentaje de riesgo configurable.

Detalles

  • Datos: Velas basadas en tiempo (predeterminado 1 hora) suministradas por el parámetro Candle Type seleccionado.
  • Entrada: Ir largo cuando la EMA rápida cruza por encima de la EMA lenta y el CCI cruza por encima de cero en la misma barra; ir corto en el cruce opuesto con el CCI rompiendo por debajo de cero.
  • Salida: Cerrar posiciones largas cuando la EMA rápida vuelve a cruzar por debajo de la EMA lenta o el precio toca el stop fijo; cerrar cortos cuando la EMA rápida cruza por encima de la EMA lenta o el precio alcanza el stop corto.
  • Riesgo: La distancia del stop equivale al mayor entre el ATR (longitud AtrPeriod) o MinStopPoints multiplicado por el paso de precio del instrumento. El tamaño de la operación es el valor de la cartera por RiskPercent, dividido por esa distancia de stop.
  • Instrumentos: Símbolos de forex o índices con seguimiento de tendencia que admiten cobertura en la versión original de MetaTrader; también aplicable a otros activos con oscilaciones de momentum claras.
  • Entorno: Diseñado para mercados de sesión continua donde las señales EMA/CCI se alinean con los controles de riesgo basados en ATR.

Parámetros

  • CandleType – Marco temporal y tipo de datos utilizado para cálculos y flujo de órdenes.
  • FastMaPeriod – Período de la EMA rápida (predeterminado 10).
  • SlowMaPeriod – Período de la EMA lenta (predeterminado 37).
  • CciPeriod – Lookback del oscilador CCI que confirma el momentum (predeterminado 39).
  • AtrPeriod – Longitud del ATR utilizado para estimar la volatilidad actual para la colocación de stops (predeterminado 3).
  • RiskPercent – Fracción del capital de la cartera actual arriesgado por operación (predeterminado 2%).
  • MinStopPoints – Distancia mínima del stop expresada en pasos de precio para emular el filtro de pips de MetaTrader (predeterminado 15).

Notas

  • Funciona mejor cuando se ejecuta en pares líquidos e índices donde los cruces EMA/CCI son fiables; los mercados delgados pueden activar salidas prematuras.
  • Como los stops se recalculan solo en la entrada, la estrategia mantiene el perfil de riesgo estable y refleja la lógica de stop-loss fijo del experto MQL original.
  • La valoración de la cartera debe ser proporcionada por la cuenta conectada para que el dimensionamiento de la posición funcione; de lo contrario, el motor recurre al Volume de la estrategia o al volumen mínimo del instrumento.
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>
/// EMA crossover with CCI confirmation and ATR based stop distance.
/// </summary>
public class Ma2CciEmaStrategy : Strategy
{
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<int> _fastMaPeriod;
	private readonly StrategyParam<int> _slowMaPeriod;
	private readonly StrategyParam<int> _cciPeriod;
	private readonly StrategyParam<int> _atrPeriod;
	private readonly StrategyParam<decimal> _riskPercent;
	private readonly StrategyParam<int> _minStopPoints;

	private ExponentialMovingAverage _fastMa = null!;
	private ExponentialMovingAverage _slowMa = null!;
	private CommodityChannelIndex _cci = null!;
	private AverageTrueRange _atr = null!;

	private decimal _previousFast;
	private decimal _previousSlow;
	private decimal _previousCci;
	private bool _hasPreviousValues;
	private decimal? _stopPrice;

	/// <summary>
	/// Candle type used to receive market data.
	/// </summary>
	public DataType CandleType { get => _candleType.Value; set => _candleType.Value = value; }

	/// <summary>
	/// Fast EMA period.
	/// </summary>
	public int FastMaPeriod { get => _fastMaPeriod.Value; set => _fastMaPeriod.Value = value; }

	/// <summary>
	/// Slow EMA period.
	/// </summary>
	public int SlowMaPeriod { get => _slowMaPeriod.Value; set => _slowMaPeriod.Value = value; }

	/// <summary>
	/// CCI calculation period.
	/// </summary>
	public int CciPeriod { get => _cciPeriod.Value; set => _cciPeriod.Value = value; }

	/// <summary>
	/// ATR period for volatility based stops.
	/// </summary>
	public int AtrPeriod { get => _atrPeriod.Value; set => _atrPeriod.Value = value; }

	/// <summary>
	/// Percentage of portfolio equity risked per trade.
	/// </summary>
	public decimal RiskPercent { get => _riskPercent.Value; set => _riskPercent.Value = value; }

	/// <summary>
	/// Minimum stop distance expressed in price steps.
	/// </summary>
	public int MinStopPoints { get => _minStopPoints.Value; set => _minStopPoints.Value = value; }

	/// <summary>
	/// Initializes a new instance of the <see cref="Ma2CciEmaStrategy"/> class.
	/// </summary>
	public Ma2CciEmaStrategy()
	{
		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
		.SetDisplay("Candle Type", "Type of candles used for calculations", "General");

		_fastMaPeriod = Param(nameof(FastMaPeriod), 10)
		.SetGreaterThanZero()
		.SetDisplay("Fast EMA", "Fast EMA period", "Indicators");

		_slowMaPeriod = Param(nameof(SlowMaPeriod), 37)
		.SetGreaterThanZero()
		.SetDisplay("Slow EMA", "Slow EMA period", "Indicators");

		_cciPeriod = Param(nameof(CciPeriod), 39)
		.SetGreaterThanZero()
		.SetDisplay("CCI Period", "CCI length", "Indicators");

		_atrPeriod = Param(nameof(AtrPeriod), 3)
		.SetGreaterThanZero()
		.SetDisplay("ATR Period", "ATR length for stop calculation", "Risk Management");

		_riskPercent = Param(nameof(RiskPercent), 2m)
		.SetDisplay("Risk %", "Portfolio percentage risked per entry", "Risk Management");

		_minStopPoints = Param(nameof(MinStopPoints), 15)
		.SetGreaterThanZero()
		.SetDisplay("Min Stop Points", "Minimum stop distance in price steps", "Risk Management");
	}

	/// <inheritdoc />
	public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
	{
		return [(Security, CandleType)];
	}

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();

		_previousFast = 0m;
		_previousSlow = 0m;
		_previousCci = 0m;
		_hasPreviousValues = false;
		_stopPrice = null;
	}

	/// <inheritdoc />
	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);

		_fastMa = new EMA { Length = FastMaPeriod };
		_slowMa = new EMA { Length = SlowMaPeriod };
		_cci = new CommodityChannelIndex { Length = CciPeriod };
		_atr = new AverageTrueRange { Length = AtrPeriod };

		var subscription = SubscribeCandles(CandleType);
		subscription
		.Bind(_fastMa, _slowMa, _cci, _atr, ProcessCandle)
		.Start();

		var area = CreateChartArea();
		if (area != null)
		{
			DrawCandles(area, subscription);
			DrawIndicator(area, _fastMa);
			DrawIndicator(area, _slowMa);
			DrawIndicator(area, _cci);
			DrawOwnTrades(area);
		}
	}

	private void ProcessCandle(ICandleMessage candle, decimal fastValue, decimal slowValue, decimal cciValue, decimal atrValue)
	{
		// Process only finished candles to avoid intrabar noise.
		if (candle.State != CandleStates.Finished)
		return;

		// Wait until all indicators are fully initialized before trading.
		if (!_fastMa.IsFormed || !_slowMa.IsFormed || !_cci.IsFormed || !_atr.IsFormed)
		return;

		// removed IFOAAT for backtesting

		if (!_hasPreviousValues)
		{
			_previousFast = fastValue;
			_previousSlow = slowValue;
			_previousCci = cciValue;
			_hasPreviousValues = true;
			return;
		}

		var fastCrossUp = _previousFast <= _previousSlow && fastValue > slowValue;
		var fastCrossDown = _previousFast >= _previousSlow && fastValue < slowValue;
		var cciCrossUp = _previousCci <= 0m && cciValue > 0m;
		var cciCrossDown = _previousCci >= 0m && cciValue < 0m;

		var stopDistance = Math.Max(atrValue, GetMinStopDistance());

		if (Position != 0m)
		{
			var exitTriggered = false;

			if (Position > 0m)
			{
				// Close long positions on stop hit or bearish crossover.
				if (_stopPrice.HasValue && candle.LowPrice <= _stopPrice.Value)
				{
					SellMarket();
					exitTriggered = true;
				}
				else if (fastCrossDown)
				{
					SellMarket();
					exitTriggered = true;
				}
			}
			else if (Position < 0m)
			{
				// Close short positions on stop hit or bullish crossover.
				if (_stopPrice.HasValue && candle.HighPrice >= _stopPrice.Value)
				{
					BuyMarket();
					exitTriggered = true;
				}
				else if (fastCrossUp)
				{
					BuyMarket();
					exitTriggered = true;
				}
			}

			if (exitTriggered)
			{
				_stopPrice = null;
				_previousFast = fastValue;
				_previousSlow = slowValue;
				_previousCci = cciValue;
				return;
			}
		}
		else
		{
			// Enter long when EMA and CCI confirm bullish momentum.
			if (fastCrossUp && cciCrossUp)
			{
				var volume = CalculateVolume(stopDistance);
				if (volume > 0m)
				{
					BuyMarket();
					_stopPrice = NormalizePrice(candle.ClosePrice - stopDistance);
				}
			}
			// Enter short when EMA and CCI confirm bearish momentum.
			else if (fastCrossDown && cciCrossDown)
			{
				var volume = CalculateVolume(stopDistance);
				if (volume > 0m)
				{
					SellMarket();
					_stopPrice = NormalizePrice(candle.ClosePrice + stopDistance);
				}
			}
		}

		_previousFast = fastValue;
		_previousSlow = slowValue;
		_previousCci = cciValue;
	}

	private decimal CalculateVolume(decimal stopDistance)
	{
		if (stopDistance <= 0m)
		return 0m;

		var equity = Portfolio?.CurrentValue ?? 0m;
		var riskAmount = equity * (RiskPercent / 100m);

		if (riskAmount <= 0m)
		return NormalizeVolume(GetBaseVolume());

		var rawVolume = riskAmount / stopDistance;
		if (rawVolume <= 0m)
		return NormalizeVolume(GetBaseVolume());

		return NormalizeVolume(rawVolume);
	}

	private decimal GetBaseVolume()
	{
		var volume = Volume;
		if (volume > 0m)
		return volume;

		var step = Security?.VolumeStep ?? 1m;
		var min = Security?.MinVolume ?? step;
		return min > 0m ? min : step;
	}

	private decimal NormalizeVolume(decimal volume)
	{
		var step = Security?.VolumeStep ?? 1m;
		if (step <= 0m)
		return Math.Max(volume, 0m);

		var normalized = Math.Round(volume / step, MidpointRounding.AwayFromZero) * step;
		var min = Security?.MinVolume ?? step;
		if (normalized < min)
		normalized = min;

		var max = Security?.MaxVolume;
		if (max.HasValue && max.Value > 0m && normalized > max.Value)
		normalized = max.Value;

		return Math.Max(normalized, 0m);
	}

	private decimal NormalizePrice(decimal price)
	{
		var step = Security?.PriceStep;
		if (!step.HasValue || step.Value <= 0m)
		return price;

		var rounded = Math.Round(price / step.Value, MidpointRounding.AwayFromZero) * step.Value;
		return rounded;
	}

	private decimal GetMinStopDistance()
	{
		var step = Security?.PriceStep ?? 0m;
		return step > 0m ? step * MinStopPoints : MinStopPoints;
	}
}