Color JFATL Digit Duplex 策略
概述
Color JFATL Digit Duplex 策略源自 MetaTrader 5 专家顾问 Exp_ColorJFatl_Digit_Duplex,在 StockSharp 高级 API 中实现为双模块系统。策略同时运行多头与空头两套信号流,全部基于 Color Jurik Fast Adaptive Trend Line(JFATL)指标。多头模块在颜色转为多头(值为 2)时尝试开仓,空头模块在颜色转为空头(值为 0)时入场。每个模块都拥有独立的平滑、价格源、数字化精度、信号柱偏移以及止损/止盈距离设置。
本转换版本实现了原始 FATL 核心权重与 Jurik 平滑算法,并将结果数字化,以便与 MetaTrader 指标保持一致。指标向策略处理器返回目标柱的颜色编码以及前一柱的颜色,从而能够完全复现原策略的触发条件。
指标逻辑
- FATL 卷积:根据所选价格类型获取最近 39 根数据,并使用原始 FATL 权重计算滤波值。
- Jurik 平滑:将 FATL 输出送入 Jurik Moving Average。由于 StockSharp 版本没有公开的相位属性,本实现通过差分调整模拟 Phase 参数带来的超前/滞后效果。
- 数字化处理:按照设定的位数对平滑结果进行四舍五入,生成与原指标一致的“Digit”输出。
- 颜色判定:若当前值高于上一值,颜色置为 2;低于上一值置为 0;否则沿用上一颜色。
SignalBar参数决定向前回看几根已完成的柱,并同时获取更早一根的颜色值。
指标以复合值形式返回:包括数字化后的 JFATL、当前颜色、前一颜色以及信号柱收盘时间。策略逻辑据此判断颜色变化并生成交易信号。
交易规则
- 多头模块
- 当
SignalBar对应的颜色由非 2 变为 2 且当前无多头持仓时开多。 - 当
SignalBar颜色变为 0 时平掉现有多头。
- 当
- 空头模块
- 当
SignalBar颜色由大于 0 变为 0 且当前无空头持仓时开空。 - 当
SignalBar颜色变为 2 时平掉现有空头。
- 当
- 仓位管理:开仓时会先使用市场单抵消相反方向的持仓,确保任意时刻仅保持一个净仓位。平仓使用
ClosePosition(),避免在帐户中同时存在多笔订单。
风险控制
多头与空头模块分别设定以价格最小变动单位计的止损与止盈距离。开仓后记录入场价并依据 PriceStep 计算绝对价格目标。在每次指标更新(即订阅的蜡烛收盘)时检查当前蜡烛的高低点:
- 多头:若最低价触及止损价或最高价触及止盈价,则立即平仓。
- 空头:若最高价触及止损价或最低价触及止盈价,则立即平仓。
当距离设为 0 时,对应保护措施关闭,仅依靠指标反向信号退出。
参数说明
| 分组 | 参数 | 描述 |
|---|---|---|
| 通用 | LongCandleType |
多头指标使用的蜡烛类型(时间框架)。 |
| 通用 | ShortCandleType |
空头指标使用的蜡烛类型。 |
| 指标(多头) | LongJmaLength |
多头 Jurik 移动平均周期。 |
| 指标(多头) | LongJmaPhase |
多头 Jurik 相位调整(−100 至 100)。 |
| 指标(多头) | LongAppliedPrice |
参与 FATL 卷积的价格源。 |
| 指标(多头) | LongDigit |
数字化位数。 |
| 指标(多头) | LongSignalBar |
信号柱偏移,0 表示最新收盘柱。 |
| 风险(多头) | LongStopLossPoints |
多头止损距离(以 price step 表示)。 |
| 风险(多头) | LongTakeProfitPoints |
多头止盈距离。 |
| 交易(多头) | EnableLongOpen |
是否允许新的多头入场。 |
| 交易(多头) | EnableLongClose |
是否允许根据指标信号平多。 |
| 指标(空头) | ShortJmaLength |
空头 Jurik 移动平均周期。 |
| 指标(空头) | ShortJmaPhase |
空头 Jurik 相位调整。 |
| 指标(空头) | ShortAppliedPrice |
空头模块使用的价格源。 |
| 指标(空头) | ShortDigit |
空头数字化位数。 |
| 指标(空头) | ShortSignalBar |
空头信号柱偏移。 |
| 风险(空头) | ShortStopLossPoints |
空头止损距离。 |
| 风险(空头) | ShortTakeProfitPoints |
空头止盈距离。 |
| 交易(空头) | EnableShortOpen |
是否允许新的空头入场。 |
| 交易(空头) | EnableShortClose |
是否允许根据指标信号平空。 |
使用提示
- 根据需求分别设定多头与空头的蜡烛类型,可使用不同时间框架。
- 调整价格源与数字化位数以贴合目标品种,与原 MT5 设置保持一致。
SignalBar控制回看几根已收盘柱。默认值 1 对应原专家顾问的上一根完成柱。- 请确保策略
Volume属性设置为希望的下单量。策略在翻仓时会自动加上当前仓位的绝对值,以实现一笔反向单即可完成反手。 - 止损止盈依赖
PriceStep。若品种未提供该信息,距离将直接按数值解释。
转换说明
- 由于 StockSharp 的 JurikMovingAverage 没有显式 Phase 属性,本实现通过对平滑输出的差分调整模拟相位效果,从而保持原策略在快速或滞后响应方面的特点。
- 原 MT5 策略可能同时持有多笔订单。本转换采用单一净仓位模型,所有交易都体现在
Strategy.Position上。 - 止损止盈检测在指标蜡烛收盘时进行,与原策略依赖已完成柱的信号频率相符,并满足高阶 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()