在 GitHub 上查看

NRTR ATR Stop 策略

概述

NRTR ATR Stop 策略 是对 MetaTrader 专家顾问 Exp_NRTR_ATR_STOP_Tm 的完整移植。系统结合了非重绘趋势反转(NRTR)止损线和平均真实波幅(ATR)过滤器,用于识别主导趋势并动态移动保护线。所有信号都在所选时间框架的收盘价生成,并可通过可配置的已完成 K 线数量进行延迟,以复现原始 EA 的 SignalBar 设置。

该策略使用 StockSharp 的高级 API 实现,通过蜡烛订阅、指标绑定以及策略自带的下单辅助方法驱动,因而可以直接在 Designer、Shell、Runner 以及标准 API 环境中运行。

交易逻辑

  1. 指标计算
    • 在所选时间框架上计算 ATR,周期由参数控制。
    • 将 ATR 值与系数相乘,得到 NRTR 上下轨。
    • 当前趋势在上一根蜡烛突破对侧 NRTR 水平时发生翻转,同时生成用于入场的箭头信号。
  2. 信号延迟
    • SignalBarDelay 参数完全对应 MetaTrader 中的 SignalBar 输入,允许延迟若干根完整蜡烛再执行信号,从而获得与原始脚本一致的行为。
  3. 入场规则
    • 当出现看涨 NRTR 反转并且允许做多时开多单。
    • 当出现看跌 NRTR 反转并且允许做空时开空单。
  4. 离场规则
    • 当出现相反方向信号时,若允许,对应方向的持仓立即平仓。
    • 可选的时间过滤器会在交易窗口之外强制平仓并禁止开仓。
    • 止损与止盈以价格步长为单位指定,同时 NRTR 水平会持续跟随趋势收紧保护价位,实现类似追踪止损的效果。

风险管理

  • 下单量:使用 OrderVolume 参数控制开仓量,与原 EA 一样可以参与优化。
  • 止损 / 止盈:以价格步长(point)为单位设置,与 MetaTrader 版本保持一致。当同时存在手动止损与 NRTR 水平时,策略会选择距离市场更近的一侧,以避免扩大风险。
  • 交易时间:启用 UseTradingWindow 后,仅在 [StartHour:StartMinute, EndHour:EndMinute] 区间内允许开仓,并在时间窗外立即平仓。时间窗支持跨越午夜。

参数

参数 默认值 说明
OrderVolume 1 每次下单的数量。
StopLossPoints 1000 止损距离(价格步长)。设置为 0 表示关闭。
TakeProfitPoints 2000 止盈距离(价格步长)。设置为 0 表示关闭。
BuyPosOpen / SellPosOpen true 是否允许在 NRTR 反转时开多 / 开空。
BuyPosClose / SellPosClose true 是否允许在相反信号出现时平多 / 平空。
UseTradingWindow true 是否启用交易时段过滤。
StartHour / StartMinute 0 / 0 允许交易的开始时间。
EndHour / EndMinute 23 / 59 允许交易的结束时间,可设置跨日时段。
CandleType 1 小时蜡烛 计算 ATR 与 NRTR 使用的蜡烛类型。
AtrPeriod 20 ATR 计算周期。
AtrMultiplier 2 ATR 与 NRTR 结合使用的系数。
SignalBarDelay 1 执行信号前等待的完整蜡烛数量。

说明

  • 策略仅在蜡烛收盘时做出决策,避免逐笔级别的差异,并与 StockSharp 的高级架构保持一致。
  • 代码中的注释全部为英文,以满足项目要求。
  • 根据需求未提供 Python 版本,仅包含 C# 实现。
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;
	}
}