GitHub で見る

ATR ステップ トレーダー戦略

概要

ATR ステップ トレーダー戦略は、MetaTrader5 エキスパート アドバイザー atrTrader.mq5 の直接ポートです。高速/低速移動平均フィルターと、平均真範囲 (ATR) ベースのブレークアウト ルールおよびピラミッド ルールを組み合わせます。このポートは、元の EA の足主導のワークフローを維持します。つまり、完了したローソク足のみが処理され、高速な SMA は一定数の足の間、低速な SMA の上または下に位置する必要があり、市場間の距離を正規化するためにすべての決定が ATR の倍数に固定されます。

指標とデータ

  • 単純移動平均 (SMA)。 2 つの移動平均 (FastPeriodSlowPeriod) は、主要なトレンド フィルターを定義します。どちらもサブスクリプションキャンドルシリーズに適用されます。
  • 平均トゥルー レンジ (ATR)。 AverageTrueRange インジケーター (AtrPeriod) は、ボラティリティを価格距離に変換します。すべてのブレイクアウト、アドオン、ストップの計算では、ATR の倍数が使用されます。
  • 最高/最低価格チャネル。 Highest および Lowest インジケーターは、最新の MomentumPeriod ローソク足の極端な高値と安値を追跡します。これらは、MQL コードからの iHighest/iLowest 呼び出しを置き換えます。
  • 時間枠。 デフォルトのローソク足タイプは 1 時間 (TimeSpan.FromHours(1)) で、元のスクリプトの PERIOD_CURRENT の動作を反映しています。 CandleType パラメータを編集することで、他の時間枠に切り替えることができます。

エントリーロジック

  1. ろうそくの火が消えるまで待ちます。 MT5 OnTick + iTime ガードとの同期を保つために、未完成のローソク足は無視されます。
  2. 移動平均ストリークカウンターを更新します。高速の SMA が低速の SMA を上回ると、強気の傾向が増します。以下に印刷すると、弱気の縞が増加します。測定値が混在すると、反対のストリークがリセットされます。
  3. 強気の連続が MomentumPeriod に達したら、終値がまだ最近の高値を少なくとも StepMultiplier * ATR 下回っているかどうかを確認します。その場合は市場で購入してください。
  4. 弱気の連続が MomentumPeriod に達したら、終値がまだ最近の安値を少なくとも StepMultiplier * ATR 上回っているかどうかを確認します。その場合は市場で売りましょう。
  5. 新しいポジションごとに方向性の状態が初期化されます。戦略はサイドごとの最高値と最低値を記憶し、後のピラミッドが参照アンカーを持つようにします。最初の注文にはボラティリティサイズのストップ (StepMultiplier * StopMultiplier * ATR) も付けられます。

ポジション管理

  • ピラミッド型: アクティブなエントリーの数が PyramidLimit を下回っている間、この戦略は価格が現在の極端な基準から +/- StepsMultiplier * ATR を離れるたびに別のユニットを追加します。これは、EA の「Steps」スケーリング グリッドを反映しており、有利な方向と不利な方向の両方に機能します。
  • 保護ストップ: 新しい注文の最初のストップは約定価格から StepMultiplier * StopMultiplier * ATR 離れた位置にあります。ピラミッドがいっぱいになると、ストップは直近終値の後ろ(ロングの場合)または前(ショートの場合)に StepMultiplier * ATR に狭められ、3 つのポジションが空いているときの EA の末尾の更新をエミュレートします。
  • 逆方向のエグジット: 価格が追跡された極値を超えて StepsMultiplier * ATR 後退した場合、この戦略は成行注文でその側のすべてのポジションを即座にエグジットします。これは、価格が最新のラダー エッジをブレイクしたときにスタック全体をダンプする EA ロジックをキャプチャしています。
  • ステート リセット: 完全なエグジット後、ストリーク カウンタと ATR ストップ リファレンスがリセットされるため、再エントリーする前に新しいトレンド シーケンスを展開する必要があります。

パラメーター

グループ 名前 説明 デフォルト
トレンドフィルター FastPeriod 短期的な方向性を測定する高速な SMA の長さ。 70
トレンドフィルター SlowPeriod 長期的な方向性を測定する遅い SMA の長さ。 180
トレンドフィルター MomentumPeriod トレンドを確認する必要がある連続終了ローソク足の数。 50
ボラティリティ AtrPeriod すべての距離計算に使用される ATR ウィンドウ。 100
エントリーロジック StepMultiplier 最初のブレイクアウトを防ぐのは ATR 倍です。 4
エントリーロジック StepsMultiplier ピラミッド層を区切る ATR の倍数。 2
リスク管理 StopMultiplier 基本ステップ距離を超えた最初の停止に追加の乗数が適用されます。 3
ポジションサイジング PyramidLimit 方向ごとの最大エントリ数。 3
取引 TradeVolume 各成行注文で送信されたストラテジーのボリューム。 1
一般 CandleType サブスクリプションに使用されるローソクのタイプ (時間枠)。 TimeFrame(1h)

実践メモ

  • StockSharp バージョンは、サイズ設定に戦略 Volume プロパティを使用します。公開前に、商品契約のサイズに合わせて TradeVolume を調整します。
  • MT5 の CTrade.Buy/Sell の使用と同様に、成行注文はすぐに約定すると想定されます。市場が薄い場合は、成行注文を指値注文または逆指値注文に置き換えることができます。
  • 高/低参照は、EA の h_price 変数と l_price 変数を複製し、新しいレイヤーが追加または削除されるたびに更新されます。これらは、はしごをいつ追加またはフラッシュするかを決定するために不可欠です。
  • EA はポジションごとにストップロスを保存し、StockSharp はストップロスを戦略レベルで管理するため、ポートはスタック全体に最も厳しいストップ ロジックを適用します。これにより、管理するブローカー側の注文が少なくなり、同じ動作 (すべてのポジションが同時に決済) が実現します。
  • 戦略は常にシミュレーションでテストしてください。 ATR 距離はボラティリティに適応しますが、ギャップやスリッページが大きい市場では、現実のリスクが予測停止距離を超える可能性があります。
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>
/// Multi-step ATR trend strategy converted from the "atrTrader" MQL5 expert advisor.
/// Filters trends with a dual moving-average stack, opens breakouts, and pyramids positions using ATR distances.
/// </summary>
public class AtrStepTraderStrategy : Strategy
{
	private readonly StrategyParam<int> _fastPeriod;
	private readonly StrategyParam<int> _slowPeriod;
	private readonly StrategyParam<int> _atrPeriod;
	private readonly StrategyParam<int> _momentumPeriod;
	private readonly StrategyParam<int> _pyramidLimit;
	private readonly StrategyParam<decimal> _stepMultiplier;
	private readonly StrategyParam<decimal> _stepsMultiplier;
	private readonly StrategyParam<decimal> _stopMultiplier;
	private readonly StrategyParam<decimal> _tradeVolume;
	private readonly StrategyParam<DataType> _candleType;

	private int _bullishStreak;
	private int _bearishStreak;
	private decimal? _previousSlow;
	private decimal? _longEntryHigh;
	private decimal? _longEntryLow;
	private decimal? _shortEntryHigh;
	private decimal? _shortEntryLow;
	private decimal? _longStopPrice;
	private decimal? _shortStopPrice;

	/// <summary>
	/// Fast moving average length.
	/// </summary>
	public int FastPeriod
	{
		get => _fastPeriod.Value;
		set => _fastPeriod.Value = value;
	}

	/// <summary>
	/// Slow moving average length.
	/// </summary>
	public int SlowPeriod
	{
		get => _slowPeriod.Value;
		set => _slowPeriod.Value = value;
	}

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

	/// <summary>
	/// Number of consecutive bars that must confirm the trend direction.
	/// </summary>
	public int MomentumPeriod
	{
		get => _momentumPeriod.Value;
		set => _momentumPeriod.Value = value;
	}

	/// <summary>
	/// Maximum number of stacked entries per direction.
	/// </summary>
	public int PyramidLimit
	{
		get => _pyramidLimit.Value;
		set => _pyramidLimit.Value = value;
	}

	/// <summary>
	/// ATR multiple used for breakout gating.
	/// </summary>
	public decimal StepMultiplier
	{
		get => _stepMultiplier.Value;
		set => _stepMultiplier.Value = value;
	}

	/// <summary>
	/// ATR multiple used for pyramiding distance checks.
	/// </summary>
	public decimal StepsMultiplier
	{
		get => _stepsMultiplier.Value;
		set => _stepsMultiplier.Value = value;
	}

	/// <summary>
	/// Additional multiplier that widens the protective stop distance.
	/// </summary>
	public decimal StopMultiplier
	{
		get => _stopMultiplier.Value;
		set => _stopMultiplier.Value = value;
	}

	/// <summary>
	/// Base order volume for market entries.
	/// </summary>
	public decimal TradeVolume
	{
		get => _tradeVolume.Value;
		set => _tradeVolume.Value = value;
	}

	/// <summary>
	/// Candle type used for calculations.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Initialize <see cref="AtrStepTraderStrategy"/>.
	/// </summary>
	public AtrStepTraderStrategy()
	{
		_fastPeriod = Param(nameof(FastPeriod), 70)
			.SetGreaterThanZero()
			.SetDisplay("Fast MA Period", "Length of the fast moving average", "Trend Filter")
			
			.SetOptimize(50, 100, 10);

		_slowPeriod = Param(nameof(SlowPeriod), 180)
			.SetGreaterThanZero()
			.SetDisplay("Slow MA Period", "Length of the slow moving average", "Trend Filter")
			
			.SetOptimize(120, 240, 20);

		_atrPeriod = Param(nameof(AtrPeriod), 100)
			.SetGreaterThanZero()
			.SetDisplay("ATR Period", "ATR window used for distance calculations", "Volatility")
			
			.SetOptimize(50, 150, 10);

		_momentumPeriod = Param(nameof(MomentumPeriod), 3)
			.SetGreaterThanZero()
			.SetDisplay("Momentum Bars", "Number of consecutive bars required for trend confirmation", "Trend Filter")
			
			.SetOptimize(30, 80, 5);

		_pyramidLimit = Param(nameof(PyramidLimit), 3)
			.SetGreaterThanZero()
			.SetDisplay("Pyramid Limit", "Maximum number of entries per direction", "Position Sizing")
			
			.SetOptimize(2, 4, 1);

		_stepMultiplier = Param(nameof(StepMultiplier), 4m)
			.SetGreaterThanZero()
			.SetDisplay("Step Multiplier", "ATR multiple for breakout validation", "Entry Logic")
			
			.SetOptimize(2m, 6m, 1m);

		_stepsMultiplier = Param(nameof(StepsMultiplier), 2m)
			.SetGreaterThanZero()
			.SetDisplay("Steps Multiplier", "ATR multiple for add-on spacing", "Entry Logic")
			
			.SetOptimize(1m, 3m, 0.5m);

		_stopMultiplier = Param(nameof(StopMultiplier), 3m)
			.SetGreaterThanZero()
			.SetDisplay("Stop Multiplier", "Extra multiplier applied on top of the step distance", "Risk Management")
			
			.SetOptimize(2m, 4m, 0.5m);

		_tradeVolume = Param(nameof(TradeVolume), 1m)
			.SetGreaterThanZero()
			.SetDisplay("Trade Volume", "Base order size for market entries", "Trading")
			
			.SetOptimize(0.5m, 2m, 0.5m);

		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(1).TimeFrame())
			.SetDisplay("Candle Type", "Time frame used for processing", "General");
	}

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

		_bullishStreak = 0;
		_bearishStreak = 0;
		_previousSlow = null;
		_longEntryHigh = null;
		_longEntryLow = null;
		_shortEntryHigh = null;
		_shortEntryLow = null;
		_longStopPrice = null;
		_shortStopPrice = null;
	}

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

		Volume = TradeVolume;

		var fastMa = new SimpleMovingAverage { Length = FastPeriod };
		var slowMa = new SimpleMovingAverage { Length = SlowPeriod };
		var atr = new AverageTrueRange { Length = AtrPeriod };
		var highest = new Highest { Length = MomentumPeriod };
		var lowest = new Lowest { Length = MomentumPeriod };

		var subscription = SubscribeCandles(CandleType);
		subscription
			.Bind(fastMa, slowMa, atr, highest, lowest, ProcessCandle)
			.Start();

		var area = CreateChartArea();
		if (area != null)
		{
			DrawCandles(area, subscription);
			DrawIndicator(area, fastMa);
			DrawIndicator(area, slowMa);
			DrawIndicator(area, atr);
			DrawIndicator(area, highest);
			DrawIndicator(area, lowest);
			DrawOwnTrades(area);
		}
	}

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

		if (atrValue <= 0m)
			return;

		UpdateMomentumCounters(fastValue, slowValue);

		var price = candle.ClosePrice;
		var previousSlow = _previousSlow;
		_previousSlow = slowValue;

		var volume = Volume;
		if (volume <= 0m)
			volume = 1m;

		var netPosition = Position;
		var longCount = netPosition > 0m ? (int)Math.Round(netPosition / volume, MidpointRounding.AwayFromZero) : 0;
		var shortCount = netPosition < 0m ? (int)Math.Round(-netPosition / volume, MidpointRounding.AwayFromZero) : 0;

		if (longCount == 0 && shortCount == 0)
		{
			if (previousSlow.HasValue && slowValue > 0m)
			{
				var bullishReady = _bullishStreak >= MomentumPeriod && price > previousSlow.Value;
				if (bullishReady)
				{
					BuyMarket(Volume);
					longCount = 1;
					_longEntryHigh = price;
					_longEntryLow = price;
					_longStopPrice = price - StepMultiplier * StopMultiplier * atrValue;
				}
			}

			if (longCount == 0 && previousSlow.HasValue && slowValue > 0m)
			{
				var bearishReady = _bearishStreak >= MomentumPeriod && price < previousSlow.Value;
				if (bearishReady)
				{
					SellMarket(Volume);
					shortCount = 1;
					_shortEntryHigh = price;
					_shortEntryLow = price;
					_shortStopPrice = price + StepMultiplier * StopMultiplier * atrValue;
				}
			}
		}
		else if (longCount > 0 && shortCount == 0)
		{
			ManageLongPosition(ref longCount, price, atrValue);
		}
		else if (shortCount > 0 && longCount == 0)
		{
			ManageShortPosition(ref shortCount, price, atrValue);
		}
	}

	private void UpdateMomentumCounters(decimal fastValue, decimal slowValue)
	{
		if (fastValue > slowValue)
		{
			_bullishStreak++;
			_bearishStreak = 0;
		}
		else if (fastValue < slowValue)
		{
			_bearishStreak++;
			_bullishStreak = 0;
		}
		else
		{
			_bullishStreak++;
			_bearishStreak++;
		}
	}

	private void ManageLongPosition(ref int longCount, decimal price, decimal atrValue)
	{
		if (_longEntryHigh is not decimal high || _longEntryLow is not decimal low)
			return;

		var stepsDistance = StepsMultiplier * atrValue;
		var stepDistance = StepMultiplier * atrValue;

		if (_longStopPrice.HasValue && price <= _longStopPrice.Value)
		{
			SellMarket(Position);
			longCount = 0;
			ResetLongState();
			return;
		}

		if (longCount < PyramidLimit)
		{
			if (price >= high + stepsDistance || price <= low - stepsDistance)
			{
				BuyMarket(Volume);
				longCount++;
				_longEntryHigh = Math.Max(high, price);
				_longEntryLow = Math.Min(low, price);
				UpdateLongStopAfterEntry(price, atrValue);
				return;
			}
		}

		if (price <= low - stepsDistance)
		{
			SellMarket(Position);
			longCount = 0;
			ResetLongState();
			return;
		}

		if (longCount >= PyramidLimit)
		{
			var tightened = price - stepDistance;
			if (!_longStopPrice.HasValue || tightened > _longStopPrice.Value)
				_longStopPrice = tightened;
		}
	}

	private void ManageShortPosition(ref int shortCount, decimal price, decimal atrValue)
	{
		if (_shortEntryHigh is not decimal high || _shortEntryLow is not decimal low)
			return;

		var stepsDistance = StepsMultiplier * atrValue;
		var stepDistance = StepMultiplier * atrValue;

		if (_shortStopPrice.HasValue && price >= _shortStopPrice.Value)
		{
			BuyMarket(Math.Abs(Position));
			shortCount = 0;
			ResetShortState();
			return;
		}

		if (shortCount < PyramidLimit)
		{
			if (price <= low - stepsDistance || price >= high + stepsDistance)
			{
				SellMarket(Volume);
				shortCount++;
				_shortEntryHigh = Math.Max(high, price);
				_shortEntryLow = Math.Min(low, price);
				UpdateShortStopAfterEntry(price, atrValue);
				return;
			}
		}

		if (price >= high + stepsDistance)
		{
			BuyMarket(Math.Abs(Position));
			shortCount = 0;
			ResetShortState();
			return;
		}

		if (shortCount >= PyramidLimit)
		{
			var tightened = price + stepDistance;
			if (!_shortStopPrice.HasValue || tightened < _shortStopPrice.Value)
				_shortStopPrice = tightened;
		}
	}

	private void UpdateLongStopAfterEntry(decimal entryPrice, decimal atrValue)
	{
		var stop = entryPrice - StepMultiplier * StopMultiplier * atrValue;
		if (!_longStopPrice.HasValue || stop > _longStopPrice.Value)
			_longStopPrice = stop;
	}

	private void UpdateShortStopAfterEntry(decimal entryPrice, decimal atrValue)
	{
		var stop = entryPrice + StepMultiplier * StopMultiplier * atrValue;
		if (!_shortStopPrice.HasValue || stop < _shortStopPrice.Value)
			_shortStopPrice = stop;
	}

	private void ResetLongState()
	{
		_longEntryHigh = null;
		_longEntryLow = null;
		_longStopPrice = null;
		_bullishStreak = 0;
	}

	private void ResetShortState()
	{
		_shortEntryHigh = null;
		_shortEntryLow = null;
		_shortStopPrice = null;
		_bearishStreak = 0;
	}
}