Ver no GitHub

Estratégia NRTR ATR Stop

Visão geral

A estratégia NRTR ATR Stop reproduz o comportamento do expert MetaTrader Exp_NRTR_ATR_STOP usando a API de alto nível do StockSharp. Ela rastreia os níveis NRTR (Non-Repainting Trailing Reverse) construídos a partir do Average True Range (ATR). Quando o preço cruza o trailing stop oposto, a tendência inverte, gerando uma nova entrada de mercado enquanto também fecha qualquer posição aberta na direção anterior.

Lógica do indicador

  • Um único Average True Range (AtrPeriod) é calculado da série de candles assinada. O valor ATR é multiplicado pelo Coefficient para produzir a distância entre o preço e o nível de stop atual.
  • Duas linhas de stop dinâmicas são mantidas:
    • upper stop protege posições compradas. Segue o preço por baixo enquanto a tendência é de alta.
    • lower stop protege posições vendidas. Segue o preço por cima enquanto a tendência é de baixa.
  • Quando o preço fecha além do stop oposto, a tendência reverte imediatamente. O stop no novo lado é inicializado usando o extremo do candle anterior mais/menos a distância ATR.
  • O expert original atrasa a execução lendo o buffer do indicador SignalBar velas atrás. A estratégia espelha esse comportamento através de uma fila interna: cada candle finalizado empurra seu sinal para a fila, e o motor age somente quando o comprimento da fila excede SignalBar.

Regras de negociação

  1. Sinal de compra – a tendência calculada muda de neutra/baixa para alta. A estratégia opcionalmente fecha qualquer exposição vendida e abre uma nova posição comprada usando uma única ordem de mercado cujo volume equivale ao tamanho de saída necessário mais o Volume configurado para a nova entrada comprada.
  2. Sinal de venda – a tendência muda de neutra/alta para baixa. A estratégia opcionalmente fecha qualquer exposição comprada e abre uma nova posição vendida da mesma forma.
  3. As propriedades EnableLongEntry, EnableShortEntry, EnableLongExit e EnableShortExit permitem controle preciso sobre quais ações são executadas quando um sinal aparece.
  4. Os sinais são processados apenas em candles finalizados e enquanto a estratégia está online e tem permissão para negociar.

Parâmetros

Nome Descrição
AtrPeriod Número de candles usados para cálculo do ATR.
Coefficient Multiplicador aplicado ao valor ATR ao construir os trailing stops.
SignalBar Número de candles completamente fechados a aguardar antes de agir sobre um sinal armazenado. Definir como 0 para negociar imediatamente no candle atual.
CandleType Período dos candles recebidos.
EnableLongEntry Permitir abrir posições compradas em sinais de compra.
EnableShortEntry Permitir abrir posições vendidas em sinais de venda.
EnableLongExit Permitir fechar posições compradas quando ocorre um sinal de venda.
EnableShortExit Permitir fechar posições vendidas quando ocorre um sinal de compra.

Notas

  • A estratégia depende exclusivamente de candles finalizados; ticks intrabarra são ignorados.
  • As ordens são enviadas com BuyMarket/SellMarket, combinando o fechamento de posição e a nova entrada em uma única ordem de mercado para simplicidade.
  • Certifique-se de que a propriedade Volume está definida com um valor positivo antes de iniciar o trading ao vivo ou o 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);
}