GitHub で見る

Russian20 タイムフィルター勢い戦略

概要

Russian20 タイム フィルター モメンタム戦略 は、Gordago Software Corp. によって元々配布されていた、MetaTrader 4 エキスパート アドバイザー Russian20-hp1.mq4 を変換したものです。このアルゴリズムは、20 期間の単純移動平均 (SMA) と 30 分のローソク足で評価された 5 期間のモメンタム インジケーターを組み合わせています。ポジションは価格の勢いとトレンドの方向が一致した場合にのみオープンされ、オプションでユーザー定義の日中取引ウィンドウに制限されます。

取引ロジック

  • データ頻度: 構成可能なローソク足タイプを使用します (デフォルト: 30 分足のローソク足、MT4 スクリプトの PERIOD_M30 と一致)。すべてのシグナルは、元のエキスパートによるバークローズの実行に忠実を保つために、完全に閉じたローソク足でのみ評価されます。
  • インジケーター:
    • 長さを調整できる単純移動平均 (デフォルトは 20)。
    • MetaTrader と同様に、構成可能なルックバック (デフォルトは 5) とニュートラル レベルが 100 に設定されたモメンタム インジケーター。
  • ロングエントリー: 以下の条件が最新の閉じたバーに一致するとトリガーされます:
    1. 終値は SMA を上回っています。
    2. 運動量はニュートラルしきい値 (デフォルトは 100) を超えて出力されます。
    3. 現在の終値は前のローソク足の終値よりも高くなっています。
  • 短いエントリ: 次の場合にトリガーされます:
    1. 終値は SMA を下回っています。
    2. モメンタムは中立の閾値を下回っています。
    3. 現在の終値は前の終値よりも低いです。
  • 終了ルール:
    • モメンタムがしきい値以下に戻った場合、またはテイクプロフィットターゲット(有効な場合)に達した場合、ロングポジションはクローズされます。
    • ショートポジションは、モメンタムがしきい値以上に上昇するか、テイクプロフィット目標に達するとクローズされます。

セッションフィルター

MetaTrader スクリプトは、オプションの取引ウィンドウ (デフォルトは 14:00 ~ 16:00) を提供しました。 StockSharp ポートは、UseTimeFilterStartHour、および EndHour パラメータを通じて同じ動作を公開します。フィルターがアクティブな場合、戦略はエントリと選択された時間外の終了の両方をスキップし、元のエキスパートの早期復帰ロジックを反映します。

リスク管理

MQL4 バージョンでは、すべての注文に固定の 20 ピップのテイクプロフィットが付加されました。変換ではこの機能が維持され、距離が「ピップ」で表現され、商品の PriceStep を介して端数ピップ価格設定(小数点以下 3/5)が自動的に調整されます。 TakeProfitPips をゼロに設定すると、利益目標が完全に無効になります。

パラメーター

パラメータ デフォルト 説明
CandleType 30分キャンドル 価格/インジケーターの計算に使用されるデータ型。
MovingAverageLength 20 SMA トレンド フィルターをルックバックします。
MomentumPeriod 5 モメンタムインジケーターを振り返ります。
MomentumThreshold 100 エントリーとエグジットに使用されるニュートラル モメンタム レベル。
TakeProfitPips 20 利益目標距離 (pips)。ゼロはターゲットを無効にします。
UseTimeFilter 日中取引セッションフィルターを有効にします。
StartHour 14 取引ウィンドウの包括的な開始時間 (0 ~ 23)。
EndHour 16 取引ウィンドウの終了時間を含みます (0 ~ 23)。

すべてのパラメータは StrategyParam<T> を通じて定義され、UI に表示され、最適化の準備が整います。

実装メモ

  • 高レベルの SubscribeCandles().Bind(...) API を使用するため、手動でシリーズを管理することなく、インジケーター値が処理ルーチンに直接ストリーミングされます。
  • 最新の終値のみを保存して連続するローソク足を比較し、大量の履歴クエリを回避し、リポジトリのパフォーマンス ガイドラインに準拠します。
  • Security.PriceStep からピップ乗数を自動的に再計算し、4/5 桁の価格設定を持つ外国為替シンボル全体で正しいテイクプロフィットディスタンスを確保します。
  • ホスト環境がサポートしている場合に便利な視覚的分析のために、オプションのチャート レンダリング フック (DrawCandlesDrawIndicatorDrawOwnTrades) を追加します。

使用のヒント

  • ローソクの種類を取引する予定の時間枠に合わせます。外国為替ペアの場合、元の 30 分設定が適切な開始点です。
  • UseTimeFilter が有効な場合は、StartHourEndHour 以下であることを確認してください。開始時間を終了時間より後に設定すると、MT4 ロジックが指定された間隔外の処理をスキップするだけなので、事実上取引が無効になります。
  • 専門家はストップロスを使用したことがないため、ライブキャピタルを取引する際には、追加のリスク管理 (手動または StockSharp 保護機能による) と戦略を組み合わせることを検討してください。
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>
/// Russian20 Time Filter Momentum strategy converted from MetaTrader 4 (Russian20-hp1.mq4).
/// Combines a 20-period simple moving average with a 5-period momentum filter and optional trading hours restriction.
/// </summary>
public class Russian20TimeFilterMomentumStrategy : Strategy
{
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<int> _movingAverageLength;
	private readonly StrategyParam<int> _momentumPeriod;
	private readonly StrategyParam<decimal> _momentumThreshold;
	private readonly StrategyParam<decimal> _takeProfitPips;
	private readonly StrategyParam<bool> _useTimeFilter;
	private readonly StrategyParam<int> _startHour;
	private readonly StrategyParam<int> _endHour;

	private SimpleMovingAverage _movingAverage;
	private Momentum _momentum;
	private decimal? _previousClose;
	private decimal? _entryPrice;
	private decimal _pipSize;
	private decimal _takeProfitOffset;

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

	/// <summary>
	/// Period of the simple moving average filter.
	/// </summary>
	public int MovingAverageLength
	{
		get => _movingAverageLength.Value;
		set => _movingAverageLength.Value = value;
	}

	/// <summary>
	/// Lookback period for the momentum indicator.
	/// </summary>
	public int MomentumPeriod
	{
		get => _momentumPeriod.Value;
		set => _momentumPeriod.Value = value;
	}

	/// <summary>
	/// Neutral momentum level used for entry and exit decisions.
	/// </summary>
	public decimal MomentumThreshold
	{
		get => _momentumThreshold.Value;
		set => _momentumThreshold.Value = value;
	}

	/// <summary>
	/// Take profit distance expressed in pips for both long and short trades.
	/// Set to zero to disable the profit target.
	/// </summary>
	public decimal TakeProfitPips
	{
		get => _takeProfitPips.Value;
		set => _takeProfitPips.Value = value;
	}

	/// <summary>
	/// Enables the optional trading session filter.
	/// </summary>
	public bool UseTimeFilter
	{
		get => _useTimeFilter.Value;
		set => _useTimeFilter.Value = value;
	}

	/// <summary>
	/// Start hour (inclusive) of the allowed trading window.
	/// </summary>
	public int StartHour
	{
		get => _startHour.Value;
		set => _startHour.Value = value;
	}

	/// <summary>
	/// End hour (inclusive) of the allowed trading window.
	/// </summary>
	public int EndHour
	{
		get => _endHour.Value;
		set => _endHour.Value = value;
	}

	/// <summary>
	/// Initializes strategy parameters with defaults aligned with the original expert advisor.
	/// </summary>
	public Russian20TimeFilterMomentumStrategy()
	{
		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
			.SetDisplay("Candle Type", "Candle type used for analysis", "General");

		_movingAverageLength = Param(nameof(MovingAverageLength), 20)
			.SetGreaterThanZero()
			.SetDisplay("SMA Length", "Simple moving average lookback", "Indicators")
			
			.SetOptimize(10, 40, 5);

		_momentumPeriod = Param(nameof(MomentumPeriod), 5)
			.SetGreaterThanZero()
			.SetDisplay("Momentum Period", "Momentum indicator lookback", "Indicators")
			
			.SetOptimize(3, 12, 1);

		_momentumThreshold = Param(nameof(MomentumThreshold), 100m)
			.SetGreaterThanZero()
			.SetDisplay("Momentum Threshold", "Neutral momentum level for signals", "Indicators");

		_takeProfitPips = Param(nameof(TakeProfitPips), 20m)
			.SetNotNegative()
			.SetDisplay("Take Profit (pips)", "Take profit distance in pips", "Risk");

		_useTimeFilter = Param(nameof(UseTimeFilter), false)
			.SetDisplay("Use Time Filter", "Restrict trading to a session", "Session");

		_startHour = Param(nameof(StartHour), 14)
			.SetDisplay("Start Hour", "Inclusive start hour of the trading session", "Session")
			.SetRange(0, 23);

		_endHour = Param(nameof(EndHour), 16)
			.SetDisplay("End Hour", "Inclusive end hour of the trading session", "Session")
			.SetRange(0, 23);
	}

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

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

		_movingAverage = null;
		_momentum = null;
		_previousClose = null;
		_entryPrice = null;
		_pipSize = 0m;
		_takeProfitOffset = 0m;
	}

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

		UpdatePipSettings();

		_movingAverage = new SimpleMovingAverage
		{
			Length = MovingAverageLength,
		};

		_momentum = new Momentum
		{
			Length = MomentumPeriod,
		};

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

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

	private void ProcessCandle(ICandleMessage candle, decimal maValue, decimal momentumValue)
	{
		// Ignore incomplete candles to mirror the bar-close execution of the MQL script.
		if (candle.State != CandleStates.Finished)
			return;

		// Honour trading session boundaries when the filter is enabled.
		if (UseTimeFilter)
		{
			var hour = candle.OpenTime.Hour;
			if (hour < StartHour || hour > EndHour)
			{
				_previousClose = candle.ClosePrice;
				return;
			}
		}

		// Ensure the infrastructure allows trading and indicators are ready.
		

		if (!_movingAverage.IsFormed || !_momentum.IsFormed)
		{
			_previousClose = candle.ClosePrice;
			return;
		}

		if (_pipSize == 0m)
			UpdatePipSettings();

		var closePrice = candle.ClosePrice;

		if (_previousClose is null)
		{
			_previousClose = closePrice;
			return;
		}

		var entryPrice = _entryPrice;

		if (Position == 0 && entryPrice.HasValue)
		{
			// Reset entry price if an external action flattened the position.
			_entryPrice = null;
			entryPrice = null;
		}

		if (Position == 0)
		{
			// Evaluate entry conditions only when flat.
			var bullishSignal = closePrice > maValue && momentumValue > MomentumThreshold && closePrice > _previousClose.Value;
			var bearishSignal = closePrice < maValue && momentumValue < MomentumThreshold && closePrice < _previousClose.Value;

			if (bullishSignal)
			{
				// Enter long on a bullish alignment of filters.
				BuyMarket();
				_entryPrice = closePrice;
			}
			else if (bearishSignal)
			{
				// Enter short on a bearish alignment of filters.
				SellMarket();
				_entryPrice = closePrice;
			}
		}
		else if (Position > 0)
		{
			// Exit long when momentum weakens or the take profit target is achieved.
			var exitByMomentum = momentumValue <= MomentumThreshold;
			var exitByTake = entryPrice.HasValue && _takeProfitOffset > 0m && closePrice >= entryPrice.Value + _takeProfitOffset;

			if (exitByMomentum || exitByTake)
			{
				if (Position > 0) SellMarket(); else if (Position < 0) BuyMarket();
				_entryPrice = null;
			}
		}
		else
		{
			// Exit short when momentum strengthens or the profit target is touched.
			var exitByMomentum = momentumValue >= MomentumThreshold;
			var exitByTake = entryPrice.HasValue && _takeProfitOffset > 0m && closePrice <= entryPrice.Value - _takeProfitOffset;

			if (exitByMomentum || exitByTake)
			{
				if (Position > 0) SellMarket(); else if (Position < 0) BuyMarket();
				_entryPrice = null;
			}
		}

		_previousClose = closePrice;
	}

	private void UpdatePipSettings()
	{
		var step = Security?.PriceStep ?? 0m;

		if (step <= 0m)
		{
			_pipSize = 1m;
		}
		else
		{
			var decimals = GetDecimalPlaces(step);
			var multiplier = decimals == 3 || decimals == 5 ? 10m : 1m;
			_pipSize = step * multiplier;
		}

		_takeProfitOffset = TakeProfitPips > 0m ? TakeProfitPips * _pipSize : 0m;
	}

	private static int GetDecimalPlaces(decimal value)
	{
		var bits = decimal.GetBits(value);
		return (bits[3] >> 16) & 0xFF;
	}
}