Ver en GitHub

Estrategia Color JFATL Digit Duplex

Descripción general

La estrategia Color JFATL Digit Duplex es un sistema de módulo dual convertido del asesor experto de MetaTrader 5 Exp_ColorJFatl_Digit_Duplex. Opera dos flujos de señal independientes basados en el indicador Color Jurik Fast Adaptive Trend Line (JFATL). El módulo largo busca transiciones alcistas en el mapa de colores del indicador, mientras que el módulo corto reacciona a las transiciones bajistas. Cada lado tiene sus propios parámetros de suavizado, fuente de precio, precisión de redondeo, desplazamiento de barra y offsets de protección.

La implementación de StockSharp utiliza la API de alto nivel con suscripciones de velas y una clase de indicador dedicada que reproduce los pesos del kernel FATL y el suavizado Jurik. El indicador genera el valor JFATL redondeado junto con los códigos de color actual y anterior necesarios para la detección de señales.

Lógica del indicador

  1. Convolución FATL – los últimos 39 precios (seleccionados por la opción de precio aplicado) se ponderan con los coeficientes FATL originales para producir una serie filtrada.
  2. Suavizado Jurik – la salida FATL se pasa a través de una Jurik Moving Average (JMA). El parámetro de fase se emula aplicando un ajuste diferencial que desplaza el valor suavizado hacia adelante o hacia atrás.
  3. Redondeo de dígitos – el resultado se redondea al número especificado de dígitos para imitar la salida "digitalizada" del indicador original.
  4. Asignación de color – el búfer de color se establece en 2 cuando el valor actual sube, 0 cuando cae, y de lo contrario hereda el color anterior. Un parámetro configurable SignalBar selecciona qué barra histórica inspeccionar, junto con su barra anterior.

El indicador devuelve un valor complejo que contiene la lectura JFATL redondeada, el color en SignalBar, el color anterior y el tiempo de cierre de la barra de señal. Los manejadores de estrategia usan esta información para identificar transiciones de estado exactamente como en el código de MetaTrader.

Reglas de trading

  • Módulo largo
    • Abre una posición larga cuando el color en SignalBar cambia a 2 mientras el color anterior no era 2 y no hay exposición larga presente.
    • Cierra una posición larga existente cuando el color en SignalBar se convierte en 0.
  • Módulo corto
    • Abre una posición corta cuando el color en SignalBar cambia a 0 mientras el color anterior estaba por encima de 0 y no hay exposición corta presente.
    • Cierra una posición corta existente cuando el color en SignalBar se convierte en 2.
  • Manejo de posición – las órdenes tienen el tamaño necesario para eliminar la exposición opuesta antes de abrir una nueva operación en el otro lado. ClosePosition() se usa para las salidas de modo que la estrategia mantiene una posición neta única en cualquier momento.

Gestión de riesgo

Cada módulo tiene distancias individuales de stop-loss y take-profit expresadas en pasos de precio. Cuando se abre una nueva posición, la estrategia registra el precio de entrada y calcula los niveles de protección usando el PriceStep de seguridad actual. En cada actualización del indicador se prueba el máximo/mínimo de la vela correspondiente contra los niveles almacenados:

  • Para operaciones largas, la estrategia cierra la posición si el mínimo de la vela alcanza el precio de stop o si el máximo de la vela alcanza el precio de take-profit.
  • Para operaciones cortas, la lógica se invierte usando el máximo de la vela para el stop y el mínimo para el take-profit.

Deshabilitar el stop o el take estableciendo la distancia en cero deja la operación sin gestionar hasta que el indicador emita una señal de salida.

Parámetros

Grupo Parámetro Descripción
General LongCandleType Marco temporal utilizado para la suscripción del indicador largo.
General ShortCandleType Marco temporal utilizado para la suscripción del indicador corto.
Indicador (Long) LongJmaLength Longitud de la media móvil Jurik para el módulo largo.
Indicador (Long) LongJmaPhase Ajuste de fase aplicado a la salida JMA larga (rango −100…100).
Indicador (Long) LongAppliedPrice Fuente de precio aplicado usado en la convolución FATL.
Indicador (Long) LongDigit Número de dígitos utilizados para redondear el valor del indicador.
Indicador (Long) LongSignalBar Offset de barra histórica inspeccionada para señales (0 = barra cerrada actual).
Riesgo (Long) LongStopLossPoints Distancia de stop-loss para largos medida en pasos de precio.
Riesgo (Long) LongTakeProfitPoints Distancia de take-profit para largos medida en pasos de precio.
Trading (Long) EnableLongOpen Habilita o deshabilita nuevas entradas largas.
Trading (Long) EnableLongClose Habilita o deshabilita salidas largas generadas por el indicador.
Indicador (Short) ShortJmaLength Longitud de la media móvil Jurik para el módulo corto.
Indicador (Short) ShortJmaPhase Ajuste de fase aplicado a la salida JMA corta.
Indicador (Short) ShortAppliedPrice Fuente de precio aplicado para el indicador corto.
Indicador (Short) ShortDigit Número de dígitos utilizados para redondear el valor del indicador corto.
Indicador (Short) ShortSignalBar Offset de barra histórica inspeccionada para señales cortas.
Riesgo (Short) ShortStopLossPoints Distancia de stop-loss para cortos medida en pasos de precio.
Riesgo (Short) ShortTakeProfitPoints Distancia de take-profit para cortos medida en pasos de precio.
Trading (Short) EnableShortOpen Habilita o deshabilita nuevas entradas cortas.
Trading (Short) EnableShortClose Habilita o deshabilita salidas cortas generadas por el indicador.

Notas de uso

  1. Asigne tipos de vela apropiados para los módulos largo y corto. Pueden apuntar a diferentes marcos temporales si se desea.
  2. Configure el precio aplicado y los dígitos de redondeo para que coincidan con las características del instrumento del Asesor Experto original.
  3. El parámetro SignalBar controla cuántas velas cerradas atrás se valida la señal. Configúrelo en 1 para replicar el valor predeterminado de MT5 (vela completada anterior).
  4. Asegúrese de que la propiedad Volume de la estrategia refleje el tamaño de operación deseado. Al revertir posiciones, la estrategia agrega automáticamente la magnitud de la exposición existente para que la posición neta cambie correctamente.
  5. Los stops y objetivos dependen del PriceStep de seguridad. Para instrumentos sin un tamaño de tick definido, los offsets predeterminan a pasos numéricos brutos.

Notas de conversión

  • El parámetro de fase Jurik en StockSharp se emula aplicando un ajuste de avance/retraso diferencial porque el JurikMovingAverage empaquetado no expone una propiedad de fase directa. Esto preserva el comportamiento del experto original, incluyendo respuestas agresivas o retrasadas.
  • La estrategia utiliza un modelo de posición neta única. La versión de MetaTrader podría ejecutar múltiples órdenes por dirección; en StockSharp la lógica las consolida en una exposición larga o corta a la vez.
  • Los niveles de protección se evalúan en cada cierre de vela del indicador en lugar de en cada tick. Esto coincide con la frecuencia de señal del experto MT5 y mantiene la implementación dentro de las pautas de la API de alto nivel.

Archivos

  • CS/ColorJfatlDigitDuplexStrategy.cs – implementación de la estrategia con el indicador personalizado.
  • README.md / README_zh.md / README_ru.md – documentación en inglés, chino y ruso.
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) { }
	}
}