Color JFATL Digit TM Strategy
Overview
The Color JFATL Digit TM Strategy is a port of the original MetaTrader 5 expert advisor that combines a Jurik-filtered FATL (Fast Adaptive Trend Line) with color-based state transitions and an optional trading session filter. The strategy monitors the slope of the smoothed FATL line: each bar is classified as bullish (color = 2), bearish (color = 0) or neutral (color = 1). Changes in these color states trigger entries, exits and position management while respecting configurable session hours, stop-loss and take-profit distances.
Logic
Custom indicator replication
- The FATL value is calculated by convolving the selected applied price with the original weight table of 39 coefficients.
- The result is smoothed using StockSharp's
JurikMovingAverage. If the library exposes aPhaseproperty it is configured via reflection to mirror the MT5 inputs. - The smoothed value is rounded to instrument precision by multiplying the price step by
10^DigitRounding, reproducing theDigitparameter from MQL5. - The difference between the current rounded value and the previous one defines the color for the bar (
2 = rising,0 = falling,1 = unchanged / inherited).
Signal evaluation
- A ring buffer keeps the most recent color codes. The
SignalBarparameter selects how many completed bars to skip (default = 1, i.e., previous closed bar). - A long entry is triggered when the preceding color was bullish (
2) and the most recent color is anything other than bullish (< 2). - A short entry is triggered when the preceding color was bearish (
0) and the most recent color is anything other than bearish (> 0). - A long exit occurs whenever the preceding color becomes bearish (
0). - A short exit occurs whenever the preceding color becomes bullish (
2). - Entries are skipped when a position already exists, replicating the single-position behaviour of the MT5 expert.
- A ring buffer keeps the most recent color codes. The
Session control and protection
- Optional session filtering (
EnableTimeFilter) mirrors the MT5 hour/minute logic, including overnight sessions when the start hour is greater than the end hour. - Whenever trading is outside the permitted window all open positions are liquidated immediately, matching the original expert.
- Stop-loss and take-profit distances expressed in points are converted into price units using the security price step and passed to
StartProtection.
- Optional session filtering (
Parameters
OrderVolume– volume per order (used for both buy and sell entries).EnableTimeFilter,StartHour,StartMinute,EndHour,EndMinute– session window settings.StopLossPoints,TakeProfitPoints– protective distances in points (0 disables the respective leg).BuyOpenEnabled,SellOpenEnabled,BuyCloseEnabled,SellCloseEnabled– enable or disable long/short entries and exits individually.SignalCandleType– timeframe used for the custom indicator and trading signals (default 4-hour candles).JmaLength,JmaPhase– Jurik smoothing settings (phase honoured when the underlying indicator exposes it).AppliedPriceMode– applied price enumeration identical to the MT5 version (close, open, median, typical, TrendFollow variants, Demark, etc.).DigitRounding– rounding multiplier that mimics theDigitinput of the MQL indicator.SignalBar– how many closed bars to look back when evaluating color transitions (default 1).
Notes
- The strategy uses
SubscribeCandlesand high-level order helpers (BuyMarket,SellMarket) as recommended by the StockSharp conversion guidelines. - Jurik phase is applied via reflection; if the runtime implementation does not expose a
Phaseproperty the default behaviour is used automatically. - Rounding requires a valid
Security.PriceStep. When unavailable, indicator values remain unrounded. - No Python version is provided, as requested.
Usage
- Attach the strategy to a security and connection capable of providing the configured
SignalCandleType. - Configure the applied price, Jurik parameters, session times and money-management inputs as desired.
- Start the strategy; it will manage a single position, respecting stop-loss/take-profit protections and the color-driven signals described above.
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;
using System.Reflection;
using StockSharp.Algo;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// Strategy based on the Color JFATL Digit indicator with optional trading window and money management controls.
/// Detects color transitions produced by the smoothed FATL curve and opens or closes positions accordingly.
/// </summary>
public class ColorJfatlDigitTmStrategy : Strategy
{
private static readonly decimal[] FatlCoefficients =
[
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,
];
private readonly StrategyParam<decimal> _orderVolume;
private readonly StrategyParam<bool> _enableTimeFilter;
private readonly StrategyParam<int> _startHour;
private readonly StrategyParam<int> _startMinute;
private readonly StrategyParam<int> _endHour;
private readonly StrategyParam<int> _endMinute;
private readonly StrategyParam<int> _stopLossPoints;
private readonly StrategyParam<int> _takeProfitPoints;
private readonly StrategyParam<bool> _buyOpen;
private readonly StrategyParam<bool> _sellOpen;
private readonly StrategyParam<bool> _buyClose;
private readonly StrategyParam<bool> _sellClose;
private readonly StrategyParam<DataType> _signalCandleType;
private readonly StrategyParam<int> _jmaLength;
private readonly StrategyParam<int> _jmaPhase;
private readonly StrategyParam<AppliedPrices> _appliedPrice;
private readonly StrategyParam<int> _digitRounding;
private readonly StrategyParam<int> _signalBar;
private ExponentialMovingAverage _jma;
private readonly List<decimal> _priceBuffer = new();
private readonly List<int> _colorHistory = new();
private decimal? _previousLine;
private DateTimeOffset _nextBuyTime = DateTimeOffset.MinValue;
private DateTimeOffset _nextSellTime = DateTimeOffset.MinValue;
/// <summary>
/// Trading volume per order.
/// </summary>
public decimal OrderVolume
{
get => _orderVolume.Value;
set => _orderVolume.Value = value;
}
/// <summary>
/// Enable or disable the session time filter.
/// </summary>
public bool EnableTimeFilter
{
get => _enableTimeFilter.Value;
set => _enableTimeFilter.Value = value;
}
/// <summary>
/// Session start hour.
/// </summary>
public int StartHour
{
get => _startHour.Value;
set => _startHour.Value = value;
}
/// <summary>
/// Session start minute.
/// </summary>
public int StartMinute
{
get => _startMinute.Value;
set => _startMinute.Value = value;
}
/// <summary>
/// Session end hour.
/// </summary>
public int EndHour
{
get => _endHour.Value;
set => _endHour.Value = value;
}
/// <summary>
/// Session end minute.
/// </summary>
public int EndMinute
{
get => _endMinute.Value;
set => _endMinute.Value = value;
}
/// <summary>
/// Stop loss distance expressed in points.
/// </summary>
public int StopLossPoints
{
get => _stopLossPoints.Value;
set => _stopLossPoints.Value = value;
}
/// <summary>
/// Take profit distance expressed in points.
/// </summary>
public int TakeProfitPoints
{
get => _takeProfitPoints.Value;
set => _takeProfitPoints.Value = value;
}
/// <summary>
/// Allow long entries.
/// </summary>
public bool BuyOpenEnabled
{
get => _buyOpen.Value;
set => _buyOpen.Value = value;
}
/// <summary>
/// Allow short entries.
/// </summary>
public bool SellOpenEnabled
{
get => _sellOpen.Value;
set => _sellOpen.Value = value;
}
/// <summary>
/// Allow long exits.
/// </summary>
public bool BuyCloseEnabled
{
get => _buyClose.Value;
set => _buyClose.Value = value;
}
/// <summary>
/// Allow short exits.
/// </summary>
public bool SellCloseEnabled
{
get => _sellClose.Value;
set => _sellClose.Value = value;
}
/// <summary>
/// Candle type used for signal calculation.
/// </summary>
public DataType SignalCandleType
{
get => _signalCandleType.Value;
set => _signalCandleType.Value = value;
}
/// <summary>
/// Jurik moving average length.
/// </summary>
public int JmaLength
{
get => _jmaLength.Value;
set => _jmaLength.Value = value;
}
/// <summary>
/// Jurik moving average phase parameter.
/// </summary>
public int JmaPhase
{
get => _jmaPhase.Value;
set => _jmaPhase.Value = value;
}
/// <summary>
/// Applied price mode.
/// </summary>
public AppliedPrices AppliedPriceMode
{
get => _appliedPrice.Value;
set => _appliedPrice.Value = value;
}
/// <summary>
/// Precision multiplier used for rounding indicator values.
/// </summary>
public int DigitRounding
{
get => _digitRounding.Value;
set => _digitRounding.Value = value;
}
/// <summary>
/// Number of bars to shift when evaluating color transitions.
/// </summary>
public int SignalBar
{
get => _signalBar.Value;
set => _signalBar.Value = value;
}
/// <summary>
/// Initializes a new instance of the <see cref="ColorJfatlDigitTmStrategy"/> class.
/// </summary>
public ColorJfatlDigitTmStrategy()
{
_orderVolume = Param(nameof(OrderVolume), 1m)
.SetGreaterThanZero()
.SetDisplay("Order Volume", "Trade volume per position", "Risk")
.SetOptimize(0.5m, 5m, 0.5m);
_enableTimeFilter = Param(nameof(EnableTimeFilter), false)
.SetDisplay("Enable Time Filter", "Restrict trading to session hours", "Session");
_startHour = Param(nameof(StartHour), 0)
.SetDisplay("Start Hour", "Session start hour", "Session");
_startMinute = Param(nameof(StartMinute), 0)
.SetDisplay("Start Minute", "Session start minute", "Session");
_endHour = Param(nameof(EndHour), 23)
.SetDisplay("End Hour", "Session end hour", "Session");
_endMinute = Param(nameof(EndMinute), 59)
.SetDisplay("End Minute", "Session end minute", "Session");
_stopLossPoints = Param(nameof(StopLossPoints), 1000)
.SetNotNegative()
.SetDisplay("Stop Loss (points)", "Protective stop in points", "Risk");
_takeProfitPoints = Param(nameof(TakeProfitPoints), 2000)
.SetNotNegative()
.SetDisplay("Take Profit (points)", "Take profit in points", "Risk");
_buyOpen = Param(nameof(BuyOpenEnabled), true)
.SetDisplay("Enable Buy Open", "Allow opening long positions", "Signals");
_sellOpen = Param(nameof(SellOpenEnabled), true)
.SetDisplay("Enable Sell Open", "Allow opening short positions", "Signals");
_buyClose = Param(nameof(BuyCloseEnabled), true)
.SetDisplay("Enable Buy Close", "Allow closing long positions", "Signals");
_sellClose = Param(nameof(SellCloseEnabled), true)
.SetDisplay("Enable Sell Close", "Allow closing short positions", "Signals");
_signalCandleType = Param(nameof(SignalCandleType), TimeSpan.FromHours(1).TimeFrame())
.SetDisplay("Signal Candle Type", "Timeframe used for indicator", "Indicator");
_jmaLength = Param(nameof(JmaLength), 14)
.SetGreaterThanZero()
.SetDisplay("JMA Length", "Period for Jurik moving average", "Indicator")
.SetOptimize(3, 30, 1);
_jmaPhase = Param(nameof(JmaPhase), -100)
.SetDisplay("JMA Phase", "Phase shift for Jurik moving average", "Indicator");
_appliedPrice = Param(nameof(AppliedPriceMode), AppliedPrices.Close)
.SetDisplay("Applied Price", "Price source for calculations", "Indicator");
_digitRounding = Param(nameof(DigitRounding), 0)
.SetNotNegative()
.SetDisplay("Digit Rounding", "Rounding precision multiplier", "Indicator");
_signalBar = Param(nameof(SignalBar), 1)
.SetGreaterThanZero()
.SetDisplay("Signal Bar", "Shift for analyzing colors", "Signals");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, SignalCandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_jma = null;
_priceBuffer.Clear();
_colorHistory.Clear();
_previousLine = null;
_nextBuyTime = DateTimeOffset.MinValue;
_nextSellTime = DateTimeOffset.MinValue;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
Volume = OrderVolume;
ConfigureJma();
var subscription = SubscribeCandles(SignalCandleType);
subscription.Bind(ProcessCandle).Start();
var priceStep = Security?.PriceStep ?? 0m;
Unit takeProfitUnit = null;
Unit stopLossUnit = null;
if (TakeProfitPoints > 0 && priceStep > 0m)
takeProfitUnit = new Unit(TakeProfitPoints * priceStep, UnitTypes.Absolute);
if (StopLossPoints > 0 && priceStep > 0m)
stopLossUnit = new Unit(StopLossPoints * priceStep, UnitTypes.Absolute);
StartProtection(takeProfit: takeProfitUnit, stopLoss: stopLossUnit);
}
private void ConfigureJma()
{
_jma = new ExponentialMovingAverage { Length = JmaLength };
}
private void ProcessCandle(ICandleMessage candle)
{
if (candle.State != CandleStates.Finished)
return;
var price = GetAppliedPrice(candle);
_priceBuffer.Add(price);
if (_priceBuffer.Count > FatlCoefficients.Length)
_priceBuffer.RemoveAt(0);
if (_priceBuffer.Count < FatlCoefficients.Length)
return;
var fatl = 0m;
for (var i = 0; i < FatlCoefficients.Length; i++)
{
var value = _priceBuffer[_priceBuffer.Count - 1 - i];
fatl += FatlCoefficients[i] * value;
}
var jmaValue = _jma.Process(new DecimalIndicatorValue(_jma, fatl, candle.OpenTime) { IsFinal = true });
if (!_jma.IsFormed)
return;
var roundedLine = RoundToStep(jmaValue.ToDecimal(), GetRoundingStep());
var color = 1;
if (_previousLine.HasValue)
{
var diff = roundedLine - _previousLine.Value;
if (diff > 0m)
color = 2;
else if (diff < 0m)
color = 0;
else if (_colorHistory.Count > 0)
color = _colorHistory[0];
}
_previousLine = roundedLine;
_colorHistory.Insert(0, color);
if (_colorHistory.Count > 100)
_colorHistory.RemoveAt(_colorHistory.Count - 1);
if (_colorHistory.Count <= SignalBar)
return;
var currentColor = _colorHistory[SignalBar - 1];
var previousColor = _colorHistory[SignalBar];
var now = candle.CloseTime;
var inSession = !EnableTimeFilter || IsWithinTradingWindow(now);
if (EnableTimeFilter && !inSession)
{
ClosePositions();
return;
}
// No bound indicators, always allow trading.
var buyOpenSignal = BuyOpenEnabled && currentColor == 2 && previousColor != 2;
var sellCloseSignal = SellCloseEnabled && currentColor == 2;
var sellOpenSignal = SellOpenEnabled && currentColor == 0 && previousColor != 0;
var buyCloseSignal = BuyCloseEnabled && currentColor == 0;
if (buyCloseSignal && Position > 0)
SellMarket();
if (sellCloseSignal && Position < 0)
BuyMarket();
if (buyOpenSignal && Position == 0 && now >= _nextBuyTime)
{
BuyMarket();
_nextBuyTime = now;
}
if (sellOpenSignal && Position == 0 && now >= _nextSellTime)
{
SellMarket();
_nextSellTime = now;
}
}
private void ClosePositions()
{
if (Position > 0)
SellMarket();
else if (Position < 0)
BuyMarket();
}
private decimal GetAppliedPrice(ICandleMessage candle)
{
return AppliedPriceMode switch
{
AppliedPrices.Close => candle.ClosePrice,
AppliedPrices.Open => candle.OpenPrice,
AppliedPrices.High => candle.HighPrice,
AppliedPrices.Low => candle.LowPrice,
AppliedPrices.Median => (candle.HighPrice + candle.LowPrice) / 2m,
AppliedPrices.Typical => (candle.HighPrice + candle.LowPrice + candle.ClosePrice) / 3m,
AppliedPrices.Weighted => (2m * candle.ClosePrice + candle.HighPrice + candle.LowPrice) / 4m,
AppliedPrices.Simple => (candle.OpenPrice + candle.ClosePrice) / 2m,
AppliedPrices.Quarter => (candle.OpenPrice + candle.ClosePrice + candle.HighPrice + candle.LowPrice) / 4m,
AppliedPrices.TrendFollow0 => candle.ClosePrice > candle.OpenPrice
? candle.HighPrice
: candle.ClosePrice < candle.OpenPrice
? candle.LowPrice
: candle.ClosePrice,
AppliedPrices.TrendFollow1 => candle.ClosePrice > candle.OpenPrice
? (candle.HighPrice + candle.ClosePrice) / 2m
: candle.ClosePrice < candle.OpenPrice
? (candle.LowPrice + candle.ClosePrice) / 2m
: candle.ClosePrice,
AppliedPrices.Demark => CalculateDemarkPrice(candle),
_ => candle.ClosePrice,
};
}
private static decimal CalculateDemarkPrice(ICandleMessage candle)
{
var res = candle.HighPrice + candle.LowPrice + candle.ClosePrice;
if (candle.ClosePrice < candle.OpenPrice)
res = (res + candle.LowPrice) / 2m;
else if (candle.ClosePrice > candle.OpenPrice)
res = (res + candle.HighPrice) / 2m;
else
res = (res + candle.ClosePrice) / 2m;
return ((res - candle.LowPrice) + (res - candle.HighPrice)) / 2m;
}
private decimal GetRoundingStep()
{
var step = Security?.PriceStep ?? 0m;
if (step <= 0m)
return 0m;
var multiplier = (decimal)Math.Pow(10, DigitRounding);
return step * multiplier;
}
private static decimal RoundToStep(decimal value, decimal step)
{
if (step <= 0m)
return value;
return Math.Round(value / step, MidpointRounding.AwayFromZero) * step;
}
private bool IsWithinTradingWindow(DateTimeOffset time)
{
var hour = time.Hour;
var minute = time.Minute;
if (StartHour < EndHour)
{
if (hour == StartHour && minute >= StartMinute)
return true;
if (hour > StartHour && hour < EndHour)
return true;
if (hour > StartHour && hour == EndHour && minute < EndMinute)
return true;
}
else if (StartHour == EndHour)
{
if (hour == StartHour && minute >= StartMinute && minute < EndMinute)
return true;
}
else
{
if (hour > StartHour || (hour == StartHour && minute >= StartMinute))
return true;
if (hour < EndHour)
return true;
if (hour == EndHour && minute < EndMinute)
return true;
}
return false;
}
/// <summary>
/// Applied price options replicated from the original MQL implementation.
/// </summary>
public enum AppliedPrices
{
Close = 1,
Open,
High,
Low,
Median,
Typical,
Weighted,
Simple,
Quarter,
TrendFollow0,
TrendFollow1,
Demark,
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Strategies")
import math
from System import TimeSpan, Decimal, DateTime
from StockSharp.Messages import DataType, CandleStates, Unit, UnitTypes
from StockSharp.Algo.Strategies import Strategy
class color_jfatl_digit_tm_strategy(Strategy):
"""Color JFATL Digit indicator strategy with trading window and SL/TP."""
FATL_COEFFICIENTS = [
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,
]
def __init__(self):
super(color_jfatl_digit_tm_strategy, self).__init__()
self._order_volume = self.Param("OrderVolume", Decimal(1)) \
.SetGreaterThanZero() \
.SetDisplay("Order Volume", "Trade volume per position", "Risk")
self._enable_time_filter = self.Param("EnableTimeFilter", False) \
.SetDisplay("Enable Time Filter", "Restrict trading to session hours", "Session")
self._start_hour = self.Param("StartHour", 0) \
.SetDisplay("Start Hour", "Session start hour", "Session")
self._start_minute = self.Param("StartMinute", 0) \
.SetDisplay("Start Minute", "Session start minute", "Session")
self._end_hour = self.Param("EndHour", 23) \
.SetDisplay("End Hour", "Session end hour", "Session")
self._end_minute = self.Param("EndMinute", 59) \
.SetDisplay("End Minute", "Session end minute", "Session")
self._stop_loss_points = self.Param("StopLossPoints", 1000) \
.SetDisplay("Stop Loss (points)", "Protective stop in points", "Risk")
self._take_profit_points = self.Param("TakeProfitPoints", 2000) \
.SetDisplay("Take Profit (points)", "Take profit in points", "Risk")
self._buy_open = self.Param("BuyOpenEnabled", True) \
.SetDisplay("Enable Buy Open", "Allow opening long positions", "Signals")
self._sell_open = self.Param("SellOpenEnabled", True) \
.SetDisplay("Enable Sell Open", "Allow opening short positions", "Signals")
self._buy_close = self.Param("BuyCloseEnabled", True) \
.SetDisplay("Enable Buy Close", "Allow closing long positions", "Signals")
self._sell_close = self.Param("SellCloseEnabled", True) \
.SetDisplay("Enable Sell Close", "Allow closing short positions", "Signals")
self._candle_type = self.Param("SignalCandleType", DataType.TimeFrame(TimeSpan.FromHours(1))) \
.SetDisplay("Signal Candle Type", "Timeframe used for indicator", "Indicator")
self._jma_length = self.Param("JmaLength", 14) \
.SetGreaterThanZero() \
.SetDisplay("JMA Length", "Period for Jurik moving average", "Indicator")
self._jma_phase = self.Param("JmaPhase", -100) \
.SetDisplay("JMA Phase", "Phase shift for Jurik moving average", "Indicator")
self._digit_rounding = self.Param("DigitRounding", 0) \
.SetDisplay("Digit Rounding", "Rounding precision multiplier", "Indicator")
self._signal_bar = self.Param("SignalBar", 1) \
.SetGreaterThanZero() \
.SetDisplay("Signal Bar", "Shift for analyzing colors", "Signals")
self._price_buffer = []
self._color_history = []
self._previous_line = None
self._ema_value = None
self._ema_count = 0
self._next_buy_time = DateTime.MinValue
self._next_sell_time = DateTime.MinValue
@property
def OrderVolume(self):
return self._order_volume.Value
@property
def EnableTimeFilter(self):
return self._enable_time_filter.Value
@property
def StartHour(self):
return self._start_hour.Value
@property
def StartMinute(self):
return self._start_minute.Value
@property
def EndHour(self):
return self._end_hour.Value
@property
def EndMinute(self):
return self._end_minute.Value
@property
def StopLossPoints(self):
return self._stop_loss_points.Value
@property
def TakeProfitPoints(self):
return self._take_profit_points.Value
@property
def BuyOpenEnabled(self):
return self._buy_open.Value
@property
def SellOpenEnabled(self):
return self._sell_open.Value
@property
def BuyCloseEnabled(self):
return self._buy_close.Value
@property
def SellCloseEnabled(self):
return self._sell_close.Value
@property
def CandleType(self):
return self._candle_type.Value
@property
def JmaLength(self):
return self._jma_length.Value
@property
def JmaPhase(self):
return self._jma_phase.Value
@property
def DigitRounding(self):
return self._digit_rounding.Value
@property
def SignalBar(self):
return self._signal_bar.Value
def OnStarted2(self, time):
super(color_jfatl_digit_tm_strategy, self).OnStarted2(time)
self.Volume = self.OrderVolume
self._ema_value = None
self._ema_count = 0
self._ema_length = self.JmaLength
self._ema_multiplier = 2.0 / (self._ema_length + 1)
subscription = self.SubscribeCandles(self.CandleType)
subscription.Bind(self.process_candle).Start()
price_step = Decimal(0)
sec = self.Security
if sec is not None and sec.PriceStep is not None and sec.PriceStep > Decimal(0):
price_step = sec.PriceStep
tp_unit = None
sl_unit = None
if self.TakeProfitPoints > 0 and price_step > Decimal(0):
tp_unit = Unit(Decimal(self.TakeProfitPoints) * price_step, UnitTypes.Absolute)
if self.StopLossPoints > 0 and price_step > Decimal(0):
sl_unit = Unit(Decimal(self.StopLossPoints) * price_step, UnitTypes.Absolute)
self.StartProtection(takeProfit=tp_unit, stopLoss=sl_unit)
def _process_ema(self, value):
"""Manual EMA matching ExponentialMovingAverage behavior."""
self._ema_count += 1
if self._ema_value is None:
self._ema_value = value
else:
self._ema_value = value * self._ema_multiplier + self._ema_value * (1.0 - self._ema_multiplier)
return self._ema_value
def _ema_is_formed(self):
return self._ema_count >= self._ema_length
def process_candle(self, candle):
if candle.State != CandleStates.Finished:
return
price = float(candle.ClosePrice)
self._price_buffer.append(price)
coeffs_len = len(self.FATL_COEFFICIENTS)
if len(self._price_buffer) > coeffs_len:
self._price_buffer.pop(0)
if len(self._price_buffer) < coeffs_len:
return
fatl = 0.0
for i in range(coeffs_len):
fatl += self.FATL_COEFFICIENTS[i] * self._price_buffer[len(self._price_buffer) - 1 - i]
jma_val = self._process_ema(fatl)
if not self._ema_is_formed():
return
rounding_step = self._get_rounding_step()
rounded_line = self._round_to_step(jma_val, rounding_step)
color = 1
if self._previous_line is not None:
diff = rounded_line - self._previous_line
if diff > 0:
color = 2
elif diff < 0:
color = 0
elif len(self._color_history) > 0:
color = self._color_history[0]
self._previous_line = rounded_line
self._color_history.insert(0, color)
if len(self._color_history) > 100:
self._color_history.pop()
if len(self._color_history) <= self.SignalBar:
return
current_color = self._color_history[self.SignalBar - 1]
previous_color = self._color_history[self.SignalBar]
now = candle.CloseTime
in_session = (not self.EnableTimeFilter) or self._is_within_trading_window(now)
if self.EnableTimeFilter and not in_session:
if self.Position > 0:
self.SellMarket()
elif self.Position < 0:
self.BuyMarket()
return
buy_open_signal = self.BuyOpenEnabled and current_color == 2 and previous_color != 2
sell_close_signal = self.SellCloseEnabled and current_color == 2
sell_open_signal = self.SellOpenEnabled and current_color == 0 and previous_color != 0
buy_close_signal = self.BuyCloseEnabled and current_color == 0
if buy_close_signal and self.Position > 0:
self.SellMarket()
if sell_close_signal and self.Position < 0:
self.BuyMarket()
if buy_open_signal and self.Position == 0 and now >= self._next_buy_time:
self.BuyMarket()
self._next_buy_time = now
if sell_open_signal and self.Position == 0 and now >= self._next_sell_time:
self.SellMarket()
self._next_sell_time = now
def _get_rounding_step(self):
sec = self.Security
if sec is None or sec.PriceStep is None or float(sec.PriceStep) <= 0:
return 0.0
step = float(sec.PriceStep)
multiplier = math.pow(10, self.DigitRounding)
return step * multiplier
def _round_to_step(self, value, step):
if step <= 0:
return value
return round(value / step) * step
def _is_within_trading_window(self, time):
h = time.Hour
m = time.Minute
sh = self.StartHour
sm = self.StartMinute
eh = self.EndHour
em = self.EndMinute
if sh < eh:
if h == sh and m >= sm:
return True
if h > sh and h < eh:
return True
if h > sh and h == eh and m < em:
return True
return False
elif sh == eh:
return h == sh and m >= sm and m < em
else:
if h > sh or (h == sh and m >= sm):
return True
if h < eh:
return True
if h == eh and m < em:
return True
return False
def OnReseted(self):
super(color_jfatl_digit_tm_strategy, self).OnReseted()
self._price_buffer = []
self._color_history = []
self._previous_line = None
self._ema_value = None
self._ema_count = 0
self._next_buy_time = DateTime.MinValue
self._next_sell_time = DateTime.MinValue
def CreateClone(self):
return color_jfatl_digit_tm_strategy()