Открыть на GitHub

Стратегия Color JFATL Digit Duplex

Обзор

Color JFATL Digit Duplex — это конвертация эксперта MetaTrader 5 Exp_ColorJFatl_Digit_Duplex на платформу StockSharp. Стратегия состоит из двух независимых модулей: первый обслуживает длинные позиции, второй — короткие. Оба модуля используют индикатор Color Jurik Fast Adaptive Trend Line (JFATL), который окрашивает линию в зависимости от направления локального тренда. Каждый блок имеет собственные параметры сглаживания, источник цены, глубину округления, сдвиг сигнальной свечи и отступы стопов/тейков.

В реализации StockSharp применяется высокоуровневый API: свечи запрашиваются через SubscribeCandles, а логика индикатора вынесена в отдельный класс. Индикатор воспроизводит исходные коэффициенты FATL, выполняет сглаживание Jurik Moving Average, округляет результат и возвращает цвет текущей и предыдущей свечей для точного определения сигналов, как в оригинальном коде MQL5.

Логика индикатора

  1. FATL-фильтр — последние 39 значений (в соответствии с выбранным типом цены) умножаются на исходные веса фильтра FATL, формируя сглаженный ряд.
  2. Сглаживание Jurik — полученный ряд проходит через Jurik Moving Average. Параметр Phase эмулируется дифференциальной поправкой, которая смещает результат вперёд или назад во времени.
  3. Округление — значение приводится к заданному количеству знаков (Digit), что соответствует «оцифрованному» выходу индикатора в MetaTrader.
  4. Определение цвета — если текущее значение больше предыдущего, присваивается цвет 2 (бычий); если меньше — цвет 0 (медвежий); при равенстве используется прошлый цвет. Параметр SignalBar указывает, какую из завершённых свечей анализировать вместе с предшествующей ей свечой.

Индикатор возвращает комплексное значение: округлённый JFATL, текущий цвет, предыдущий цвет и время закрытия сигнальной свечи. Стратегия использует эти данные для генерации торговых команд.

Правила торговли

  • Длинный модуль
    • Открывает покупку, когда цвет на SignalBar изменяется на 2, а предыдущий цвет был отличен от 2 и текущая позиция не положительная.
    • Закрывает длинную позицию, когда цвет на SignalBar становится 0.
  • Короткий модуль
    • Открывает продажу, когда цвет на SignalBar меняется на 0, предыдущий цвет был > 0 и текущая позиция не отрицательная.
    • Закрывает короткую позицию, когда цвет на SignalBar принимает значение 2.
  • Управление позицией — при открытии противоположной позиции стратегия добавляет величину текущего объёма, чтобы полностью закрыть прежнее направление. Для выхода используется ClosePosition(), поэтому одновременно поддерживается только одна чистая позиция.

Управление рисками

Для длинной и короткой частей задаются собственные стоп-лоссы и тейк-профиты в шагах цены. После открытия сделки фиксируется цена входа, и на её основе рассчитываются уровни защиты с учётом Security.PriceStep. На каждом завершении свечи, поступающей от индикатора, выполняется проверка:

  • Для длинных позиций при пробитии минимумом стоп-уровня или максимумом тейк-уровня позиция закрывается.
  • Для коротких позиций условия зеркальны: максимум тестирует стоп, минимум — тейк.

Если расстояние равно нулю, соответствующая защита отключена, и выход осуществляется только по сигналу индикатора.

Параметры

Группа Параметр Описание
Общие LongCandleType Тип свечей/таймфрейм для длинного индикатора.
Общие ShortCandleType Тип свечей для короткого индикатора.
Индикатор (Long) LongJmaLength Длина Jurik Moving Average для длинного модуля.
Индикатор (Long) LongJmaPhase Фазовый сдвиг Jurik (диапазон −100…100).
Индикатор (Long) LongAppliedPrice Источник цен, используемый в фильтре FATL.
Индикатор (Long) LongDigit Количество знаков при округлении значения индикатора.
Индикатор (Long) LongSignalBar Номер завершённой свечи, по которой анализируется сигнал.
Риск (Long) LongStopLossPoints Стоп-лосс в шагах цены для длинных позиций.
Риск (Long) LongTakeProfitPoints Тейк-профит в шагах цены для длинных позиций.
Торговля (Long) EnableLongOpen Разрешение на открытие новых длинных позиций.
Торговля (Long) EnableLongClose Разрешение на закрытие длинных позиций по сигналу.
Индикатор (Short) ShortJmaLength Длина Jurik Moving Average для короткого модуля.
Индикатор (Short) ShortJmaPhase Фазовый сдвиг Jurik для короткого модуля.
Индикатор (Short) ShortAppliedPrice Источник цен для короткого индикатора.
Индикатор (Short) ShortDigit Количество знаков округления для короткого индикатора.
Индикатор (Short) ShortSignalBar Номер свечи для анализа коротких сигналов.
Риск (Short) ShortStopLossPoints Стоп-лосс в шагах цены для коротких позиций.
Риск (Short) ShortTakeProfitPoints Тейк-профит в шагах цены для коротких позиций.
Торговля (Short) EnableShortOpen Разрешение на открытие новых коротких позиций.
Торговля (Short) EnableShortClose Разрешение на закрытие коротких позиций по сигналу.

Рекомендации по использованию

  1. Настройте подходящие таймфреймы для каждого модуля; при необходимости можно использовать разные интервалы.
  2. Подберите тип цены и степень округления под характеристики инструмента и параметры оригинального советника.
  3. SignalBar определяет, сколько закрытых свечей назад ищется сигнал. Значение 1 соответствует стандартной логике MT5 (предыдущая завершённая свеча).
  4. Проверьте свойство Volume у стратегии — оно определяет торговый объём. При развороте позиции стратегия автоматически добавляет текущий объём для полного закрытия противоположной стороны.
  5. Стопы и тейки рассчитываются через PriceStep. Если шаг цены неизвестен, используется прямой числовой отступ.

Особенности конверсии

  • Поскольку библиотека StockSharp не предоставляет явного свойства Phase у JurikMovingAverage, фазовый сдвиг реализован через дифференциальную поправку, что сохраняет характер реагирования, знакомый по MQL5.
  • В отличие от оригинала, здесь используется единая нетто-позиция вместо множества отдельных ордеров. Это лучше сочетается с моделью портфеля StockSharp.
  • Контроль стопов и тейков выполняется на закрытии свечей индикаторного таймфрейма. Такой подход соответствует требованиям высокоуровневого API и совпадает с частотой сигналов исходной стратегии.

Состав репозитория

  • CS/ColorJfatlDigitDuplexStrategy.cs — код стратегии и индикатора.
  • README.md / README_zh.md / README_ru.md — документация на английском, китайском и русском языках.
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) { }
	}
}