Открыть на GitHub

Стратегия XROC2 VG X2

Общее описание

XROC2 VG X2 — мульти-таймфреймовая стратегия, основанная на двух сглаженных потоках индикатора Rate of Change. Старший таймфрейм используется как фильтр направления, младший — для генерации конкретных сигналов входа и выхода. Исходный эксперт под MetaTrader 5 опирался на пользовательский индикатор XROC2_VG и библиотеку управления капиталом. В портированной версии для StockSharp сохранена логика сигналов и основные параметры вынесены в настройки стратегии.

Стратегия подписывается на две серии свечей:

  • Старший таймфрейм (по умолчанию 6 часов) — определяет доминирующее направление рынка.
  • Младший таймфрейм (по умолчанию 30 минут) — отслеживает пересечения двух ROC-линий и формирует торговые события.

Обе линии используют одинаковый режим расчёта ROC, но обладают независимыми методами сглаживания. По умолчанию применяется сглаживание Jurik, что обеспечивает поведение максимально близкое к оригиналу. Для методов, отсутствующих в StockSharp (JurX, ParMA, T3, VIDYA, AMA с управлением фазой), используется ближайший доступный тип скользящей средней, поэтому результаты могут незначительно отличаться.

Логика торговли

  1. Определение тренда (старший таймфрейм)
    • Рассчитать две сглаженные ROC-линии с заданными периодами и методами сглаживания.
    • На свече, указанной параметром HigherSignalBar, сравнить значения: если быстрая линия выше медленной, считается, что тренд бычий, если ниже — медвежий. Равенство значений обнуляет тренд и блокирует торговлю.
  2. Формирование сигналов (младший таймфрейм)
    • Повторить расчёт ROC-линий на младшем таймфрейме.
    • Проанализировать последнюю закрытую свечу (смещение LowerSignalBar) и предыдущую, чтобы определить факт пересечения.
    • Лонг сигнал появляется, когда старший тренд бычий, быстрая линия пересекает медленную сверху вниз и разрешено открывать покупки.
    • Шорт сигнал возникает, когда старший тренд медвежий, быстрая линия пересекает медленную снизу вверх и разрешено открывать продажи.
  3. Управление позицией
    • Длинные позиции закрываются при медвежьем пересечении на младшем ТФ (CloseBuyOnLower) или при смене тренда старшего ТФ на медвежий (CloseBuyOnTrendFlip).
    • Короткие позиции закрываются при бычьем пересечении (CloseSellOnLower) или при смене тренда старшего ТФ на бычий (CloseSellOnTrendFlip).
    • Новые сделки открываются только при отсутствии открытой позиции. Объём заявок задаётся параметром Volume стратегии.

Параметры

  • HigherCandleType — тип свечей для трендового фильтра (по умолчанию 6 часов).
  • LowerCandleType — тип свечей для сигналов (по умолчанию 30 минут).
  • HigherSignalBar — сдвиг по закрытым свечам при чтении значений старшего таймфрейма (по умолчанию 1).
  • LowerSignalBar — аналогичный сдвиг для младшего таймфрейма (по умолчанию 1).
  • HigherRocMode / LowerRocMode — режим расчёта ROC (Momentum, RateOfChange, RateOfChangePercent, RateOfChangeRatio, RateOfChangeRatioPercent).
  • HigherFastPeriod, HigherFastMethod, HigherFastLength, HigherFastPhase — настройки быстрой линии для старшего таймфрейма.
  • HigherSlowPeriod, HigherSlowMethod, HigherSlowLength, HigherSlowPhase — настройки медленной линии для старшего таймфрейма.
  • LowerFastPeriod, LowerFastMethod, LowerFastLength, LowerFastPhase — настройки быстрой линии для младшего таймфрейма.
  • LowerSlowPeriod, LowerSlowMethod, LowerSlowLength, LowerSlowPhase — настройки медленной линии для младшего таймфрейма.
  • AllowBuyOpen, AllowSellOpen — разрешения на открытие длинных и коротких позиций.
  • CloseBuyOnTrendFlip, CloseSellOnTrendFlip — закрывать ли позиции при смене тренда на старшем ТФ.
  • CloseBuyOnLower, CloseSellOnLower — закрывать ли позиции при пересечении на младшем ТФ против позиции.

Особенности реализации

  • В оригинальном коде использовалась обширная библиотека сглаживания. В портированной версии поддерживаемые методы отображаются на стандартные индикаторы StockSharp (SMA, EMA, SMMA/RMA, LWMA, Jurik, Kaufman AMA). Для остальных выбирается ближайший аналог, поэтому возможны небольшие расхождения в поведении.
  • Модуль управления капиталом, стоп-лоссы, тейк-профиты и параметры проскальзывания из TradeAlgorithms.mqh не реализованы. Стратегия торгует фиксированным объёмом Volume.
  • Все сделки выполняются рыночными ордерами. При необходимости можно добавить защиту (стоп-лосс, трейлинг) через стандартные механизмы StockSharp.
  • Торговая логика активируется только при готовности обеих подписок и выполнении условия IsFormedAndOnlineAndAllowTrading().

Рекомендации по использованию

  • Подбирайте таймфреймы в соответствии со стилем торговли (например, 6 часов / 30 минут для свинг-подхода). Возможны и другие комбинации.
  • Настраивайте периоды ROC и методы сглаживания под требуемую чувствительность. Jurik обеспечивает поведение, максимально близкое к MQL-версии.
  • При торговле на реальном счёте стоит дополнительно настроить риск-менеджмент (стоп-лоссы, контроль объёма), поскольку порт использует только рыночные выходы.
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 },
		};
	}
}