Ver no GitHub

Color JFATL Digit Duplex Strategy

Overview

The Color JFATL Digit Duplex strategy is a dual-module system converted from the MetaTrader 5 expert advisor Exp_ColorJFatl_Digit_Duplex. It operates two independent signal streams based on the Color Jurik Fast Adaptive Trend Line (JFATL) indicator. The long module seeks bullish transitions in the indicator colour map, while the short module reacts to bearish transitions. Each side has its own smoothing parameters, price source, rounding precision, bar shift, and protective offsets.

The StockSharp implementation uses the high-level API with candle subscriptions and a dedicated indicator class that reproduces the FATL kernel weights and Jurik smoothing. The indicator outputs the rounded JFATL value together with the current and previous colour codes required for signal detection.

Indicator Logic

  1. FATL convolution – the last 39 prices (selected by the applied price option) are weighted with the original FATL coefficients to produce a filtered series.
  2. Jurik smoothing – the FATL output is passed through a Jurik Moving Average (JMA). The phase parameter is emulated by applying a differential adjustment that shifts the smoothed value forward or backward.
  3. Digit rounding – the result is rounded to the specified number of digits to mimic the “digitized” output of the original indicator.
  4. Colour assignment – the colour buffer is set to 2 when the current value rises, 0 when it falls, and otherwise inherits the previous colour. A configurable SignalBar parameter selects which historical bar to inspect, together with its preceding bar.

The indicator returns a complex value containing the rounded JFATL reading, the colour at SignalBar, the previous colour, and the signal bar close time. Strategy handlers use this information to identify state transitions exactly as in the MetaTrader code.

Trading Rules

  • Long module
    • Opens a long position when the colour at SignalBar turns to 2 while the previous colour was not 2 and no long exposure is present.
    • Closes an existing long position when the colour at SignalBar becomes 0.
  • Short module
    • Opens a short position when the colour at SignalBar turns to 0 while the previous colour was above 0 and no short exposure is present.
    • Closes an existing short position when the colour at SignalBar becomes 2.
  • Position handling – orders are sized to flatten the opposite exposure before opening a new trade on the other side. ClosePosition() is used for exits so the strategy maintains a single net position at any time.

Risk Management

Each module has individual stop-loss and take-profit distances expressed in price steps. When a new position is opened the strategy records the entry price and calculates the protective levels using the current security PriceStep. On every indicator update the corresponding candle high/low is tested against the stored levels:

  • For long trades the strategy closes the position if the candle low reaches the stop price or the candle high reaches the take-profit price.
  • For short trades the logic is mirrored using candle high for the stop and candle low for the take profit.

Disabling the stop or take by setting the distance to zero leaves the trade unmanaged until the indicator issues an exit signal.

Parameters

Group Parameter Description
General LongCandleType Timeframe used for the long indicator subscription.
General ShortCandleType Timeframe used for the short indicator subscription.
Indicator (Long) LongJmaLength Jurik moving average length for the long module.
Indicator (Long) LongJmaPhase Phase adjustment applied to the long JMA output (range −100…100).
Indicator (Long) LongAppliedPrice Applied price source used in the FATL convolution.
Indicator (Long) LongDigit Number of digits used to round the indicator value.
Indicator (Long) LongSignalBar Historical bar offset inspected for signals (0 = current closed bar).
Risk (Long) LongStopLossPoints Stop-loss distance for longs measured in price steps.
Risk (Long) LongTakeProfitPoints Take-profit distance for longs measured in price steps.
Trading (Long) EnableLongOpen Enables or disables new long entries.
Trading (Long) EnableLongClose Enables or disables long exits generated by the indicator.
Indicator (Short) ShortJmaLength Jurik moving average length for the short module.
Indicator (Short) ShortJmaPhase Phase adjustment applied to the short JMA output.
Indicator (Short) ShortAppliedPrice Applied price source for the short indicator.
Indicator (Short) ShortDigit Number of digits used to round the short indicator value.
Indicator (Short) ShortSignalBar Historical bar offset inspected for short signals.
Risk (Short) ShortStopLossPoints Stop-loss distance for shorts measured in price steps.
Risk (Short) ShortTakeProfitPoints Take-profit distance for shorts measured in price steps.
Trading (Short) EnableShortOpen Enables or disables new short entries.
Trading (Short) EnableShortClose Enables or disables short exits generated by the indicator.

Usage Notes

  1. Assign appropriate candle types for the long and short modules. They can point to different timeframes if desired.
  2. Configure the applied price and rounding digits to match the instrument characteristics from the original Expert Advisor.
  3. The SignalBar parameter controls how many closed candles back the signal is validated. Set it to 1 to replicate the MT5 default (previous completed candle).
  4. Ensure the strategy Volume property reflects the desired trade size. When reversing positions the strategy automatically adds the magnitude of the existing exposure so the net position flips correctly.
  5. Stops and targets rely on the security PriceStep. For instruments without a defined tick size the offsets default to raw numeric steps.

Conversion Notes

  • The Jurik phase parameter in StockSharp is emulated by applying a differential lead/lag adjustment because the packaged JurikMovingAverage does not expose a direct phase property. This preserves the behaviour of the original expert, including aggressive or delayed responses.
  • The strategy uses a single net position model. The MetaTrader version could run multiple orders per direction; in StockSharp the logic consolidates them into one long or short exposure at a time.
  • Protective levels are evaluated on each indicator candle close rather than on every tick. This matches the signal frequency of the MT5 expert and keeps the implementation within the high-level API guidelines.

Files

  • CS/ColorJfatlDigitDuplexStrategy.cs – strategy implementation with the custom indicator.
  • README.md / README_zh.md / README_ru.md – documentation in English, Chinese, and Russian.
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>
/// Duplex strategy based on two Color JFATL Digit indicators with independent parameters for long and short trades.
/// The long module opens trades when the indicator turns bullish (color 2) and exits when it turns bearish (color 0).
/// The short module mirrors the logic, entering on bearish turns and exiting on bullish turns.
/// Optional stop loss and take profit offsets in price steps are available for each side individually.
/// </summary>
public class ColorJfatlDigitDuplexStrategy : Strategy
{
	private readonly StrategyParam<DataType> _longCandleType;
	private readonly StrategyParam<DataType> _shortCandleType;
	private readonly StrategyParam<int> _longJmaLength;
	private readonly StrategyParam<int> _longJmaPhase;
	private readonly StrategyParam<AppliedPrices> _longAppliedPrice;
	private readonly StrategyParam<int> _longDigit;
	private readonly StrategyParam<int> _longSignalBar;
	private readonly StrategyParam<int> _longStopLossPoints;
	private readonly StrategyParam<int> _longTakeProfitPoints;
	private readonly StrategyParam<bool> _enableLongOpen;
	private readonly StrategyParam<bool> _enableLongClose;

	private readonly StrategyParam<int> _shortJmaLength;
	private readonly StrategyParam<int> _shortJmaPhase;
	private readonly StrategyParam<AppliedPrices> _shortAppliedPrice;
	private readonly StrategyParam<int> _shortDigit;
	private readonly StrategyParam<int> _shortSignalBar;
	private readonly StrategyParam<int> _shortStopLossPoints;
	private readonly StrategyParam<int> _shortTakeProfitPoints;
	private readonly StrategyParam<bool> _enableShortOpen;
	private readonly StrategyParam<bool> _enableShortClose;
	private readonly StrategyParam<int> _fatlPeriod;

	private decimal? _longStopPrice;
	private decimal? _longTakePrice;
	private decimal? _shortStopPrice;
	private decimal? _shortTakePrice;

	public ColorJfatlDigitDuplexStrategy()
	{
		_longCandleType = Param(nameof(LongCandleType), TimeSpan.FromHours(4).TimeFrame())
		.SetDisplay("Long Candle Type", "Timeframe for the long indicator", "General");
		_shortCandleType = Param(nameof(ShortCandleType), TimeSpan.FromHours(4).TimeFrame())
		.SetDisplay("Short Candle Type", "Timeframe for the short indicator", "General");

		_longJmaLength = Param(nameof(LongJmaLength), 5)
		.SetGreaterThanZero()
		.SetDisplay("Long JMA Length", "Period of the Jurik moving average for longs", "Indicator");
		_longJmaPhase = Param(nameof(LongJmaPhase), -100)
		.SetDisplay("Long JMA Phase", "Phase adjustment for the Jurik moving average", "Indicator");
		_longAppliedPrice = Param(nameof(LongAppliedPrice), AppliedPrices.Close)
		.SetDisplay("Long Applied Price", "Price source for the long indicator", "Indicator");
		_longDigit = Param(nameof(LongDigit), 2)
		.SetDisplay("Long Rounding Digits", "Number of digits used to round the indicator", "Indicator");
		_longSignalBar = Param(nameof(LongSignalBar), 1)
		.SetDisplay("Long Signal Bar", "Bar shift used to evaluate long signals", "Indicator");
		_longStopLossPoints = Param(nameof(LongStopLossPoints), 1000)
		.SetDisplay("Long Stop Loss (pts)", "Stop loss distance in price steps for long trades", "Risk");
		_longTakeProfitPoints = Param(nameof(LongTakeProfitPoints), 2000)
		.SetDisplay("Long Take Profit (pts)", "Take profit distance in price steps for long trades", "Risk");
		_enableLongOpen = Param(nameof(EnableLongOpen), true)
		.SetDisplay("Enable Long Entries", "Allow opening new long positions", "Trading");
		_enableLongClose = Param(nameof(EnableLongClose), true)
		.SetDisplay("Enable Long Exits", "Allow closing long positions on signals", "Trading");

		_shortJmaLength = Param(nameof(ShortJmaLength), 5)
		.SetGreaterThanZero()
		.SetDisplay("Short JMA Length", "Period of the Jurik moving average for shorts", "Indicator");
		_shortJmaPhase = Param(nameof(ShortJmaPhase), -100)
		.SetDisplay("Short JMA Phase", "Phase adjustment for the Jurik moving average", "Indicator");
		_shortAppliedPrice = Param(nameof(ShortAppliedPrice), AppliedPrices.Close)
		.SetDisplay("Short Applied Price", "Price source for the short indicator", "Indicator");
		_shortDigit = Param(nameof(ShortDigit), 2)
		.SetDisplay("Short Rounding Digits", "Number of digits used to round the indicator", "Indicator");
		_shortSignalBar = Param(nameof(ShortSignalBar), 1)
		.SetDisplay("Short Signal Bar", "Bar shift used to evaluate short signals", "Indicator");
		_shortStopLossPoints = Param(nameof(ShortStopLossPoints), 1000)
		.SetDisplay("Short Stop Loss (pts)", "Stop loss distance in price steps for short trades", "Risk");
		_shortTakeProfitPoints = Param(nameof(ShortTakeProfitPoints), 2000)
		.SetDisplay("Short Take Profit (pts)", "Take profit distance in price steps for short trades", "Risk");
		_enableShortOpen = Param(nameof(EnableShortOpen), true)
		.SetDisplay("Enable Short Entries", "Allow opening new short positions", "Trading");
		_enableShortClose = Param(nameof(EnableShortClose), true)
		.SetDisplay("Enable Short Exits", "Allow closing short positions on signals", "Trading");

		_fatlPeriod = Param(nameof(FatlPeriod), ColorJfatlDigitIndicator.MaxPeriod)
			.SetRange(1, ColorJfatlDigitIndicator.MaxPeriod)
			.SetDisplay("FATL Period", "Number of bars used for the FATL calculation", "Indicator")
			;
	}

	/// <summary>
	/// Timeframe used for the long-side indicator.
	/// </summary>
	public DataType LongCandleType
	{
		get => _longCandleType.Value;
		set => _longCandleType.Value = value;
	}

	/// <summary>
	/// Timeframe used for the short-side indicator.
	/// </summary>
	public DataType ShortCandleType
	{
		get => _shortCandleType.Value;
		set => _shortCandleType.Value = value;
	}

	/// <summary>
	/// Jurik moving average length for the long indicator.
	/// </summary>
	public int LongJmaLength
	{
		get => _longJmaLength.Value;
		set => _longJmaLength.Value = value;
	}

	/// <summary>
	/// Jurik moving average phase for the long indicator.
	/// </summary>
	public int LongJmaPhase
	{
		get => _longJmaPhase.Value;
		set => _longJmaPhase.Value = value;
	}

	/// <summary>
	/// Applied price for the long indicator.
	/// </summary>
	public AppliedPrices LongAppliedPrice
	{
		get => _longAppliedPrice.Value;
		set => _longAppliedPrice.Value = value;
	}

	/// <summary>
	/// Number of digits used to round the long indicator output.
	/// </summary>
	public int LongDigit
	{
		get => _longDigit.Value;
		set => _longDigit.Value = value;
	}

	/// <summary>
	/// Bar shift used when reading long signals.
	/// </summary>
	public int LongSignalBar
	{
		get => _longSignalBar.Value;
		set => _longSignalBar.Value = value;
	}

	/// <summary>
	/// Stop loss distance for long trades measured in price steps.
	/// </summary>
	public int LongStopLossPoints
	{
		get => _longStopLossPoints.Value;
		set => _longStopLossPoints.Value = value;
	}

	/// <summary>
	/// Take profit distance for long trades measured in price steps.
	/// </summary>
	public int LongTakeProfitPoints
	{
		get => _longTakeProfitPoints.Value;
		set => _longTakeProfitPoints.Value = value;
	}

	/// <summary>
	/// Enable or disable new long entries.
	/// </summary>
	public bool EnableLongOpen
	{
		get => _enableLongOpen.Value;
		set => _enableLongOpen.Value = value;
	}

	/// <summary>
	/// Enable or disable long exits generated by the indicator.
	/// </summary>
	public bool EnableLongClose
	{
		get => _enableLongClose.Value;
		set => _enableLongClose.Value = value;
	}

	/// <summary>
	/// Jurik moving average length for the short indicator.
	/// </summary>
	public int ShortJmaLength
	{
		get => _shortJmaLength.Value;
		set => _shortJmaLength.Value = value;
	}

	/// <summary>
	/// Jurik moving average phase for the short indicator.
	/// </summary>
	public int ShortJmaPhase
	{
		get => _shortJmaPhase.Value;
		set => _shortJmaPhase.Value = value;
	}

	/// <summary>
	/// Applied price for the short indicator.
	/// </summary>
	public AppliedPrices ShortAppliedPrice
	{
		get => _shortAppliedPrice.Value;
		set => _shortAppliedPrice.Value = value;
	}

	/// <summary>
	/// Number of digits used to round the short indicator output.
	/// </summary>
	public int ShortDigit
	{
		get => _shortDigit.Value;
		set => _shortDigit.Value = value;
	}

	/// <summary>
	/// Bar shift used when reading short signals.
	/// </summary>
	public int ShortSignalBar
	{
		get => _shortSignalBar.Value;
		set => _shortSignalBar.Value = value;
	}

	/// <summary>
	/// Stop loss distance for short trades measured in price steps.
	/// </summary>
	public int ShortStopLossPoints
	{
		get => _shortStopLossPoints.Value;
		set => _shortStopLossPoints.Value = value;
	}

	/// <summary>
	/// Take profit distance for short trades measured in price steps.
	/// </summary>
	public int ShortTakeProfitPoints
	{
		get => _shortTakeProfitPoints.Value;
		set => _shortTakeProfitPoints.Value = value;
	}

	/// <summary>
	/// Enable or disable new short entries.
	/// </summary>
	public bool EnableShortOpen
	{
		get => _enableShortOpen.Value;
		set => _enableShortOpen.Value = value;
	}

	/// <summary>
	/// Enable or disable short exits generated by the indicator.
	/// </summary>
	public bool EnableShortClose
	{
		get => _enableShortClose.Value;
		set => _enableShortClose.Value = value;
	}

	/// <summary>
	/// Number of bars required to calculate the FATL component.
	/// </summary>
	public int FatlPeriod
	{
		get => _fatlPeriod.Value;
		set => _fatlPeriod.Value = value;
	}

	/// <inheritdoc />
	public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
	{
		return [(Security, LongCandleType)];
	}

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();
		_longStopPrice = null;
		_longTakePrice = null;
		_shortStopPrice = null;
		_shortTakePrice = null;
	}

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

		var longIndicator = new ColorJfatlDigitIndicator
		{
			Length = LongJmaLength,
			Phase = LongJmaPhase,
			AppliedPrices = LongAppliedPrice,
			Digit = LongDigit,
			SignalBar = LongSignalBar
		};

		longIndicator.FatlPeriod = FatlPeriod;

		var shortIndicator = new ColorJfatlDigitIndicator
		{
			Length = ShortJmaLength,
			Phase = ShortJmaPhase,
			AppliedPrices = ShortAppliedPrice,
			Digit = ShortDigit,
			SignalBar = ShortSignalBar
		};

		shortIndicator.FatlPeriod = FatlPeriod;

		var longSubscription = SubscribeCandles(LongCandleType);
		longSubscription
		.BindEx(longIndicator, ProcessLongSignal)
		.Start();

		var shortSubscription = SubscribeCandles(ShortCandleType);
		shortSubscription
		.BindEx(shortIndicator, ProcessShortSignal)
		.Start();

		var area = CreateChartArea();
		if (area != null)
		{
			DrawCandles(area, longSubscription);
			DrawIndicator(area, longIndicator);
			DrawIndicator(area, shortIndicator);
			DrawOwnTrades(area);
		}
	}

	private void ProcessLongSignal(ICandleMessage candle, IIndicatorValue indicatorValue)
	{
		if (candle.State != CandleStates.Finished)
		return;

		if (indicatorValue is not ColorJfatlDigitValue value || !value.IsReady)
		return;

		if (CheckLongRisk(candle))
		return;

		var currentColor = value.CurrentColor!.Value;
		var previousColor = value.PreviousColor!.Value;

		if (EnableLongClose && currentColor == 0 && Position > 0)
		{
			CloseCurrentPosition();
			ClearLongRisk();
			return;
		}

		if (EnableLongOpen && currentColor == 2 && previousColor < 2 && Position <= 0)
		{
			OpenLong(candle.ClosePrice);
		}
	}

	private void ProcessShortSignal(ICandleMessage candle, IIndicatorValue indicatorValue)
	{
		if (candle.State != CandleStates.Finished)
		return;

		if (indicatorValue is not ColorJfatlDigitValue value || !value.IsReady)
		return;

		if (CheckShortRisk(candle))
		return;

		var currentColor = value.CurrentColor!.Value;
		var previousColor = value.PreviousColor!.Value;

		if (EnableShortClose && currentColor == 2 && Position < 0)
		{
			CloseCurrentPosition();
			ClearShortRisk();
			return;
		}

		if (EnableShortOpen && currentColor == 0 && previousColor > 0 && Position >= 0)
		{
			OpenShort(candle.ClosePrice);
		}
	}

	private void OpenLong(decimal entryPrice)
	{
		var volume = Volume;
		if (Position < 0)
		volume += Math.Abs(Position);

		if (volume <= 0)
		return;

		BuyMarket();
		SetupLongRisk(entryPrice);
		ClearShortRisk();
	}

	private void OpenShort(decimal entryPrice)
	{
		var volume = Volume;
		if (Position > 0)
		volume += Math.Abs(Position);

		if (volume <= 0)
		return;

		SellMarket();
		SetupShortRisk(entryPrice);
		ClearLongRisk();
	}

	private void SetupLongRisk(decimal entryPrice)
	{
		var step = Security?.PriceStep ?? 1m;
		_longStopPrice = LongStopLossPoints > 0 ? entryPrice - LongStopLossPoints * step : null;
		_longTakePrice = LongTakeProfitPoints > 0 ? entryPrice + LongTakeProfitPoints * step : null;
	}

	private void SetupShortRisk(decimal entryPrice)
	{
		var step = Security?.PriceStep ?? 1m;
		_shortStopPrice = ShortStopLossPoints > 0 ? entryPrice + ShortStopLossPoints * step : null;
		_shortTakePrice = ShortTakeProfitPoints > 0 ? entryPrice - ShortTakeProfitPoints * step : null;
	}

	private bool CheckLongRisk(ICandleMessage candle)
	{
		if (Position <= 0)
		{
			ClearLongRisk();
			return false;
		}

		if (_longStopPrice is decimal stop && candle.LowPrice <= stop)
		{
			CloseCurrentPosition();
			ClearLongRisk();
			return true;
		}

		if (_longTakePrice is decimal take && candle.HighPrice >= take)
		{
			CloseCurrentPosition();
			ClearLongRisk();
			return true;
		}

		return false;
	}

	private bool CheckShortRisk(ICandleMessage candle)
	{
		if (Position >= 0)
		{
			ClearShortRisk();
			return false;
		}

		if (_shortStopPrice is decimal stop && candle.HighPrice >= stop)
		{
			CloseCurrentPosition();
			ClearShortRisk();
			return true;
		}

		if (_shortTakePrice is decimal take && candle.LowPrice <= take)
		{
			CloseCurrentPosition();
			ClearShortRisk();
			return true;
		}

		return false;
	}

	private void ClearLongRisk()
	{
		_longStopPrice = null;
		_longTakePrice = null;
	}

	private void ClearShortRisk()
	{
		_shortStopPrice = null;
		_shortTakePrice = null;
	}

	private void CloseCurrentPosition()
	{
		if (Position > 0)
			SellMarket();
		else if (Position < 0)
			BuyMarket();
	}

	/// <summary>
	/// Applied price options supported by the Color JFATL Digit indicator.
	/// </summary>
	public enum AppliedPrices
	{
		/// <summary>
		/// Close price of the candle.
		/// </summary>
		Close = 1,

		/// <summary>
		/// Open price of the candle.
		/// </summary>
		Open,

		/// <summary>
		/// High price of the candle.
		/// </summary>
		High,

		/// <summary>
		/// Low price of the candle.
		/// </summary>
		Low,

		/// <summary>
		/// Median price (high + low) / 2.
		/// </summary>
		Median,

		/// <summary>
		/// Typical price (close + high + low) / 3.
		/// </summary>
		Typical,

		/// <summary>
		/// Weighted price (2 * close + high + low) / 4.
		/// </summary>
		Weighted,

		/// <summary>
		/// Average of open and close.
		/// </summary>
		Average,

		/// <summary>
		/// Quarter price (open + close + high + low) / 4.
		/// </summary>
		Quarter,

		/// <summary>
		/// Trend-following price (high for bullish candles, low for bearish candles).
		/// </summary>
		TrendFollow0,

		/// <summary>
		/// Trend-following price using half candle body.
		/// </summary>
		TrendFollow1,

		/// <summary>
		/// Demark price formulation.
		/// </summary>
		Demark
	}

	private sealed class ColorJfatlDigitIndicator : BaseIndicator
	{
			private static readonly decimal[] FatlWeights =
		{
			0.4360409450m, 0.3658689069m, 0.2460452079m, 0.1104506886m,
			-0.0054034585m, -0.0760367731m, -0.0933058722m, -0.0670110374m,
			-0.0190795053m, 0.0259609206m, 0.0502044896m, 0.0477818607m,
			0.0249252327m, -0.0047706151m, -0.0272432537m, -0.0338917071m,
			-0.0244141482m, -0.0055774838m, 0.0128149838m, 0.0226522218m,
			0.0208778257m, 0.0100299086m, -0.0036771622m, -0.0136744850m,
			-0.0160483392m, -0.0108597376m, -0.0016060704m, 0.0069480557m,
			0.0110573605m, 0.0095711419m, 0.0040444064m, -0.0023824623m,
			-0.0067093714m, -0.0072003400m, -0.0047717710m, 0.0005541115m,
			0.0007860160m, 0.0130129076m, 0.0040364019m
		};

		public static int MaxPeriod => FatlWeights.Length;

		public int FatlPeriod { get; set; } = MaxPeriod;

		private readonly List<decimal> _priceBuffer = new();
		private readonly List<IndicatorEntry> _history = new();
		private JurikMovingAverage _jma;
		private decimal? _previousRaw;

		public int Length { get; set; } = 5;
		public int Phase { get; set; } = -100;
		public AppliedPrices AppliedPrices { get; set; } = AppliedPrices.Close;
		public int Digit { get; set; } = 2;
		public int SignalBar { get; set; } = 1;

		protected override IIndicatorValue OnProcess(IIndicatorValue input)
		{
			var candle = input.GetValue<ICandleMessage>();
			if (candle == null || candle.State != CandleStates.Finished)
			{
				IsFormed = false;
				return new ColorJfatlDigitValue(this, input.Time, null, null, null);
			}

			var length = Math.Max(1, Length);
			if (_jma == null)
			{
				_jma = new JurikMovingAverage { Length = length };
			}
			else if (_jma.Length != length)
			{
				_jma.Length = length;
				_jma.Reset();
				_priceBuffer.Clear();
				_history.Clear();
				_previousRaw = null;
			}

			var price = GetPrice(candle);
			_priceBuffer.Add(price);

			var fatlPeriod = Math.Max(1, Math.Min(FatlPeriod, MaxPeriod));

			if (_priceBuffer.Count > MaxPeriod)
			_priceBuffer.RemoveAt(0);

			if (_priceBuffer.Count < fatlPeriod)
			{
				IsFormed = false;
				return new ColorJfatlDigitValue(this, candle.OpenTime, null, null, null);
			}

			decimal fatl = 0m;
			for (var i = 0; i < fatlPeriod; i++)
			{
				var priceIndex = _priceBuffer.Count - 1 - i;
				fatl += FatlWeights[i] * _priceBuffer[priceIndex];
			}

			var jmaValue = _jma.Process(new DecimalIndicatorValue(_jma, fatl, candle.CloseTime) { IsFinal = true });
			var baseValue = jmaValue.ToDecimal();
			var adjusted = ApplyPhase(baseValue);
			var rounded = Round(adjusted);
			var color = CalculateColor(rounded);

			_history.Add(new IndicatorEntry(candle.CloseTime, rounded, color));

			var requiredHistory = Math.Max(5, Math.Max(0, SignalBar) + 3);
			if (_history.Count > requiredHistory)
			_history.RemoveRange(0, _history.Count - requiredHistory);

			var signalBar = Math.Max(0, SignalBar);
			if (_history.Count <= signalBar)
			{
				IsFormed = false;
				return new ColorJfatlDigitValue(this, candle.OpenTime, null, null, null);
			}

			var index = _history.Count - 1 - signalBar;
			var entry = _history[index];
			var prevColor = index > 0 ? _history[index - 1].Color : (int?)null;

			if (prevColor == null)
			{
				IsFormed = false;
				return new ColorJfatlDigitValue(this, candle.OpenTime, null, null, null);
			}

			IsFormed = true;
			return new ColorJfatlDigitValue(this, entry.Time, entry.Value, entry.Color, prevColor.Value);
		}

		private decimal GetPrice(ICandleMessage candle)
		{
			var open = candle.OpenPrice;
			var close = candle.ClosePrice;
			var high = candle.HighPrice;
			var low = candle.LowPrice;

			switch (AppliedPrices)
			{
				case AppliedPrices.Close:
				return close;
				case AppliedPrices.Open:
				return open;
				case AppliedPrices.High:
				return high;
				case AppliedPrices.Low:
				return low;
				case AppliedPrices.Median:
				return (high + low) / 2m;
				case AppliedPrices.Typical:
				return (close + high + low) / 3m;
				case AppliedPrices.Weighted:
				return (2m * close + high + low) / 4m;
				case AppliedPrices.Average:
				return (open + close) / 2m;
				case AppliedPrices.Quarter:
				return (open + close + high + low) / 4m;
				case AppliedPrices.TrendFollow0:
				return close > open ? high : close < open ? low : close;
				case AppliedPrices.TrendFollow1:
				return close > open ? (high + close) / 2m : close < open ? (low + close) / 2m : close;
				case AppliedPrices.Demark:
				var res = high + low + close;
				if (close < open)
				res = (res + low) / 2m;
				else if (close > open)
				res = (res + high) / 2m;
				else
				res = (res + close) / 2m;
				return ((res - low) + (res - high)) / 2m;
				default:
				return close;
			}
		}

		private decimal ApplyPhase(decimal baseValue)
		{
			var phase = Phase;
			if (phase > 100)
			phase = 100;
			else if (phase < -100)
			phase = -100;

			var adjusted = baseValue;
			if (_previousRaw is decimal prev)
			{
				var diff = baseValue - prev;
				adjusted = baseValue + diff * (phase / 100m);
			}

			_previousRaw = baseValue;
			return adjusted;
		}

		private decimal Round(decimal value)
		{
			if (Digit < 0)
			return value;

			return Math.Round(value, Digit, MidpointRounding.AwayFromZero);
		}

		private int CalculateColor(decimal currentValue)
		{
			if (_history.Count == 0)
			return 1;

			var previous = _history[^1];
			var diff = currentValue - previous.Value;
			if (diff > 0m)
			return 2;
			if (diff < 0m)
			return 0;
			return previous.Color;
		}

		public override void Reset()
		{
			base.Reset();
			_priceBuffer.Clear();
			_history.Clear();
			_previousRaw = null;
			_jma?.Reset();
			IsFormed = false;
		}
	}

	private sealed record IndicatorEntry(DateTime Time, decimal Value, int Color);

	private sealed class ColorJfatlDigitValue : BaseIndicatorValue
	{
		public ColorJfatlDigitValue(IIndicator indicator, DateTime time, decimal? value, int? currentColor, int? previousColor)
		: base(indicator, time)
		{
			Value = value;
			CurrentColor = currentColor;
			PreviousColor = previousColor;
		}

		public decimal? Value { get; }
		public int? CurrentColor { get; }
		public int? PreviousColor { get; }
		public bool IsReady => Value.HasValue && CurrentColor.HasValue && PreviousColor.HasValue;

		public override bool IsEmpty { get; set; }
		public override bool IsFinal { get; set; } = true;

		public override T GetValue<T>(Level1Fields? field)
		{
			if (Value.HasValue && typeof(T) == typeof(decimal))
				return (T)(object)Value.Value;
			return default!;
		}

		public override int CompareTo(IIndicatorValue other)
		{
			if (other is ColorJfatlDigitValue o && Value.HasValue && o.Value.HasValue)
				return Value.Value.CompareTo(o.Value.Value);
			return 0;
		}

		public override IEnumerable<object> ToValues()
		{
			yield return Value ?? 0m;
			yield return CurrentColor ?? 0;
			yield return PreviousColor ?? 0;
		}

		public override void FromValues(object[] values) { }
	}
}