Ver en GitHub

XROC2 VG Time Filter Strategy

This strategy replicates the MetaTrader expert advisor Exp_XROC2_VG_Tm using the StockSharp high-level API. It builds two smoothed rate-of-change (ROC) curves and opens contrarian trades when the faster curve crosses the slower one. A trading session filter and optional protective targets reproduce the original money-management settings.

Trading logic

  • Two ROC values are calculated from the closing price using independent lookbacks.
  • Each ROC stream is smoothed with a configurable moving-average method.
  • Signals are evaluated on a shifted bar index, matching the original SignalBar behavior.
  • When the fast line was above the slow line on the prior bar but falls below it on the signal bar, the strategy closes any short position and may open a long position.
  • When the fast line was below the slow line on the prior bar but rises above it on the signal bar, the strategy closes any long position and may open a short position.
  • An optional trading window can flat all positions outside the allowed session before any new trades are placed.

The order side switches only after the previous position is fully closed, mimicking the MetaTrader trade algorithms.

Indicators

  • Fast ROC – Momentum, percent or ratio of price change over RocPeriod1 bars, smoothed with SmoothMethod1 and length SmoothLength1.
  • Slow ROC – Same calculation over RocPeriod2 bars, smoothed with SmoothMethod2 and length SmoothLength2.
  • Supported smoothing methods: Simple, Exponential, Smoothed (RMA) and Weighted moving averages. The original JJMA/VIDYA/AMA options are approximated by exponential smoothing.

Risk management

  • StopLoss and TakeProfit specify optional fixed-distance exits in absolute price units. When either threshold is hit the position is closed immediately.
  • OrderVolume defines the size of all new positions.
  • Indicator-based exits may also flatten positions even if the protective targets are disabled.

Session filter

  • UseTimeFilter toggles the time-of-day window.
  • StartTime / EndTime specify the session bounds. When the interval wraps around midnight the window is treated as two segments, exactly as in the MQL version.
  • If a position is still open when the window closes it is liquidated at market before the strategy evaluates fresh entries.

Parameters

Parameter Description
CandleType Candle data type used for calculations (default: 4-hour candles).
RocPeriod1, RocPeriod2 Lookback lengths for the fast and slow ROC streams.
SmoothLength1, SmoothLength2 Smoothing lengths for each stream.
SmoothMethod1, SmoothMethod2 Moving-average types applied to the ROC outputs.
RocType ROC calculation formula: momentum, percent change or ratio.
SignalShift Number of completed bars back used to read signal values.
AllowBuyOpen, AllowSellOpen Enable or disable opening long/short positions.
AllowBuyClose, AllowSellClose Enable or disable indicator-based exits for long/short positions.
UseTimeFilter Activates the trading session window.
StartTime, EndTime Session start and end times.
OrderVolume Volume for each new trade.
StopLoss, TakeProfit Optional absolute distances for protective exits.

Implementation notes

  • The strategy keeps short histories of prices and smoothed values instead of using indicator buffers, which reproduces the original SignalBar offset without relying on GetValue.
  • JJMA, VIDYA and AMA smoothing from the MQL indicator are mapped to exponential smoothing to stay within the StockSharp standard indicator set.
  • All comments in the code are in English and the namespace follows the repository guidelines.
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;
	}
}