Auf GitHub ansehen

Color JFATL Digit TM Strategie

Übersicht

Die Color JFATL Digit TM Strategie ist ein Port des ursprünglichen MetaTrader 5 Expert Advisors, der eine Jurik-gefilterte FATL (Fast Adaptive Trend Line) mit farbbasierten Zustandsübergängen und einem optionalen Trading-Sitzungsfilter kombiniert. Die Strategie überwacht die Steigung der geglätteten FATL-Linie: Jeder Balken wird als bullisch (Farbe = 2), bärisch (Farbe = 0) oder neutral (Farbe = 1) klassifiziert. Änderungen dieser Farbzustände lösen Einstiege, Ausstiege und Positionsverwaltung aus, während konfigurierbare Sitzungsstunden, Stop-Loss- und Take-Profit-Abstände berücksichtigt werden.

Logik

  1. Replikation des benutzerdefinierten Indikators

    • Der FATL-Wert wird durch Faltung des ausgewählten angewandten Preises mit der ursprünglichen Gewichtstabelle von 39 Koeffizienten berechnet.
    • Das Ergebnis wird mit StockSharps JurikMovingAverage geglättet. Wenn die Bibliothek eine Phase-Eigenschaft bereitstellt, wird sie durch Reflexion konfiguriert, um die MT5-Eingaben widerzuspiegeln.
    • Der geglättete Wert wird auf Instrumentenpräzision gerundet, indem der Preisschritt mit 10^DigitRounding multipliziert wird, was den Digit-Parameter von MQL5 reproduziert.
    • Die Differenz zwischen dem aktuellen gerundeten Wert und dem vorherigen definiert die Farbe für den Balken (2 = steigend, 0 = fallend, 1 = unverändert / geerbt).
  2. Signalauswertung

    • Ein Ringpuffer hält die aktuellsten Farbcodes. Der Parameter SignalBar legt fest, wie viele abgeschlossene Balken übersprungen werden sollen (Standard = 1, d.h. vorheriger geschlossener Balken).
    • Ein Long-Einstieg wird ausgelöst, wenn die vorherige Farbe bullisch war (2) und die jüngste Farbe alles andere als bullisch ist (< 2).
    • Ein Short-Einstieg wird ausgelöst, wenn die vorherige Farbe bärisch war (0) und die jüngste Farbe alles andere als bärisch ist (> 0).
    • Ein Long-Ausstieg erfolgt, wenn die vorherige Farbe bärisch wird (0).
    • Ein Short-Ausstieg erfolgt, wenn die vorherige Farbe bullisch wird (2).
    • Einstiege werden übersprungen, wenn bereits eine Position besteht, was das Einzelpositionsverhalten des MT5-Experten repliziert.
  3. Sitzungssteuerung und Schutz

    • Optionale Sitzungsfilterung (EnableTimeFilter) spiegelt die Stunden/Minuten-Logik von MT5 wider, einschließlich Übernacht-Sitzungen, wenn die Startstunde größer als die Endstunde ist.
    • Wenn der Handel außerhalb des erlaubten Fensters liegt, werden alle offenen Positionen sofort liquidiert, was dem ursprünglichen Experten entspricht.
    • Stop-Loss- und Take-Profit-Abstände in Punkten werden in Preiseinheiten umgerechnet und an StartProtection übergeben.

Parameter

  • OrderVolume – Volumen pro Order (für Kauf- und Verkaufseinstiege).
  • EnableTimeFilter, StartHour, StartMinute, EndHour, EndMinute – Sitzungsfenstereinstellungen.
  • StopLossPoints, TakeProfitPoints – Schutzabstände in Punkten (0 deaktiviert das jeweilige Bein).
  • BuyOpenEnabled, SellOpenEnabled, BuyCloseEnabled, SellCloseEnabled – Long/Short-Einstiege und -Ausstiege einzeln aktivieren oder deaktivieren.
  • SignalCandleType – Zeitrahmen für den benutzerdefinierten Indikator und Handelssignale (Standard 4-Stunden-Kerzen).
  • JmaLength, JmaPhase – Jurik-Glättungseinstellungen (Phase wird angewendet, wenn der zugrunde liegende Indikator sie bereitstellt).
  • AppliedPriceMode – Angewandter Preis-Enumeration identisch mit der MT5-Version (Schlusskurs, Eröffnung, Median, Typical, TrendFollow-Varianten, Demark usw.).
  • DigitRounding – Rundungsmultiplikator, der den Digit-Eingabewert des MQL-Indikators imitiert.
  • SignalBar – Wie viele geschlossene Balken beim Auswerten von Farbübergängen zurückgeschaut wird (Standard 1).

Hinweise

  • Die Strategie verwendet SubscribeCandles und High-Level-Order-Helfer (BuyMarket, SellMarket), wie von den StockSharp-Konvertierungsrichtlinien empfohlen.
  • Die Jurik-Phase wird durch Reflexion angewendet; wenn die Laufzeitimplementierung keine Phase-Eigenschaft bereitstellt, wird automatisch das Standardverhalten verwendet.
  • Das Runden erfordert einen gültigen Security.PriceStep. Wenn nicht verfügbar, bleiben Indikatorwerte ungerundet.

Verwendung

  1. Hängen Sie die Strategie an ein Wertpapier und eine Verbindung, die den konfigurierten SignalCandleType bereitstellen kann.
  2. Konfigurieren Sie den angewandten Preis, Jurik-Parameter, Sitzungszeiten und Geldmanagement-Eingaben wie gewünscht.
  3. Starten Sie die Strategie; sie verwaltet eine einzelne Position, respektiert Stop-Loss/Take-Profit-Schutz und die oben beschriebenen farbgesteuerten Signale.
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,
	}
}