GitHub で見る

NRTR ATR Stop戦略

概要

NRTR ATR Stop戦略は、MetaTraderエキスパートアドバイザーExp_NRTR_ATR_STOP_Tmの直接変換です。このシステムは、非再描画トレンドリバーサル(NRTR)ストップと平均真の値幅(ATR)フィルターを組み合わせて、支配的なトレンドを判断し、保護レベルをトレーリングします。取引決定は選択した時間軸のクローズ時に生成され、元のシグナルシフトを模倣するために完全に形成されたバーの設定可能な数だけ遅延できます。

この戦略はStockSharpの高レベルAPIの上に実装されています。すべての取引ロジックはキャンドルサブスクリプション、インジケーターバインディング、および管理された注文ヘルパーによって駆動され、Designer、Shell、Runner、APIの各製品との互換性を確保しています。

取引ロジック

  1. インジケーター計算
    • ATRは提供された期間で選択された時間軸で計算されます。
    • ATR値に係数を掛けてNRTRの上限と下限レベルを構築します。
    • 前のキャンドルが反対のNRTRレベルを突破すると、トレンド方向が変化します;これらのイベントはエントリーをトリガーできる矢印シグナルも作成します。
  2. シグナル遅延
    • SignalBarDelayパラメーターはMetaTraderのSignalBar入力を再現します。選択した数の完成したキャンドルだけ実行を遅延させ、戦略がソースエキスパートと同様に過去のシグナルを評価できるようにします。
  3. エントリー
    • ロングポジションは、強気のNRTRリバーサルが発生し、ロングエントリーが有効な場合に開かれます。
    • ショートポジションは、弱気のNRTRリバーサルが発生し、ショートエントリーが有効な場合に開かれます。
  4. エグジット
    • 方向リバーサルは、そのサイドのクローズが許可されている場合、反対のポジションを閉じます。
    • オプションのセッションフィルターは、許可された取引ウィンドウ外のすべてのポジションを強制的に閉じることができます。
    • 追加のリスク管理は価格ステップで表されたストップロスとテイクプロフィット距離によって処理されます。NRTRレベルはトレンドの方向に保護ストップを締めることで、アクティブなポジションもトレーリングします。

リスク管理

  • ボリューム: 取引は設定可能なOrderVolumeパラメーターで開かれます。ボリュームは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 NRTRレベル構築時にATRに適用する係数。
SignalBarDelay 1 シグナル実行を遅延させる完成したバーの数。

注意事項

  • 戦略はキャンドルレベルの処理のみを使用します;元のEAのティック単位の複製は、StockSharpの高レベルアーキテクチャと一致するよう意図的に避けています。
  • コード内のコメントはプロジェクトガイドラインに従って英語で提供されています。
  • Python版はユーザーリクエストに合わせて意図的に省略されています。
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;
	}
}