Ver en GitHub

EMA Estrategia cruzada 2

Descripción general

Esta estrategia es una versión StockSharp del asesor experto MetaTrader 4 "EMA_CROSS_2" del repositorio MQL. El EA original monitorea dos promedios móviles exponenciales (EMA) y coloca órdenes de mercado cada vez que los promedios intercambian órdenes. La conversión mantiene la naturaleza contraria del script: compra cuando el EMA largo se mueve por encima del EMA corto y vende cuando el EMA corto sube por encima del EMA largo), mientras envuelve la lógica en la infraestructura estratégica de alto nivel StockSharp.

La estrategia opera con velas completadas proporcionadas por el tipo de datos de vela configurable. Las señales se evalúan al cerrar la vela para evitar activaciones repetidas dentro de la misma barra. La gestión de riesgos imita el comportamiento de MetaTrader mediante el uso de distancias de obtención de beneficios, stop loss y trailing stop expresadas en puntos del corredor (escalones de precios).

Lógica de trading

  1. Cálculo del indicador
    • Calcule las EMA de período corto y largo en cada vela completa.
    • Omita la primera actualización del indicador, que coincide con el indicador first_time original que ignoró la primera evaluación.
    • Luego, detecte un cambio de dirección cuando se invierta el orden relativo entre el EMA largo y corto.
  2. Interpretación de señal
    • Cuando el EMA largo se mueve por encima del EMA corto, el EA original abrió una operación de compra. El puerto StockSharp mantiene esta regla contraria a pesar de que se comporta de manera opuesta a un sistema cruzado clásico.
    • Cuando el EMA corto cierra por encima del EMA largo, la estrategia abre una operación de venta.
    • Solo se permiten nuevas posiciones cuando no hay ninguna exposición abierta actualmente, lo que replica la condición OrdersTotal() < 1.
  3. Ejecución de orden
    • Las operaciones se envían como órdenes de mercado con un volumen fijo configurable.
    • Al entrar, la estrategia registra los precios de stop-loss y take-profit utilizando la distancia de pip proporcionada a través de los parámetros.
  4. Gestión de riesgos
    • En cada vela terminada, la estrategia verifica si la acción del precio tocó los niveles almacenados de stop-loss o take-profit. Al superar cualquiera de los niveles, se cierra toda la posición con una orden de mercado.
    • Se aplica un trailing stop (también definido en los puntos del corredor) una vez que el precio se mueve favorablemente más que la distancia de seguimiento. Para posiciones largas el tope protector se desplaza hacia arriba; para posiciones cortas, sigue el precio hacia abajo.
    • Cuando la posición se vuelve plana, se borran los niveles de protección almacenados.

Parámetros

Nombre Descripción Predeterminado
CandleType Serie de velas utilizadas para cálculos de indicadores y detección de señales. plazo de 15 minutos
OrderVolume Volumen de cada orden de mercado en lotes/contratos. 2
TakeProfitPoints Distancia al nivel de toma de ganancias expresada en puntos de corredor (escalones de precio). Un valor de 0 deshabilita la toma de ganancias. 20
StopLossPoints Distancia al nivel de stop-loss expresada en puntos del corredor. Un valor de 0 desactiva el stop-loss. 30
TrailingStopPoints Distancia utilizada al seguir la posición abierta. 0 desactiva el trailing stop. 50
ShortEmaPeriod Duración de la EMA rápida. 5
LongEmaPeriod Duración del EMA lenta. 60

Notas de implementación

  • La estrategia utiliza SubscribeCandles().Bind(shortEma, longEma, ProcessCandle) para conectar datos de velas con indicadores EMA, siguiendo el patrón preferido de alto nivel API.
  • Los valores del indicador se reciben como decimales listos para usar en la devolución de llamada vinculante, por lo que no es necesaria la indexación manual del búfer.
  • Las distancias de protección se convierten de MetaTrader puntos a precios de StockSharp multiplicando por el instrumento PriceStep. Si el instrumento utiliza un precio de pip fraccionario (3 o 5 decimales), el asistente calcula el tamaño del pip en consecuencia.
  • El comportamiento de stop-loss, take-profit y trailing se implementan internamente con las salidas del mercado porque StockSharp no expone el mismo flujo de trabajo OrderModify que MetaTrader 4. La gestión comercial resultante refleja la lógica original: los niveles se verifican en cada vela y las salidas ocurren inmediatamente una vez que se superan.
  • La primera evaluación cruzada se ignora intencionalmente para reproducir la protección first_time que evitó transacciones prematuras en el script MQL.

Diferencias con la versión MetaTrader

  • Gestión del dinero: el EA original siempre intercambiaba el parámetro Lots. La conversión expone el mismo concepto a través de OrderVolume y también lo asigna a la propiedad de estrategia Volume para que los diseñadores y optimizadores puedan reutilizarlo.
  • Colocación de órdenes: MetaTrader aplicó stop-loss y take-profit directamente dentro de OrderSend. En StockSharp estos niveles son rastreados por la estrategia y cerrados con órdenes de mercado cuando se superan.
  • Precisión de trailing stop: las paradas movidas EA utilizan datos de tick (Bid/Ask). El puerto actualiza la lógica final al cerrar la vela, que es la granularidad más fina disponible dentro de este proyecto de muestra. Las reglas de distancia y activación siguen siendo idénticas.
  • Se simplificaron el manejo y el registro de errores; El registro StockSharp proporciona información detallada a través del registro de estrategia estándar.

Consejos de uso

  • Alinee CandleType con el período de tiempo utilizado durante las pruebas retrospectivas del EA original para mantener un comportamiento de indicador comparable.
  • Cuando opere con símbolos cotizados con pips fraccionarios, asegúrese de que las distancias de puntos configuradas reflejen el número deseado de pips (por ejemplo, en EURUSD 10 puntos equivalen a 1 pip).
  • Establezca OrderVolume en el tamaño del contrato esperado por su lugar de ejecución. La estrategia no realiza escalado de volumen automático.
  • Utilice los controles de optimización integrados en cada parámetro para explorar combinaciones de EMA períodos y distancias de riesgo tal como optimizaría las entradas en MetaTrader.

Archivos

  • CS/EmaCross2Strategy.cs – StockSharp implementación de la lógica comercial.
  • README.md – Documentación en inglés (este archivo).
  • README_zh.md – Traducción al chino.
  • README_ru.md – traducción al ruso.
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>
/// Counter-trend EMA crossover strategy converted from the MetaTrader 4 expert "EMA_CROSS_2".
/// Buys when the long EMA rises above the short EMA, and sells when the short EMA climbs above the long EMA.
/// Incorporates MetaTrader-style risk management with point-based stop-loss, take-profit, and trailing stop levels.
/// </summary>
public class EmaCross2Strategy : Strategy
{
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<decimal> _orderVolume;
	private readonly StrategyParam<decimal> _takeProfitPoints;
	private readonly StrategyParam<decimal> _stopLossPoints;
	private readonly StrategyParam<decimal> _trailingStopPoints;
	private readonly StrategyParam<int> _shortEmaPeriod;
	private readonly StrategyParam<int> _longEmaPeriod;

	private ExponentialMovingAverage _shortEma;
	private ExponentialMovingAverage _longEma;

	private bool _skipFirstSignal = true;
	private int _lastDirection;
	private decimal? _stopLossPrice;
	private decimal? _takeProfitPrice;
	private decimal _pointSize;
	private decimal _entryPrice;

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

	/// <summary>
	/// Order volume applied to new market orders.
	/// </summary>
	public decimal OrderVolume
	{
		get => _orderVolume.Value;
		set => _orderVolume.Value = value;
	}

	/// <summary>
	/// Take-profit distance expressed in broker points.
	/// </summary>
	public decimal TakeProfitPoints
	{
		get => _takeProfitPoints.Value;
		set => _takeProfitPoints.Value = value;
	}

	/// <summary>
	/// Stop-loss distance expressed in broker points.
	/// </summary>
	public decimal StopLossPoints
	{
		get => _stopLossPoints.Value;
		set => _stopLossPoints.Value = value;
	}

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

	/// <summary>
	/// Period of the short EMA.
	/// </summary>
	public int ShortEmaPeriod
	{
		get => _shortEmaPeriod.Value;
		set => _shortEmaPeriod.Value = value;
	}

	/// <summary>
	/// Period of the long EMA.
	/// </summary>
	public int LongEmaPeriod
	{
		get => _longEmaPeriod.Value;
		set => _longEmaPeriod.Value = value;
	}

	/// <summary>
	/// Initialize strategy parameters.
	/// </summary>
	public EmaCross2Strategy()
	{
		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(15).TimeFrame())
		.SetDisplay("Candle Type", "Time frame used for EMA calculations", "General");

		_orderVolume = Param(nameof(OrderVolume), 2m)
		.SetGreaterThanZero()
		.SetDisplay("Order Volume", "Volume of each market order", "Trading")
		
		.SetOptimize(0.1m, 5m, 0.1m);

		_takeProfitPoints = Param(nameof(TakeProfitPoints), 500m)
		.SetNotNegative()
		.SetDisplay("Take Profit (points)", "Distance from entry to take-profit in broker points", "Risk")
		
		.SetOptimize(0m, 200m, 5m);

		_stopLossPoints = Param(nameof(StopLossPoints), 500m)
		.SetNotNegative()
		.SetDisplay("Stop Loss (points)", "Distance from entry to stop-loss in broker points", "Risk")
		
		.SetOptimize(0m, 200m, 5m);

		_trailingStopPoints = Param(nameof(TrailingStopPoints), 500m)
		.SetNotNegative()
		.SetDisplay("Trailing Stop (points)", "Trailing distance maintained after entry", "Risk")
		
		.SetOptimize(0m, 200m, 5m);

		_shortEmaPeriod = Param(nameof(ShortEmaPeriod), 5)
		.SetGreaterThanZero()
		.SetDisplay("Short EMA", "Length of the fast EMA", "Indicators")
		
		.SetOptimize(2, 40, 1);

		_longEmaPeriod = Param(nameof(LongEmaPeriod), 60)
		.SetGreaterThanZero()
		.SetDisplay("Long EMA", "Length of the slow EMA", "Indicators")
		
		.SetOptimize(10, 200, 5);
	}

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

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

		Volume = OrderVolume;
		_shortEma = null;
		_longEma = null;
		_skipFirstSignal = true;
		_lastDirection = 0;
		_stopLossPrice = null;
		_takeProfitPrice = null;
		_pointSize = 0m;
		_entryPrice = 0m;
	}

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

		Volume = OrderVolume;
		_pointSize = CalculatePointSize();

		_shortEma = new EMA { Length = ShortEmaPeriod };
		_longEma = new EMA { Length = LongEmaPeriod };

		var subscription = SubscribeCandles(CandleType);
		subscription
		.Bind(_shortEma, _longEma, ProcessCandle)
		.Start();

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

	private void ProcessCandle(ICandleMessage candle, decimal shortEmaValue, decimal longEmaValue)
	{
		// Work only with finished candles to avoid repeated signals inside the same bar.
		if (candle.State != CandleStates.Finished)
		return;

		if (_pointSize <= 0m)
		_pointSize = CalculatePointSize();

		if (CheckRisk(candle))
		return;

		if (Position != 0)
		UpdateTrailingStop(candle);
		else if (_stopLossPrice.HasValue || _takeProfitPrice.HasValue)
		ResetRiskLevels();

		var signal = EvaluateCross(longEmaValue, shortEmaValue);

		if (signal == 0)
		return;

		if (!IsFormedAndOnlineAndAllowTrading())
		return;

		if (Position != 0)
		return;

		var volume = OrderVolume;
		if (volume <= 0m)
		volume = 1m;

		if (signal == 1)
		{
			BuyMarket(volume);
			SetRiskLevels(candle.ClosePrice, true);
		}
		else if (signal == 2)
		{
			SellMarket(volume);
			SetRiskLevels(candle.ClosePrice, false);
		}
	}

	private int EvaluateCross(decimal longValue, decimal shortValue)
	{
		var currentDirection = 0;

		if (longValue > shortValue)
		currentDirection = 1;
		else if (longValue < shortValue)
		currentDirection = 2;

		if (_skipFirstSignal)
		{
			_skipFirstSignal = false;
			return 0;
		}

		if (currentDirection != 0 && currentDirection != _lastDirection)
		{
			_lastDirection = currentDirection;
			return _lastDirection;
		}

		return 0;
	}

	private bool CheckRisk(ICandleMessage candle)
	{
		if (Position > 0)
		{
			var size = Math.Abs(Position);

			if (_stopLossPrice.HasValue && candle.LowPrice <= _stopLossPrice.Value)
			{
				SellMarket(size);
				ResetRiskLevels();
				return true;
			}

			if (_takeProfitPrice.HasValue && candle.HighPrice >= _takeProfitPrice.Value)
			{
				SellMarket(size);
				ResetRiskLevels();
				return true;
			}
		}
		else if (Position < 0)
		{
			var size = Math.Abs(Position);

			if (_stopLossPrice.HasValue && candle.HighPrice >= _stopLossPrice.Value)
			{
				BuyMarket(size);
				ResetRiskLevels();
				return true;
			}

			if (_takeProfitPrice.HasValue && candle.LowPrice <= _takeProfitPrice.Value)
			{
				BuyMarket(size);
				ResetRiskLevels();
				return true;
			}
		}
		else if (_stopLossPrice.HasValue || _takeProfitPrice.HasValue)
		{
			ResetRiskLevels();
		}

		return false;
	}

	private void UpdateTrailingStop(ICandleMessage candle)
	{
		if (TrailingStopPoints <= 0m || _pointSize <= 0m)
		return;

		var distance = TrailingStopPoints * _pointSize;
		if (distance <= 0m)
		return;

		var entryPrice = _entryPrice > 0 ? _entryPrice : candle.ClosePrice;

		if (Position > 0)
		{
			var profit = candle.ClosePrice - entryPrice;
			if (profit > distance)
			{
				var candidate = candle.ClosePrice - distance;
				if (!_stopLossPrice.HasValue || _stopLossPrice.Value < candidate)
				_stopLossPrice = candidate;
			}
		}
		else if (Position < 0)
		{
			var profit = entryPrice - candle.ClosePrice;
			if (profit > distance)
			{
				var candidate = candle.ClosePrice + distance;
				if (!_stopLossPrice.HasValue || _stopLossPrice.Value > candidate)
				_stopLossPrice = candidate;
			}
		}
	}

	private void SetRiskLevels(decimal executionPrice, bool isLong)
	{
		if (_pointSize <= 0m)
		{
			ResetRiskLevels();
			return;
		}

		_stopLossPrice = StopLossPoints > 0m
		? executionPrice + (isLong ? -1m : 1m) * StopLossPoints * _pointSize
		: null;

		_takeProfitPrice = TakeProfitPoints > 0m
		? executionPrice + (isLong ? 1m : -1m) * TakeProfitPoints * _pointSize
		: null;
	}

	private void ResetRiskLevels()
	{
		_stopLossPrice = null;
		_takeProfitPrice = null;
	}

	/// <inheritdoc />
	protected override void OnOwnTradeReceived(MyTrade trade)
	{
		base.OnOwnTradeReceived(trade);

		if (Position != 0 && _entryPrice == 0m)
			_entryPrice = trade.Trade.Price;

		if (Position == 0m)
			_entryPrice = 0m;
	}

	private decimal CalculatePointSize()
	{
		var step = Security?.PriceStep ?? 0m;
		return step > 0m ? step : 1m;
	}
}