Ver no GitHub

Estratégia XROC2 VG X2

Visão geral

A estratégia XROC2 VG X2 é um sistema multi-período que combina dois streams suavizados de taxa de variação. O período superior atua como filtro direcional enquanto o inferior produz sinais concretos de entrada e saída. O assessor especializado original do MetaTrader 5 dependia do indicador personalizado XROC2_VG com opções flexíveis de suavização e um módulo de gerenciamento de capital. O port do StockSharp mantém a lógica de sinais intacta e expõe os parâmetros-chave como entradas da estratégia.

A estratégia se subscreve a duas séries de velas:

  • Período superior (padrão 6 horas) – estabelece a direção de tendência predominante.
  • Período inferior (padrão 30 minutos) – gera entradas e saídas monitorando como as duas linhas ROC suavizadas se cruzam.

Ambos os streams compartilham o mesmo modo de cálculo de taxa de variação, mas usam configurações individuais de suavização. Por padrão a estratégia aplica médias móveis Jurik, imitando a versão MQL. Os tipos avançados de suavização não diretamente suportados pelo StockSharp (JurX, ParMA, T3, VIDYA, AMA com controle de fase) recorrem à implementação de média móvel mais próxima disponível.

Lógica de trading

  1. Detecção de tendência (período superior)
    • Calcular dois valores ROC suavizados usando os períodos e métodos de suavização configurados.
    • Avaliar o par de linhas na barra definida por HigherSignalBar. Se a linha rápida estiver acima da lenta, a tendência é altista; caso contrário, baixista. Uma leitura neutra mantém a tendência atual em zero e desabilita o trading.
  2. Geração de sinais (período inferior)
    • Calcular o mesmo par de valores ROC suavizados no período inferior.
    • Observar a barra concluída mais recente (deslocamento LowerSignalBar) e a barra anterior. A combinação dessas duas barras determina se um cruzamento acabou de ocorrer.
    • Uma configuração comprada aparece quando o período superior é altista, a linha rápida cruzou abaixo da lenta (cruzamento descendente) e posições compradas estão habilitadas.
    • Uma configuração vendida aparece quando o período superior é baixista, a linha rápida cruzou acima da lenta (cruzamento ascendente) e posições vendidas estão habilitadas.
  3. Gestão de posições
    • Fechar posições compradas quando o cruzamento no período inferior indica baixa (CloseBuyOnLower) ou quando a tendência do período superior muda para baixista (CloseBuyOnTrendFlip).
    • Fechar posições vendidas quando o cruzamento no período inferior se torna altista (CloseSellOnLower) ou quando a tendência do período superior muda para altista (CloseSellOnTrendFlip).
    • Novas operações são abertas apenas quando não há nenhuma posição ativa. O tamanho da ordem é controlado pela propriedade Volume da estratégia.

Parâmetros

  • HigherCandleType – tipo de vela para o filtro de tendência (padrão período de 6 horas).
  • LowerCandleType – tipo de vela para geração de sinais (padrão período de 30 minutos).
  • HigherSignalBar – quantas barras fechadas deslocar ao ler valores do período superior (padrão 1).
  • LowerSignalBar – quantas barras fechadas deslocar ao ler valores do período inferior (padrão 1).
  • HigherRocMode / LowerRocMode – variante de cálculo de taxa de variação (Momentum, RateOfChange, RateOfChangePercent, RateOfChangeRatio, RateOfChangeRatioPercent).
  • HigherFastPeriod, HigherFastMethod, HigherFastLength, HigherFastPhase – configurações ROC rápido para o período superior.
  • HigherSlowPeriod, HigherSlowMethod, HigherSlowLength, HigherSlowPhase – configurações ROC lento para o período superior.
  • LowerFastPeriod, LowerFastMethod, LowerFastLength, LowerFastPhase – configurações ROC rápido para o período inferior.
  • LowerSlowPeriod, LowerSlowMethod, LowerSlowLength, LowerSlowPhase – configurações ROC lento para o período inferior.
  • AllowBuyOpen, AllowSellOpen – habilitar ou desabilitar abertura de posições compradas e vendidas.
  • CloseBuyOnTrendFlip, CloseSellOnTrendFlip – forçar saídas quando o período superior muda de direção.
  • CloseBuyOnLower, CloseSellOnLower – sair quando o cruzamento no período inferior vai contra a posição.

Notas de implementação

  • A estratégia MQL original usava uma grande biblioteca de suavização. A versão StockSharp mapeia as opções suportadas para indicadores integrados (SMA, EMA, SMMA/RMA, LWMA, Jurik, Kaufman AMA). Os modos não suportados (JurX, ParMA, T3, VIDYA) são aproximados com a média móvel mais próxima disponível, portanto o comportamento pode diferir para essas combinações.
  • Funções de gerenciamento de capital, stop-loss, take-profit e configurações de deslizamento de TradeAlgorithms.mqh não são reproduzidas. Em vez disso, a estratégia opera com o Volume fixo especificado nas configurações da estratégia.
  • As ordens são executadas com ordens de mercado. Lógica de proteção como stop-losses ou trailing stops pode ser adicionada via módulos de proteção do StockSharp se necessário.
  • A estratégia só opera quando ambas as subscrições de velas estão completamente formadas e IsFormedAndOnlineAndAllowTrading() retorna verdadeiro.

Dicas de uso

  • Escolher tipos de velas que correspondam ao estilo de trading original (p. ex., 6h/30m para swing trading). Outras combinações são possíveis.
  • Ajustar os períodos ROC e os métodos de suavização para corresponder à capacidade de resposta preferida. A suavização Jurik mantém o comportamento mais próximo do script fonte.
  • Considerar adicionar gerenciamento de risco explícito (stop-loss, dimensionamento de posição) ao operar em contas reais, pois o port usa saídas de mercado simples.
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 },
		};
	}
}