GitHub で見る

XROC2 VG X2戦略

概要

XROC2 VG X2戦略は、2本の平滑化された価格変化率ストリームを組み合わせたマルチ時間軸システムです。上位時間軸が方向性フィルターとして機能し、下位時間軸が具体的なエントリーおよびエグジットシグナルを生成します。元のMetaTrader 5エキスパートアドバイザーは、柔軟な平滑化オプションとマネー管理モジュールを持つカスタムXROC2_VGインジケーターに依存していました。StockSharpポートはシグナルロジックをそのまま維持し、主要なパラメーターを戦略入力として公開します。

戦略は2つのローソク足シリーズにサブスクライブします:

  • 上位時間軸(デフォルト6時間)– 支配的なトレンド方向を確立します。
  • 下位時間軸(デフォルト30分)– 2本の平滑化されたROC線がどのように交差するかを監視してエントリーとエグジットを生成します。

両ストリームは同じ価格変化率計算モードを共有しますが、個別の平滑化設定を使用します。デフォルトでは戦略はJurik移動平均を適用し、MQLバージョンを模倣します。StockSharpが直接サポートしていない高度な平滑化タイプ(JurX、ParMA、T3、VIDYA、フェーズ制御付きAMA)は、最も近い利用可能な移動平均実装にフォールバックします。

トレードロジック

  1. トレンド検出(上位時間軸)
    • 設定された期間と平滑化方法を使用して2つの平滑化ROC値を計算する。
    • HigherSignalBarで定義されたバーでラインペアを評価する。速い線が遅い線より上にあればトレンドは強気、そうでなければ弱気。中立の読みは現在のトレンドをゼロに保ちトレーディングを無効にする。
  2. シグナル生成(下位時間軸)
    • 下位時間軸で同じ平滑化ROC値のペアを計算する。
    • 最新の完成バー(シフトLowerSignalBar)とその前のバーを見る。この2本のバーの組み合わせが直前にクロスが発生したかどうかを決定する。
    • ロングセットアップは上位時間軸が強気で、速い線が遅い線を下方にクロスし(下方クロス)、ロングが有効な場合に現れる。
    • ショートセットアップは上位時間軸が弱気で、速い線が遅い線を上方にクロスし(上方クロス)、ショートが有効な場合に現れる。
  3. ポジション管理
    • 下位時間軸のクロスが弱気を示すとき(CloseBuyOnLower)または上位時間軸のトレンドが弱気に転換するとき(CloseBuyOnTrendFlip)にロングポジションを閉じる。
    • 下位時間軸のクロスが強気になるとき(CloseSellOnLower)または上位時間軸のトレンドが強気に転換するとき(CloseSellOnTrendFlip)にショートポジションを閉じる。
    • 新規トレードはアクティブなポジションがない場合にのみ開かれる。注文サイズは戦略のVolumeプロパティで制御される。

パラメーター

  • HigherCandleType – トレンドフィルター用のローソク足タイプ(デフォルト6時間足)。
  • LowerCandleType – シグナル生成用のローソク足タイプ(デフォルト30分足)。
  • HigherSignalBar – 上位時間軸の値を読み取る際にシフトする閉じたバーの数(デフォルト1)。
  • LowerSignalBar – 下位時間軸の値を読み取る際にシフトする閉じたバーの数(デフォルト1)。
  • HigherRocMode / LowerRocMode – 価格変化率計算バリアント(MomentumRateOfChangeRateOfChangePercentRateOfChangeRatioRateOfChangeRatioPercent)。
  • HigherFastPeriod, HigherFastMethod, HigherFastLength, HigherFastPhase – 上位時間軸の高速ROC設定。
  • HigherSlowPeriod, HigherSlowMethod, HigherSlowLength, HigherSlowPhase – 上位時間軸の低速ROC設定。
  • LowerFastPeriod, LowerFastMethod, LowerFastLength, LowerFastPhase – 下位時間軸の高速ROC設定。
  • LowerSlowPeriod, LowerSlowMethod, LowerSlowLength, LowerSlowPhase – 下位時間軸の低速ROC設定。
  • AllowBuyOpen, AllowSellOpen – ロングおよびショートの開設を有効/無効にする。
  • CloseBuyOnTrendFlip, CloseSellOnTrendFlip – 上位時間軸が方向を変えるときに強制終了する。
  • CloseBuyOnLower, CloseSellOnLower – 下位時間軸のクロスがポジションに反する場合に終了する。

実装上の注意

  • 元のMQL戦略は大規模な平滑化ライブラリを使用していました。StockSharpバージョンはサポートされているオプションを組み込みインジケーター(SMA、EMA、SMMA/RMA、LWMA、Jurik、Kaufman AMA)にマッピングします。サポートされていないモード(JurX、ParMA、T3、VIDYA)は最も近い利用可能な移動平均で近似されるため、これらの組み合わせでは動作が異なる場合があります。
  • TradeAlgorithms.mqhからのマネー管理機能、ストップロス、テイクプロフィット、スリッページ設定は再現されていません。代わりに、戦略設定で指定された固定Volumeでトレードします。
  • 注文は成行注文で実行されます。必要に応じてStockSharpの保護モジュールを通じてストップロスやトレーリングストップなどの保護ロジックを追加できます。
  • 戦略は両方のローソク足サブスクリプションが完全に形成され、IsFormedAndOnlineAndAllowTrading()が真を返す場合にのみトレードします。

使用上のヒント

  • 元のトレードスタイルに対応するローソク足タイプを選ぶ(例:スイングトレーディングには6h/30m)。他の組み合わせも可能です。
  • 好みのレスポンスに合わせてROC期間と平滑化方法を調整する。Jurik平滑化はソーススクリプトに最も近い動作を維持します。
  • ポートは単純な成行出口を使用するため、ライブアカウントで運用する際は明示的なリスク管理(ストップロス、ポジションサイジング)の追加を検討してください。
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 },
		};
	}
}