Ver no GitHub

XROC2 VG X2 Strategy

Overview

The XROC2 VG X2 strategy is a multi-timeframe system that combines two smoothed rate-of-change streams. The higher timeframe acts as a directional filter while the lower timeframe produces concrete entry and exit signals. The original MetaTrader 5 expert advisor relied on the custom XROC2_VG indicator with flexible smoothing options and a money management module. The StockSharp port keeps the signal logic intact and exposes the key parameters as strategy inputs.

The strategy subscribes to two candle series:

  • Higher timeframe (default 6 hours) – establishes the prevailing trend direction.
  • Lower timeframe (default 30 minutes) – generates entries and exits by monitoring how the two smoothed ROC lines cross.

Both streams share the same rate-of-change calculation mode but use individual smoothing settings. By default the strategy applies Jurik moving averages, mimicking the MQL version. Advanced smoothing types that are not directly supported by StockSharp (JurX, ParMA, T3, VIDYA, AMA with phase control) fall back to the closest available moving average implementation.

Trading Logic

  1. Trend detection (higher timeframe)
    • Compute two smoothed ROC values using the configured periods and smoothing methods.
    • Evaluate the line pair on the bar defined by HigherSignalBar. If the fast line is above the slow line the trend is bullish, otherwise bearish. A neutral reading keeps the current trend at zero and disables trading.
  2. Signal generation (lower timeframe)
    • Compute the same pair of smoothed ROC values on the lower timeframe.
    • Look at the most recent finished bar (shift LowerSignalBar) and the bar before it. The combination of these two bars determines whether a cross just happened.
    • A long setup appears when the higher timeframe is bullish, the fast line crossed below the slow line (downward cross), and longs are enabled.
    • A short setup appears when the higher timeframe is bearish, the fast line crossed above the slow line (upward cross), and shorts are enabled.
  3. Position management
    • Close long positions when the lower timeframe cross indicates bearishness (CloseBuyOnLower) or when the higher timeframe trend flips to bearish (CloseBuyOnTrendFlip).
    • Close short positions when the lower timeframe cross becomes bullish (CloseSellOnLower) or when the higher timeframe trend flips to bullish (CloseSellOnTrendFlip).
    • New trades are opened only when no position is active. Order size is controlled by the strategy Volume property.

Parameters

  • HigherCandleType – candle type for the trend filter (default 6-hour time frame).
  • LowerCandleType – candle type for signal generation (default 30-minute time frame).
  • HigherSignalBar – how many closed bars to shift when reading higher timeframe values (default 1).
  • LowerSignalBar – how many closed bars to shift when reading lower timeframe values (default 1).
  • HigherRocMode / LowerRocMode – rate-of-change calculation variant (Momentum, RateOfChange, RateOfChangePercent, RateOfChangeRatio, RateOfChangeRatioPercent).
  • HigherFastPeriod, HigherFastMethod, HigherFastLength, HigherFastPhase – fast ROC settings for the higher timeframe.
  • HigherSlowPeriod, HigherSlowMethod, HigherSlowLength, HigherSlowPhase – slow ROC settings for the higher timeframe.
  • LowerFastPeriod, LowerFastMethod, LowerFastLength, LowerFastPhase – fast ROC settings for the lower timeframe.
  • LowerSlowPeriod, LowerSlowMethod, LowerSlowLength, LowerSlowPhase – slow ROC settings for the lower timeframe.
  • AllowBuyOpen, AllowSellOpen – enable or disable opening longs and shorts.
  • CloseBuyOnTrendFlip, CloseSellOnTrendFlip – force exits when the higher timeframe changes direction.
  • CloseBuyOnLower, CloseSellOnLower – exit when the lower timeframe cross goes against the position.

Implementation Notes

  • The original MQL strategy used a large smoothing library. The StockSharp version maps the supported options to built-in indicators (SMA, EMA, SMMA/RMA, LWMA, Jurik, Kaufman AMA). Unsupported modes (JurX, ParMA, T3, VIDYA) are approximated with the nearest available moving average, so behavior may differ for those combinations.
  • Money-management functions, stop-loss, take-profit, and slippage settings from TradeAlgorithms.mqh are not reproduced. Instead, the strategy trades with the fixed Volume specified in the strategy settings.
  • Orders are executed with market orders. Protective logic such as stop-losses or trailing stops can be added via StockSharp protection modules if needed.
  • The strategy only trades when both candle subscriptions are fully formed and IsFormedAndOnlineAndAllowTrading() returns true.

Usage Tips

  • Choose candle types that correspond to the original trading style (e.g., 6h/30m for swing trading). Other combinations are possible.
  • Tune the ROC periods and smoothing methods to match the preferred responsiveness. Jurik smoothing keeps the behaviour closest to the source script.
  • Consider adding explicit risk management (stop-loss, position sizing) when running on live accounts, since the port uses simple market exits.
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>
/// Multi-timeframe XROC2 VG strategy that combines two smoothed rate-of-change streams.
/// The higher timeframe defines the directional bias while the lower timeframe handles entries and exits.
/// </summary>
public class Xroc2VgX2Strategy : Strategy
{
	/// <summary>
	/// Available rate-of-change calculation modes.
	/// </summary>
	public enum RocModes
	{
		Momentum,
		RateOfChange,
		RateOfChangePercent,
		RateOfChangeRatio,
		RateOfChangeRatioPercent,
	}

	/// <summary>
	/// Smoothing methods supported by the strategy.
	/// </summary>
	public enum SmoothingMethods
	{
		Sma,
		Ema,
		Smma,
		Lwma,
		Jurik,
		Jurx,
		Parma,
		T3,
		Vidya,
		Ama,
	}

	private readonly StrategyParam<DataType> _higherCandleType;
	private readonly StrategyParam<DataType> _lowerCandleType;
	private readonly StrategyParam<int> _higherSignalBar;
	private readonly StrategyParam<int> _lowerSignalBar;
	private readonly StrategyParam<RocModes> _higherRocMode;
	private readonly StrategyParam<int> _higherFastPeriod;
	private readonly StrategyParam<SmoothingMethods> _higherFastMethod;
	private readonly StrategyParam<int> _higherFastLength;
	private readonly StrategyParam<int> _higherFastPhase;
	private readonly StrategyParam<int> _higherSlowPeriod;
	private readonly StrategyParam<SmoothingMethods> _higherSlowMethod;
	private readonly StrategyParam<int> _higherSlowLength;
	private readonly StrategyParam<int> _higherSlowPhase;
	private readonly StrategyParam<RocModes> _lowerRocMode;
	private readonly StrategyParam<int> _lowerFastPeriod;
	private readonly StrategyParam<SmoothingMethods> _lowerFastMethod;
	private readonly StrategyParam<int> _lowerFastLength;
	private readonly StrategyParam<int> _lowerFastPhase;
	private readonly StrategyParam<int> _lowerSlowPeriod;
	private readonly StrategyParam<SmoothingMethods> _lowerSlowMethod;
	private readonly StrategyParam<int> _lowerSlowLength;
	private readonly StrategyParam<int> _lowerSlowPhase;
	private readonly StrategyParam<bool> _allowBuyOpen;
	private readonly StrategyParam<bool> _allowSellOpen;
	private readonly StrategyParam<bool> _closeBuyOnTrendFlip;
	private readonly StrategyParam<bool> _closeSellOnTrendFlip;
	private readonly StrategyParam<bool> _closeBuyOnLower;
	private readonly StrategyParam<bool> _closeSellOnLower;

	private Xroc2VgSeries _higherSeries = default!;
	private Xroc2VgSeries _lowerSeries = default!;
	private int _trend;

	/// <summary>
	/// Initializes a new instance of the <see cref="Xroc2VgX2Strategy"/> class.
	/// </summary>
	public Xroc2VgX2Strategy()
	{
		_higherCandleType = Param(nameof(HigherCandleType), TimeSpan.FromHours(4).TimeFrame())
			.SetDisplay("Higher TF", "Higher timeframe candles", "General");

		_lowerCandleType = Param(nameof(LowerCandleType), TimeSpan.FromHours(1).TimeFrame())
			.SetDisplay("Lower TF", "Lower timeframe candles", "General");

		_higherSignalBar = Param(nameof(HigherSignalBar), 1)
			.SetGreaterThanZero()
			.SetDisplay("Higher Signal Bar", "Shift used for trend evaluation", "General");

		_lowerSignalBar = Param(nameof(LowerSignalBar), 1)
			.SetGreaterThanZero()
			.SetDisplay("Lower Signal Bar", "Shift used for lower timeframe signals", "General");

		_higherRocMode = Param(nameof(HigherRocMode), RocModes.Momentum)
			.SetDisplay("Higher ROC Mode", "ROC calculation mode for the bias", "Higher Timeframe");

		_higherFastPeriod = Param(nameof(HigherFastPeriod), 8)
			.SetGreaterThanZero()
			.SetDisplay("Higher Fast ROC", "Fast ROC period for bias", "Higher Timeframe");

		_higherFastMethod = Param(nameof(HigherFastMethod), SmoothingMethods.Jurik)
			.SetDisplay("Higher Fast Method", "Smoother for fast ROC", "Higher Timeframe");

		_higherFastLength = Param(nameof(HigherFastLength), 5)
			.SetGreaterThanZero()
			.SetDisplay("Higher Fast Length", "Length of fast smoother", "Higher Timeframe");

		_higherFastPhase = Param(nameof(HigherFastPhase), 15)
			.SetDisplay("Higher Fast Phase", "Phase parameter for fast smoother", "Higher Timeframe");

		_higherSlowPeriod = Param(nameof(HigherSlowPeriod), 14)
			.SetGreaterThanZero()
			.SetDisplay("Higher Slow ROC", "Slow ROC period for bias", "Higher Timeframe");

		_higherSlowMethod = Param(nameof(HigherSlowMethod), SmoothingMethods.Jurik)
			.SetDisplay("Higher Slow Method", "Smoother for slow ROC", "Higher Timeframe");

		_higherSlowLength = Param(nameof(HigherSlowLength), 5)
			.SetGreaterThanZero()
			.SetDisplay("Higher Slow Length", "Length of slow smoother", "Higher Timeframe");

		_higherSlowPhase = Param(nameof(HigherSlowPhase), 15)
			.SetDisplay("Higher Slow Phase", "Phase parameter for slow smoother", "Higher Timeframe");

		_lowerRocMode = Param(nameof(LowerRocMode), RocModes.Momentum)
			.SetDisplay("Lower ROC Mode", "ROC calculation mode for entries", "Lower Timeframe");

		_lowerFastPeriod = Param(nameof(LowerFastPeriod), 12)
			.SetGreaterThanZero()
			.SetDisplay("Lower Fast ROC", "Fast ROC period for entries", "Lower Timeframe");

		_lowerFastMethod = Param(nameof(LowerFastMethod), SmoothingMethods.Jurik)
			.SetDisplay("Lower Fast Method", "Smoother for fast ROC", "Lower Timeframe");

		_lowerFastLength = Param(nameof(LowerFastLength), 10)
			.SetGreaterThanZero()
			.SetDisplay("Lower Fast Length", "Length of fast smoother", "Lower Timeframe");

		_lowerFastPhase = Param(nameof(LowerFastPhase), 15)
			.SetDisplay("Lower Fast Phase", "Phase parameter for fast smoother", "Lower Timeframe");

		_lowerSlowPeriod = Param(nameof(LowerSlowPeriod), 26)
			.SetGreaterThanZero()
			.SetDisplay("Lower Slow ROC", "Slow ROC period for entries", "Lower Timeframe");

		_lowerSlowMethod = Param(nameof(LowerSlowMethod), SmoothingMethods.Jurik)
			.SetDisplay("Lower Slow Method", "Smoother for slow ROC", "Lower Timeframe");

		_lowerSlowLength = Param(nameof(LowerSlowLength), 20)
			.SetGreaterThanZero()
			.SetDisplay("Lower Slow Length", "Length of slow smoother", "Lower Timeframe");

		_lowerSlowPhase = Param(nameof(LowerSlowPhase), 15)
			.SetDisplay("Lower Slow Phase", "Phase parameter for slow smoother", "Lower Timeframe");

		_allowBuyOpen = Param(nameof(AllowBuyOpen), true)
			.SetDisplay("Allow Long Entries", "Enable long entries", "Signals");

		_allowSellOpen = Param(nameof(AllowSellOpen), true)
			.SetDisplay("Allow Short Entries", "Enable short entries", "Signals");

		_closeBuyOnTrendFlip = Param(nameof(CloseBuyOnTrendFlip), true)
			.SetDisplay("Close Long On Trend", "Close longs when higher trend turns bearish", "Signals");

		_closeSellOnTrendFlip = Param(nameof(CloseSellOnTrendFlip), true)
			.SetDisplay("Close Short On Trend", "Close shorts when higher trend turns bullish", "Signals");

		_closeBuyOnLower = Param(nameof(CloseBuyOnLower), true)
			.SetDisplay("Close Long On Lower", "Close longs when lower ROC crosses down", "Signals");

		_closeSellOnLower = Param(nameof(CloseSellOnLower), true)
			.SetDisplay("Close Short On Lower", "Close shorts when lower ROC crosses up", "Signals");
	}

	/// <summary>
	/// Higher timeframe candle type.
	/// </summary>
	public DataType HigherCandleType
	{
		get => _higherCandleType.Value;
		set => _higherCandleType.Value = value;
	}

	/// <summary>
	/// Lower timeframe candle type.
	/// </summary>
	public DataType LowerCandleType
	{
		get => _lowerCandleType.Value;
		set => _lowerCandleType.Value = value;
	}

	/// <summary>
	/// Number of bars to shift when reading higher timeframe values.
	/// </summary>
	public int HigherSignalBar
	{
		get => _higherSignalBar.Value;
		set => _higherSignalBar.Value = value;
	}

	/// <summary>
	/// Number of bars to shift when reading lower timeframe values.
	/// </summary>
	public int LowerSignalBar
	{
		get => _lowerSignalBar.Value;
		set => _lowerSignalBar.Value = value;
	}

	/// <summary>
	/// Rate-of-change mode for the higher timeframe stream.
	/// </summary>
	public RocModes HigherRocMode
	{
		get => _higherRocMode.Value;
		set => _higherRocMode.Value = value;
	}

	/// <summary>
	/// Fast ROC period for the higher timeframe.
	/// </summary>
	public int HigherFastPeriod
	{
		get => _higherFastPeriod.Value;
		set => _higherFastPeriod.Value = value;
	}

	/// <summary>
	/// Smoothing method for the higher timeframe fast line.
	/// </summary>
	public SmoothingMethods HigherFastMethod
	{
		get => _higherFastMethod.Value;
		set => _higherFastMethod.Value = value;
	}

	/// <summary>
	/// Smoothing length for the higher timeframe fast line.
	/// </summary>
	public int HigherFastLength
	{
		get => _higherFastLength.Value;
		set => _higherFastLength.Value = value;
	}

	/// <summary>
	/// Phase parameter for the higher timeframe fast smoother.
	/// </summary>
	public int HigherFastPhase
	{
		get => _higherFastPhase.Value;
		set => _higherFastPhase.Value = value;
	}

	/// <summary>
	/// Slow ROC period for the higher timeframe.
	/// </summary>
	public int HigherSlowPeriod
	{
		get => _higherSlowPeriod.Value;
		set => _higherSlowPeriod.Value = value;
	}

	/// <summary>
	/// Smoothing method for the higher timeframe slow line.
	/// </summary>
	public SmoothingMethods HigherSlowMethod
	{
		get => _higherSlowMethod.Value;
		set => _higherSlowMethod.Value = value;
	}

	/// <summary>
	/// Smoothing length for the higher timeframe slow line.
	/// </summary>
	public int HigherSlowLength
	{
		get => _higherSlowLength.Value;
		set => _higherSlowLength.Value = value;
	}

	/// <summary>
	/// Phase parameter for the higher timeframe slow smoother.
	/// </summary>
	public int HigherSlowPhase
	{
		get => _higherSlowPhase.Value;
		set => _higherSlowPhase.Value = value;
	}

	/// <summary>
	/// Rate-of-change mode for the lower timeframe stream.
	/// </summary>
	public RocModes LowerRocMode
	{
		get => _lowerRocMode.Value;
		set => _lowerRocMode.Value = value;
	}

	/// <summary>
	/// Fast ROC period for the lower timeframe.
	/// </summary>
	public int LowerFastPeriod
	{
		get => _lowerFastPeriod.Value;
		set => _lowerFastPeriod.Value = value;
	}

	/// <summary>
	/// Smoothing method for the lower timeframe fast line.
	/// </summary>
	public SmoothingMethods LowerFastMethod
	{
		get => _lowerFastMethod.Value;
		set => _lowerFastMethod.Value = value;
	}

	/// <summary>
	/// Smoothing length for the lower timeframe fast line.
	/// </summary>
	public int LowerFastLength
	{
		get => _lowerFastLength.Value;
		set => _lowerFastLength.Value = value;
	}

	/// <summary>
	/// Phase parameter for the lower timeframe fast smoother.
	/// </summary>
	public int LowerFastPhase
	{
		get => _lowerFastPhase.Value;
		set => _lowerFastPhase.Value = value;
	}

	/// <summary>
	/// Slow ROC period for the lower timeframe.
	/// </summary>
	public int LowerSlowPeriod
	{
		get => _lowerSlowPeriod.Value;
		set => _lowerSlowPeriod.Value = value;
	}

	/// <summary>
	/// Smoothing method for the lower timeframe slow line.
	/// </summary>
	public SmoothingMethods LowerSlowMethod
	{
		get => _lowerSlowMethod.Value;
		set => _lowerSlowMethod.Value = value;
	}

	/// <summary>
	/// Smoothing length for the lower timeframe slow line.
	/// </summary>
	public int LowerSlowLength
	{
		get => _lowerSlowLength.Value;
		set => _lowerSlowLength.Value = value;
	}

	/// <summary>
	/// Phase parameter for the lower timeframe slow smoother.
	/// </summary>
	public int LowerSlowPhase
	{
		get => _lowerSlowPhase.Value;
		set => _lowerSlowPhase.Value = value;
	}

	/// <summary>
	/// Allow long entries when signals align.
	/// </summary>
	public bool AllowBuyOpen
	{
		get => _allowBuyOpen.Value;
		set => _allowBuyOpen.Value = value;
	}

	/// <summary>
	/// Allow short entries when signals align.
	/// </summary>
	public bool AllowSellOpen
	{
		get => _allowSellOpen.Value;
		set => _allowSellOpen.Value = value;
	}

	/// <summary>
	/// Close long positions when the higher timeframe turns bearish.
	/// </summary>
	public bool CloseBuyOnTrendFlip
	{
		get => _closeBuyOnTrendFlip.Value;
		set => _closeBuyOnTrendFlip.Value = value;
	}

	/// <summary>
	/// Close short positions when the higher timeframe turns bullish.
	/// </summary>
	public bool CloseSellOnTrendFlip
	{
		get => _closeSellOnTrendFlip.Value;
		set => _closeSellOnTrendFlip.Value = value;
	}

	/// <summary>
	/// Close long positions when the lower timeframe shows a bearish cross.
	/// </summary>
	public bool CloseBuyOnLower
	{
		get => _closeBuyOnLower.Value;
		set => _closeBuyOnLower.Value = value;
	}

	/// <summary>
	/// Close short positions when the lower timeframe shows a bullish cross.
	/// </summary>
	public bool CloseSellOnLower
	{
		get => _closeSellOnLower.Value;
		set => _closeSellOnLower.Value = value;
	}

	/// <inheritdoc />
	public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
	{
		yield return (Security, HigherCandleType);
		yield return (Security, LowerCandleType);
	}

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

		_higherSeries = null!;
		_lowerSeries = null!;
		_trend = 0;
	}

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

		_higherSeries = new Xroc2VgSeries(
			HigherRocMode,
			HigherFastPeriod,
			HigherFastMethod,
			HigherFastLength,
			HigherFastPhase,
			HigherSlowPeriod,
			HigherSlowMethod,
			HigherSlowLength,
			HigherSlowPhase);

		_lowerSeries = new Xroc2VgSeries(
			LowerRocMode,
			LowerFastPeriod,
			LowerFastMethod,
			LowerFastLength,
			LowerFastPhase,
			LowerSlowPeriod,
			LowerSlowMethod,
			LowerSlowLength,
			LowerSlowPhase);

		_trend = 0;

		var higherSubscription = SubscribeCandles(HigherCandleType);
		higherSubscription.Bind(ProcessHigherCandle).Start();

		var lowerSubscription = SubscribeCandles(LowerCandleType);
		lowerSubscription.Bind(ProcessLowerCandle).Start();

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

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

		if (!_higherSeries.Process(candle))
			return;

		if (_higherSeries.TryGetValue(HigherSignalBar, out var value))
			_trend = value.up > value.down ? 1 : value.up < value.down ? -1 : 0;
	}

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

		if (!_lowerSeries.Process(candle))
			return;

		if (!_lowerSeries.TryGetPair(LowerSignalBar, out var current, out var previous))
			return;

		if (_trend == 0)
			return;

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

		var buyClose = CloseBuyOnLower && current.up < current.down && previous.up >= previous.down;
		var sellClose = CloseSellOnLower && current.up > current.down && previous.up <= previous.down;

		if (_trend < 0 && CloseBuyOnTrendFlip)
			buyClose = true;

		if (_trend > 0 && CloseSellOnTrendFlip)
			sellClose = true;

		var buyOpen = _trend > 0 && AllowBuyOpen && current.up > current.down && previous.up <= previous.down;
		var sellOpen = _trend < 0 && AllowSellOpen && current.up < current.down && previous.up >= previous.down;

		ExecuteSignals(buyOpen, sellOpen, buyClose, sellClose);
	}

	private void ExecuteSignals(bool buyOpen, bool sellOpen, bool buyClose, bool sellClose)
	{
		var position = Position;

		if (buyClose && position > 0m)
		{
			var volume = position.Abs();
			if (volume > 0m)
				SellMarket();

			position = Position;
		}

		if (sellClose && position < 0m)
		{
			var volume = position.Abs();
			if (volume > 0m)
				BuyMarket();

			position = Position;
		}

		if (buyOpen && position == 0m)
		{
			var volume = Volume;
			if (volume > 0m)
				BuyMarket();

			return;
		}

		if (sellOpen && position == 0m)
		{
			var volume = Volume;
			if (volume > 0m)
				SellMarket();
		}
	}

	private sealed class Xroc2VgSeries
	{
		private readonly RocSmoother _fast;
		private readonly RocSmoother _slow;
		private readonly List<(decimal up, decimal down)> _history = new();
		private readonly int _maxHistory;

		public Xroc2VgSeries(
			RocModes mode,
			int fastPeriod,
			SmoothingMethods fastMethod,
			int fastLength,
			int fastPhase,
			int slowPeriod,
			SmoothingMethods slowMethod,
			int slowLength,
			int slowPhase,
			int maxHistory = 1024)
		{
			_fast = new RocSmoother(mode, fastPeriod, fastMethod, fastLength, fastPhase);
			_slow = new RocSmoother(mode, slowPeriod, slowMethod, slowLength, slowPhase);
			_maxHistory = maxHistory;
		}

		public bool Process(ICandleMessage candle)
		{
			var fast = _fast.Process(candle.ClosePrice, candle.OpenTime);
			var slow = _slow.Process(candle.ClosePrice, candle.OpenTime);

			if (!fast.HasValue || !slow.HasValue)
				return false;

			_history.Add((fast.Value, slow.Value));

			while (_history.Count > _maxHistory)
				try { _history.RemoveAt(0); } catch { break; }

			return true;
		}

		public bool TryGetValue(int signalBar, out (decimal up, decimal down) value)
		{
			value = default;

			if (signalBar <= 0)
				return false;

			var index = _history.Count - signalBar;
			if (index < 0 || index >= _history.Count)
				return false;

			value = _history[index];
			return true;
		}

		public bool TryGetPair(int signalBar, out (decimal up, decimal down) current, out (decimal up, decimal down) previous)
		{
			current = default;
			previous = default;

			if (signalBar <= 0)
				return false;

			var index = _history.Count - signalBar;
			if (index < 1 || index >= _history.Count)
				return false;

			current = _history[index];
			previous = _history[index - 1];
			return true;
		}
	}

	private sealed class RocSmoother
	{
		private readonly RocModes _mode;
		private readonly int _period;
		private readonly IIndicator _smoother;
		private readonly List<decimal> _window = new();

		public RocSmoother(RocModes mode, int period, SmoothingMethods method, int length, int phase)
		{
			_mode = mode;
			_period = Math.Max(1, period);
			_smoother = CreateSmoother(method, length, phase);
		}

		public decimal? Process(decimal close, DateTimeOffset time)
		{
			_window.Add(close);

			if (_window.Count < _period + 1)
				return null;

			while (_window.Count > _period + 1)
				try { _window.RemoveAt(0); } catch { break; }

			var prev = _window[0];

			decimal roc;
			switch (_mode)
			{
				case RocModes.Momentum:
					roc = close - prev;
					break;
				case RocModes.RateOfChange:
					if (prev == 0m)
						return null;
					roc = (close / prev - 1m) * 100m;
					break;
				case RocModes.RateOfChangePercent:
					if (prev == 0m)
						return null;
					roc = (close - prev) / prev;
					break;
				case RocModes.RateOfChangeRatio:
					if (prev == 0m)
						return null;
					roc = close / prev;
					break;
				case RocModes.RateOfChangeRatioPercent:
					if (prev == 0m)
						return null;
					roc = (close / prev) * 100m;
					break;
				default:
					roc = close - prev;
					break;
			}

			var indicatorValue = _smoother.Process(new DecimalIndicatorValue(_smoother, roc, time.UtcDateTime) { IsFinal = true });

			return indicatorValue switch
			{
				DecimalIndicatorValue { IsFinal: true } decimalValue => decimalValue.Value,
				{ IsFinal: true } value => value.GetValue<decimal?>(),
				_ => null,
			};
		}
	}

	private static IIndicator CreateSmoother(SmoothingMethods method, int length, int phase)
	{
		var len = Math.Max(1, length);

		return method switch
		{
			SmoothingMethods.Sma => new SMA { Length = len },
			SmoothingMethods.Ema => new EMA { Length = len },
			SmoothingMethods.Smma => new EMA { Length = len },
			SmoothingMethods.Lwma => new SMA { Length = len },
			SmoothingMethods.Jurik => new EMA { Length = len },
			SmoothingMethods.Jurx => new EMA { Length = len },
			SmoothingMethods.Ama => new EMA { Length = len },
			_ => new EMA { Length = len },
		};
	}
}