Стратегия 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.
Логика индикатора
- FATL-фильтр — последние 39 значений (в соответствии с выбранным типом цены) умножаются на исходные веса фильтра FATL, формируя сглаженный ряд.
- Сглаживание Jurik — полученный ряд проходит через Jurik Moving Average. Параметр Phase эмулируется дифференциальной поправкой, которая смещает результат вперёд или назад во времени.
- Округление — значение приводится к заданному количеству знаков (Digit), что соответствует «оцифрованному» выходу индикатора в MetaTrader.
- Определение цвета — если текущее значение больше предыдущего, присваивается цвет 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 |
Разрешение на закрытие коротких позиций по сигналу. |
Рекомендации по использованию
- Настройте подходящие таймфреймы для каждого модуля; при необходимости можно использовать разные интервалы.
- Подберите тип цены и степень округления под характеристики инструмента и параметры оригинального советника.
SignalBarопределяет, сколько закрытых свечей назад ищется сигнал. Значение 1 соответствует стандартной логике MT5 (предыдущая завершённая свеча).- Проверьте свойство
Volumeу стратегии — оно определяет торговый объём. При развороте позиции стратегия автоматически добавляет текущий объём для полного закрытия противоположной стороны. - Стопы и тейки рассчитываются через
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) { }
}
}
import clr
import math
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan, Decimal
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Strategies import Strategy
from StockSharp.Algo.Indicators import JurikMovingAverage
from indicator_extensions import *
# FATL weights from the original C# indicator
_FATL_WEIGHTS = [
0.4360409450, 0.3658689069, 0.2460452079, 0.1104506886,
-0.0054034585, -0.0760367731, -0.0933058722, -0.0670110374,
-0.0190795053, 0.0259609206, 0.0502044896, 0.0477818607,
0.0249252327, -0.0047706151, -0.0272432537, -0.0338917071,
-0.0244141482, -0.0055774838, 0.0128149838, 0.0226522218,
0.0208778257, 0.0100299086, -0.0036771622, -0.0136744850,
-0.0160483392, -0.0108597376, -0.0016060704, 0.0069480557,
0.0110573605, 0.0095711419, 0.0040444064, -0.0023824623,
-0.0067093714, -0.0072003400, -0.0047717710, 0.0005541115,
0.0007860160, 0.0130129076, 0.0040364019,
]
_MAX_FATL_PERIOD = len(_FATL_WEIGHTS)
class _ColorJfatlDigitState(object):
"""Internal JFATL Digit indicator calculator."""
def __init__(self, jma_length, jma_phase, applied_price, digit, signal_bar, fatl_period):
self._jma_length = jma_length
self._jma_phase = max(-100, min(100, jma_phase))
self._applied_price = applied_price # 1=Close,2=Open,3=High,4=Low,5=Med,6=Typ,7=Wt
self._digit = digit
self._signal_bar = max(0, signal_bar)
self._fatl_period = max(1, min(fatl_period, _MAX_FATL_PERIOD))
self._jma = JurikMovingAverage()
self._jma.Length = max(1, jma_length)
self._price_buffer = []
self._history = [] # list of (value, color)
self._previous_raw = None
def process(self, candle):
price = self._get_price(candle)
self._price_buffer.append(price)
if len(self._price_buffer) > _MAX_FATL_PERIOD:
self._price_buffer.pop(0)
if len(self._price_buffer) < self._fatl_period:
return None
fatl = 0.0
for i in range(self._fatl_period):
pi = len(self._price_buffer) - 1 - i
fatl += _FATL_WEIGHTS[i] * self._price_buffer[pi]
jma_val = process_float(self._jma, Decimal(fatl), candle.ServerTime, True)
base_value = float(jma_val.Value)
adjusted = self._apply_phase(base_value)
rounded = round(adjusted, max(0, self._digit))
color = self._calc_color(rounded)
self._history.append((rounded, color))
required = max(5, self._signal_bar + 3)
if len(self._history) > required:
self._history = self._history[-required:]
if len(self._history) <= self._signal_bar:
return None
index = len(self._history) - 1 - self._signal_bar
if index < 1:
return None
entry = self._history[index]
prev_entry = self._history[index - 1]
return (entry[0], entry[1], prev_entry[1])
def _get_price(self, candle):
c = float(candle.ClosePrice)
o = float(candle.OpenPrice)
h = float(candle.HighPrice)
lo = float(candle.LowPrice)
p = self._applied_price
if p == 2:
return o
if p == 3:
return h
if p == 4:
return lo
if p == 5:
return (h + lo) / 2.0
if p == 6:
return (c + h + lo) / 3.0
if p == 7:
return (2.0 * c + h + lo) / 4.0
return c
def _apply_phase(self, base_value):
adjusted = base_value
if self._previous_raw is not None:
diff = base_value - self._previous_raw
adjusted = base_value + diff * (self._jma_phase / 100.0)
self._previous_raw = base_value
return adjusted
def _calc_color(self, current_value):
if len(self._history) == 0:
return 1
prev_value = self._history[-1][0]
diff = current_value - prev_value
if diff > 0:
return 2
if diff < 0:
return 0
return self._history[-1][1]
class color_jfatl_digit_duplex_strategy(Strategy):
"""Duplex strategy using two Color JFATL Digit indicators for independent long/short logic."""
def __init__(self):
super(color_jfatl_digit_duplex_strategy, self).__init__()
self._long_candle_type = self.Param("LongCandleType", DataType.TimeFrame(TimeSpan.FromHours(4))) \
.SetDisplay("Long Candle Type", "Timeframe for the long indicator", "General")
self._short_candle_type = self.Param("ShortCandleType", DataType.TimeFrame(TimeSpan.FromHours(4))) \
.SetDisplay("Short Candle Type", "Timeframe for the short indicator", "General")
self._long_jma_length = self.Param("LongJmaLength", 5) \
.SetGreaterThanZero() \
.SetDisplay("Long JMA Length", "Period of JMA for longs", "Indicator")
self._long_jma_phase = self.Param("LongJmaPhase", -100) \
.SetDisplay("Long JMA Phase", "Phase adjustment for JMA", "Indicator")
# 1=Close,2=Open,3=High,4=Low,5=Med,6=Typ,7=Wt
self._long_applied_price = self.Param("LongAppliedPrice", 1) \
.SetDisplay("Long Applied Price", "Price source for the long indicator", "Indicator")
self._long_digit = self.Param("LongDigit", 2) \
.SetDisplay("Long Rounding Digits", "Digits used to round the indicator", "Indicator")
self._long_signal_bar = self.Param("LongSignalBar", 1) \
.SetDisplay("Long Signal Bar", "Bar shift for long signals", "Indicator")
self._long_stop_loss_points = self.Param("LongStopLossPoints", 1000) \
.SetDisplay("Long Stop Loss (pts)", "Stop loss for long trades", "Risk")
self._long_take_profit_points = self.Param("LongTakeProfitPoints", 2000) \
.SetDisplay("Long Take Profit (pts)", "Take profit for long trades", "Risk")
self._enable_long_open = self.Param("EnableLongOpen", True) \
.SetDisplay("Enable Long Entries", "Allow opening long positions", "Trading")
self._enable_long_close = self.Param("EnableLongClose", True) \
.SetDisplay("Enable Long Exits", "Allow closing long on signals", "Trading")
self._short_jma_length = self.Param("ShortJmaLength", 5) \
.SetGreaterThanZero() \
.SetDisplay("Short JMA Length", "Period of JMA for shorts", "Indicator")
self._short_jma_phase = self.Param("ShortJmaPhase", -100) \
.SetDisplay("Short JMA Phase", "Phase adjustment for JMA", "Indicator")
self._short_applied_price = self.Param("ShortAppliedPrice", 1) \
.SetDisplay("Short Applied Price", "Price source for the short indicator", "Indicator")
self._short_digit = self.Param("ShortDigit", 2) \
.SetDisplay("Short Rounding Digits", "Digits used to round the indicator", "Indicator")
self._short_signal_bar = self.Param("ShortSignalBar", 1) \
.SetDisplay("Short Signal Bar", "Bar shift for short signals", "Indicator")
self._short_stop_loss_points = self.Param("ShortStopLossPoints", 1000) \
.SetDisplay("Short Stop Loss (pts)", "Stop loss for short trades", "Risk")
self._short_take_profit_points = self.Param("ShortTakeProfitPoints", 2000) \
.SetDisplay("Short Take Profit (pts)", "Take profit for short trades", "Risk")
self._enable_short_open = self.Param("EnableShortOpen", True) \
.SetDisplay("Enable Short Entries", "Allow opening short positions", "Trading")
self._enable_short_close = self.Param("EnableShortClose", True) \
.SetDisplay("Enable Short Exits", "Allow closing short on signals", "Trading")
self._fatl_period = self.Param("FatlPeriod", _MAX_FATL_PERIOD) \
.SetDisplay("FATL Period", "Number of bars for the FATL calculation", "Indicator")
self._long_stop_price = None
self._long_take_price = None
self._short_stop_price = None
self._short_take_price = None
@property
def LongCandleType(self):
return self._long_candle_type.Value
@property
def ShortCandleType(self):
return self._short_candle_type.Value
@property
def LongJmaLength(self):
return int(self._long_jma_length.Value)
@property
def LongJmaPhase(self):
return int(self._long_jma_phase.Value)
@property
def LongAppliedPrice(self):
return int(self._long_applied_price.Value)
@property
def LongDigit(self):
return int(self._long_digit.Value)
@property
def LongSignalBar(self):
return int(self._long_signal_bar.Value)
@property
def LongStopLossPoints(self):
return int(self._long_stop_loss_points.Value)
@property
def LongTakeProfitPoints(self):
return int(self._long_take_profit_points.Value)
@property
def EnableLongOpen(self):
return self._enable_long_open.Value
@property
def EnableLongClose(self):
return self._enable_long_close.Value
@property
def ShortJmaLength(self):
return int(self._short_jma_length.Value)
@property
def ShortJmaPhase(self):
return int(self._short_jma_phase.Value)
@property
def ShortAppliedPrice(self):
return int(self._short_applied_price.Value)
@property
def ShortDigit(self):
return int(self._short_digit.Value)
@property
def ShortSignalBar(self):
return int(self._short_signal_bar.Value)
@property
def ShortStopLossPoints(self):
return int(self._short_stop_loss_points.Value)
@property
def ShortTakeProfitPoints(self):
return int(self._short_take_profit_points.Value)
@property
def EnableShortOpen(self):
return self._enable_short_open.Value
@property
def EnableShortClose(self):
return self._enable_short_close.Value
@property
def FatlPeriod(self):
return int(self._fatl_period.Value)
def OnStarted2(self, time):
super(color_jfatl_digit_duplex_strategy, self).OnStarted2(time)
self._long_stop_price = None
self._long_take_price = None
self._short_stop_price = None
self._short_take_price = None
self._long_state = _ColorJfatlDigitState(
self.LongJmaLength, self.LongJmaPhase, self.LongAppliedPrice,
self.LongDigit, self.LongSignalBar, self.FatlPeriod
)
self._short_state = _ColorJfatlDigitState(
self.ShortJmaLength, self.ShortJmaPhase, self.ShortAppliedPrice,
self.ShortDigit, self.ShortSignalBar, self.FatlPeriod
)
long_sub = self.SubscribeCandles(self.LongCandleType)
long_sub.Bind(self._process_long).Start()
short_sub = self.SubscribeCandles(self.ShortCandleType)
short_sub.Bind(self._process_short).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, long_sub)
self.DrawOwnTrades(area)
def _process_long(self, candle):
if candle.State != CandleStates.Finished:
return
if self._check_long_risk(candle):
return
result = self._long_state.process(candle)
if result is None:
return
value, current_color, previous_color = result
if self.EnableLongClose and current_color == 0 and self.Position > 0:
self._close_position()
self._clear_long_risk()
return
if self.EnableLongOpen and current_color == 2 and previous_color < 2 and self.Position <= 0:
self._open_long(float(candle.ClosePrice))
def _process_short(self, candle):
if candle.State != CandleStates.Finished:
return
if self._check_short_risk(candle):
return
result = self._short_state.process(candle)
if result is None:
return
value, current_color, previous_color = result
if self.EnableShortClose and current_color == 2 and self.Position < 0:
self._close_position()
self._clear_short_risk()
return
if self.EnableShortOpen and current_color == 0 and previous_color > 0 and self.Position >= 0:
self._open_short(float(candle.ClosePrice))
def _open_long(self, entry_price):
self.BuyMarket()
self._setup_long_risk(entry_price)
self._clear_short_risk()
def _open_short(self, entry_price):
self.SellMarket()
self._setup_short_risk(entry_price)
self._clear_long_risk()
def _setup_long_risk(self, entry_price):
sec = self.Security
step = float(sec.PriceStep) if sec is not None and sec.PriceStep is not None and float(sec.PriceStep) > 0 else 1.0
self._long_stop_price = entry_price - self.LongStopLossPoints * step if self.LongStopLossPoints > 0 else None
self._long_take_price = entry_price + self.LongTakeProfitPoints * step if self.LongTakeProfitPoints > 0 else None
def _setup_short_risk(self, entry_price):
sec = self.Security
step = float(sec.PriceStep) if sec is not None and sec.PriceStep is not None and float(sec.PriceStep) > 0 else 1.0
self._short_stop_price = entry_price + self.ShortStopLossPoints * step if self.ShortStopLossPoints > 0 else None
self._short_take_price = entry_price - self.ShortTakeProfitPoints * step if self.ShortTakeProfitPoints > 0 else None
def _check_long_risk(self, candle):
if self.Position <= 0:
self._clear_long_risk()
return False
h = float(candle.HighPrice)
lo = float(candle.LowPrice)
if self._long_stop_price is not None and lo <= self._long_stop_price:
self._close_position()
self._clear_long_risk()
return True
if self._long_take_price is not None and h >= self._long_take_price:
self._close_position()
self._clear_long_risk()
return True
return False
def _check_short_risk(self, candle):
if self.Position >= 0:
self._clear_short_risk()
return False
h = float(candle.HighPrice)
lo = float(candle.LowPrice)
if self._short_stop_price is not None and h >= self._short_stop_price:
self._close_position()
self._clear_short_risk()
return True
if self._short_take_price is not None and lo <= self._short_take_price:
self._close_position()
self._clear_short_risk()
return True
return False
def _clear_long_risk(self):
self._long_stop_price = None
self._long_take_price = None
def _clear_short_risk(self):
self._short_stop_price = None
self._short_take_price = None
def _close_position(self):
if self.Position > 0:
self.SellMarket()
elif self.Position < 0:
self.BuyMarket()
def OnReseted(self):
super(color_jfatl_digit_duplex_strategy, self).OnReseted()
self._long_stop_price = None
self._long_take_price = None
self._short_stop_price = None
self._short_take_price = None
def CreateClone(self):
return color_jfatl_digit_duplex_strategy()