Ver en GitHub

Estrategia NRTR ATR Stop

Descripción general

La Estrategia NRTR ATR Stop es una conversión directa del asesor experto de MetaTrader Exp_NRTR_ATR_STOP_Tm. El sistema combina un stop de Reversión de Tendencia No Repintable (NRTR) con un filtro de Rango Verdadero Promedio (ATR) para determinar la tendencia dominante y arrastrar los niveles de protección. Las decisiones de trading se generan en el cierre del marco temporal seleccionado y pueden retrasarse por un número configurable de barras completamente formadas para imitar el desplazamiento de señal original.

La estrategia está implementada sobre la API de alto nivel de StockSharp. Toda la lógica de trading está impulsada por suscripciones de velas, vínculos de indicadores y asistentes de órdenes gestionadas, garantizando compatibilidad con los productos Designer, Shell, Runner y API.

Lógica de trading

  1. Cálculo del indicador
    • El ATR se calcula en el marco temporal seleccionado con el período proporcionado.
    • El valor del ATR se multiplica por un coeficiente para construir los niveles superior e inferior del NRTR.
    • La dirección de la tendencia cambia cuando la vela anterior rompe el nivel NRTR opuesto; estos eventos también crean señales de flecha que pueden activar entradas.
  2. Retraso de señal
    • El parámetro SignalBarDelay reproduce la entrada SignalBar de MetaTrader. Retrasa la ejecución por el número elegido de velas completadas, permitiendo que la estrategia evalúe señales históricas exactamente como el experto fuente.
  3. Entradas
    • Una posición larga se abre cuando ocurre una reversión alcista de NRTR y las entradas largas están habilitadas.
    • Una posición corta se abre cuando ocurre una reversión bajista de NRTR y las entradas cortas están habilitadas.
  4. Salidas
    • Las reversiones direccionales cierran cualquier posición opuesta si el cierre está permitido para ese lado.
    • Un filtro de sesión opcional puede forzar el cierre de todas las posiciones fuera de la ventana de trading permitida.
    • La gestión de riesgo adicional se maneja a través de distancias de stop-loss y take-profit expresadas en pasos de precio. El nivel NRTR también arrastra una posición activa ajustando el stop de protección en la dirección de la tendencia.

Gestión de riesgo

  • Volumen: Las operaciones se abren con el parámetro configurable OrderVolume. El volumen puede optimizarse al igual que en la versión de MetaTrader.
  • Stop-loss / take-profit: Las distancias se especifican en múltiplos del paso de precio del instrumento, coincidiendo con la configuración original basada en puntos. Cuando tanto un stop manual como un nivel NRTR están disponibles, el precio de protección se elige de forma conservadora (más cercano al mercado) para evitar ampliar el riesgo.
  • Control de sesión: Cuando UseTradingWindow está habilitado, la estrategia solo abre posiciones dentro del intervalo [StartHour:StartMinute, EndHour:EndMinute] definido y cierra cualquier posición abierta tan pronto como el mercado abandona esa ventana.

Parámetros

Nombre Predeterminado Descripción
OrderVolume 1 Volumen utilizado al enviar órdenes de mercado.
StopLossPoints 1000 Distancia de stop en pasos de precio. Poner en 0 para deshabilitar.
TakeProfitPoints 2000 Distancia de take-profit en pasos de precio. Poner en 0 para deshabilitar.
BuyPosOpen / SellPosOpen true Permitir la apertura de posiciones largas o cortas en reversiones NRTR.
BuyPosClose / SellPosClose true Permitir el cierre de posiciones largas o cortas cuando aparece una señal opuesta.
UseTradingWindow true Habilitar el filtro de tiempo que imita el asesor experto original.
StartHour / StartMinute 0 / 0 Inicio de la sesión de trading permitida.
EndHour / EndMinute 23 / 59 Fin de la sesión de trading permitida. Soporta rangos nocturnos.
CandleType Marco temporal de 1 hora Tipo de vela utilizado para los cálculos de ATR y NRTR.
AtrPeriod 20 Número de barras utilizadas para calcular el ATR.
AtrMultiplier 2 Coeficiente aplicado al ATR al construir los niveles NRTR.
SignalBarDelay 1 Número de barras completadas para retrasar la ejecución de la señal.

Notas

  • La estrategia usa solo procesamiento a nivel de vela; la replicación tick a tick del EA original se evita intencionalmente para mantenerse consistente con la arquitectura de alto nivel de StockSharp.
  • Los comentarios dentro del código están en inglés según los requisitos del proyecto.
  • Se omite intencionalmente una versión en Python para cumplir con la solicitud del usuario.
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>
/// NRTR ATR Stop strategy converted from the original MetaTrader expert.
/// The strategy relies on an ATR-based trailing reversal level to determine trend changes.
/// </summary>
public class NrtrAtrStopStrategy : Strategy
{
	private readonly StrategyParam<decimal> _orderVolume;
	private readonly StrategyParam<decimal> _stopLossPoints;
	private readonly StrategyParam<decimal> _takeProfitPoints;
	private readonly StrategyParam<bool> _buyPosOpen;
	private readonly StrategyParam<bool> _sellPosOpen;
	private readonly StrategyParam<bool> _buyPosClose;
	private readonly StrategyParam<bool> _sellPosClose;
	private readonly StrategyParam<bool> _useTradingWindow;
	private readonly StrategyParam<int> _startHour;
	private readonly StrategyParam<int> _startMinute;
	private readonly StrategyParam<int> _endHour;
	private readonly StrategyParam<int> _endMinute;
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<int> _atrPeriod;
	private readonly StrategyParam<decimal> _atrMultiplier;
	private readonly StrategyParam<int> _signalBarDelay;

	private ATR _atrIndicator = null!;
	private decimal? _previousUpLine;
	private decimal? _previousDownLine;
	private int _previousTrend;
	private bool _hasPreviousCandle;
	private decimal _prevCandleHigh;
	private decimal _prevCandleLow;
	private decimal? _longStop;
	private decimal? _longTarget;
	private decimal? _shortStop;
	private decimal? _shortTarget;
	private readonly List<NrtrSignal> _signalQueue = new();

	private readonly struct NrtrSignal
	{
		public NrtrSignal(decimal? upLine, decimal? downLine, bool buySignal, bool sellSignal)
		{
			UpLine = upLine;
			DownLine = downLine;
			BuySignal = buySignal;
			SellSignal = sellSignal;
		}

		public decimal? UpLine { get; }
		public decimal? DownLine { get; }
		public bool BuySignal { get; }
		public bool SellSignal { get; }
	}

	/// <summary>
	/// Volume used when opening new positions.
	/// </summary>
	public decimal OrderVolume
	{
		get => _orderVolume.Value;
		set => _orderVolume.Value = value;
	}

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

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

	/// <summary>
	/// Allow opening long positions.
	/// </summary>
	public bool BuyPosOpen
	{
		get => _buyPosOpen.Value;
		set => _buyPosOpen.Value = value;
	}

	/// <summary>
	/// Allow opening short positions.
	/// </summary>
	public bool SellPosOpen
	{
		get => _sellPosOpen.Value;
		set => _sellPosOpen.Value = value;
	}

	/// <summary>
	/// Allow closing long positions on indicator signals.
	/// </summary>
	public bool BuyPosClose
	{
		get => _buyPosClose.Value;
		set => _buyPosClose.Value = value;
	}

	/// <summary>
	/// Allow closing short positions on indicator signals.
	/// </summary>
	public bool SellPosClose
	{
		get => _sellPosClose.Value;
		set => _sellPosClose.Value = value;
	}

	/// <summary>
	/// Enable the trading window restriction.
	/// </summary>
	public bool UseTradingWindow
	{
		get => _useTradingWindow.Value;
		set => _useTradingWindow.Value = value;
	}

	/// <summary>
	/// Start hour for the trading window.
	/// </summary>
	public int StartHour
	{
		get => _startHour.Value;
		set => _startHour.Value = value;
	}

	/// <summary>
	/// Start minute for the trading window.
	/// </summary>
	public int StartMinute
	{
		get => _startMinute.Value;
		set => _startMinute.Value = value;
	}

	/// <summary>
	/// End hour for the trading window.
	/// </summary>
	public int EndHour
	{
		get => _endHour.Value;
		set => _endHour.Value = value;
	}

	/// <summary>
	/// End minute for the trading window.
	/// </summary>
	public int EndMinute
	{
		get => _endMinute.Value;
		set => _endMinute.Value = value;
	}

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

	/// <summary>
	/// Period of the ATR indicator.
	/// </summary>
	public int AtrPeriod
	{
		get => _atrPeriod.Value;
		set => _atrPeriod.Value = value;
	}

	/// <summary>
	/// Multiplier applied to the ATR value when building the NRTR levels.
	/// </summary>
	public decimal AtrMultiplier
	{
		get => _atrMultiplier.Value;
		set => _atrMultiplier.Value = value;
	}

	/// <summary>
	/// Number of fully closed bars to delay signal execution.
	/// </summary>
	public int SignalBarDelay
	{
		get => _signalBarDelay.Value;
		set => _signalBarDelay.Value = value;
	}

	/// <summary>
	/// Initializes strategy parameters with defaults converted from MQL inputs.
	/// </summary>
	public NrtrAtrStopStrategy()
	{
		_orderVolume = Param(nameof(OrderVolume), 1m)
		.SetGreaterThanZero()
		.SetDisplay("Order Volume", "Volume used when opening positions", "Trading")
		
		.SetOptimize(1m, 5m, 1m);

		_stopLossPoints = Param(nameof(StopLossPoints), 1000m)
		.SetNotNegative()
		.SetDisplay("Stop Loss (points)", "Stop-loss distance measured in price steps", "Risk Management");

		_takeProfitPoints = Param(nameof(TakeProfitPoints), 2000m)
		.SetNotNegative()
		.SetDisplay("Take Profit (points)", "Take-profit distance measured in price steps", "Risk Management");

		_buyPosOpen = Param(nameof(BuyPosOpen), true)
		.SetDisplay("Enable Long Entries", "Allow opening long positions", "Trading");

		_sellPosOpen = Param(nameof(SellPosOpen), true)
		.SetDisplay("Enable Short Entries", "Allow opening short positions", "Trading");

		_buyPosClose = Param(nameof(BuyPosClose), true)
		.SetDisplay("Close Long Positions", "Allow long exits generated by the indicator", "Trading");

		_sellPosClose = Param(nameof(SellPosClose), true)
		.SetDisplay("Close Short Positions", "Allow short exits generated by the indicator", "Trading");

		_useTradingWindow = Param(nameof(UseTradingWindow), true)
		.SetDisplay("Use Trading Window", "Restrict trading to a specific intraday window", "Session");

		_startHour = Param(nameof(StartHour), 0)
		.SetDisplay("Start Hour", "Hour when trading becomes available", "Session")
		.SetOptimize(0, 23, 1);

		_startMinute = Param(nameof(StartMinute), 0)
		.SetDisplay("Start Minute", "Minute when trading becomes available", "Session")
		.SetOptimize(0, 59, 1);

		_endHour = Param(nameof(EndHour), 23)
		.SetDisplay("End Hour", "Hour when trading stops", "Session")
		.SetOptimize(0, 23, 1);

		_endMinute = Param(nameof(EndMinute), 59)
		.SetDisplay("End Minute", "Minute when trading stops", "Session")
		.SetOptimize(0, 59, 1);

		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
		.SetDisplay("Candle Type", "Primary timeframe used by the NRTR ATR Stop indicator", "General");

		_atrPeriod = Param(nameof(AtrPeriod), 20)
		.SetGreaterThanZero()
		.SetDisplay("ATR Period", "Number of bars used to calculate ATR", "Indicator")
		
		.SetOptimize(10, 40, 5);

		_atrMultiplier = Param(nameof(AtrMultiplier), 2m)
		.SetGreaterThanZero()
		.SetDisplay("ATR Multiplier", "Multiplier applied to the ATR value", "Indicator")
		
		.SetOptimize(1m, 4m, 0.5m);

		_signalBarDelay = Param(nameof(SignalBarDelay), 1)
		.SetNotNegative()
		.SetDisplay("Signal Bar", "Number of closed bars to wait before acting", "Indicator")
		
		.SetOptimize(0, 3, 1);
	}

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

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

		_previousUpLine = null;
		_previousDownLine = null;
		_previousTrend = 0;
		_hasPreviousCandle = false;
		_prevCandleHigh = 0m;
		_prevCandleLow = 0m;
		_longStop = null;
		_longTarget = null;
		_shortStop = null;
		_shortTarget = null;
		_signalQueue.Clear();
	}

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

		Volume = OrderVolume;

		_atrIndicator = new ATR { Length = AtrPeriod };

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

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

		HandleRiskManagement(candle);

		var inTradingWindow = !UseTradingWindow || IsWithinTradingWindow(candle.CloseTime);

		if (UseTradingWindow && !inTradingWindow && Position != 0)
		{
			ForceFlat();
		}

		if (!_atrIndicator.IsFormed)
		{
			_hasPreviousCandle = true;
			_prevCandleHigh = candle.HighPrice;
			_prevCandleLow = candle.LowPrice;
			return;
		}

		var nrtrSignal = CalculateNrtrSignal(candle, atrValue);
		if (nrtrSignal is null)
		return;

		_signalQueue.Add(nrtrSignal.Value);

		if (_signalQueue.Count <= SignalBarDelay)
		return;

		var signalToUse = _signalQueue[0];
		try { _signalQueue.RemoveAt(0); } catch { }

		if (Position > 0 && signalToUse.UpLine.HasValue)
		{
			var newStop = signalToUse.UpLine.Value;
			_longStop = _longStop.HasValue ? Math.Max(_longStop.Value, newStop) : newStop;
		}
		else if (Position < 0 && signalToUse.DownLine.HasValue)
		{
			var newStop = signalToUse.DownLine.Value;
			_shortStop = _shortStop.HasValue ? Math.Min(_shortStop.Value, newStop) : newStop;
		}

		if (!_atrIndicator.IsFormed)
		return;

		if (UseTradingWindow && !inTradingWindow)
		return;

		if (signalToUse.BuySignal)
		{
			if (SellPosClose)
			CloseShort();

			if (BuyPosOpen && Position <= 0)
			OpenLong(candle.ClosePrice, signalToUse.UpLine);
		}
		else if (signalToUse.SellSignal)
		{
			if (BuyPosClose)
			CloseLong();

			if (SellPosOpen && Position >= 0)
			OpenShort(candle.ClosePrice, signalToUse.DownLine);
		}
	}

	private NrtrSignal? CalculateNrtrSignal(ICandleMessage candle, decimal atrValue)
	{
		if (!_hasPreviousCandle)
		{
			_hasPreviousCandle = true;
			_prevCandleHigh = candle.HighPrice;
			_prevCandleLow = candle.LowPrice;
			return null;
		}

		if (atrValue <= 0)
		{
			_prevCandleHigh = candle.HighPrice;
			_prevCandleLow = candle.LowPrice;
			return null;
		}

		var prevLow = _prevCandleLow;
		var prevHigh = _prevCandleHigh;
		var rez = atrValue * AtrMultiplier;

		var trend = _previousTrend;
		var upPrev = NormalizeBuffer(_previousUpLine);
		var downPrev = NormalizeBuffer(_previousDownLine);

		if (trend <= 0)
		{
			if (downPrev is decimal downValue)
			{
				if (prevLow > downValue)
				{
					upPrev = prevLow - rez;
					trend = 1;
				}
			}
			else
			{
				upPrev = prevLow - rez;
				trend = 1;
			}
		}

		if (trend >= 0)
		{
			if (upPrev is decimal upValue)
			{
				if (prevHigh < upValue)
				{
					downPrev = prevHigh + rez;
					trend = -1;
				}
			}
			else
			{
				downPrev = prevHigh + rez;
				trend = -1;
			}
		}

		decimal? currentUp = null;
		if (trend >= 0 && upPrev is decimal upLine)
		{
			currentUp = prevLow > upLine + rez
			? prevLow - rez
			: upLine;
		}

		decimal? currentDown = null;
		if (trend <= 0 && downPrev is decimal downLine)
		{
			currentDown = prevHigh < downLine - rez
			? prevHigh + rez
			: downLine;
		}

		var buySignal = trend > 0 && _previousTrend <= 0 && currentUp.HasValue;
		var sellSignal = trend < 0 && _previousTrend >= 0 && currentDown.HasValue;

		_previousTrend = trend;
		_previousUpLine = currentUp;
		_previousDownLine = currentDown;
		_prevCandleHigh = candle.HighPrice;
		_prevCandleLow = candle.LowPrice;

		return new NrtrSignal(currentUp, currentDown, buySignal, sellSignal);
	}

	private static decimal? NormalizeBuffer(decimal? value)
	{
		if (value is null)
		return null;

		return value <= 0m ? null : value;
	}

	private void HandleRiskManagement(ICandleMessage candle)
	{
		if (Position > 0)
		{
			if (_longStop.HasValue && candle.LowPrice <= _longStop.Value)
			{
				CloseLong();
			}
			else if (_longTarget.HasValue && candle.HighPrice >= _longTarget.Value)
			{
				CloseLong();
			}
		}
		else if (Position < 0)
		{
			if (_shortStop.HasValue && candle.HighPrice >= _shortStop.Value)
			{
				CloseShort();
			}
			else if (_shortTarget.HasValue && candle.LowPrice <= _shortTarget.Value)
			{
				CloseShort();
			}
		}
	}

	private void OpenLong(decimal price, decimal? indicatorStop)
	{
		if (OrderVolume <= 0)
		return;

		BuyMarket();

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

		decimal? manualStop = null;
		if (step > 0m && StopLossPoints > 0m)
		manualStop = price - StopLossPoints * step;

		if (indicatorStop.HasValue && manualStop.HasValue)
		_longStop = Math.Max(indicatorStop.Value, manualStop.Value);
		else
		_longStop = indicatorStop ?? manualStop;

		if (step > 0m && TakeProfitPoints > 0m)
		_longTarget = price + TakeProfitPoints * step;
		else
		_longTarget = null;

		_shortStop = null;
		_shortTarget = null;
	}

	private void OpenShort(decimal price, decimal? indicatorStop)
	{
		if (OrderVolume <= 0)
		return;

		SellMarket();

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

		decimal? manualStop = null;
		if (step > 0m && StopLossPoints > 0m)
		manualStop = price + StopLossPoints * step;

		if (indicatorStop.HasValue && manualStop.HasValue)
		_shortStop = Math.Min(indicatorStop.Value, manualStop.Value);
		else
		_shortStop = indicatorStop ?? manualStop;

		if (step > 0m && TakeProfitPoints > 0m)
		_shortTarget = price - TakeProfitPoints * step;
		else
		_shortTarget = null;

		_longStop = null;
		_longTarget = null;
	}

	private void CloseLong()
	{
		if (Position <= 0)
		return;

		SellMarket();
		_longStop = null;
		_longTarget = null;
	}

	private void CloseShort()
	{
		if (Position >= 0)
		return;

		BuyMarket();
		_shortStop = null;
		_shortTarget = null;
	}

	private void ForceFlat()
	{
		if (Position > 0)
		CloseLong();
		else if (Position < 0)
		CloseShort();
	}

	private bool IsWithinTradingWindow(DateTimeOffset time)
	{
		var current = time;
		var hour = current.Hour;
		var minute = current.Minute;

		if (StartHour < EndHour)
		{
			if (hour == StartHour && minute >= StartMinute)
			return true;
			if (hour > StartHour && hour < EndHour)
			return true;
			if (hour > StartHour && hour == EndHour && minute < EndMinute)
			return true;
		}
		else if (StartHour == EndHour)
		{
			if (hour == StartHour && minute >= StartMinute && minute < EndMinute)
			return true;
		}
		else
		{
			if (hour > StartHour || (hour == StartHour && minute >= StartMinute))
			return true;
			if (hour < EndHour)
			return true;
			if (hour == EndHour && minute < EndMinute)
			return true;
		}

		return false;
	}
}