Ver en GitHub

Estrategia de Doblador con Hedge y Trailing

Descripción general

La Estrategia de Doblador con Hedge y Trailing es una conversión directa a la API de alto nivel de StockSharp del expert advisor de MetaTrader 5 Doubler.mq5. El algoritmo abre inmediatamente una posición de mercado larga y corta simétrica siempre que no exista exposición, luego gestiona ambas patas con reglas independientes de stop-loss, take-profit y trailing stop. La conversión preserva el comportamiento de hedging del programa MQL original mientras adapta la gestión del riesgo a las primitivas de StockSharp (órdenes de mercado, suscripciones Level1 y parámetros de estrategia).

A diferencia de las estrategias direccionales, el sistema mantiene ambas direcciones activas hasta que cada pata sale por su propia lógica protectora. Una vez que ambas patas están planas, la estrategia recrea el hedge, manteniendo continuamente la exposición emparejada.

Características clave

  • Hedging automático – abre una orden de compra y venta con el mismo volumen siempre que la estrategia no tenga posiciones activas.
  • Controles de riesgo basados en pips – stop-loss, take-profit y offsets de trailing se configuran en pips y se convierten internamente a pasos de precio inspeccionando el paso de precio del valor y la precisión decimal (los instrumentos de 3/5 decimales se escalan automáticamente por un factor de 10).
  • Trailing independiente por pata – cada pata rastrea el mejor bid/ask actual. Cuando el precio se mueve más de TrailingStopPips + TrailingStepPips a favor, el nivel de stop se avanza en TrailingStopPips respetando la condición del paso de trailing, reflejando exactamente la lógica del EA original.
  • Validación de volumen – el volumen de la orden se valida contra MinVolume, MaxVolume y VolumeStep, generando una excepción cuando el tamaño solicitado viola las restricciones del intercambio.
  • Diagnósticos opcionales – la bandera LogTradeDetails habilita mensajes informativos detallados (entradas, salidas, ajustes de trailing) que ayudan durante las pruebas o el monitoreo en vivo.

Parámetros

Parámetro Descripción Predeterminado Notas
OrderVolume Volumen de cada pata del hedge (órdenes de compra y venta). 1 Debe respetar los límites de volumen del intercambio; normalizado al VolumeStep más cercano.
StopLossPips Distancia del stop-loss en pips. 150 0 desactiva el stop-loss.
TakeProfitPips Distancia del take-profit en pips. 300 0 desactiva el take-profit.
TrailingStopPips Distancia del trailing stop en pips. 5 Si es mayor que cero, TrailingStepPips también debe ser positivo.
TrailingStepPips Movimiento adicional mínimo antes de que avance el trailing stop. 5 Barrera que evita que el stop se mueva con demasiada frecuencia.
LogTradeDetails Habilita el registro detallado de ejecuciones y actualizaciones de trailing. false Establecer en true para ejecuciones de depuración.

Lógica de trading

Entrada

  1. Suscribirse a actualizaciones Level1 (mejor bid/ask).
  2. Cuando tanto _longPosition como _shortPosition son nulos y no hay órdenes de entrada pendientes, registrar dos órdenes de mercado: una de compra y una de venta con OrderVolume cada una.
  3. Después de confirmar las ejecuciones, la estrategia registra los precios de entrada, los niveles iniciales de stop/take y reinicia los rastreadores de trailing.

Gestión del riesgo

  • Stop-loss – para cada pata el stop inicial se coloca a StopLossPips de distancia del precio de entrada. Una distancia de stop de 0 desactiva completamente el stop protector.
  • Take-profit – take-profit simétrico en TakeProfitPips. Un valor de 0 desactiva los objetivos de beneficio.
  • Cierre forzado – si NormalizeVolume detecta un tamaño no válido (demasiado pequeño/grande o que no coincide con VolumeStep), la estrategia genera una excepción para evitar enviar una orden inválida.

Comportamiento del trailing stop

  1. Cuando el precio se mueve favorablemente al menos TrailingStopPips + TrailingStepPips, el stop avanza a currentPrice ± TrailingStopPips.
  2. La verificación del paso de trailing reproduce la condición MQL: el stop solo se mueve si el nuevo nivel está al menos TrailingStepPips más cerca del precio que el stop existente, o si todavía no existe stop.
  3. Para posiciones largas se usa el mejor bid como precio de referencia; para posiciones cortas se usa el mejor ask para que los niveles de salida reflejen precios de ejecución realistas.

Salida

  • Cada pata sale de forma independiente cuando se cumple su condición de stop-loss, trailing stop o take-profit. Las órdenes de salida se registran como órdenes de mercado, y una vez que una pata está plana, su estado interno se limpia.
  • Después de que ambas patas se cierran, la próxima actualización Level1 desencadena un nuevo par con hedge.

Requisitos de datos

  • Level1 (mejor bid/ask) – necesario para instantáneas del precio de entrada, cálculos de trailing y activadores de salida.
  • No es necesaria ninguna suscripción de velas o trades; la estrategia reacciona exclusivamente a actualizaciones Level1.

Notas sobre la conversión

  • Las distancias en pips se convierten a offsets de precio absolutos multiplicando por el PriceStep del valor. Los instrumentos cotizados con 3 o 5 decimales reciben automáticamente un ajuste ×10, coincidiendo con la definición de pip usada en el expert de MetaTrader.
  • La estrategia se basa en los métodos de Strategy de alto nivel de StockSharp (RegisterOrder, StartProtection, SubscribeLevel1) y evita las operaciones de conector de bajo nivel.
  • El hedging se implementa a través de objetos PositionState internos para que las patas largas y cortas se rastreen incluso cuando el broker/portafolio usa posiciones netas.
  • La conversión es autónoma y no modifica ni requiere ningún arnés de prueba del repositorio.
using System;
using System.Collections.Generic;

using Ecng.Common;

using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;

namespace StockSharp.Samples.Strategies;

/// <summary>
/// Doubler strategy using double EMA confirmation with trailing stop management.
/// Enters long when both fast and medium EMAs are above slow EMA.
/// Enters short when both fast and medium EMAs are below slow EMA.
/// </summary>
public class DoublerStrategy : Strategy
{
	private readonly StrategyParam<int> _fastPeriod;
	private readonly StrategyParam<int> _medPeriod;
	private readonly StrategyParam<int> _slowPeriod;
	private readonly StrategyParam<int> _stopLossPoints;
	private readonly StrategyParam<int> _takeProfitPoints;

	private ExponentialMovingAverage _fast;
	private ExponentialMovingAverage _med;
	private ExponentialMovingAverage _slow;

	private decimal _entryPrice;
	private int _cooldown;

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

	/// <summary>
	/// Medium EMA period.
	/// </summary>
	public int MedPeriod
	{
		get => _medPeriod.Value;
		set => _medPeriod.Value = value;
	}

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

	/// <summary>
	/// Stop-loss distance in price steps.
	/// </summary>
	public int StopLossPoints
	{
		get => _stopLossPoints.Value;
		set => _stopLossPoints.Value = value;
	}

	/// <summary>
	/// Take-profit distance in price steps.
	/// </summary>
	public int TakeProfitPoints
	{
		get => _takeProfitPoints.Value;
		set => _takeProfitPoints.Value = value;
	}

	/// <summary>
	/// Initializes strategy parameters.
	/// </summary>
	public DoublerStrategy()
	{
		_fastPeriod = Param(nameof(FastPeriod), 20)
			.SetGreaterThanZero()
			.SetDisplay("Fast Period", "Fast EMA period", "Indicator");

		_medPeriod = Param(nameof(MedPeriod), 50)
			.SetGreaterThanZero()
			.SetDisplay("Medium Period", "Medium EMA period", "Indicator");

		_slowPeriod = Param(nameof(SlowPeriod), 200)
			.SetGreaterThanZero()
			.SetDisplay("Slow Period", "Slow EMA period", "Indicator");

		_stopLossPoints = Param(nameof(StopLossPoints), 150)
			.SetNotNegative()
			.SetDisplay("Stop Loss", "Stop-loss distance in price steps", "Risk");

		_takeProfitPoints = Param(nameof(TakeProfitPoints), 300)
			.SetNotNegative()
			.SetDisplay("Take Profit", "Take-profit distance in price steps", "Risk");
	}

	/// <inheritdoc />
	public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
	{
		yield return (Security, TimeSpan.FromMinutes(5).TimeFrame());
	}

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

		_fast = null;
		_med = null;
		_slow = null;
		_entryPrice = 0;
		_cooldown = 0;
	}

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

		_fast = new ExponentialMovingAverage { Length = FastPeriod };
		_med = new ExponentialMovingAverage { Length = MedPeriod };
		_slow = new ExponentialMovingAverage { Length = SlowPeriod };

		var subscription = SubscribeCandles(TimeSpan.FromMinutes(5).TimeFrame());
		subscription.Bind(_fast, _med, _slow, ProcessCandle);
		subscription.Start();
	}

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

		if (!_fast.IsFormed || !_med.IsFormed || !_slow.IsFormed)
			return;

		if (_cooldown > 0)
		{
			_cooldown--;
			return;
		}

		var close = candle.ClosePrice;
		var step = Security?.PriceStep ?? 1m;

		// Check SL/TP
		if (Position > 0 && _entryPrice > 0)
		{
			if (StopLossPoints > 0 && close <= _entryPrice - StopLossPoints * step)
			{
				SellMarket();
				_entryPrice = 0;
				_cooldown = 100;
				return;
			}

			if (TakeProfitPoints > 0 && close >= _entryPrice + TakeProfitPoints * step)
			{
				SellMarket();
				_entryPrice = 0;
				_cooldown = 100;
				return;
			}
		}
		else if (Position < 0 && _entryPrice > 0)
		{
			if (StopLossPoints > 0 && close >= _entryPrice + StopLossPoints * step)
			{
				BuyMarket();
				_entryPrice = 0;
				_cooldown = 100;
				return;
			}

			if (TakeProfitPoints > 0 && close <= _entryPrice - TakeProfitPoints * step)
			{
				BuyMarket();
				_entryPrice = 0;
				_cooldown = 100;
				return;
			}
		}

		// Double confirmation: both fast and med above slow for long
		if (fastValue > slowValue && medValue > slowValue && Position <= 0)
		{
			if (Position < 0)
				BuyMarket();

			BuyMarket();
			_entryPrice = close;
			_cooldown = 100;
		}
		// Double confirmation: both fast and med below slow for short
		else if (fastValue < slowValue && medValue < slowValue && Position >= 0)
		{
			if (Position > 0)
				SellMarket();

			SellMarket();
			_entryPrice = close;
			_cooldown = 100;
		}
	}
}