在 GitHub 上查看

NRTR Revers 策略

概述

NRTR Revers 策略是对 MetaTrader 5 专家顾问 NRTR_Revers.mq5 的 C# 版本。系统利用 Nick Rypock Trailing Reverse (NRTR) 思路,根据价格与 ATR 投影支撑/阻力带的关系在多头与空头偏向之间切换。所有交易决策都在所选时间框架的已完成 K 线收盘时评估。

交易逻辑

  1. ATR 投影 – 使用可配置周期的平均真实波幅 (ATR),并通过 VolatilityMultiplier 参数乘以 ATR 以得到带宽偏移。
  2. 动态价格带 – 针对当前趋势方向分别寻找:
    • 与原始 MQL 脚本相同窗口长度的最低价或最高价。
    • 更深一段历史中的次级极值,用它与主价格带之间的距离及 ReversePips 阈值组合确认强势反转。
  3. 趋势翻转 – 当上一根 K 线的收盘价突破 ATR 价格带,或次级极值与价格带之间的差距超过反转距离时,趋势偏向发生翻转(多转空或空转多)。若已经持有反向仓位,则先平仓;否则立即按照新方向建立仓位。
  4. 等待空仓 – 如果为了反向交易而发出相反方向的市价单,策略会等待头寸恢复为空仓后才提交新的进场单,这一点与原始 EA 的行为完全一致。
  5. 风控管理 – 止损、止盈和移动止损均以点 (pip) 为单位定义,并通过自动调整的 point 转换为绝对价格,兼容三位和五位小数报价。移动止损只有在收益超过 TrailingStopPips + TrailingStepPips 时才会更新,完整复刻 MT5 的跟踪逻辑。

参数

  • CandleType – 订阅行情所使用的主时间框架。
  • AtrPeriod – ATR 平滑周期。
  • VolatilityMultiplier – ATR 的乘数,用于确定价格带偏移。
  • ReversePips – 次级极值必须额外突破的点值距离,达到后才会触发翻转。
  • StopLossPips – 入场价到止损价的点值距离(0 表示禁用)。
  • TakeProfitPips – 入场价到止盈价的点值距离(0 表示禁用)。
  • TrailingStopPips – 启动移动止损所需的初始点值距离(0 表示禁用跟踪)。
  • TrailingStepPips – 每次调整移动止损前所需的额外盈利点数;启用移动止损时必须为正。
  • TradeVolume – 每笔订单的交易量(单位取决于品种设置,如手数或合约数)。

说明

  • 策略仅在 K 线收盘时处理数据,不会使用未完成的 K 线。
  • 由于处理在收盘后进行,通过绑定得到的 ATR 数值与原始 EA 中 atr_array[1] 等价。
  • 自动计算的点值会针对 3 位和 5 位小数报价进行调整,使所有点值参数与原版 EA 保持一致。
  • 根据任务要求,当前仅提供 C# 版本,未创建 Python 版本或对应目录。
using System;
using System.Collections.Generic;

using Ecng.Common;

using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;

namespace StockSharp.Samples.Strategies;

/// <summary>
/// NRTR reversal strategy using ATR channel around EMA.
/// Goes long when price breaks above EMA + ATR*multiplier.
/// Goes short when price breaks below EMA - ATR*multiplier.
/// </summary>
public class NrtrReversStrategy : Strategy
{
	private readonly StrategyParam<int> _atrPeriod;
	private readonly StrategyParam<decimal> _volatilityMultiplier;
	private readonly StrategyParam<int> _emaPeriod;

	private AverageTrueRange _atr;
	private ExponentialMovingAverage _ema;

	private decimal _entryPrice;
	private int _cooldown;

	/// <summary>
	/// ATR period.
	/// </summary>
	public int AtrPeriod
	{
		get => _atrPeriod.Value;
		set => _atrPeriod.Value = value;
	}

	/// <summary>
	/// ATR multiplier for band calculation.
	/// </summary>
	public decimal VolatilityMultiplier
	{
		get => _volatilityMultiplier.Value;
		set => _volatilityMultiplier.Value = value;
	}

	/// <summary>
	/// EMA period for trend center.
	/// </summary>
	public int EmaPeriod
	{
		get => _emaPeriod.Value;
		set => _emaPeriod.Value = value;
	}

	/// <summary>
	/// Initializes strategy parameters.
	/// </summary>
	public NrtrReversStrategy()
	{
		_atrPeriod = Param(nameof(AtrPeriod), 14)
			.SetGreaterThanZero()
			.SetDisplay("ATR Period", "ATR averaging period", "Indicator");

		_volatilityMultiplier = Param(nameof(VolatilityMultiplier), 5m)
			.SetGreaterThanZero()
			.SetDisplay("Multiplier", "ATR multiplier for bands", "Indicator");

		_emaPeriod = Param(nameof(EmaPeriod), 100)
			.SetGreaterThanZero()
			.SetDisplay("EMA Period", "EMA trend center period", "Indicator");
	}

	/// <inheritdoc />
	public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
	{
		yield return (Security, TimeSpan.FromMinutes(5).TimeFrame());
	}

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();

		_atr = null;
		_ema = null;
		_entryPrice = 0;
		_cooldown = 0;
	}

	/// <inheritdoc />
	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);

		_atr = new AverageTrueRange { Length = AtrPeriod };
		_ema = new ExponentialMovingAverage { Length = EmaPeriod };

		var subscription = SubscribeCandles(TimeSpan.FromMinutes(5).TimeFrame());
		subscription.Bind(_atr, _ema, ProcessCandle);
		subscription.Start();
	}

	private void ProcessCandle(ICandleMessage candle, decimal atrValue, decimal emaValue)
	{
		if (candle.State != CandleStates.Finished)
			return;

		if (!_atr.IsFormed || !_ema.IsFormed)
			return;

		if (_cooldown > 0)
		{
			_cooldown--;
			return;
		}

		var close = candle.ClosePrice;
		var bandOffset = atrValue * VolatilityMultiplier;

		var upperBand = emaValue + bandOffset;
		var lowerBand = emaValue - bandOffset;

		// Buy: price breaks above upper band
		if (close > upperBand && Position <= 0)
		{
			if (Position < 0)
				BuyMarket();

			BuyMarket();
			_entryPrice = close;
			_cooldown = 50;
		}
		// Sell: price breaks below lower band
		else if (close < lowerBand && Position >= 0)
		{
			if (Position > 0)
				SellMarket();

			SellMarket();
			_entryPrice = close;
			_cooldown = 50;
		}
		// Exit long: price returns to EMA
		else if (Position > 0 && close < emaValue)
		{
			SellMarket();
			_entryPrice = 0;
			_cooldown = 50;
		}
		// Exit short: price returns to EMA
		else if (Position < 0 && close > emaValue)
		{
			BuyMarket();
			_entryPrice = 0;
			_cooldown = 50;
		}
	}
}