GitHub で見る

XROC2 VG 時間フィルター戦略

この戦略はStockSharpの高レベルAPIを使用してMetaTraderエキスパートアドバイザー Exp_XROC2_VG_Tm を再現します。2本の平滑化された価格変化率(ROC)曲線を構築し、より速い曲線がより遅い曲線を交差するときに逆張りのトレードを行います。取引セッションフィルターとオプションの保護ターゲットにより、元のマネー管理設定を再現します。

トレードロジック

  • 独立したルックバック期間を使用して終値から2つのROC値を計算する。
  • 各ROCストリームは設定可能な移動平均手法で平滑化される。
  • シグナルは元のSignalBar動作に合わせてシフトされたバーインデックスで評価される。
  • 前のバーで速い線が遅い線を上回っていたが、シグナルバーで下回った場合、戦略はショートポジションを閉じてロングポジションを開く可能性がある。
  • 前のバーで速い線が遅い線を下回っていたが、シグナルバーで上回った場合、戦略はロングポジションを閉じてショートポジションを開く可能性がある。
  • オプションの取引ウィンドウは、新しいトレードを開始する前に、許可されたセッション外のすべてのポジションを清算できる。

注文サイドは前のポジションが完全に閉じられた後にのみ切り替わり、MetaTraderのトレードアルゴリズムを模倣します。

インジケーター

  • 高速ROCRocPeriod1バーにわたる価格変化のモメンタム、パーセンテージ、または比率で、SmoothMethod1と長さSmoothLength1で平滑化。
  • 低速ROCRocPeriod2バーにわたる同じ計算で、SmoothMethod2と長さSmoothLength2で平滑化。
  • サポートされている平滑化方法:単純、指数、平滑(RMA)、加重移動平均。元のJJMA/VIDYA/AMAオプションは指数平滑化で近似されます。

リスク管理

  • StopLossTakeProfitは絶対価格単位での固定距離出口をオプションで指定します。いずれかの閾値に達すると、ポジションは即座に閉じられます。
  • OrderVolumeはすべての新規ポジションのサイズを定義します。
  • インジケーターベースの出口は、保護ターゲットが無効な場合でもポジションを清算することがあります。

セッションフィルター

  • UseTimeFilterは時間帯ウィンドウを切り替えます。
  • StartTime / EndTimeはセッションの境界を指定します。間隔が真夜中をまたぐ場合、ウィンドウはMQLバージョンと同様に2つのセグメントとして扱われます。
  • ウィンドウが閉じるときにポジションがまだ開いている場合、戦略が新規エントリーを評価する前に市場価格で清算されます。

パラメーター

パラメーター 説明
CandleType 計算に使用するローソク足データタイプ(デフォルト:4時間足)。
RocPeriod1, RocPeriod2 高速・低速ROCストリームのルックバック長。
SmoothLength1, SmoothLength2 各ストリームの平滑化長。
SmoothMethod1, SmoothMethod2 ROC出力に適用する移動平均タイプ。
RocType ROC計算式:モメンタム、パーセント変化、または比率。
SignalShift シグナル値を読み取るために使用する完成バーの遡り数。
AllowBuyOpen, AllowSellOpen ロング/ショートポジションの開設を有効/無効にする。
AllowBuyClose, AllowSellClose ロング/ショートポジションのインジケーターベース出口を有効/無効にする。
UseTimeFilter 取引セッションウィンドウを有効にする。
StartTime, EndTime セッションの開始・終了時刻。
OrderVolume 各新規トレードのボリューム。
StopLoss, TakeProfit 保護出口のオプション絶対距離。

実装上の注意

  • 戦略はインジケーターバッファを使用する代わりに価格と平滑化値の短い履歴を保持し、GetValueに依存せずに元のSignalBarオフセットを再現します。
  • MQLインジケーターのJJMA、VIDYA、AMA平滑化は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>
/// XROC2 VG with time filter strategy converted from MetaTrader 5.
/// </summary>
public class Xroc2VgTmStrategy : Strategy
{
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<int> _rocPeriod1;
	private readonly StrategyParam<int> _rocPeriod2;
	private readonly StrategyParam<int> _smoothLength1;
	private readonly StrategyParam<int> _smoothLength2;
	private readonly StrategyParam<SmoothingMethods> _smoothMethod1;
	private readonly StrategyParam<SmoothingMethods> _smoothMethod2;
	private readonly StrategyParam<RocCalculationTypes> _rocType;
	private readonly StrategyParam<int> _signalShift;
	private readonly StrategyParam<bool> _allowBuyOpen;
	private readonly StrategyParam<bool> _allowSellOpen;
	private readonly StrategyParam<bool> _allowBuyClose;
	private readonly StrategyParam<bool> _allowSellClose;
	private readonly StrategyParam<bool> _useTimeFilter;
	private readonly StrategyParam<TimeSpan> _startTime;
	private readonly StrategyParam<TimeSpan> _endTime;
	private readonly StrategyParam<decimal> _orderVolume;
	private readonly StrategyParam<decimal> _stopLoss;
	private readonly StrategyParam<decimal> _takeProfit;

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

	private IIndicator _smoothFast;
	private IIndicator _smoothSlow;

	private decimal? _longEntryPrice;
	private decimal? _shortEntryPrice;

	/// <summary>
	/// Rate-of-change calculation mode.
	/// </summary>
	public enum RocCalculationTypes
	{
		/// <summary>Momentum (difference between closes).</summary>
		Momentum,

		/// <summary>Rate of change in percent.</summary>
		RateOfChange,

		/// <summary>Relative rate of change (fraction).</summary>
		Percent,

		/// <summary>Price ratio.</summary>
		Ratio,

		/// <summary>Price ratio scaled by 100.</summary>
		RatioPercent
	}

	/// <summary>
	/// Smoothing method used for ROC lines.
	/// </summary>
	public enum SmoothingMethods
	{
		/// <summary>Simple moving average.</summary>
		Simple,

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

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

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

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

		_rocPeriod1 = Param(nameof(RocPeriod1), 5)
			.SetGreaterThanZero()
			.SetDisplay("Fast ROC Period", "Lookback for the first ROC line", "Indicator")
			;

		_rocPeriod2 = Param(nameof(RocPeriod2), 10)
			.SetGreaterThanZero()
			.SetDisplay("Slow ROC Period", "Lookback for the second ROC line", "Indicator")
			;

		_smoothLength1 = Param(nameof(SmoothLength1), 5)
			.SetGreaterThanZero()
			.SetDisplay("Fast Smoothing", "Smoothing length for the first line", "Indicator");

		_smoothLength2 = Param(nameof(SmoothLength2), 5)
			.SetGreaterThanZero()
			.SetDisplay("Slow Smoothing", "Smoothing length for the second line", "Indicator");

		_smoothMethod1 = Param(nameof(SmoothMethod1), SmoothingMethods.Exponential)
			.SetDisplay("Fast Method", "Smoothing method for the first line", "Indicator");

		_smoothMethod2 = Param(nameof(SmoothMethod2), SmoothingMethods.Exponential)
			.SetDisplay("Slow Method", "Smoothing method for the second line", "Indicator");

		_rocType = Param(nameof(RocType), RocCalculationTypes.Momentum)
			.SetDisplay("ROC Mode", "Calculation used for rate of change", "Indicator");

		_signalShift = Param(nameof(SignalShift), 0)
			.SetNotNegative()
			.SetDisplay("Signal Shift", "Bars back to read the signals", "Logic");

		_allowBuyOpen = Param(nameof(AllowBuyOpen), true)
			.SetDisplay("Allow Long Entry", "Enable opening long positions", "Trading");

		_allowSellOpen = Param(nameof(AllowSellOpen), true)
			.SetDisplay("Allow Short Entry", "Enable opening short positions", "Trading");

		_allowBuyClose = Param(nameof(AllowBuyClose), true)
			.SetDisplay("Allow Long Exit", "Enable closing long positions by indicator", "Trading");

		_allowSellClose = Param(nameof(AllowSellClose), true)
			.SetDisplay("Allow Short Exit", "Enable closing short positions by indicator", "Trading");

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

		_startTime = Param(nameof(StartTime), TimeSpan.Zero)
			.SetDisplay("Start Time", "Session start time", "Timing");

		_endTime = Param(nameof(EndTime), new TimeSpan(23, 59, 0))
			.SetDisplay("End Time", "Session end time", "Timing");

		_orderVolume = Param(nameof(OrderVolume), 1m)
			.SetGreaterThanZero()
			.SetDisplay("Order Volume", "Volume for new positions", "Trading");

		_stopLoss = Param(nameof(StopLoss), 0m)
			.SetNotNegative()
			.SetDisplay("Stop Loss", "Protective stop distance in price units", "Risk");

		_takeProfit = Param(nameof(TakeProfit), 0m)
			.SetNotNegative()
			.SetDisplay("Take Profit", "Target distance in price units", "Risk");
	}

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

	/// <summary>
	/// Lookback of the first ROC line.
	/// </summary>
	public int RocPeriod1
	{
		get => _rocPeriod1.Value;
		set => _rocPeriod1.Value = value;
	}

	/// <summary>
	/// Lookback of the second ROC line.
	/// </summary>
	public int RocPeriod2
	{
		get => _rocPeriod2.Value;
		set => _rocPeriod2.Value = value;
	}

	/// <summary>
	/// Smoothing length applied to the first line.
	/// </summary>
	public int SmoothLength1
	{
		get => _smoothLength1.Value;
		set => _smoothLength1.Value = value;
	}

	/// <summary>
	/// Smoothing length applied to the second line.
	/// </summary>
	public int SmoothLength2
	{
		get => _smoothLength2.Value;
		set => _smoothLength2.Value = value;
	}

	/// <summary>
	/// Smoothing method for the first line.
	/// </summary>
	public SmoothingMethods SmoothMethod1
	{
		get => _smoothMethod1.Value;
		set => _smoothMethod1.Value = value;
	}

	/// <summary>
	/// Smoothing method for the second line.
	/// </summary>
	public SmoothingMethods SmoothMethod2
	{
		get => _smoothMethod2.Value;
		set => _smoothMethod2.Value = value;
	}

	/// <summary>
	/// Type of ROC calculation.
	/// </summary>
	public RocCalculationTypes RocType
	{
		get => _rocType.Value;
		set => _rocType.Value = value;
	}

	/// <summary>
	/// Number of bars back used for signal evaluation.
	/// </summary>
	public int SignalShift
	{
		get => _signalShift.Value;
		set => _signalShift.Value = value;
	}

	/// <summary>
	/// Enables long entries.
	/// </summary>
	public bool AllowBuyOpen
	{
		get => _allowBuyOpen.Value;
		set => _allowBuyOpen.Value = value;
	}

	/// <summary>
	/// Enables short entries.
	/// </summary>
	public bool AllowSellOpen
	{
		get => _allowSellOpen.Value;
		set => _allowSellOpen.Value = value;
	}

	/// <summary>
	/// Enables closing long positions by indicator signals.
	/// </summary>
	public bool AllowBuyClose
	{
		get => _allowBuyClose.Value;
		set => _allowBuyClose.Value = value;
	}

	/// <summary>
	/// Enables closing short positions by indicator signals.
	/// </summary>
	public bool AllowSellClose
	{
		get => _allowSellClose.Value;
		set => _allowSellClose.Value = value;
	}

	/// <summary>
	/// Turns the time filter on or off.
	/// </summary>
	public bool UseTimeFilter
	{
		get => _useTimeFilter.Value;
		set => _useTimeFilter.Value = value;
	}

	/// <summary>
	/// Trading session start time.
	/// </summary>
	public TimeSpan StartTime
	{
		get => _startTime.Value;
		set => _startTime.Value = value;
	}

	/// <summary>
	/// Trading session end time.
	/// </summary>
	public TimeSpan EndTime
	{
		get => _endTime.Value;
		set => _endTime.Value = value;
	}

	/// <summary>
	/// Order volume used for new positions.
	/// </summary>
	public decimal OrderVolume
	{
		get => _orderVolume.Value;
		set => _orderVolume.Value = value;
	}

	/// <summary>
	/// Protective stop distance in price units.
	/// </summary>
	public decimal StopLoss
	{
		get => _stopLoss.Value;
		set => _stopLoss.Value = value;
	}

	/// <summary>
	/// Take-profit distance in price units.
	/// </summary>
	public decimal TakeProfit
	{
		get => _takeProfit.Value;
		set => _takeProfit.Value = value;
	}

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

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

		_closeHistory.Clear();
		_fastHistory.Clear();
		_slowHistory.Clear();

		_smoothFast = null;
		_smoothSlow = null;

		_longEntryPrice = null;
		_shortEntryPrice = null;
	}

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

		_smoothFast = CreateSmoothingIndicator(SmoothMethod1, SmoothLength1);
		_smoothSlow = CreateSmoothingIndicator(SmoothMethod2, SmoothLength2);

		_closeHistory.Clear();
		_fastHistory.Clear();
		_slowHistory.Clear();

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

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

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

		var capacity = Math.Max(Math.Max(RocPeriod1, RocPeriod2) + SignalShift + 5, 8);
		UpdateHistory(_closeHistory, candle.ClosePrice, capacity);

		var fastRoc = CalculateRoc(RocPeriod1);
		var slowRoc = CalculateRoc(RocPeriod2);

		if (fastRoc is null || slowRoc is null)
			return;

		var fastValue = _smoothFast.Process(new DecimalIndicatorValue(_smoothFast, fastRoc.Value, candle.OpenTime) { IsFinal = true });
		var slowValue = _smoothSlow.Process(new DecimalIndicatorValue(_smoothSlow, slowRoc.Value, candle.OpenTime) { IsFinal = true });

		// Skip until we have enough data for both smoothing indicators

		var fastDecimal = fastValue.GetValue<decimal>();
		var slowDecimal = slowValue.GetValue<decimal>();

		var historyCapacity = SignalShift + 3;
		UpdateHistory(_fastHistory, fastDecimal, historyCapacity);
		UpdateHistory(_slowHistory, slowDecimal, historyCapacity);

		if (_fastHistory.Count <= SignalShift + 1 || _slowHistory.Count <= SignalShift + 1)
			return;

		var fastCurrent = _fastHistory[SignalShift];
		var fastPrevious = _fastHistory[SignalShift + 1];
		var slowCurrent = _slowHistory[SignalShift];
		var slowPrevious = _slowHistory[SignalShift + 1];

		var buyOpenSignal = AllowBuyOpen && fastPrevious <= slowPrevious && fastCurrent > slowCurrent;
		var sellOpenSignal = AllowSellOpen && fastPrevious >= slowPrevious && fastCurrent < slowCurrent;
		var buyCloseSignal = AllowBuyClose && fastCurrent < slowCurrent;
		var sellCloseSignal = AllowSellClose && fastCurrent > slowCurrent;

		var tradeAllowed = !UseTimeFilter || IsWithinTradeWindow(candle.OpenTime);

		if (UseTimeFilter && !tradeAllowed && Position != 0)
		{
			if (Position > 0)
				SellMarket();
			else
				BuyMarket();
			ResetPositionState();
			return;
		}

		if (TryApplyRiskManagement(candle))
			return;

		if (sellCloseSignal && Position < 0)
		{
			BuyMarket();
			ResetPositionState();
			return;
		}

		if (buyCloseSignal && Position > 0)
		{
			SellMarket();
			ResetPositionState();
			return;
		}

		if (!tradeAllowed)
			return;

		//if (!IsFormedAndOnlineAndAllowTrading())
		//	return;

		if (Position != 0)
			return;

		if (buyOpenSignal)
		{
			BuyMarket();
			_longEntryPrice = candle.ClosePrice;
			_shortEntryPrice = null;
		}
		else if (sellOpenSignal)
		{
			SellMarket();
			_shortEntryPrice = candle.ClosePrice;
			_longEntryPrice = null;
		}
	}

	private bool TryApplyRiskManagement(ICandleMessage candle)
	{
		if (StopLoss <= 0m && TakeProfit <= 0m)
			return false;

		if (Position > 0 && _longEntryPrice is decimal longEntry)
		{
			if (StopLoss > 0m)
			{
				var stopLevel = longEntry - StopLoss;
				if (candle.LowPrice <= stopLevel)
				{
					SellMarket();
					ResetPositionState();
					return true;
				}
			}

			if (TakeProfit > 0m)
			{
				var targetLevel = longEntry + TakeProfit;
				if (candle.HighPrice >= targetLevel)
				{
					SellMarket();
					ResetPositionState();
					return true;
				}
			}
		}
		else if (Position < 0 && _shortEntryPrice is decimal shortEntry)
		{
			if (StopLoss > 0m)
			{
				var stopLevel = shortEntry + StopLoss;
				if (candle.HighPrice >= stopLevel)
				{
					BuyMarket();
					ResetPositionState();
					return true;
				}
			}

			if (TakeProfit > 0m)
			{
				var targetLevel = shortEntry - TakeProfit;
				if (candle.LowPrice <= targetLevel)
				{
					BuyMarket();
					ResetPositionState();
					return true;
				}
			}
		}

		return false;
	}

	private decimal? CalculateRoc(int period)
	{
		if (period <= 0 || _closeHistory.Count <= period)
			return null;

		var current = _closeHistory[0];
		var previous = _closeHistory[period];

		if (previous == 0m && (RocType == RocCalculationTypes.RateOfChange || RocType == RocCalculationTypes.Percent || RocType == RocCalculationTypes.Ratio || RocType == RocCalculationTypes.RatioPercent))
			return null;

		return RocType switch
		{
			RocCalculationTypes.Momentum => current - previous,
			RocCalculationTypes.RateOfChange => previous == 0m ? null : (decimal?)((current / previous) - 1m) * 100m,
			RocCalculationTypes.Percent => previous == 0m ? null : (decimal?)((current - previous) / previous),
			RocCalculationTypes.Ratio => previous == 0m ? null : (decimal?)(current / previous),
			RocCalculationTypes.RatioPercent => previous == 0m ? null : (decimal?)(current / previous * 100m),
			_ => current - previous
		};
	}

	private bool IsWithinTradeWindow(DateTimeOffset time)
	{
		var currentMinutes = time.TimeOfDay.TotalMinutes;
		var startMinutes = StartTime.TotalMinutes;
		var endMinutes = EndTime.TotalMinutes;

		if (startMinutes < endMinutes)
			return currentMinutes >= startMinutes && currentMinutes < endMinutes;

		if (startMinutes > endMinutes)
			return currentMinutes >= startMinutes || currentMinutes < endMinutes;

		return false;
	}

	private static void UpdateHistory(List<decimal> history, decimal value, int capacity)
	{
		history.Insert(0, value);
		if (history.Count > capacity)
			history.RemoveAt(history.Count - 1);
	}

	private void ResetPositionState()
	{
		_longEntryPrice = null;
		_shortEntryPrice = null;
	}

	private static IIndicator CreateSmoothingIndicator(SmoothingMethods method, int length)
	{
		IIndicator indicator = method switch
		{
			SmoothingMethods.Simple => new SMA { Length = length },
			SmoothingMethods.Smoothed => new EMA { Length = length },
			SmoothingMethods.Weighted => new SMA { Length = length },
			_ => new EMA { Length = length }
		};

		return indicator;
	}
}