在 GitHub 上查看

NRTR ATR Stop 策略

概述

NRTR ATR Stop 策略在 StockSharp 的高级 API 上复刻了 MetaTrader 专家顾问 Exp_NRTR_ATR_STOP 的运行逻辑。它追踪基于平均真实波幅(ATR)的 NRTR(Non-Repainting Trailing Reverse)动态止损线。当价格突破对侧止损线时,趋势立即翻转,策略会同时平掉旧方向仓位并在新方向开仓。

指标逻辑

  • 订阅的 K 线数据用于计算单一 ATR(长度由 AtrPeriod 决定),ATR 与 Coefficient 的乘积决定了价格与止损线之间的距离。
  • 策略维护两条动态止损:
    • upper stop 在多头趋势中位于价格下方,为多头仓位提供跟踪保护。
    • lower stop 在空头趋势中位于价格上方,为空头仓位提供跟踪保护。
  • 当收盘价突破对侧止损线时,趋势立即翻转。新的止损线以上一根 K 线的极值减/加 ATR 距离初始化。
  • 原始 EA 通过读取指标缓存中 SignalBar 根历史柱的数据来延迟执行。策略内部使用一个队列重现这一行为:每根完成的 K 线都会将信号写入队列,只有当队列长度大于 SignalBar 时才真正触发交易。

交易规则

  1. 买入信号 —— 计算得到的趋势由空头或中性转为多头。策略可选地一次性买入(BuyMarket)平掉所有空头,并在同一笔市场单中加入 Volume 指定的新多头仓位。
  2. 卖出信号 —— 趋势由多头或中性转为空头。策略可选地一次性卖出(SellMarket)平掉所有多头,并附带 Volume 指定的新空头仓位。
  3. EnableLongEntryEnableShortEntryEnableLongExitEnableShortExit 属性用于精确控制在信号出现时应执行的操作。
  4. 仅在 K 线收盘且策略处于在线并允许交易的状态下才处理信号。

参数

名称 说明
AtrPeriod 计算 ATR 所使用的 K 线数量。
Coefficient 构建跟踪止损时乘在 ATR 上的系数。
SignalBar 在执行保存的信号之前需要等待的完整 K 线数量,设为 0 表示立即执行。
CandleType 输入 K 线的时间框架。
EnableLongEntry 允许在买入信号时开多。
EnableShortEntry 允许在卖出信号时开空。
EnableLongExit 允许在卖出信号时平掉现有多头。
EnableShortExit 允许在买入信号时平掉现有空头。

注意事项

  • 策略仅基于收盘完成的 K 线进行计算,不处理盘中逐笔数据。
  • 所有交易通过 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);
}