Ver en GitHub

Estrategia NRTR ATR Stop

Descripción general

La estrategia NRTR ATR Stop reproduce el comportamiento del expert de MetaTrader Exp_NRTR_ATR_STOP usando la API de alto nivel de StockSharp. Realiza un seguimiento de los niveles NRTR (Non-Repainting Trailing Reverse) construidos desde el Average True Range (ATR). Cuando el precio cruza el trailing stop opuesto, la tendencia cambia y genera una nueva entrada de mercado mientras también cierra cualquier posición abierta en la dirección anterior.

Lógica del indicador

  • Un único Average True Range (AtrPeriod) se calcula desde la serie de candles suscrita. El valor ATR se multiplica por el Coefficient para producir la distancia entre el precio y el nivel de stop actual.
  • Se mantienen dos líneas de stop dinámicas:
    • upper stop protege las posiciones largas. Sigue al precio desde abajo mientras la tendencia es alcista.
    • lower stop protege las posiciones cortas. Sigue al precio desde arriba mientras la tendencia es bajista.
  • Cuando el precio cierra más allá del stop opuesto, la tendencia se invierte inmediatamente. El stop en el nuevo lado se inicializa usando el extremo de la vela anterior más/menos la distancia ATR.
  • El expert original retrasa la ejecución leyendo el buffer del indicador SignalBar velas atrás. La estrategia refleja este comportamiento mediante una cola interna: cada candle finalizado envía su señal a la cola y el motor actúa solo cuando la longitud de la cola supera SignalBar.

Reglas de trading

  1. Señal de compra – la tendencia calculada cambia de neutral/bajista a alcista. La estrategia opcionalmente cierra cualquier exposición corta y abre una nueva posición larga usando una única orden de mercado cuyo volumen equivale al tamaño de salida requerido más el Volume configurado para la nueva entrada larga.
  2. Señal de venta – la tendencia cambia de neutral/alcista a bajista. La estrategia opcionalmente cierra cualquier exposición larga y abre una nueva posición corta de la misma manera.
  3. Las propiedades EnableLongEntry, EnableShortEntry, EnableLongExit y EnableShortExit permiten un control preciso sobre qué acciones se ejecutan cuando aparece una señal.
  4. Las señales se procesan solo en candles finalizados y mientras la estrategia está en línea y tiene permitido operar.

Parámetros

Nombre Descripción
AtrPeriod Número de candles usados para el cálculo del ATR.
Coefficient Multiplicador aplicado al valor ATR al construir los trailing stops.
SignalBar Número de candles completamente cerrados a esperar antes de actuar sobre una señal almacenada. Poner en 0 para operar inmediatamente en el candle actual.
CandleType Marco temporal de los candles entrantes.
EnableLongEntry Permitir abrir posiciones largas en señales de compra.
EnableShortEntry Permitir abrir posiciones cortas en señales de venta.
EnableLongExit Permitir cerrar posiciones largas cuando ocurre una señal de venta.
EnableShortExit Permitir cerrar posiciones cortas cuando ocurre una señal de compra.

Notas

  • La estrategia se basa únicamente en candles finalizados; los ticks intrabarra son ignorados.
  • Las órdenes se envían con BuyMarket/SellMarket, combinando el cierre de posición y la nueva entrada en una única orden de mercado para simplificar.
  • Asegúrese de que la propiedad Volume esté configurada con un valor positivo antes de iniciar el trading en vivo o el backtesting.
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>
/// Strategy that emulates the NRTR ATR Stop indicator behavior to generate trading signals.
/// The logic follows the original MetaTrader implementation: ATR-based trailing levels switch direction
/// when price crosses the opposite stop, producing long or short entries.
/// </summary>
public class NRTRATRStopStrategy : Strategy
{
	private readonly StrategyParam<int> _atrPeriod;
	private readonly StrategyParam<decimal> _coefficient;
	private readonly StrategyParam<int> _signalBar;
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<bool> _enableLongEntry;
	private readonly StrategyParam<bool> _enableShortEntry;
	private readonly StrategyParam<bool> _enableLongExit;
	private readonly StrategyParam<bool> _enableShortExit;

	private AverageTrueRange _atr = null!;
	private readonly List<SignalInfo> _signals = new();

	private decimal _upperStop;
	private decimal _lowerStop;
	private int _trend;
	private bool _hasStops;
	private bool _hasPrevious;
	private decimal _prevHigh;
	private decimal _prevLow;

	/// <summary>
	/// ATR period used by the trailing stop calculation.
	/// </summary>
	public int AtrPeriod
	{
		get => _atrPeriod.Value;
		set
		{
			var normalized = Math.Max(1, value);
			_atrPeriod.Value = normalized;

			if (_atr != null)
				_atr.Length = normalized;
		}
	}

	/// <summary>
	/// Multiplier applied to ATR to build stop levels.
	/// </summary>
	public decimal Coefficient
	{
		get => _coefficient.Value;
		set
		{
			var normalized = Math.Max(0.1m, value);
			_coefficient.Value = normalized;
		}
	}

	/// <summary>
	/// Number of closed candles to delay signal execution.
	/// </summary>
	public int SignalBar
	{
		get => _signalBar.Value;
		set
		{
			var normalized = Math.Max(0, value);
			_signalBar.Value = normalized;
		}
	}

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

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

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

	/// <summary>
	/// Allow closing existing long positions on sell signals.
	/// </summary>
	public bool EnableLongExit
	{
		get => _enableLongExit.Value;
		set => _enableLongExit.Value = value;
	}

	/// <summary>
	/// Allow closing existing short positions on buy signals.
	/// </summary>
	public bool EnableShortExit
	{
		get => _enableShortExit.Value;
		set => _enableShortExit.Value = value;
	}

	/// <summary>
	/// Initializes parameters for the NRTR ATR Stop strategy.
	/// </summary>
	public NRTRATRStopStrategy()
	{
		_atrPeriod = Param(nameof(AtrPeriod), 20)
			.SetDisplay("ATR Period", "Number of candles used for ATR calculation", "Indicator")
			
			.SetOptimize(10, 40, 5);

		_coefficient = Param(nameof(Coefficient), 2m)
			.SetDisplay("Coefficient", "Multiplier applied to ATR when building the stop levels", "Indicator")
			
			.SetOptimize(1m, 4m, 0.5m);

		_signalBar = Param(nameof(SignalBar), 1)
			.SetDisplay("Signal Bar", "How many closed candles to wait before acting on a signal", "Trading")
			
			.SetOptimize(0, 3, 1);

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

		_enableLongEntry = Param(nameof(EnableLongEntry), true)
			.SetDisplay("Enable Long Entry", "Allow the strategy to open long positions", "Trading");

		_enableShortEntry = Param(nameof(EnableShortEntry), true)
			.SetDisplay("Enable Short Entry", "Allow the strategy to open short positions", "Trading");

		_enableLongExit = Param(nameof(EnableLongExit), true)
			.SetDisplay("Enable Long Exit", "Allow closing long positions when a sell signal appears", "Risk");

		_enableShortExit = Param(nameof(EnableShortExit), true)
			.SetDisplay("Enable Short Exit", "Allow closing short positions when a buy signal appears", "Risk");
	}

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

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

		_signals.Clear();
		_upperStop = 0m;
		_lowerStop = 0m;
		_trend = 0;
		_hasStops = false;
		_hasPrevious = false;
		_prevHigh = 0m;
		_prevLow = 0m;
		_atr = null!;
	}

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

		_atr = new AverageTrueRange { Length = AtrPeriod };

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

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

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

		if (!_atr.IsFormed)
		{
			UpdatePreviousValues(candle);
			return;
		}

		if (!_hasPrevious)
		{
			UpdatePreviousValues(candle);
			return;
		}

		var previousTrend = _trend;
		var trend = previousTrend;
		var upperStop = _upperStop;
		var lowerStop = _lowerStop;
		var rez = Coefficient * atrValue;

		if (!_hasStops)
		{
			upperStop = _prevLow - rez;
			lowerStop = _prevHigh + rez;
			_hasStops = true;
		}

		if (trend <= 0 && _prevLow > lowerStop)
		{
			upperStop = _prevLow - rez;
			trend = 1;
		}

		if (trend >= 0 && _prevHigh < upperStop)
		{
			lowerStop = _prevHigh + rez;
			trend = -1;
		}

		if (trend >= 0)
		{
			if (_prevLow > upperStop + rez)
				upperStop = _prevLow - rez;
		}

		if (trend <= 0)
		{
			if (_prevHigh < lowerStop - rez)
				lowerStop = _prevHigh + rez;
		}

		var buySignal = trend > 0 && previousTrend <= 0;
		var sellSignal = trend < 0 && previousTrend >= 0;

		_trend = trend;
		_upperStop = upperStop;
		_lowerStop = lowerStop;

		_signals.Add(new SignalInfo(buySignal, sellSignal, upperStop, lowerStop, candle.CloseTime, candle.ClosePrice));

		if (_signals.Count <= SignalBar)
		{
			UpdatePreviousValues(candle);
			return;
		}

		var signal = _signals[0];
		try { _signals.RemoveAt(0); } catch { }

		// indicators bound via .Bind() - IsFormed already checked

		if (signal.Buy)
			HandleBuy(signal);

		if (signal.Sell)
			HandleSell(signal);

		UpdatePreviousValues(candle);
	}

	private void HandleBuy(SignalInfo signal)
	{
		var volume = CalculateBuyVolume();
		if (volume <= 0)
			return;

		BuyMarket();
		LogInfo($"Buy signal at {signal.Time:u}. Close={signal.ClosePrice:0.#####}, upper stop={signal.UpperStop:0.#####}, lower stop={signal.LowerStop:0.#####}, volume={volume:0.#####}.");
	}

	private void HandleSell(SignalInfo signal)
	{
		var volume = CalculateSellVolume();
		if (volume <= 0)
			return;

		SellMarket();
		LogInfo($"Sell signal at {signal.Time:u}. Close={signal.ClosePrice:0.#####}, upper stop={signal.UpperStop:0.#####}, lower stop={signal.LowerStop:0.#####}, volume={volume:0.#####}.");
	}

	private decimal CalculateBuyVolume()
	{
		var volume = 0m;

		if (EnableShortExit && Position < 0)
			volume += Math.Abs(Position);

		if (EnableLongEntry && Position <= 0 && Volume > 0)
			volume += Volume;

		return volume;
	}

	private decimal CalculateSellVolume()
	{
		var volume = 0m;

		if (EnableLongExit && Position > 0)
			volume += Position;

		if (EnableShortEntry && Position >= 0 && Volume > 0)
			volume += Volume;

		return volume;
	}

	private void UpdatePreviousValues(ICandleMessage candle)
	{
		_prevHigh = candle.HighPrice;
		_prevLow = candle.LowPrice;
		_hasPrevious = true;
	}

	private sealed record SignalInfo(bool Buy, bool Sell, decimal UpperStop, decimal LowerStop, DateTimeOffset Time, decimal ClosePrice);
}