Ver en GitHub

Estrategia Stopreversal Tm

Descripción general

La estrategia Stopreversal Tm es una traducción directa del asesor experto original de MetaTrader 5 Exp_Stopreversal_Tm.mq5. La idea de trading sigue el indicador personalizado Stopreversal, que mantiene un trailing stop dinámico alrededor del precio y genera alertas de reversión cada vez que el precio cruza ese límite de seguimiento. La estrategia opera en un único instrumento y un único feed de velas y está diseñada para el trading de reversión de tendencia con un filtro de sesión definido por el usuario.

Generación de señales

El indicador Stopreversal calcula un precio de referencia a partir del modo de precio aplicado seleccionado y luego ajusta un nivel de trailing stop por Sensitivity (el parámetro nPips). Cada vez que el nuevo precio aplicado sube por encima del trailing stop mientras la barra anterior estaba por debajo, se produce una señal alcista. A la inversa, aparece una señal bajista cuando el nuevo precio cae por debajo del trailing stop después de haber estado por encima. Cada señal alcista solicita simultáneamente el cierre de posiciones cortas existentes y la apertura de un nuevo largo, mientras que cada señal bajista cierra largos y abre cortos.

Para reproducir el comportamiento de la implementación original de MetaTrader, la estrategia puede retrasar la ejecución de señales en varias barras completadas (Signal Bar Delay). Esto replica la entrada SignalBar del asesor experto y previene el trading en la vela que aún se está formando.

Filtro de sesión y manejo de posiciones

El asesor experto permitía el trading solo dentro de una ventana de tiempo especificada. La estrategia convertida mantiene la misma lógica: cuando el indicador Use Time Filter está habilitado, las órdenes se permiten solo dentro de la sesión configurada por Start Hour/Minute y End Hour/Minute. Si la hora actual sale de la ventana permitida, cualquier posición abierta se cierra inmediatamente. Las salidas impulsadas por señales permanecen activas incluso cuando la sesión está deshabilitada.

La estrategia trabaja en posiciones netas. Siempre se ejecuta una acción de cierre antes de una entrada opuesta, garantizando que la dirección cambia sin exposiciones superpuestas.

Parámetros

  • Allow Buy Entries / Allow Sell Entries – habilitar o deshabilitar la apertura de nuevas posiciones largas o cortas cuando se recibe la señal correspondiente.
  • Allow Long Exits / Allow Short Exits – controlar si las señales opuestas pueden cerrar posiciones existentes.
  • Use Time Filter – activa la ventana de sesión de trading.
  • Start Hour / Start Minute / End Hour / End Minute – define el inicio inclusivo y el fin exclusivo de la ventana de trading. El filtro de tiempo soporta sesiones nocturnas donde la hora de fin es anterior a la hora de inicio.
  • Sensitivity (nPips) – distancia relativa (expresada como multiplicador, p. ej., 0.004 = 0.4%) usada para mover el trailing stop más cerca o más lejos del precio.
  • Signal Bar Delay (SignalBar) – número de velas completadas a esperar antes de actuar sobre una señal. 0 ejecuta inmediatamente en la vela de cierre, 1 reproduce el comportamiento predeterminado de actuar en la barra anterior.
  • Candle Type – marco temporal de la suscripción de velas usada para los cálculos del indicador.
  • Applied Price – elección de la serie de precio (cierre, apertura, precio mediano, modos de seguimiento de tendencia, precio Demark, etc.) que alimenta el cálculo del trailing stop.

Notas de implementación

  • El indicador está implementado directamente dentro de la estrategia sin depender de búferes externos, asegurando que la lógica del trailing stop nPips coincida con el código MQL5 original.
  • La gestión de sesión y la secuencia de señales siguen al experto original, incluida la prioridad de cerrar la exposición existente antes de abrir nuevas operaciones.
  • La conversión se centra en la API de alto nivel de StockSharp: suscripciones de velas, cola de señales retrasadas y órdenes de mercado (BuyMarket / SellMarket). Las características de gestión monetaria vinculadas a las métricas de cuenta de MetaTrader se omitieron porque las estrategias de StockSharp ya operan con tamaños de posición explícitos.
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>
/// Stopreversal trailing stop strategy with a configurable trading session filter.
/// </summary>
public class StopreversalTmStrategy : Strategy
{
	/// <summary>
	/// Available price sources for the Stopreversal trailing stop.
	/// </summary>
	public enum StopreversalAppliedPrices
	{
		Close = 1,
		Open,
		High,
		Low,
		Median,
		Typical,
		Weighted,
		Simple,
		Quarter,
		TrendFollow0,
		TrendFollow1,
		Demark
	}

	private readonly StrategyParam<bool> _allowBuyEntry;
	private readonly StrategyParam<bool> _allowSellEntry;
	private readonly StrategyParam<bool> _allowBuyExit;
	private readonly StrategyParam<bool> _allowSellExit;
	private readonly StrategyParam<bool> _useTimeFilter;
	private readonly StrategyParam<int> _startHour;
	private readonly StrategyParam<int> _startMinute;
	private readonly StrategyParam<int> _endHour;
	private readonly StrategyParam<int> _endMinute;
	private readonly StrategyParam<decimal> _nPips;
	private readonly StrategyParam<int> _signalBar;
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<StopreversalAppliedPrices> _appliedPrice;

	private readonly List<SignalInfo> _signalQueue = new();

	private decimal? _previousAppliedPrice;
	private decimal? _previousStopLevel;

	public StopreversalTmStrategy()
	{
		_allowBuyEntry = Param(nameof(AllowBuyEntry), true)
			.SetDisplay("Allow Buy Entries", "Enable opening long positions on bullish signals", "Signals")
			;

		_allowSellEntry = Param(nameof(AllowSellEntry), true)
			.SetDisplay("Allow Sell Entries", "Enable opening short positions on bearish signals", "Signals")
			;

		_allowBuyExit = Param(nameof(AllowBuyExit), true)
			.SetDisplay("Allow Long Exits", "Close existing long positions when a sell signal arrives", "Signals")
			;

		_allowSellExit = Param(nameof(AllowSellExit), true)
			.SetDisplay("Allow Short Exits", "Close existing short positions when a buy signal arrives", "Signals")
			;

		_useTimeFilter = Param(nameof(UseTimeFilter), false)
			.SetDisplay("Use Time Filter", "Restrict trading to the configured session", "Session");

		_startHour = Param(nameof(StartHour), 0)
			.SetRange(0, 23)
			.SetDisplay("Start Hour", "Session start hour (0-23)", "Session")
			;

		_startMinute = Param(nameof(StartMinute), 0)
			.SetRange(0, 59)
			.SetDisplay("Start Minute", "Session start minute (0-59)", "Session")
			;

		_endHour = Param(nameof(EndHour), 23)
			.SetRange(0, 23)
			.SetDisplay("End Hour", "Session end hour (0-23)", "Session")
			;

		_endMinute = Param(nameof(EndMinute), 59)
			.SetRange(0, 59)
			.SetDisplay("End Minute", "Session end minute (0-59)", "Session")
			;

		_nPips = Param(nameof(Npips), 0.004m)
			.SetGreaterThanZero()
			.SetDisplay("Sensitivity", "Relative offset used to build the trailing stop", "Indicator")
			;

		_signalBar = Param(nameof(SignalBar), 1)
			.SetNotNegative()
			.SetDisplay("Signal Bar Delay", "Number of completed bars to wait before acting", "Indicator")
			;

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

		_appliedPrice = Param(nameof(AppliedPrice), StopreversalAppliedPrices.Close)
			.SetDisplay("Applied Price", "Price source for the trailing stop", "Indicator")
			;
	}

	public bool AllowBuyEntry { get => _allowBuyEntry.Value; set => _allowBuyEntry.Value = value; }
	public bool AllowSellEntry { get => _allowSellEntry.Value; set => _allowSellEntry.Value = value; }
	public bool AllowBuyExit { get => _allowBuyExit.Value; set => _allowBuyExit.Value = value; }
	public bool AllowSellExit { get => _allowSellExit.Value; set => _allowSellExit.Value = value; }
	public bool UseTimeFilter { get => _useTimeFilter.Value; set => _useTimeFilter.Value = value; }
	public int StartHour { get => _startHour.Value; set => _startHour.Value = value; }
	public int StartMinute { get => _startMinute.Value; set => _startMinute.Value = value; }
	public int EndHour { get => _endHour.Value; set => _endHour.Value = value; }
	public int EndMinute { get => _endMinute.Value; set => _endMinute.Value = value; }
	public decimal Npips { get => _nPips.Value; set => _nPips.Value = value; }
	public int SignalBar { get => _signalBar.Value; set => _signalBar.Value = value; }
	public DataType CandleType { get => _candleType.Value; set => _candleType.Value = value; }
	public StopreversalAppliedPrices AppliedPrice { get => _appliedPrice.Value; set => _appliedPrice.Value = value; }

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

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();
		_previousAppliedPrice = null;
		_previousStopLevel = null;
		_signalQueue.Clear();
	}

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

		_previousAppliedPrice = null;
		_previousStopLevel = null;
		_signalQueue.Clear();

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

		// no protection needed
	}

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

		var price = GetAppliedPrice(candle);

		if (_previousAppliedPrice is null || _previousStopLevel is null)
		{
			_previousAppliedPrice = price;
			_previousStopLevel = price;
			EnqueueSignal(new SignalInfo(false, false, false, false), candle.CloseTime);
			return;
		}

		var prevPrice = _previousAppliedPrice.Value;
		var prevStop = _previousStopLevel.Value;

		var trailingStop = CalculateTrailingStop(price, prevPrice, prevStop);

		var buySignal = price > trailingStop && prevPrice < prevStop;
		var sellSignal = price < trailingStop && prevPrice > prevStop;

		_previousStopLevel = trailingStop;
		_previousAppliedPrice = price;

		var action = new SignalInfo(
			buySignal && AllowBuyEntry,
			sellSignal && AllowSellEntry,
			sellSignal && AllowBuyExit,
			buySignal && AllowSellExit
		);

		EnqueueSignal(action, candle.CloseTime);
	}

	private void EnqueueSignal(SignalInfo signal, DateTime currentTime)
	{
		_signalQueue.Add(signal);

		while (_signalQueue.Count > SignalBar)
		{
			var action = _signalQueue[0];
			try { _signalQueue.RemoveAt(0); } catch { }
			HandleSignal(action, currentTime);
		}
	}

	private void HandleSignal(SignalInfo signal, DateTime currentTime)
	{
		var inWindow = !UseTimeFilter || IsWithinTradingWindow(currentTime);

		if (UseTimeFilter && !inWindow && Position != 0)
		{
			if (Position > 0)
				SellMarket();
			else
				BuyMarket();
		}

		if (signal.CloseLong && Position > 0)
			SellMarket();

		if (signal.CloseShort && Position < 0)
			BuyMarket();

		if (!UseTimeFilter || inWindow)
		{
			if (signal.OpenLong && Position <= 0)
				BuyMarket();

			if (signal.OpenShort && Position >= 0)
				SellMarket();
		}
	}

	private decimal CalculateTrailingStop(decimal price, decimal prevPrice, decimal prevStop)
	{
		var shift = Npips;

		if (price == prevStop)
			return prevStop;

		if (prevPrice < prevStop && price < prevStop)
			return Math.Min(prevStop, price * (1 + shift));

		if (prevPrice > prevStop && price > prevStop)
			return Math.Max(prevStop, price * (1 - shift));

		return price > prevStop
			? price * (1 - shift)
			: price * (1 + shift);
	}

	private decimal GetAppliedPrice(ICandleMessage candle)
	{
		return AppliedPrice switch
		{
			StopreversalAppliedPrices.Close => candle.ClosePrice,
			StopreversalAppliedPrices.Open => candle.OpenPrice,
			StopreversalAppliedPrices.High => candle.HighPrice,
			StopreversalAppliedPrices.Low => candle.LowPrice,
			StopreversalAppliedPrices.Median => (candle.HighPrice + candle.LowPrice) / 2m,
			StopreversalAppliedPrices.Typical => (candle.ClosePrice + candle.HighPrice + candle.LowPrice) / 3m,
			StopreversalAppliedPrices.Weighted => (2m * candle.ClosePrice + candle.HighPrice + candle.LowPrice) / 4m,
			StopreversalAppliedPrices.Simple => (candle.OpenPrice + candle.ClosePrice) / 2m,
			StopreversalAppliedPrices.Quarter => (candle.OpenPrice + candle.ClosePrice + candle.HighPrice + candle.LowPrice) / 4m,
			StopreversalAppliedPrices.TrendFollow0 => candle.ClosePrice > candle.OpenPrice
				? candle.HighPrice
				: candle.ClosePrice < candle.OpenPrice
					? candle.LowPrice
					: candle.ClosePrice,
			StopreversalAppliedPrices.TrendFollow1 => candle.ClosePrice > candle.OpenPrice
				? (candle.HighPrice + candle.ClosePrice) / 2m
				: candle.ClosePrice < candle.OpenPrice
					? (candle.LowPrice + candle.ClosePrice) / 2m
					: candle.ClosePrice,
			StopreversalAppliedPrices.Demark => CalculateDemarkPrice(candle),
			_ => candle.ClosePrice
		};
	}

	private static decimal CalculateDemarkPrice(ICandleMessage candle)
	{
		var result = candle.HighPrice + candle.LowPrice + candle.ClosePrice;

		if (candle.ClosePrice < candle.OpenPrice)
			result = (result + candle.LowPrice) / 2m;
		else if (candle.ClosePrice > candle.OpenPrice)
			result = (result + candle.HighPrice) / 2m;
		else
			result = (result + candle.ClosePrice) / 2m;

		return ((result - candle.LowPrice) + (result - candle.HighPrice)) / 2m;
	}

	private bool IsWithinTradingWindow(DateTime time)
	{
		var start = new TimeSpan(StartHour, StartMinute, 0);
		var end = new TimeSpan(EndHour, EndMinute, 0);
		var current = time.TimeOfDay;

		if (start == end)
			return false;

		if (start < end)
			return current >= start && current < end;

		return current >= start || current < end;
	}

	private readonly struct SignalInfo
	{
		public SignalInfo(bool openLong, bool openShort, bool closeLong, bool closeShort)
		{
			OpenLong = openLong;
			OpenShort = openShort;
			CloseLong = closeLong;
			CloseShort = closeShort;
		}

		public bool OpenLong { get; }
		public bool OpenShort { get; }
		public bool CloseLong { get; }
		public bool CloseShort { get; }
	}
}