GitHub で見る

Autotrader モメンタム戦略

概要

Autotrader モメンタム戦略は、MetaTrader 5 のエキスパートアドバイザー Autotrader Momentum (barabashkakvn 版) を変換したものです。このアルゴリズムは、モニタリングバーの終値と過去の参照バーの終値を比較することで直近のモメンタムを評価します。強気のモメンタム転換が検出された場合は買い、弱気の転換が現れた場合は売りを実行します。すべての注文は StockSharp の高レベル取引 API を使用して市場価格で執行されます。

この実装は、ポイントベースのリスク管理という元の焦点を維持しています。ストップロス、テイクプロフィット、トレーリングストップの距離はピップ単位で定義され、インストゥルメントの PriceStep に基づいて価格オフセットに自動変換されます。MQL コードで使用された同じ 10 倍の調整を適用することで、3 桁および 5 桁の小数点以下の気配値のサポートが維持されます。トレーリングロジックは、新しいエントリーを検討する前に完了した各ローソク足で評価され、リスク管理が保護的な決済を優先する EA の動作を反映します。

取引ロジック

  1. 設定された CandleType にサブスクライブし、完了したローソク足のみを処理して、元の EA の「新しいバー」ロジックに対応する。
  2. max(CurrentBarIndex, ComparableBarIndex) + 1 のサイズの終値のローリングウィンドウを維持する。
  3. モニタリングバー (CurrentBarIndex、デフォルト 0) の終値と履歴バー (ComparableBarIndex、デフォルト 15) の終値を比較する。
  4. モニタリングの終値が参照終値より大きい場合、ショートポジションをクローズして設定済みの取引量で買いを入れる。
  5. モニタリングの終値が参照終値より小さい場合、ロングポジションをクローズして設定済みの取引量で売りを入れる。
  6. 各エントリーでは平均エントリー価格を再計算し、ストップロス、テイクプロフィット、トレーリングストップのレベルを更新する。

StockSharp 戦略はネットポジションで機能するため、反転時は反対のエクスポージャーをクローズするために必要なボリュームと設定済みのベースボリュームを組み合わせます。これは、まず反対側をクローズしてから要求されたサイズの新しい注文を出す MQL の動作に対応します。

パラメーター

  • CandleType – 価格比較に使用する時間軸。デフォルト: 1 時間。
  • TradeVolume – ベース成行注文量。既存ポジションを反転させるために必要なボリュームに加えて、各シグナルに適用されます。
  • StopLossPips – ピップ単位の保護ストップ距離。固定ストップロスを無効にするには 0 に設定。
  • TakeProfitPips – ピップ単位の利益目標距離。固定テイクプロフィットを無効にするには 0 に設定。
  • TrailingStopPips – トレーリングストップが維持する距離。トレーリングを無効にするには 0 に設定。
  • TrailingStepPips – トレーリングストップを進める前に必要な最小有利移動。トレーリングが有効な場合は正の値である必要があります。
  • CurrentBarIndex – モニタリングローソク足のインデックス (0 = 最も直近の完了バー)。
  • ComparableBarIndex – モメンタム比較に使用する履歴バーのインデックス。

すべてのピップベースの設定は、インストゥルメントの PriceStep を使用して価格オフセットに変換されます。ステップが 3 桁または 5 桁の小数点以下を表す場合、オフセットは MetaTrader のピップ定義を再現するために 10 倍されます。

リスク管理

  • 固定ストップとターゲット: StopLossPips または TakeProfitPips がゼロより大きい場合、戦略は平均エントリー価格に対する相対的な価格レベルを維持します。
  • トレーリングストップ: TrailingStopPipsTrailingStepPips の両方が正の場合に有効になります。トレーリングロジックは、平均エントリー価格から少なくとも TrailingStopPips + TrailingStepPips だけ価格が動いた後にのみ保護ストップを移動させます。これは、ストップを引き締める前に動きが十分に大きいことを確認した EA の要件を再現します。
  • 状態リセット: ポジションがゼロに戻るたびに(戦略主導の決済または外部介入のいずれかによって)、古くなったストップまたはテイクプロフィットレベルを避けるためにキャッシュされたリスク状態がクリアされます。

実装上の注意

  • この戦略は StockSharp の高レベル市場 API (BuyMarketSellMarket) のみに依存し、変換ガイドラインに忠実であるためインジケーターコレクションを使用しません。
  • 終値はシンプルなローリングリストにバッファリングされるため、CurrentBarIndexComparableBarIndex は再起動なしに実行時に変更できます。
  • StockSharp はネットポジションで動作するため、ストップロスとテイクプロフィットのレベルは集計されたエクスポージャーに対して追跡されます。同じ方向に追加注文が積み重なる場合、コードはリスクレベルを更新する前にボリューム加重平均エントリー価格を再計算します。
  • トレーリングストップの調整と保護的な決済は各ローソク足の新しいシグナルの前に処理され、そのバーに対してすでに決済が発行されている場合に新しいエントリーが評価されるのを防ぎます。

元の戦略リファレンス

  • ソース: MQL/22409/Autotrader Momentum.mq5
  • 著者: barabashkakvn (MetaTrader コミュニティ)
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>
/// Momentum strategy converted from the MetaTrader 5 expert advisor "Autotrader Momentum".
/// Compares the most recent closing price with a historical reference bar and reverses positions when momentum shifts.
/// Includes configurable fixed stops, take profit targets, and an optional trailing stop engine measured in pips.
/// </summary>
public class AutotraderMomentumStrategy : Strategy
{
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<decimal> _tradeVolume;
	private readonly StrategyParam<int> _stopLossPips;
	private readonly StrategyParam<int> _takeProfitPips;
	private readonly StrategyParam<int> _trailingStopPips;
	private readonly StrategyParam<int> _trailingStepPips;
	private readonly StrategyParam<int> _cooldownBars;
	private readonly StrategyParam<int> _currentBarIndex;
	private readonly StrategyParam<int> _comparableBarIndex;

	private readonly List<decimal> _closeHistory = new();

	private decimal? _entryPrice;
	private decimal? _stopPrice;
	private decimal? _takeProfitPrice;
	private bool _isLongPosition;

	private decimal _pipValue;
	private decimal _stopLossOffset;
	private decimal _takeProfitOffset;
	private decimal _trailingStopOffset;
	private decimal _trailingStepOffset;
	private int _cooldownLeft;

	/// <summary>
	/// Initializes a new instance of the <see cref="AutotraderMomentumStrategy"/> class.
	/// </summary>
	public AutotraderMomentumStrategy()
	{
		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
			.SetDisplay("Candle Type", "Primary timeframe for price comparisons", "Data");

		_tradeVolume = Param(nameof(TradeVolume), 1m)
			.SetDisplay("Trade Volume", "Base order volume used for market entries", "Trading")
			.SetGreaterThanZero();

		_stopLossPips = Param(nameof(StopLossPips), 50)
			.SetDisplay("Stop Loss (pips)", "Protective stop distance expressed in pips", "Risk")
			.SetNotNegative();

		_takeProfitPips = Param(nameof(TakeProfitPips), 50)
			.SetDisplay("Take Profit (pips)", "Profit target distance expressed in pips", "Risk")
			.SetNotNegative();

		_trailingStopPips = Param(nameof(TrailingStopPips), 0)
			.SetDisplay("Trailing Stop (pips)", "Distance maintained by the trailing stop in pips", "Risk")
			.SetNotNegative();

		_trailingStepPips = Param(nameof(TrailingStepPips), 5)
			.SetDisplay("Trailing Step (pips)", "Minimum progress before the trailing stop advances", "Risk")
			.SetNotNegative();

		_cooldownBars = Param(nameof(CooldownBars), 2)
			.SetDisplay("Cooldown Bars", "Bars to wait after entries and exits", "Risk")
			.SetNotNegative();

		_currentBarIndex = Param(nameof(CurrentBarIndex), 0)
			.SetDisplay("Current Bar Index", "Index of the candle used as the signal source", "Logic")
			.SetNotNegative();

		_comparableBarIndex = Param(nameof(ComparableBarIndex), 8)
			.SetDisplay("Comparable Bar Index", "Historical candle index used for momentum comparison", "Logic")
			.SetNotNegative();
	}

	/// <summary>
	/// Gets or sets the candle type used for generating signals.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Gets or sets the base order volume.
	/// </summary>
	public decimal TradeVolume
	{
		get => _tradeVolume.Value;
		set => _tradeVolume.Value = value;
	}

	/// <summary>
	/// Gets or sets the stop-loss distance in pips.
	/// </summary>
	public int StopLossPips
	{
		get => _stopLossPips.Value;
		set => _stopLossPips.Value = value;
	}

	/// <summary>
	/// Gets or sets the take-profit distance in pips.
	/// </summary>
	public int TakeProfitPips
	{
		get => _takeProfitPips.Value;
		set => _takeProfitPips.Value = value;
	}

	/// <summary>
	/// Gets or sets the trailing stop distance in pips.
	/// </summary>
	public int TrailingStopPips
	{
		get => _trailingStopPips.Value;
		set => _trailingStopPips.Value = value;
	}

	/// <summary>
	/// Gets or sets the trailing step distance in pips.
	/// </summary>
	public int TrailingStepPips
	{
		get => _trailingStepPips.Value;
		set => _trailingStepPips.Value = value;
	}

	/// <summary>
	/// Gets or sets the index of the candle considered the "current" bar in comparisons.
	/// </summary>
	public int CurrentBarIndex
	{
		get => _currentBarIndex.Value;
		set => _currentBarIndex.Value = value;
	}

	/// <summary>
	/// Gets or sets the index of the historical bar used for comparison.
	/// </summary>
	public int ComparableBarIndex
	{
		get => _comparableBarIndex.Value;
		set => _comparableBarIndex.Value = value;
	}

	public int CooldownBars
	{
		get => _cooldownBars.Value;
		set => _cooldownBars.Value = value;
	}

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

		_closeHistory.Clear();
		ResetPositionState();

		_pipValue = 0m;
		_stopLossOffset = 0m;
		_takeProfitOffset = 0m;
		_trailingStopOffset = 0m;
		_trailingStepOffset = 0m;
		_cooldownLeft = 0;
	}

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

		if (TrailingStopPips > 0 && TrailingStepPips <= 0)
			throw new InvalidOperationException("Trailing step must be positive when trailing stop is enabled.");

		Volume = TradeVolume;

		_pipValue = CalculatePipValue();
		_stopLossOffset = StopLossPips > 0 ? StopLossPips * _pipValue : 0m;
		_takeProfitOffset = TakeProfitPips > 0 ? TakeProfitPips * _pipValue : 0m;
		_trailingStopOffset = TrailingStopPips > 0 ? TrailingStopPips * _pipValue : 0m;
		_trailingStepOffset = TrailingStepPips > 0 ? TrailingStepPips * _pipValue : 0m;
		_cooldownLeft = 0;

		var subscription = SubscribeCandles(CandleType);
		subscription
			.Bind(ProcessCandle)
			.Start();
	}

	private void ProcessCandle(ICandleMessage candle)
	{
		// Ignore incomplete candles to mirror the original new-bar processing style.
		if (candle.State != CandleStates.Finished)
			return;

		if (_cooldownLeft > 0)
			_cooldownLeft--;

		// Update trailing and risk management before evaluating fresh signals.
		UpdateTrailingStop(candle);
		var exitTriggered = ManageProtectiveExits(candle);

		// Maintain the rolling window of closes used for momentum comparisons.
		UpdateCloseHistory(candle.ClosePrice);

		// Skip signal generation if an exit order has just been triggered on this bar.
		if (exitTriggered)
			return;

		if (!IsFormedAndOnlineAndAllowTrading())
			return;

		if (_cooldownLeft > 0)
			return;

		var requiredHistory = Math.Max(CurrentBarIndex, ComparableBarIndex) + 1;
		if (_closeHistory.Count < requiredHistory)
			return;

		var currentClose = GetCloseAtIndex(CurrentBarIndex);
		var comparableClose = GetCloseAtIndex(ComparableBarIndex);
		if (currentClose == null || comparableClose == null)
			return;

		// Enter long when the monitored bar closes above the reference bar.
		if (currentClose > comparableClose && Position <= 0)
		{
			EnterPosition(true, candle);
		}
		// Enter short when the monitored bar closes below the reference bar.
		else if (currentClose < comparableClose && Position >= 0)
		{
			EnterPosition(false, candle);
		}
	}

	private void UpdateCloseHistory(decimal closePrice)
	{
		var maxCount = Math.Max(CurrentBarIndex, ComparableBarIndex) + 1;
		if (maxCount <= 0)
			maxCount = 1;

		_closeHistory.Add(closePrice);
		if (_closeHistory.Count > maxCount)
			_closeHistory.RemoveAt(0);
	}

	private decimal? GetCloseAtIndex(int indexFromCurrent)
	{
		if (indexFromCurrent < 0)
			return null;

		var targetIndex = _closeHistory.Count - 1 - indexFromCurrent;
		if (targetIndex < 0 || targetIndex >= _closeHistory.Count)
			return null;

		return _closeHistory[targetIndex];
	}

	private void EnterPosition(bool isLong, ICandleMessage candle)
	{
		var baseVolume = TradeVolume;
		if (baseVolume <= 0m)
			return;

		var previousPosition = Position;
		decimal volume;

		if (isLong)
		{
			volume = baseVolume;
			if (previousPosition < 0m)
				volume += Math.Abs(previousPosition);

			if (volume <= 0m)
				return;

			// Buy enough volume to close any short exposure and add the configured trade size.
			BuyMarket(volume);

			if (previousPosition <= 0m)
			{
				// Treat reversals and fresh entries as a brand-new long position.
				_entryPrice = candle.ClosePrice;
			}
			else
			{
				// Blend the existing average price with the new fill to keep risk metrics consistent.
				var existingVolume = previousPosition;
				var totalVolume = existingVolume + baseVolume;
				if (totalVolume > 0m)
				{
					var existingEntry = _entryPrice ?? candle.ClosePrice;
					_entryPrice = (existingEntry * existingVolume + candle.ClosePrice * baseVolume) / totalVolume;
				}
			}

			_isLongPosition = true;
		}
		else
		{
			volume = baseVolume;
			if (previousPosition > 0m)
				volume += previousPosition;

			if (volume <= 0m)
				return;

			// Sell enough volume to close any long exposure and add the configured trade size.
			SellMarket(volume);

			if (previousPosition >= 0m)
			{
				// Treat reversals and fresh entries as a brand-new short position.
				_entryPrice = candle.ClosePrice;
			}
			else
			{
				// Blend the existing short average price with the new fill.
				var existingVolume = Math.Abs(previousPosition);
				var totalVolume = existingVolume + baseVolume;
				if (totalVolume > 0m)
				{
					var existingEntry = _entryPrice ?? candle.ClosePrice;
					_entryPrice = (existingEntry * existingVolume + candle.ClosePrice * baseVolume) / totalVolume;
				}
			}

			_isLongPosition = false;
		}

		_stopPrice = CalculateStopPrice(_isLongPosition, _entryPrice);
		_takeProfitPrice = CalculateTakeProfit(_isLongPosition, _entryPrice);
		_cooldownLeft = CooldownBars;
	}

	private decimal? CalculateStopPrice(bool isLong, decimal? entryPrice)
	{
		if (entryPrice == null || _stopLossOffset <= 0m)
			return null;

		return isLong ? entryPrice - _stopLossOffset : entryPrice + _stopLossOffset;
	}

	private decimal? CalculateTakeProfit(bool isLong, decimal? entryPrice)
	{
		if (entryPrice == null || _takeProfitOffset <= 0m)
			return null;

		return isLong ? entryPrice + _takeProfitOffset : entryPrice - _takeProfitOffset;
	}

	private void UpdateTrailingStop(ICandleMessage candle)
	{
		if (_trailingStopOffset <= 0m || _trailingStepOffset <= 0m || _entryPrice == null)
			return;

		if (Position > 0m)
		{
			var progress = candle.HighPrice - _entryPrice.Value;
			if (progress <= _trailingStopOffset + _trailingStepOffset)
				return;

			// Shift the trailing stop only when the move is large enough to respect the configured step.
			var desiredStop = candle.ClosePrice - _trailingStopOffset;
			if (_stopPrice is decimal currentStop)
			{
				if (desiredStop - currentStop >= _trailingStepOffset)
					_stopPrice = desiredStop;
			}
			else
			{
				_stopPrice = desiredStop;
			}
		}
		else if (Position < 0m)
		{
			var progress = _entryPrice.Value - candle.LowPrice;
			if (progress <= _trailingStopOffset + _trailingStepOffset)
				return;

			var desiredStop = candle.ClosePrice + _trailingStopOffset;
			if (_stopPrice is decimal currentStop)
			{
				if (currentStop - desiredStop >= _trailingStepOffset)
					_stopPrice = desiredStop;
			}
			else
			{
				_stopPrice = desiredStop;
			}
		}
	}

	private bool ManageProtectiveExits(ICandleMessage candle)
	{
		if (Position > 0m)
		{
			// Close the long position if the bar traded through the stop level.
			if (_stopPrice is decimal stop && candle.LowPrice <= stop)
			{
				SellMarket(Position);
				ResetPositionState();
				_cooldownLeft = CooldownBars;
				return true;
			}

			// Lock in profits once the take-profit threshold has been reached.
			if (_takeProfitPrice is decimal take && candle.HighPrice >= take)
			{
				SellMarket(Position);
				ResetPositionState();
				_cooldownLeft = CooldownBars;
				return true;
			}
		}
		else if (Position < 0m)
		{
			var volume = Math.Abs(Position);

			if (_stopPrice is decimal stop && candle.HighPrice >= stop)
			{
				BuyMarket(volume);
				ResetPositionState();
				_cooldownLeft = CooldownBars;
				return true;
			}

			if (_takeProfitPrice is decimal take && candle.LowPrice <= take)
			{
				BuyMarket(volume);
				ResetPositionState();
				_cooldownLeft = CooldownBars;
				return true;
			}
		}
		else
		{
			// Ensure cached state is flushed once all positions are closed externally.
			ResetPositionState();
		}

		return false;
	}

	private void ResetPositionState()
	{
		_entryPrice = null;
		_stopPrice = null;
		_takeProfitPrice = null;
		_isLongPosition = false;
	}

	private decimal CalculatePipValue()
	{
		var step = Security?.PriceStep ?? 0m;
		if (step <= 0m)
			return 1m;

		var scaled = step;
		var digits = 0;
		while (scaled < 1m && digits < 10)
		{
			scaled *= 10m;
			digits++;
		}

		// Adjust for three and five decimal quotes to emulate the MetaTrader point multiplier.
		var adjust = (digits == 3 || digits == 5) ? 10m : 1m;
		return step * adjust;
	}
}