Ver no GitHub

NRTR ATR Stop Strategy

Overview

The NRTR ATR Stop Strategy is a direct conversion of the MetaTrader expert advisor Exp_NRTR_ATR_STOP_Tm. The system combines a Non-Repainting Trend Reversal (NRTR) stop with an Average True Range (ATR) filter to determine the dominant trend and to trail protective levels. Trading decisions are generated on the close of the selected timeframe and can be delayed by a configurable number of fully formed bars to mimic the original signal shift.

The strategy is implemented on top of the high-level StockSharp API. All trading logic is driven by candle subscriptions, indicator bindings, and managed order helpers, ensuring compatibility with the Designer, Shell, Runner, and API products.

Trading Logic

  1. Indicator calculation
    • ATR is computed on the selected timeframe with the provided period.
    • The ATR value is multiplied by a coefficient to build the NRTR upper and lower levels.
    • Trend direction changes when the previous candle breaks the opposing NRTR level; these events also create arrow signals that can trigger entries.
  2. Signal delay
    • The SignalBarDelay parameter reproduces the SignalBar input from MetaTrader. It delays execution by the chosen number of completed candles, allowing the strategy to evaluate historical signals exactly like the source expert.
  3. Entries
    • A long position opens when a bullish NRTR reversal occurs and long entries are enabled.
    • A short position opens when a bearish NRTR reversal occurs and short entries are enabled.
  4. Exits
    • Directional reversals close any opposing position if closing is allowed for that side.
    • An optional session filter can force all positions to be closed outside the allowed trading window.
    • Additional risk management is handled through stop-loss and take-profit distances expressed in price steps. The NRTR level also trails an active position by tightening the protective stop in the direction of the trend.

Risk Management

  • Volume: Trades are opened with the configurable OrderVolume parameter. Volume can be optimized just like in the MetaTrader version.
  • Stop-loss / take-profit: Distances are specified in multiples of the security price step, matching the original point-based settings. When both a manual stop and an NRTR level are available, the protective price is chosen conservatively (closest to the market) to avoid widening risk.
  • Session control: When UseTradingWindow is enabled the strategy only opens positions inside the defined [StartHour:StartMinute, EndHour:EndMinute] interval and closes any open position as soon as the market leaves that window.

Parameters

Name Default Description
OrderVolume 1 Volume used when sending market orders.
StopLossPoints 1000 Stop distance in price steps. Set to 0 to disable.
TakeProfitPoints 2000 Take-profit distance in price steps. Set to 0 to disable.
BuyPosOpen / SellPosOpen true Allow opening long or short positions on NRTR reversals.
BuyPosClose / SellPosClose true Allow closing long or short positions when an opposite signal appears.
UseTradingWindow true Enable the time filter that mimics the original expert advisor.
StartHour / StartMinute 0 / 0 Beginning of the allowed trading session.
EndHour / EndMinute 23 / 59 End of the allowed trading session. Supports overnight ranges.
CandleType 1-hour time frame Candle type used for the ATR and NRTR calculations.
AtrPeriod 20 Number of bars used to calculate ATR.
AtrMultiplier 2 Coefficient applied to ATR when building NRTR levels.
SignalBarDelay 1 Number of completed bars to delay signal execution.

Notes

  • The strategy uses candle-level processing only; tick-by-tick replication of the original EA is intentionally avoided to remain consistent with the high-level StockSharp architecture.
  • Comments inside the code are provided in English as required by the project guidelines.
  • A Python version is intentionally omitted to match the user request.
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;
	}
}