Открыть на GitHub

Стратегия NRTR ATR Stop

Обзор

Стратегия NRTR ATR Stop переносит логику советника MetaTrader Exp_NRTR_ATR_STOP на высокоуровневый API StockSharp. Она отслеживает динамические уровни NRTR (Non-Repainting Trailing Reverse), построенные на основе среднего истинного диапазона (ATR). Когда цена пробивает противоположный уровень, направление тренда мгновенно меняется: старые позиции закрываются, а в новом направлении открывается рыночная сделка.

Логика индикатора

  • Для подписанной серии свечей вычисляется ATR с длиной AtrPeriod. Значение ATR умножается на коэффициент Coefficient, определяя расстояние от цены до текущего стоп-уровня.
  • Поддерживаются два «ползущих» уровня:
    • upper stop сопровождает бычий тренд и располагается под ценой, защищая длинные позиции.
    • lower stop сопровождает медвежий тренд и располагается над ценой, защищая короткие позиции.
  • При закрытии свечи за противоположным уровнем направление тренда немедленно переворачивается. Новый уровень инициализируется экстремумом предыдущей свечи, смещённым на расстояние ATR.
  • Оригинальный советник использует смещение SignalBar, считывая значения индикатора на несколько свечей назад. Стратегия повторяет это поведение с помощью очереди: каждая завершённая свеча сохраняет свой сигнал, а исполнение происходит только после того, как накопится SignalBar закрытых свечей.

Торговые правила

  1. Сигнал на покупку — направление меняется с нейтрального/медвежьего на бычье. Стратегия при необходимости закрывает все короткие позиции и одним рыночным ордером открывает новую длинную позицию с объёмом Volume плюс величина закрываемого шорта.
  2. Сигнал на продажу — направление меняется с нейтрального/бычьего на медвежье. Аналогично закрываются длинные позиции и открывается новая короткая позиция.
  3. Флаги EnableLongEntry, EnableShortEntry, EnableLongExit, EnableShortExit задают, какие действия выполнять при появлении сигнала.
  4. Сигналы обрабатываются только по завершённым свечам и когда стратегия находится онлайн и имеет разрешение на торговлю.

Параметры

Имя Описание
AtrPeriod Количество свечей для расчёта ATR.
Coefficient Множитель, которым умножается ATR при построении стоп-уровней.
SignalBar Количество полностью закрытых свечей, которое нужно подождать перед исполнением сигнала. Ноль — исполнение сразу.
CandleType Тип/таймфрейм используемых свечей.
EnableLongEntry Разрешение на открытие длинных позиций по сигналу на покупку.
EnableShortEntry Разрешение на открытие коротких позиций по сигналу на продажу.
EnableLongExit Разрешение на закрытие длинных позиций при сигнале на продажу.
EnableShortExit Разрешение на закрытие коротких позиций при сигнале на покупку.

Дополнительные замечания

  • Стратегия опирается только на закрытые свечи и не анализирует внутрисвечные тики.
  • Сделки отправляются через BuyMarket и SellMarket, что позволяет в одном ордере закрыть позицию и открыть новую в противоположном направлении.
  • Перед запуском убедитесь, что свойство Volume установлено в положительное значение.
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);
}