Ver en GitHub

Estrategia EMA Crossover con Trailing

Descripción general

Esta estrategia es un port de StockSharp del asesor experto MQL5 "Intersection 2 iMA". Opera en dos medias móviles exponenciales (EMAs) y reacciona a los cruces que ocurren en velas completamente formadas. El experto original fue diseñado para MetaTrader 5 y gestionaba el volumen de operaciones de forma dinámica; en esta conversión el tamaño de la orden está controlado por un parámetro configurable mientras se preserva la lógica de cruce y trailing.

Lógica de trading

  1. Generación de señales
    • Calcular las EMAs rápida y lenta en la serie de velas seleccionada.
    • Un cruce alcista (EMA rápida cruzando por encima de la EMA lenta) dispara una señal de compra cuando la vela anterior cerró con la EMA rápida por debajo o igual a la EMA lenta y los valores actuales muestran la EMA rápida por encima de la EMA lenta.
    • Un cruce bajista (EMA rápida cruzando por debajo de la EMA lenta) espeja la regla anterior y produce una señal de venta.
  2. Ejecución de órdenes
    • Cuando se produce una señal de compra y no existe posición larga, la estrategia envía una orden de compra a mercado.
    • Cuando se produce una señal de venta y no existe posición corta, la estrategia envía una orden de venta a mercado.
    • Si hay una posición opuesta, el volumen de la orden se incrementa para cerrar la posición existente antes de establecer la nueva, coincidiendo con el comportamiento del EA fuente que primero cerraba operaciones opuestas.
  3. Gestión del trailing stop
    • Un trailing stop escalonado mantiene una distancia fija (en pasos de precio) del precio más favorable.
    • El stop solo se mueve cuando el precio ha avanzado un paso definido por el usuario, previniendo modificaciones constantes de órdenes.
    • Si el precio viola el nivel del trailing, la posición se cierra con una orden de mercado.

Parámetros

Nombre Descripción Predeterminado
FastPeriod Longitud de la EMA rápida. 4
SlowPeriod Longitud de la EMA lenta. 18
TrailingStopPoints Distancia entre precio de mercado y trailing stop en pasos de precio (puntos). Un valor de 0 deshabilita el trailing. 20
TrailingStepPoints Progreso mínimo en pasos de precio antes de que el trailing stop avance. 5
CandleType Serie de datos de velas usada para los cálculos (marco temporal). Velas de 15 minutos
TradeVolume Tamaño de la orden para entradas a mercado. 1

Notas de implementación

  • La estrategia usa la API de alto nivel SubscribeCandles().Bind(...) para conectar datos de velas con indicadores EMA, asegurando que no se necesite gestión manual de buffers.
  • Las distancias de trailing se calculan multiplicando el número configurado de puntos por el PriceStep del instrumento, replicando la lógica de ajuste de dígitos encontrada en la versión MQL.
  • Los trailing stops se implementan internamente usando salidas a mercado, porque StockSharp no expone el mismo helper PositionModify usado en MetaTrader. El comportamiento sigue siendo equivalente: una vez que se viola el nivel del trailing, la posición se sale inmediatamente.
  • Los parámetros se exponen a través de StrategyParam<T> para que puedan optimizarse en el diseñador o ajustarse desde la UI.

Consejos de uso

  • Alinear el CandleType con el marco temporal usado en backtests o trading en vivo para mantener los valores del indicador consistentes.
  • Al operar instrumentos con tamaños de tick pequeños, ajustar TrailingStopPoints y TrailingStepPoints correspondientemente; la distancia de precio efectiva equivale a puntos × PriceStep.
  • Establecer TradeVolume para que coincida con el contrato o tamaño de lote deseado. La estrategia incrementa automáticamente el importe de la orden para cerrar una posición opuesta cuando aparece una nueva señal.

Diferencias respecto al Asesor Experto original

  • La gestión de capital en MetaTrader usaba MoneyFixedMargin; la versión de StockSharp expone un parámetro de volumen de orden fijo en su lugar, dejando el dimensionamiento avanzado de posiciones a la configuración externa.
  • El EA ofrecía una entrada InpCloseHalf no utilizada. No tenía efecto en el código fuente y fue omitida.
  • El trailing stop se gestiona internamente en lugar de modificar órdenes de stop-loss, ya que esto simplifica la ejecución dentro de StockSharp manteniendo la lógica de salida idéntica.
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 strategy with trailing stop logic converted from the MQL5 "Intersection 2 iMA" expert advisor.
/// The strategy opens trades on moving average crossovers and maintains a stepped trailing stop.
/// </summary>
public class EmaCrossoverTrailingStrategy : Strategy
{
	private readonly StrategyParam<int> _fastPeriod;
	private readonly StrategyParam<int> _slowPeriod;
	private readonly StrategyParam<decimal> _trailingStopPoints;
	private readonly StrategyParam<decimal> _trailingStepPoints;
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<decimal> _tradeVolume;

	private ExponentialMovingAverage _fastEma = null!;
	private ExponentialMovingAverage _slowEma = null!;

	private decimal? _previousFastValue;
	private decimal? _previousSlowValue;

	private decimal? _longStopPrice;
	private decimal? _shortStopPrice;

	private decimal _stopDistance;
	private decimal _stepDistance;

	/// <summary>
	/// Initializes <see cref="EmaCrossoverTrailingStrategy"/>.
	/// </summary>
	public EmaCrossoverTrailingStrategy()
	{
		_fastPeriod = Param(nameof(FastPeriod), 4)
			.SetGreaterThanZero()
			.SetDisplay("Fast EMA", "Period of the fast exponential moving average", "Moving Averages")
			
			.SetOptimize(2, 20, 1);

		_slowPeriod = Param(nameof(SlowPeriod), 18)
			.SetGreaterThanZero()
			.SetDisplay("Slow EMA", "Period of the slow exponential moving average", "Moving Averages")
			
			.SetOptimize(10, 60, 2);

		_trailingStopPoints = Param(nameof(TrailingStopPoints), 20m)
			.SetNotNegative()
			.SetDisplay("Trailing Stop (points)", "Distance from price to trailing stop expressed in price steps", "Risk Management")
			
			.SetOptimize(5m, 40m, 5m);

		_trailingStepPoints = Param(nameof(TrailingStepPoints), 5m)
			.SetNotNegative()
			.SetDisplay("Trailing Step (points)", "Minimum price advancement before the trailing stop is moved", "Risk Management")
			
			.SetOptimize(1m, 10m, 1m);

		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
			.SetDisplay("Candle Type", "Type of candles used for calculations", "General");

		_tradeVolume = Param(nameof(TradeVolume), 1m)
			.SetGreaterThanZero()
			.SetDisplay("Volume", "Order volume used for entries", "General");
	}

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

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

	/// <summary>
	/// Trailing stop distance expressed in price steps.
	/// </summary>
	public decimal TrailingStopPoints
	{
		get => _trailingStopPoints.Value;
		set => _trailingStopPoints.Value = value;
	}

	/// <summary>
	/// Minimum move required before shifting the trailing stop.
	/// </summary>
	public decimal TrailingStepPoints
	{
		get => _trailingStepPoints.Value;
		set => _trailingStepPoints.Value = value;
	}

	/// <summary>
	/// Candle type used for analysis.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Volume used for market orders.
	/// </summary>
	public decimal TradeVolume
	{
		get => _tradeVolume.Value;
		set => _tradeVolume.Value = value;
	}

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

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

		Volume = TradeVolume;
		_previousFastValue = null;
		_previousSlowValue = null;
		_longStopPrice = null;
		_shortStopPrice = null;
		_fastEma = null!;
		_slowEma = null!;
		_stopDistance = 0m;
		_stepDistance = 0m;
	}

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

		Volume = TradeVolume;

		_fastEma = new EMA { Length = FastPeriod };
		_slowEma = new EMA { Length = SlowPeriod };

		_stopDistance = CalculateDistance(TrailingStopPoints);
		_stepDistance = CalculateDistance(TrailingStepPoints);

		var subscription = SubscribeCandles(CandleType);
		subscription
			.Bind(_fastEma, _slowEma, ProcessCandle)
			.Start();

		var area = CreateChartArea();
		if (area != null)
		{
			DrawCandles(area, subscription);
			DrawIndicator(area, _fastEma);
			DrawIndicator(area, _slowEma);
			DrawOwnTrades(area);
		}
	}

	private void ProcessCandle(ICandleMessage candle, decimal fastValue, decimal slowValue)
	{
		if (candle.State != CandleStates.Finished)
			return;

		_stopDistance = CalculateDistance(TrailingStopPoints);
		_stepDistance = CalculateDistance(TrailingStepPoints);

		UpdateTrailingStops(candle);

		if (!_fastEma.IsFormed || !_slowEma.IsFormed)
		{
			_previousFastValue = fastValue;
			_previousSlowValue = slowValue;
			return;
		}

		// indicators bound via .Bind()

		if (_previousFastValue is null || _previousSlowValue is null)
		{
			_previousFastValue = fastValue;
			_previousSlowValue = slowValue;
			return;
		}

		var fastPrev = _previousFastValue.Value;
		var slowPrev = _previousSlowValue.Value;

		var crossedUp = fastPrev <= slowPrev && fastValue > slowValue;
		var crossedDown = fastPrev >= slowPrev && fastValue < slowValue;

		if (crossedUp && Position <= 0)
		{
			var volumeToBuy = TradeVolume;

			if (Position < 0)
				volumeToBuy += Math.Abs(Position);

			if (volumeToBuy > 0)
			{
				BuyMarket();
				InitializeLongTrailing(candle.ClosePrice);
			}
		}
		else if (crossedDown && Position >= 0)
		{
			var volumeToSell = TradeVolume;

			if (Position > 0)
				volumeToSell += Math.Abs(Position);

			if (volumeToSell > 0)
			{
				SellMarket();
				InitializeShortTrailing(candle.ClosePrice);
			}
		}

		_previousFastValue = fastValue;
		_previousSlowValue = slowValue;
	}

	private decimal CalculateDistance(decimal points)
	{
		if (points <= 0m)
			return 0m;

		var priceStep = Security?.PriceStep ?? 0m;

		if (priceStep <= 0m)
			priceStep = 1m;

		return points * priceStep;
	}

	private void InitializeLongTrailing(decimal price)
	{
		if (_stopDistance <= 0m)
		{
			_longStopPrice = null;
			return;
		}

		_longStopPrice = price - _stopDistance;
		_shortStopPrice = null;
	}

	private void InitializeShortTrailing(decimal price)
	{
		if (_stopDistance <= 0m)
		{
			_shortStopPrice = null;
			return;
		}

		_shortStopPrice = price + _stopDistance;
		_longStopPrice = null;
	}

	private void UpdateTrailingStops(ICandleMessage candle)
	{
		if (_stopDistance <= 0m)
		{
			_longStopPrice = null;
			_shortStopPrice = null;
			return;
		}

		if (Position > 0)
		{
			if (_longStopPrice is null)
			{
				InitializeLongTrailing(candle.ClosePrice);
			}
			else
			{
				var newStop = candle.ClosePrice - _stopDistance;

				if (newStop - _longStopPrice.Value >= _stepDistance)
					_longStopPrice = newStop;

				if (candle.LowPrice <= _longStopPrice.Value)
				{
					SellMarket();
					_longStopPrice = null;
				}
			}
		}
		else if (Position < 0)
		{
			if (_shortStopPrice is null)
			{
				InitializeShortTrailing(candle.ClosePrice);
			}
			else
			{
				var newStop = candle.ClosePrice + _stopDistance;

				if (_shortStopPrice.Value - newStop >= _stepDistance)
					_shortStopPrice = newStop;

				if (candle.HighPrice >= _shortStopPrice.Value)
				{
					BuyMarket();
					_shortStopPrice = null;
				}
			}
		}
		else
		{
			_longStopPrice = null;
			_shortStopPrice = null;
		}
	}
}