GitHub で見る

Momentum M15戦略

この戦略はMetaTrader 5エキスパートアドバイザーMomentum-M15(オリジナルファイルMomentum-M15.mq5)の直接ポートです。 15分足ローソク足を取引し、シフトされた移動平均フィルターとバー始値で評価されるモメンタムオシレーターを組み合わせます。論理はシフトされた平均の反対側に価格がある場合に極端なモメンタムに逆張りすることを目的とし、ギャップガードとオプションのトレーリングストップがエクスポージャーを制限します。

変換のハイライト

  • インジケーターはStockSharpコンポーネントで再作成されます:設定可能な移動平均(デフォルトは平滑化)と選択したローソク足価格で動作する組み込みの Momentumオシレーター(デフォルトはOpen)。
  • MetaTraderの水平MAシフトは、インジケーター値をバッファリングし、MaShift本の完成したバー前の値を取得することでエミュレートされます。カスタムインジケーター数学は再実装されません。
  • モメンタム単調性チェックは最新の履歴値を再利用し、エントリーまたはエグジットウィンドウに必要な要素のみを保持し、オリジナルのCheckMO_Up / CheckMO_Downヘルパーを反映します。
  • 大きなギャップロックアウト(GapLevel/GapTimeout)が保持されます。MQLバージョンで定義されたポイントベースの閾値をStockSharpの価格ステップに変換するために価格ステップ情報が使用されます。
  • トレーリングストップ管理は、価格が追跡されたレベルをクロスしたときの市場エグジットによって内部的に処理され、完了したバーごとにストップロス注文を修正したMQLルーチンと一致します。

パラメータ

名前 説明 デフォルト
TradeVolume 各エントリーに使用される注文サイズ。 0.1
CandleType 主要な時間軸(デフォルトで15分足)。 15m
MaPeriod 移動平均のルックバック長。 26
MaShift 移動平均を水平にシフトするバーの数。 8
MaMethod 移動平均タイプ(SimpleExponentialSmoothedWeighted)。 Smoothed
MaPrice 移動平均に送られるローソク足価格。 Low
MomentumPeriod モメンタムのルックバック長。 23
MomentumPrice モメンタムオシレーターに使用されるローソク足価格。 Open
MomentumThreshold ロング/ショートセットアップを分けるベースモメンタムレベル。 100
MomentumShift 非対称な境界を構築するためにMomentumThresholdに加算/減算される値。 -0.2
MomentumOpenLength ロングを開く前の非増加モメンタムシーケンスに必要なバー数 / ショートの場合は非減少。 6
MomentumCloseLength ポジションを閉じる前の同じ単調シーケンスに必要なバー数。 10
GapLevel 新しいエントリーを一時停止する最小正のギャップ(価格ステップ単位)。 30
GapTimeout 大きなギャップの後に取引を無効のままにするバーの数。 100
TrailingStop 価格ステップで測定されたオプションのトレーリングストップ距離。 0 (無効)

取引ルール

エントリー条件

  • ロングエントリー

    • 最新のモメンタムがMomentumThreshold + MomentumShiftを下回っています(デフォルトのシフト-0.2では、これは メインの閾値をわずかに下回ります)。
    • 前のバーの終値と現在のバーの始値の両方がシフトされた移動平均を下回っています
    • モメンタムがMomentumOpenLengthバーの間、非増加でした(MQLソースのCheckMO_Downと一致)。
  • ショートエントリー

    • 最新のモメンタムがMomentumThreshold - MomentumShiftを上回っています(デフォルトのシフトでは100をわずかに上回ります)。
    • 前のバーの終値と現在のバーの始値の両方がシフトされた移動平均を上回っています
    • モメンタムがMomentumOpenLengthバーの間、非減少でした(CheckMO_Upと一致)。

エントリーはポジションが開かれておらず、ギャップフィルターによって取引が停止されていない場合にのみ評価されます。

エグジット条件

  • ロングポジションは以下のいずれかが真の場合に閉じます:

    • モメンタムがMomentumCloseLengthバーの間、非増加でした。
    • 前のバーの終値がシフトされた移動平均を下回ります。
    • トレーリングストップ(有効の場合)が触れられます。ストップはローソク足の安値から価格ステップで表された設定距離を引いた値を追跡します。
  • ショートポジションは以下のいずれかが真の場合に閉じます:

    • モメンタムがMomentumCloseLengthバーの間、非減少でした。
    • 前のバーの終値がシフトされた移動平均を上回ります。
    • トレーリングストップ(有効の場合)が触れられます。ストップはローソク足の高値に設定距離を加えた値を追跡します。

ギャップ停止ロジック

オリジナルのエキスパートアドバイザーは強い上昇ギャップの後に取引を一時停止しました。StockSharpバージョンは現在のバーの始値と前の終値の差を価格ステップで測定します:

  1. ギャップがGapLevelを超えると、ロックアウトタイマーがGapTimeoutにリセットされます。
  2. タイマーは各閉じたバーの後にデクリメントされ、ゼロに達した後にのみ取引が再開されます。

注意事項と前提条件

  • すべての計算はStockSharpの高レベルAPIの慣行に合わせて完成したローソク足(CandleStates.Finished)を使用します。 その結果、条件が観察された後の次のバーで注文が発行されます。これは元の戦略が新しいバーの最初のティックでトリガーされた方法と一致しています。
  • MetaTraderの「pip」の概念はSecurity.PriceStepで近似されます。インジケーターに適切なステップデータがない場合、 ギャップフィルターとトレーリングストップは静かに無効になります。
  • 移動平均価格とモメンタム入力は独立して変更でき、オリジナルの入力パラメーターの柔軟性を複製します。
  • 自動化されたストップ注文は登録されません。代わりに、市場エグジットがMQLコードがPositionModifyを通じて発行したストップ調整を再現します。

使用のヒント

  1. 希望する証券を割り当て、CandleTypeがバックテスト中に使用された履歴時間軸と一致することを確認してください(オリジナルスクリプトでは15分バー)。
  2. 取引場所でサポートされているロットサイズにTradeVolumeを設定してください。
  3. モメンタム単調性フィルターがどれほど厳格であるべきかを制御するためにMomentumOpenLength / MomentumCloseLengthを調整してください。
  4. デフォルトの「pip」スケールを正確に反映したい場合は、取引所の価格ステップとインジケーターの1 pipとの比率に従ってTrailingStopGapLevelを設定してください。
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 based strategy converted from the MetaTrader 5 "Momentum-M15" expert advisor.
/// </summary>
public class MomentumM15Strategy : Strategy
{
	/// <summary>
	/// Moving average method options aligned with the original expert advisor inputs.
	/// </summary>
	public enum MovingAverageMethods
	{
		/// <summary>
		/// Simple moving average.
		/// </summary>
		Simple,

		/// <summary>
		/// Exponential moving average.
		/// </summary>
		Exponential,

		/// <summary>
		/// Smoothed moving average.
		/// </summary>
		Smoothed,

		/// <summary>
		/// Weighted moving average.
		/// </summary>
		Weighted
	}

	public enum CandlePrices
	{
		Open,
		High,
		Low,
		Close,
		Median,
		Typical,
		Weighted
	}

	private readonly StrategyParam<decimal> _volumeParam;
	private readonly StrategyParam<DataType> _candleTypeParam;
	private readonly StrategyParam<int> _maPeriodParam;
	private readonly StrategyParam<int> _maShiftParam;
	private readonly StrategyParam<MovingAverageMethods> _maMethodParam;
	private readonly StrategyParam<CandlePrices> _maPriceParam;
	private readonly StrategyParam<int> _momentumPeriodParam;
	private readonly StrategyParam<CandlePrices> _momentumPriceParam;
	private readonly StrategyParam<decimal> _momentumThresholdParam;
	private readonly StrategyParam<decimal> _momentumShiftParam;
	private readonly StrategyParam<int> _momentumOpenLengthParam;
	private readonly StrategyParam<int> _momentumCloseLengthParam;
	private readonly StrategyParam<int> _gapLevelParam;
	private readonly StrategyParam<int> _gapTimeoutParam;
	private readonly StrategyParam<decimal> _trailingStopParam;

	private IIndicator _ma = null!;
	private Momentum _momentum = null!;
	private readonly List<decimal> _maHistory = new();
	private readonly List<decimal> _momentumHistory = new();
	private decimal? _previousClose;
	private decimal? _longTrailingStop;
	private decimal? _shortTrailingStop;
	private int _gapTimer;

	/// <summary>
	/// Initializes a new instance of <see cref="MomentumM15Strategy"/>.
	/// </summary>
	public MomentumM15Strategy()
	{
		_volumeParam = Param(nameof(TradeVolume), 0.1m)
			.SetGreaterThanZero()
			.SetDisplay("Trade Volume", "Default order volume", "Trading")
			
			.SetOptimize(0.05m, 0.5m, 0.05m);

		_candleTypeParam = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
			.SetDisplay("Candle Type", "Primary timeframe for calculations", "Common");

		_maPeriodParam = Param(nameof(MaPeriod), 26)
			.SetGreaterThanZero()
			.SetDisplay("MA Period", "Moving average lookback length", "Indicators")
			
			.SetOptimize(10, 60, 5);

		_maShiftParam = Param(nameof(MaShift), 8)
			.SetNotNegative()
			.SetDisplay("MA Shift", "Horizontal shift applied to moving average", "Indicators");

		_maMethodParam = Param(nameof(MaMethod), MovingAverageMethods.Smoothed)
			.SetDisplay("MA Method", "Type of moving average", "Indicators");

		_maPriceParam = Param(nameof(MaPrice), CandlePrices.Low)
			.SetDisplay("MA Price", "Price source for moving average", "Indicators");

		_momentumPeriodParam = Param(nameof(MomentumPeriod), 23)
			.SetGreaterThanZero()
			.SetDisplay("Momentum Period", "Momentum indicator lookback", "Indicators")
			
			.SetOptimize(10, 40, 1);

		_momentumPriceParam = Param(nameof(MomentumPrice), CandlePrices.Open)
			.SetDisplay("Momentum Price", "Price source for momentum", "Indicators");

		_momentumThresholdParam = Param(nameof(MomentumThreshold), 100m)
			.SetDisplay("Momentum Threshold", "Baseline momentum threshold", "Trading Rules");

		_momentumShiftParam = Param(nameof(MomentumShift), -0.2m)
			.SetDisplay("Momentum Shift", "Shift applied to momentum threshold", "Trading Rules");

		_momentumOpenLengthParam = Param(nameof(MomentumOpenLength), 6)
			.SetNotNegative()
			.SetDisplay("Momentum Open Length", "Bars required for monotonic momentum on entries", "Trading Rules");

		_momentumCloseLengthParam = Param(nameof(MomentumCloseLength), 10)
			.SetNotNegative()
			.SetDisplay("Momentum Close Length", "Bars required for monotonic momentum on exits", "Trading Rules");

		_gapLevelParam = Param(nameof(GapLevel), 30)
			.SetNotNegative()
			.SetDisplay("Gap Level", "Minimum gap in price steps to pause trading", "Risk Management");

		_gapTimeoutParam = Param(nameof(GapTimeout), 100)
			.SetNotNegative()
			.SetDisplay("Gap Timeout", "Number of bars to skip after a large gap", "Risk Management");

		_trailingStopParam = Param(nameof(TrailingStop), 0m)
			.SetNotNegative()
			.SetDisplay("Trailing Stop", "Trailing stop distance in price steps", "Risk Management");
	}

	/// <summary>
	/// Default trade volume.
	/// </summary>
	public decimal TradeVolume
	{
		get => _volumeParam.Value;
		set => _volumeParam.Value = value;
	}

	/// <summary>
	/// Candle type for the strategy.
	/// </summary>
	public DataType CandleType
	{
		get => _candleTypeParam.Value;
		set => _candleTypeParam.Value = value;
	}

	/// <summary>
	/// Moving average period.
	/// </summary>
	public int MaPeriod
	{
		get => _maPeriodParam.Value;
		set => _maPeriodParam.Value = value;
	}

	/// <summary>
	/// Number of bars to shift the moving average.
	/// </summary>
	public int MaShift
	{
		get => _maShiftParam.Value;
		set => _maShiftParam.Value = value;
	}

	/// <summary>
	/// Moving average calculation method.
	/// </summary>
	public MovingAverageMethods MaMethod
	{
		get => _maMethodParam.Value;
		set => _maMethodParam.Value = value;
	}

	/// <summary>
	/// Price source for the moving average.
	/// </summary>
	public CandlePrices MaPrice
	{
		get => _maPriceParam.Value;
		set => _maPriceParam.Value = value;
	}

	/// <summary>
	/// Momentum indicator lookback period.
	/// </summary>
	public int MomentumPeriod
	{
		get => _momentumPeriodParam.Value;
		set => _momentumPeriodParam.Value = value;
	}

	/// <summary>
	/// Price source for the momentum indicator.
	/// </summary>
	public CandlePrices MomentumPrice
	{
		get => _momentumPriceParam.Value;
		set => _momentumPriceParam.Value = value;
	}

	/// <summary>
	/// Baseline momentum threshold.
	/// </summary>
	public decimal MomentumThreshold
	{
		get => _momentumThresholdParam.Value;
		set => _momentumThresholdParam.Value = value;
	}

	/// <summary>
	/// Shift applied to the momentum threshold.
	/// </summary>
	public decimal MomentumShift
	{
		get => _momentumShiftParam.Value;
		set => _momentumShiftParam.Value = value;
	}

	/// <summary>
	/// Sequence length for entry momentum validation.
	/// </summary>
	public int MomentumOpenLength
	{
		get => _momentumOpenLengthParam.Value;
		set => _momentumOpenLengthParam.Value = value;
	}

	/// <summary>
	/// Sequence length for exit momentum validation.
	/// </summary>
	public int MomentumCloseLength
	{
		get => _momentumCloseLengthParam.Value;
		set => _momentumCloseLengthParam.Value = value;
	}

	/// <summary>
	/// Minimum gap (in price steps) that suspends new entries.
	/// </summary>
	public int GapLevel
	{
		get => _gapLevelParam.Value;
		set => _gapLevelParam.Value = value;
	}

	/// <summary>
	/// Number of bars to wait after a gap before trading resumes.
	/// </summary>
	public int GapTimeout
	{
		get => _gapTimeoutParam.Value;
		set => _gapTimeoutParam.Value = value;
	}

	/// <summary>
	/// Trailing stop distance expressed in price steps.
	/// </summary>
	public decimal TrailingStop
	{
		get => _trailingStopParam.Value;
		set => _trailingStopParam.Value = value;
	}

	/// <inheritdoc />
	public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
	{
		return [(Security, CandleType)];
	}

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

		_ma = null!;
		_momentum = null!;
		_maHistory.Clear();
		_momentumHistory.Clear();
		_previousClose = null;
		_longTrailingStop = null;
		_shortTrailingStop = null;
		_gapTimer = 0;
	}

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

		_ma = CreateMovingAverage(MaMethod, MaPeriod);
		_momentum = new Momentum { Length = MomentumPeriod };

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

		var area = CreateChartArea();
		if (area != null)
		{
			DrawCandles(area, subscription);
			DrawOwnTrades(area);
		}
	}

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

		if (_ma is null || _momentum is null)
		return;

		var maValue = ProcessMovingAverage(candle);
		var momentumValue = ProcessMomentum(candle);

		if (maValue is null || momentumValue is null)
		{
			_previousClose = candle.ClosePrice;
			return;
		}

		var previousClose = _previousClose;
		_previousClose = candle.ClosePrice;

		if (previousClose is null)
		return;

		HandleGapFilter(previousClose.Value, candle.OpenPrice);

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

		if (Position == 0)
		{
			TryOpenPositions(previousClose.Value, candle.OpenPrice, maValue.Value, momentumValue.Value);
		}
		else
		{
			ManageExistingPosition(previousClose.Value, candle, maValue.Value, momentumValue.Value);
		}
	}

	private decimal? ProcessMovingAverage(ICandleMessage candle)
	{
		var price = GetPrice(candle, MaPrice);
		var value = _ma.Process(new DecimalIndicatorValue(_ma, price, candle.OpenTime) { IsFinal = true });

		if (value.IsEmpty || !_ma.IsFormed)
		return null;

		var ma = value.ToDecimal();
		_maHistory.Add(ma);

		var maxCount = MaShift + 1;
		while (_maHistory.Count > maxCount)
		_maHistory.RemoveAt(0);

		var index = _maHistory.Count - 1 - MaShift;
		if (index < 0 || index >= _maHistory.Count)
		return null;

		return _maHistory[index];
	}

	private decimal? ProcessMomentum(ICandleMessage candle)
	{
		var price = GetPrice(candle, MomentumPrice);
		var value = _momentum.Process(new DecimalIndicatorValue(_momentum, price, candle.OpenTime) { IsFinal = true });

		if (value.IsEmpty || !_momentum.IsFormed)
		return null;

		var momentum = value.ToDecimal();
		_momentumHistory.Add(momentum);

		var maxLen = Math.Max(Math.Max(MomentumOpenLength, MomentumCloseLength), 1);
		while (_momentumHistory.Count > maxLen)
		_momentumHistory.RemoveAt(0);

		return momentum;
	}

	private void HandleGapFilter(decimal previousClose, decimal currentOpen)
	{
		var priceStep = Security.PriceStep ?? 0m;
		if (priceStep <= 0m)
		return;

		var gap = (currentOpen - previousClose) / priceStep;
		if (gap > GapLevel)
		_gapTimer = GapTimeout;
	}

	private void TryOpenPositions(decimal previousClose, decimal currentOpen, decimal maValue, decimal momentumValue)
	{
		var longMomentumOk = MomentumOpenLength > 0 && IsMomentumDownSequence(MomentumOpenLength);
		var shortMomentumOk = MomentumOpenLength > 0 && IsMomentumUpSequence(MomentumOpenLength);

		var longCondition = momentumValue < MomentumThreshold + MomentumShift
		&& previousClose < maValue
		&& currentOpen < maValue
		&& longMomentumOk;

		var shortCondition = momentumValue > MomentumThreshold - MomentumShift
		&& previousClose > maValue
		&& currentOpen > maValue
		&& shortMomentumOk;

		if (longCondition)
		{
			BuyMarket(TradeVolume);
			_longTrailingStop = null;
			_shortTrailingStop = null;
		}
		else if (shortCondition)
		{
			SellMarket(TradeVolume);
			_longTrailingStop = null;
			_shortTrailingStop = null;
		}
	}

	private void ManageExistingPosition(decimal previousClose, ICandleMessage candle, decimal maValue, decimal momentumValue)
	{
		if (Position > 0)
		{
			var exitMomentum = MomentumCloseLength > 0 && IsMomentumDownSequence(MomentumCloseLength);
			var shouldClose = exitMomentum || previousClose < maValue;

			if (shouldClose)
			{
				SellMarket(Position);
				_longTrailingStop = null;
				return;
			}

			UpdateLongTrailingStop(candle);
		}
		else if (Position < 0)
		{
			var exitMomentum = MomentumCloseLength > 0 && IsMomentumUpSequence(MomentumCloseLength);
			var shouldClose = exitMomentum || previousClose > maValue;

			if (shouldClose)
			{
				BuyMarket(Math.Abs(Position));
				_shortTrailingStop = null;
				return;
			}

			UpdateShortTrailingStop(candle);
		}
	}

	private void UpdateLongTrailingStop(ICandleMessage candle)
	{
		if (TrailingStop <= 0m)
		return;

		var priceStep = Security.PriceStep ?? 0m;
		if (priceStep <= 0m)
		return;

		var distance = TrailingStop * priceStep;
		var candidate = candle.LowPrice - distance;

		if (_longTrailingStop is null || candidate > _longTrailingStop)
		_longTrailingStop = candidate;

		if (_longTrailingStop is decimal stop && candle.LowPrice <= stop)
		{
			SellMarket(Position);
			_longTrailingStop = null;
		}
	}

	private void UpdateShortTrailingStop(ICandleMessage candle)
	{
		if (TrailingStop <= 0m)
		return;

		var priceStep = Security.PriceStep ?? 0m;
		if (priceStep <= 0m)
		return;

		var distance = TrailingStop * priceStep;
		var candidate = candle.HighPrice + distance;

		if (_shortTrailingStop is null || candidate < _shortTrailingStop)
		_shortTrailingStop = candidate;

		if (_shortTrailingStop is decimal stop && candle.HighPrice >= stop)
		{
			BuyMarket(Math.Abs(Position));
			_shortTrailingStop = null;
		}
	}

	private bool IsMomentumDownSequence(int length)
	{
		if (length <= 0 || _momentumHistory.Count < length)
		return false;

		var start = _momentumHistory.Count - length;
		var previous = _momentumHistory[start];

		for (var i = start + 1; i < _momentumHistory.Count; i++)
		{
			var current = _momentumHistory[i];
			if (current > previous)
			return false;

			previous = current;
		}

		return true;
	}

	private bool IsMomentumUpSequence(int length)
	{
		if (length <= 0 || _momentumHistory.Count < length)
		return false;

		var start = _momentumHistory.Count - length;
		var previous = _momentumHistory[start];

		for (var i = start + 1; i < _momentumHistory.Count; i++)
		{
			var current = _momentumHistory[i];
			if (current < previous)
			return false;

			previous = current;
		}

		return true;
	}

	private static decimal GetPrice(ICandleMessage candle, CandlePrices price)
	{
		return price switch
		{
			CandlePrices.Open => candle.OpenPrice,
			CandlePrices.High => candle.HighPrice,
			CandlePrices.Low => candle.LowPrice,
			CandlePrices.Close => candle.ClosePrice,
			CandlePrices.Median => (candle.HighPrice + candle.LowPrice) / 2m,
			CandlePrices.Typical => (candle.HighPrice + candle.LowPrice + candle.ClosePrice) / 3m,
			CandlePrices.Weighted => (candle.HighPrice + candle.LowPrice + 2m * candle.ClosePrice) / 4m,
			_ => candle.ClosePrice,
		};
	}

	private static IIndicator CreateMovingAverage(MovingAverageMethods method, int period)
	{
		return method switch
		{
			MovingAverageMethods.Simple => new SimpleMovingAverage { Length = period },
			MovingAverageMethods.Exponential => new ExponentialMovingAverage { Length = period },
			MovingAverageMethods.Smoothed => new SmoothedMovingAverage { Length = period },
			MovingAverageMethods.Weighted => new WeightedMovingAverage { Length = period },
			_ => new SimpleMovingAverage { Length = period },
		};
	}
}