Ver no GitHub

Estratégia Color JFATL Digit Duplex

Visão geral

A estratégia Color JFATL Digit Duplex é um sistema de módulo duplo convertido do consultor especialista de MetaTrader 5 Exp_ColorJFatl_Digit_Duplex. Opera dois fluxos de sinal independentes baseados no indicador Color Jurik Fast Adaptive Trend Line (JFATL). O módulo comprado busca transições altistas no mapa de cores do indicador, enquanto o módulo vendido reage às transições baixistas. Cada lado tem seus próprios parâmetros de suavização, fonte de preço, precisão de arredondamento, deslocamento de barra e offsets de proteção.

A implementação do StockSharp usa a API de alto nível com assinaturas de candles e uma classe de indicador dedicada que reproduz os pesos do kernel FATL e a suavização Jurik. O indicador gera o valor JFATL arredondado junto com os códigos de cor atual e anterior necessários para a detecção de sinais.

Lógica do indicador

  1. Convolução FATL – os últimos 39 preços (selecionados pela opção de preço aplicado) são ponderados com os coeficientes FATL originais para produzir uma série filtrada.
  2. Suavização Jurik – a saída FATL é passada por uma Jurik Moving Average (JMA). O parâmetro de fase é emulado aplicando um ajuste diferencial que desloca o valor suavizado para frente ou para trás.
  3. Arredondamento de dígitos – o resultado é arredondado para o número especificado de dígitos para imitar a saída "digitalizada" do indicador original.
  4. Atribuição de cor – o buffer de cor é definido como 2 quando o valor atual sobe, 0 quando cai, e caso contrário herda a cor anterior. Um parâmetro configurável SignalBar seleciona qual barra histórica inspecionar, junto com sua barra anterior.

O indicador retorna um valor complexo contendo a leitura JFATL arredondada, a cor em SignalBar, a cor anterior e o tempo de fechamento da barra de sinal. Os manipuladores de estratégia usam essa informação para identificar transições de estado exatamente como no código do MetaTrader.

Regras de negociação

  • Módulo comprado
    • Abre uma posição comprada quando a cor em SignalBar muda para 2 enquanto a cor anterior não era 2 e não há exposição comprada presente.
    • Fecha uma posição comprada existente quando a cor em SignalBar se torna 0.
  • Módulo vendido
    • Abre uma posição vendida quando a cor em SignalBar muda para 0 enquanto a cor anterior estava acima de 0 e não há exposição vendida presente.
    • Fecha uma posição vendida existente quando a cor em SignalBar se torna 2.
  • Gerenciamento de posição – as ordens são dimensionadas para eliminar a exposição oposta antes de abrir uma nova negociação no outro lado. ClosePosition() é usado para saídas de modo que a estratégia mantém uma única posição líquida a qualquer momento.

Gestão de risco

Cada módulo tem distâncias individuais de stop-loss e take-profit expressas em passos de preço. Quando uma nova posição é aberta, a estratégia registra o preço de entrada e calcula os níveis de proteção usando o PriceStep atual do instrumento. Em cada atualização do indicador, a máxima/mínima do candle correspondente é testada contra os níveis armazenados:

  • Para negociações compradas, a estratégia fecha a posição se a mínima do candle atingir o preço de stop ou a máxima do candle atingir o preço de take-profit.
  • Para negociações vendidas, a lógica é espelhada usando a máxima do candle para o stop e a mínima para o take-profit.

Desabilitar o stop ou take definindo a distância como zero deixa a negociação sem gerenciamento até que o indicador emita um sinal de saída.

Parâmetros

Grupo Parâmetro Descrição
Geral LongCandleType Período usado para a assinatura do indicador comprado.
Geral ShortCandleType Período usado para a assinatura do indicador vendido.
Indicador (Long) LongJmaLength Comprimento da média móvel Jurik para o módulo comprado.
Indicador (Long) LongJmaPhase Ajuste de fase aplicado à saída JMA comprada (intervalo −100…100).
Indicador (Long) LongAppliedPrice Fonte de preço aplicado usada na convolução FATL.
Indicador (Long) LongDigit Número de dígitos usados para arredondar o valor do indicador.
Indicador (Long) LongSignalBar Offset de barra histórica inspecionada para sinais (0 = barra fechada atual).
Risco (Long) LongStopLossPoints Distância de stop-loss para comprados medida em passos de preço.
Risco (Long) LongTakeProfitPoints Distância de take-profit para comprados medida em passos de preço.
Negociação (Long) EnableLongOpen Habilita ou desabilita novas entradas compradas.
Negociação (Long) EnableLongClose Habilita ou desabilita saídas compradas geradas pelo indicador.
Indicador (Short) ShortJmaLength Comprimento da média móvel Jurik para o módulo vendido.
Indicador (Short) ShortJmaPhase Ajuste de fase aplicado à saída JMA vendida.
Indicador (Short) ShortAppliedPrice Fonte de preço aplicado para o indicador vendido.
Indicador (Short) ShortDigit Número de dígitos usados para arredondar o valor do indicador vendido.
Indicador (Short) ShortSignalBar Offset de barra histórica inspecionada para sinais vendidos.
Risco (Short) ShortStopLossPoints Distância de stop-loss para vendidos medida em passos de preço.
Risco (Short) ShortTakeProfitPoints Distância de take-profit para vendidos medida em passos de preço.
Negociação (Short) EnableShortOpen Habilita ou desabilita novas entradas vendidas.
Negociação (Short) EnableShortClose Habilita ou desabilita saídas vendidas geradas pelo indicador.

Notas de uso

  1. Atribua tipos de candle apropriados para os módulos comprado e vendido. Eles podem apontar para diferentes períodos se desejado.
  2. Configure o preço aplicado e os dígitos de arredondamento para corresponder às características do instrumento do Consultor Especialista original.
  3. O parâmetro SignalBar controla quantos candles fechados atrás o sinal é validado. Defina-o como 1 para replicar o padrão MT5 (candle completado anterior).
  4. Certifique-se de que a propriedade Volume da estratégia reflita o tamanho de negociação desejado. Ao reverter posições, a estratégia adiciona automaticamente a magnitude da exposição existente para que a posição líquida mude corretamente.
  5. Stops e alvos dependem do PriceStep do instrumento. Para instrumentos sem um tamanho de tick definido, os offsets padrão para passos numéricos brutos.

Notas de conversão

  • O parâmetro de fase Jurik no StockSharp é emulado aplicando um ajuste diferencial de avanço/atraso porque o JurikMovingAverage empacotado não expõe uma propriedade de fase direta. Isso preserva o comportamento do especialista original, incluindo respostas agressivas ou atrasadas.
  • A estratégia usa um modelo de posição líquida única. A versão MetaTrader poderia executar múltiplas ordens por direção; no StockSharp a lógica as consolida em uma exposição comprada ou vendida por vez.
  • Os níveis de proteção são avaliados em cada fechamento de candle do indicador em vez de em cada tick. Isso corresponde à frequência de sinal do especialista MT5 e mantém a implementação dentro das diretrizes da API de alto nível.

Arquivos

  • CS/ColorJfatlDigitDuplexStrategy.cs – implementação da estratégia com o indicador personalizado.
  • README.md / README_zh.md / README_ru.md – documentação em inglês, chinês e russo.
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) { }
	}
}