Auf GitHub ansehen

Strategie XFatlXSatlCloud Duplex

Überblick

XFatlXSatlCloud Duplex ist eine bidirektionale Strategie, konvertiert aus dem ursprünglichen MQL5 Expert Advisor. Sie handelt Kreuzungen des XFatlXSatlCloud-Indikators, der einen schnellen digitalen FATL-Filter mit einem langsameren SATL-Filter verbindet und dann beide mit konfigurierbaren gleitenden Durchschnitten glättet. Getrennte Konfigurationen können auf Long- und Short-Seiten angewendet werden, einschließlich verschiedener Zeitrahmen, Glättungsmethoden und angewandter Preisquellen.

Handelslogik

Die Strategie bewertet nur abgeschlossene Kerzen. Zwei unabhängige Abonnements laufen parallel: eines steuert die Long-Logik und das andere die Short-Logik. Jedes Abonnement speist den in C# implementierten XFatlXSatlCloud-Indikator und produziert das folgende Verhalten:

  • Long-Einstieg – ausgelöst, wenn die schnelle Linie am durch LongSignalBar definierten Balken die langsame Linie von unten nach oben kreuzt. Wenn eine Short-Position offen ist, wird sie zuerst geschlossen (nur wenn ShortAllowClose aktiviert ist). Dann wird eine Market-Kauforder mit LongVolume Kontrakten gesendet und der Einstiegspreis für Risikoprüfungen aufgezeichnet.
  • Long-Ausstieg – ausgeführt, wenn die schnelle Linie am verschobenen Balken unter die langsame Linie fällt. Optionale preisbasierte Stop-Loss- und Take-Profit-Checks (LongStopLoss, LongTakeProfit) können die Position früher schließen, wenn der Kerzenbereich die definierten Offsets verletzt.
  • Short-Einstieg – ausgelöst, wenn die schnelle Linie am durch ShortSignalBar definierten Balken die langsame Linie von oben nach unten kreuzt. Bestehende Long-Exposition wird zuerst abgebaut, wenn LongAllowClose aktiviert ist. Danach wird eine Market-Verkaufsorder mit ShortVolume Kontrakten gesendet.
  • Short-Ausstieg – ausgeführt, wenn die schnelle Linie am verschobenen Balken über die langsame Linie steigt. Optionale Risikokontrollen (ShortStopLoss, ShortTakeProfit) überwachen intrabar Extreme.

Alle Indikatorwerte werden nur bei abgeschlossenen Kerzen berechnet, sodass jede Entscheidung auf finalen Daten basiert und das ursprüngliche MQL-Verhalten widerspiegelt.

Risikomanagement

Die Strategie verfolgt den letzten Einstiegspreis separat für Long- und Short-Positionen. Wenn ein Stop-Loss- oder Take-Profit-Offset angegeben ist und die aktuelle Kerze den entsprechenden Schwellenwert überschreitet, wird die Position sofort geschlossen (abhängig vom relevanten AllowClose-Flag). Offsets werden in absoluten Preiseinheiten des gehandelten Instruments gemessen.

Parameter

Gruppe Name Beschreibung
Trading LongVolume Ordergröße für Long-Einstiege (größer als null).
Trading ShortVolume Ordergröße für Short-Einstiege (größer als null).
Trading LongAllowOpen Öffnen neuer Long-Positionen aktivieren oder deaktivieren.
Trading LongAllowClose Long-Ausstiege aktivieren oder deaktivieren (für Stops und Kreuzungsausstiege benötigt).
Trading ShortAllowOpen Öffnen neuer Short-Positionen aktivieren oder deaktivieren.
Trading ShortAllowClose Short-Ausstiege aktivieren oder deaktivieren.
Signals LongSignalBar Anzahl abgeschlossener Balken, die beim Prüfen des Crossovers für Longs zurückgeblickt werden.
Signals ShortSignalBar Anzahl abgeschlossener Balken, die beim Prüfen des Crossovers für Shorts zurückgeblickt werden.
Data LongCandleType Kerzentyp (Zeitrahmen) für das Long-Indikator-Abonnement.
Data ShortCandleType Kerzentyp für das Short-Indikator-Abonnement.
Indicators LongMethod1 Glättungsmethode für die FATL-Ausgabe auf der Long-Seite. Unterstützte Werte: SMA, EMA, SMMA, LWMA, Jurik, ZeroLag, Kaufman.
Indicators LongLength1 Länge des schnellen Long-Glätters.
Indicators LongPhase1 Phasenparameter für den schnellen Glätter (aus Kompatibilitätsgründen beibehalten, nur Jurik verwendet es konzeptionell).
Indicators LongMethod2 Glättungsmethode für die SATL-Ausgabe auf der Long-Seite (gleicher unterstützter Satz wie oben).
Indicators LongLength2 Länge des langsamen Long-Glätters.
Indicators LongPhase2 Phasenparameter für den langsamen Long-Glätter.
Indicators LongAppliedPrice Angewandter Preis für den Long-Indikator (Schluss, Eröffnung, Median, typisch, gewichtet, einfach, Quartal, Trendfolgend oder Demark).
Indicators ShortMethod1 Glättungsmethode für die schnelle Short-Linie.
Indicators ShortLength1 Länge des schnellen Short-Glätters.
Indicators ShortPhase1 Phasenparameter für den schnellen Short-Glätter.
Indicators ShortMethod2 Glättungsmethode für die langsame Short-Linie.
Indicators ShortLength2 Länge des langsamen Short-Glätters.
Indicators ShortPhase2 Phasenparameter für den langsamen Short-Glätter.
Indicators ShortAppliedPrice Angewandter Preis für den Short-Indikator.
Risk LongStopLoss Absolute Preisdistanz für den Long-Stop-Loss (0 deaktiviert die Prüfung).
Risk LongTakeProfit Absolute Preisdistanz für den Long-Take-Profit (0 deaktiviert die Prüfung).
Risk ShortStopLoss Absolute Preisdistanz für den Short-Stop-Loss (0 deaktiviert die Prüfung).
Risk ShortTakeProfit Absolute Preisdistanz für den Short-Take-Profit (0 deaktiviert die Prüfung).

Implementierungshinweise

  • Der XFatlXSatlCloud-Indikator ist als High-Level-StockSharp-Indikator implementiert. Die schnellen und langsamen Komponenten werden durch Anwendung der ursprünglichen FATL/SATL-Finite-Impulse-Response-Koeffizienten gefolgt von benutzerausgewählten Glättungsindikatoren erzeugt.
  • Nur allgemein verfügbare StockSharp-gleitende Durchschnitte werden exponiert (Sma, Ema, Smma, Lwma, Jurik, ZeroLag, Kaufman). Andere MQL-Glättungsfamilien (wie Parabolic oder T3) sind nicht enthalten.
  • LongSignalBar und ShortSignalBar imitieren den ursprünglichen SignalBar-Parameter. Ein Wert von 1 bedeutet "vorherigen abgeschlossenen Balken verwenden" bei der Crossover-Erkennung.
  • Stop-Loss- und Take-Profit-Offsets erwarten absolute Preisdistanzen. Sie werden mit dem Kerzenhoch/-tief relativ zum aufgezeichneten Einstiegspreis angewendet und hängen nicht von broker-spezifischen Punkt-Werten ab.
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 XFatlXSatlCloud indicator crossovers.
/// </summary>
public class XFatlXSatlCloudDuplexStrategy : Strategy
{
	/// <summary>
	/// Supported smoothing methods for XFatlXSatlCloud indicator.
	/// </summary>
	public enum XmaMethods
	{
		/// <summary>
		/// Simple moving average.
		/// </summary>
		Sma,

		/// <summary>
		/// Exponential moving average.
		/// </summary>
		Ema,

		/// <summary>
		/// Smoothed moving average (RMA).
		/// </summary>
		Smma,

		/// <summary>
		/// Linear weighted moving average.
		/// </summary>
		Lwma,

		/// <summary>
		/// Jurik moving average.
		/// </summary>
		Jurik,

		/// <summary>
		/// Zero lag exponential moving average.
		/// </summary>
		ZeroLag,

		/// <summary>
		/// Kaufman adaptive moving average.
		/// </summary>
		Kaufman,
	}

	public enum AppliedPrices
	{
		Close,
		Open,
		High,
		Low,
		Median,
		Typical,
		Weighted,
		Simple,
		Quarter,
		TrendFollow0,
		TrendFollow1,
		Demark,
	}

	private readonly StrategyParam<decimal> _longVolume;
	private readonly StrategyParam<decimal> _shortVolume;
	private readonly StrategyParam<bool> _longAllowOpen;
	private readonly StrategyParam<bool> _longAllowClose;
	private readonly StrategyParam<bool> _shortAllowOpen;
	private readonly StrategyParam<bool> _shortAllowClose;
	private readonly StrategyParam<int> _longSignalBar;
	private readonly StrategyParam<int> _shortSignalBar;
	private readonly StrategyParam<DataType> _longCandleType;
	private readonly StrategyParam<DataType> _shortCandleType;
	private readonly StrategyParam<XmaMethods> _longMethod1;
	private readonly StrategyParam<int> _longLength1;
	private readonly StrategyParam<int> _longPhase1;
	private readonly StrategyParam<XmaMethods> _longMethod2;
	private readonly StrategyParam<int> _longLength2;
	private readonly StrategyParam<int> _longPhase2;
	private readonly StrategyParam<AppliedPrices> _longPriceType;
	private readonly StrategyParam<XmaMethods> _shortMethod1;
	private readonly StrategyParam<int> _shortLength1;
	private readonly StrategyParam<int> _shortPhase1;
	private readonly StrategyParam<XmaMethods> _shortMethod2;
	private readonly StrategyParam<int> _shortLength2;
	private readonly StrategyParam<int> _shortPhase2;
	private readonly StrategyParam<AppliedPrices> _shortPriceType;
	private readonly StrategyParam<decimal> _longStopLoss;
	private readonly StrategyParam<decimal> _longTakeProfit;
	private readonly StrategyParam<decimal> _shortStopLoss;
	private readonly StrategyParam<decimal> _shortTakeProfit;

	private XFatlXSatlCloudIndicator _longIndicator = null!;
	private XFatlXSatlCloudIndicator _shortIndicator = null!;
	private readonly List<(decimal fast, decimal slow)> _longHistory = new();
	private readonly List<(decimal fast, decimal slow)> _shortHistory = new();
	private decimal? _longEntryPrice;
	private decimal? _shortEntryPrice;

	/// <summary>
	/// Initializes a new instance of the <see cref="XFatlXSatlCloudDuplexStrategy"/>.
	/// </summary>
	public XFatlXSatlCloudDuplexStrategy()
	{
		_longVolume = Param(nameof(LongVolume), 1m)
			.SetGreaterThanZero()
			.SetDisplay("Long Volume", "Order volume for long entries", "Trading");

		_shortVolume = Param(nameof(ShortVolume), 1m)
			.SetGreaterThanZero()
			.SetDisplay("Short Volume", "Order volume for short entries", "Trading");

		_longAllowOpen = Param(nameof(LongAllowOpen), true)
			.SetDisplay("Enable Long Entries", "Allow opening long positions", "Trading");

		_longAllowClose = Param(nameof(LongAllowClose), true)
			.SetDisplay("Enable Long Exits", "Allow closing long positions", "Trading");

		_shortAllowOpen = Param(nameof(ShortAllowOpen), true)
			.SetDisplay("Enable Short Entries", "Allow opening short positions", "Trading");

		_shortAllowClose = Param(nameof(ShortAllowClose), true)
			.SetDisplay("Enable Short Exits", "Allow closing short positions", "Trading");

		_longSignalBar = Param(nameof(LongSignalBar), 1)
			.SetNotNegative()
			.SetDisplay("Long Signal Shift", "Bars to look back for long signals", "Signals");

		_shortSignalBar = Param(nameof(ShortSignalBar), 1)
			.SetNotNegative()
			.SetDisplay("Short Signal Shift", "Bars to look back for short signals", "Signals");

		_longCandleType = Param(nameof(LongCandleType), TimeSpan.FromHours(4).TimeFrame())
			.SetDisplay("Long Candle Type", "Timeframe for long indicator", "Data");

		_shortCandleType = Param(nameof(ShortCandleType), TimeSpan.FromHours(4).TimeFrame())
			.SetDisplay("Short Candle Type", "Timeframe for short indicator", "Data");

		_longMethod1 = Param(nameof(LongMethod1), XmaMethods.Jurik)
			.SetDisplay("Long Fast Method", "Smoothing method for the fast long line", "Indicators");

		_longLength1 = Param(nameof(LongLength1), 3)
			.SetGreaterThanZero()
			.SetDisplay("Long Fast Length", "Length for the fast long smoother", "Indicators");

		_longPhase1 = Param(nameof(LongPhase1), 15)
			.SetDisplay("Long Fast Phase", "Phase parameter for the fast long smoother", "Indicators");

		_longMethod2 = Param(nameof(LongMethod2), XmaMethods.Jurik)
			.SetDisplay("Long Slow Method", "Smoothing method for the slow long line", "Indicators");

		_longLength2 = Param(nameof(LongLength2), 5)
			.SetGreaterThanZero()
			.SetDisplay("Long Slow Length", "Length for the slow long smoother", "Indicators");

		_longPhase2 = Param(nameof(LongPhase2), 15)
			.SetDisplay("Long Slow Phase", "Phase parameter for the slow long smoother", "Indicators");

		_longPriceType = Param(nameof(LongAppliedPrice), AppliedPrices.Close)
			.SetDisplay("Long Applied Price", "Price type used for the long indicator", "Indicators");

		_shortMethod1 = Param(nameof(ShortMethod1), XmaMethods.Jurik)
			.SetDisplay("Short Fast Method", "Smoothing method for the fast short line", "Indicators");

		_shortLength1 = Param(nameof(ShortLength1), 3)
			.SetGreaterThanZero()
			.SetDisplay("Short Fast Length", "Length for the fast short smoother", "Indicators");

		_shortPhase1 = Param(nameof(ShortPhase1), 15)
			.SetDisplay("Short Fast Phase", "Phase parameter for the fast short smoother", "Indicators");

		_shortMethod2 = Param(nameof(ShortMethod2), XmaMethods.Jurik)
			.SetDisplay("Short Slow Method", "Smoothing method for the slow short line", "Indicators");

		_shortLength2 = Param(nameof(ShortLength2), 5)
			.SetGreaterThanZero()
			.SetDisplay("Short Slow Length", "Length for the slow short smoother", "Indicators");

		_shortPhase2 = Param(nameof(ShortPhase2), 15)
			.SetDisplay("Short Slow Phase", "Phase parameter for the slow short smoother", "Indicators");

		_shortPriceType = Param(nameof(ShortAppliedPrice), AppliedPrices.Close)
			.SetDisplay("Short Applied Price", "Price type used for the short indicator", "Indicators");

		_longStopLoss = Param(nameof(LongStopLoss), 0m)
			.SetNotNegative()
			.SetDisplay("Long Stop Loss", "Price distance for long stop loss (0 disables)", "Risk");

		_longTakeProfit = Param(nameof(LongTakeProfit), 0m)
			.SetNotNegative()
			.SetDisplay("Long Take Profit", "Price distance for long take profit (0 disables)", "Risk");

		_shortStopLoss = Param(nameof(ShortStopLoss), 0m)
			.SetNotNegative()
			.SetDisplay("Short Stop Loss", "Price distance for short stop loss (0 disables)", "Risk");

		_shortTakeProfit = Param(nameof(ShortTakeProfit), 0m)
			.SetNotNegative()
			.SetDisplay("Short Take Profit", "Price distance for short take profit (0 disables)", "Risk");
	}

	/// <summary>
	/// Gets or sets volume for long trades.
	/// </summary>
	public decimal LongVolume
	{
		get => _longVolume.Value;
		set => _longVolume.Value = value;
	}

	/// <summary>
	/// Gets or sets volume for short trades.
	/// </summary>
	public decimal ShortVolume
	{
		get => _shortVolume.Value;
		set => _shortVolume.Value = value;
	}

	/// <summary>
	/// Allow opening long trades.
	/// </summary>
	public bool LongAllowOpen
	{
		get => _longAllowOpen.Value;
		set => _longAllowOpen.Value = value;
	}

	/// <summary>
	/// Allow closing long trades.
	/// </summary>
	public bool LongAllowClose
	{
		get => _longAllowClose.Value;
		set => _longAllowClose.Value = value;
	}

	/// <summary>
	/// Allow opening short trades.
	/// </summary>
	public bool ShortAllowOpen
	{
		get => _shortAllowOpen.Value;
		set => _shortAllowOpen.Value = value;
	}

	/// <summary>
	/// Allow closing short trades.
	/// </summary>
	public bool ShortAllowClose
	{
		get => _shortAllowClose.Value;
		set => _shortAllowClose.Value = value;
	}

	/// <summary>
	/// Number of bars to shift long signals.
	/// </summary>
	public int LongSignalBar
	{
		get => _longSignalBar.Value;
		set => _longSignalBar.Value = value;
	}

	/// <summary>
	/// Number of bars to shift short signals.
	/// </summary>
	public int ShortSignalBar
	{
		get => _shortSignalBar.Value;
		set => _shortSignalBar.Value = value;
	}

	/// <summary>
	/// Candle type for long indicator.
	/// </summary>
	public DataType LongCandleType
	{
		get => _longCandleType.Value;
		set => _longCandleType.Value = value;
	}

	/// <summary>
	/// Candle type for short indicator.
	/// </summary>
	public DataType ShortCandleType
	{
		get => _shortCandleType.Value;
		set => _shortCandleType.Value = value;
	}

	/// <summary>
	/// Smoothing method for fast long line.
	/// </summary>
	public XmaMethods LongMethod1
	{
		get => _longMethod1.Value;
		set => _longMethod1.Value = value;
	}

	/// <summary>
	/// Length for fast long smoother.
	/// </summary>
	public int LongLength1
	{
		get => _longLength1.Value;
		set => _longLength1.Value = value;
	}

	/// <summary>
	/// Phase for fast long smoother.
	/// </summary>
	public int LongPhase1
	{
		get => _longPhase1.Value;
		set => _longPhase1.Value = value;
	}

	/// <summary>
	/// Smoothing method for slow long line.
	/// </summary>
	public XmaMethods LongMethod2
	{
		get => _longMethod2.Value;
		set => _longMethod2.Value = value;
	}

	/// <summary>
	/// Length for slow long smoother.
	/// </summary>
	public int LongLength2
	{
		get => _longLength2.Value;
		set => _longLength2.Value = value;
	}

	/// <summary>
	/// Phase for slow long smoother.
	/// </summary>
	public int LongPhase2
	{
		get => _longPhase2.Value;
		set => _longPhase2.Value = value;
	}

	/// <summary>
	/// Applied price for long calculations.
	/// </summary>
	public AppliedPrices LongAppliedPrice
	{
		get => _longPriceType.Value;
		set => _longPriceType.Value = value;
	}

	/// <summary>
	/// Smoothing method for fast short line.
	/// </summary>
	public XmaMethods ShortMethod1
	{
		get => _shortMethod1.Value;
		set => _shortMethod1.Value = value;
	}

	/// <summary>
	/// Length for fast short smoother.
	/// </summary>
	public int ShortLength1
	{
		get => _shortLength1.Value;
		set => _shortLength1.Value = value;
	}

	/// <summary>
	/// Phase for fast short smoother.
	/// </summary>
	public int ShortPhase1
	{
		get => _shortPhase1.Value;
		set => _shortPhase1.Value = value;
	}

	/// <summary>
	/// Smoothing method for slow short line.
	/// </summary>
	public XmaMethods ShortMethod2
	{
		get => _shortMethod2.Value;
		set => _shortMethod2.Value = value;
	}

	/// <summary>
	/// Length for slow short smoother.
	/// </summary>
	public int ShortLength2
	{
		get => _shortLength2.Value;
		set => _shortLength2.Value = value;
	}

	/// <summary>
	/// Phase for slow short smoother.
	/// </summary>
	public int ShortPhase2
	{
		get => _shortPhase2.Value;
		set => _shortPhase2.Value = value;
	}

	/// <summary>
	/// Applied price for short calculations.
	/// </summary>
	public AppliedPrices ShortAppliedPrice
	{
		get => _shortPriceType.Value;
		set => _shortPriceType.Value = value;
	}

	/// <summary>
	/// Stop loss distance for long positions.
	/// </summary>
	public decimal LongStopLoss
	{
		get => _longStopLoss.Value;
		set => _longStopLoss.Value = value;
	}

	/// <summary>
	/// Take profit distance for long positions.
	/// </summary>
	public decimal LongTakeProfit
	{
		get => _longTakeProfit.Value;
		set => _longTakeProfit.Value = value;
	}

	/// <summary>
	/// Stop loss distance for short positions.
	/// </summary>
	public decimal ShortStopLoss
	{
		get => _shortStopLoss.Value;
		set => _shortStopLoss.Value = value;
	}

	/// <summary>
	/// Take profit distance for short positions.
	/// </summary>
	public decimal ShortTakeProfit
	{
		get => _shortTakeProfit.Value;
		set => _shortTakeProfit.Value = value;
	}

	/// <inheritdoc />
	public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
	{
		var result = new List<(Security, DataType)> { (Security, LongCandleType) };

		if (ShortCandleType != LongCandleType)
			result.Add((Security, ShortCandleType));

		return result;
	}

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();
		_longHistory.Clear();
		_shortHistory.Clear();
		_longEntryPrice = null;
		_shortEntryPrice = null;
	}

	/// <inheritdoc />
	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);

		// Create indicator instances with current parameter values for both directions.
		_longIndicator = new XFatlXSatlCloudIndicator(LongMethod1, LongLength1, LongPhase1, LongMethod2, LongLength2, LongPhase2, LongAppliedPrice);
		_shortIndicator = new XFatlXSatlCloudIndicator(ShortMethod1, ShortLength1, ShortPhase1, ShortMethod2, ShortLength2, ShortPhase2, ShortAppliedPrice);

		// Subscribe to candles that drive the long side of the strategy.
		var longSubscription = SubscribeCandles(LongCandleType);
		longSubscription.BindEx(_longIndicator, ProcessLong).Start();

		// Subscribe separately for the short side (timeframe can differ from the long one).
		var shortSubscription = SubscribeCandles(ShortCandleType);
		shortSubscription.BindEx(_shortIndicator, ProcessShort).Start();
	}

	private void ProcessLong(ICandleMessage candle, IIndicatorValue indicatorValue)
	{
		if (candle.State != CandleStates.Finished || !indicatorValue.IsFinal)
			return;

		var value = (XFatlXSatlValue)indicatorValue;
		// Store the latest indicator readings so we can evaluate the configured shift.
		_longHistory.Insert(0, (value.Fast, value.Slow));
		var maxSize = Math.Max(LongSignalBar + 2, 2);
		if (_longHistory.Count > maxSize)
			_longHistory.RemoveAt(_longHistory.Count - 1);

		// Risk management can close the position immediately before analyzing crossovers.
		if (HandleLongRisk(candle))
			return;

		if (_longHistory.Count <= LongSignalBar + 1)
			return;

		var current = _longHistory[LongSignalBar];
		var previous = _longHistory[LongSignalBar + 1];
		var crossUp = current.fast > current.slow && previous.fast <= previous.slow;
		var crossDown = current.fast < current.slow && previous.fast >= previous.slow;

		// Close an existing long when the fast line drops below the slow line.
		if (LongAllowClose && crossDown && Position > 0m)
		{
			SellMarket(Position);
			_longEntryPrice = null;
		}

		if (!LongAllowOpen || !crossUp)
			return;

		// Flatten shorts before reversing into a long position.
		if (Position < 0m)
		{
			if (!ShortAllowClose)
				return;

			BuyMarket(-Position);
			_shortEntryPrice = null;
		}

		// Open the long trade only if no opposite exposure remains.
		if (Position <= 0m)
		{
			BuyMarket(LongVolume);
			_longEntryPrice = candle.ClosePrice;
		}
	}

	private bool HandleLongRisk(ICandleMessage candle)
	{
		if (!LongAllowClose || Position <= 0m || _longEntryPrice is not decimal entry)
			return false;

		// Hard stop: candle low moved below entry minus configured distance.
		if (LongStopLoss > 0m && candle.LowPrice <= entry - LongStopLoss)
		{
			SellMarket(Position);
			_longEntryPrice = null;
			return true;
		}

		// Hard target: candle high exceeded entry plus configured distance.
		if (LongTakeProfit > 0m && candle.HighPrice >= entry + LongTakeProfit)
		{
			SellMarket(Position);
			_longEntryPrice = null;
			return true;
		}

		return false;
	}

	private void ProcessShort(ICandleMessage candle, IIndicatorValue indicatorValue)
	{
		if (candle.State != CandleStates.Finished || !indicatorValue.IsFinal)
			return;

		var value = (XFatlXSatlValue)indicatorValue;
		// Maintain the rolling history for the short configuration.
		_shortHistory.Insert(0, (value.Fast, value.Slow));
		var maxSize = Math.Max(ShortSignalBar + 2, 2);
		if (_shortHistory.Count > maxSize)
			_shortHistory.RemoveAt(_shortHistory.Count - 1);

		// Stop or target may close the short before trend analysis.
		if (HandleShortRisk(candle))
			return;

		if (_shortHistory.Count <= ShortSignalBar + 1)
			return;

		var current = _shortHistory[ShortSignalBar];
		var previous = _shortHistory[ShortSignalBar + 1];
		var crossDown = current.fast < current.slow && previous.fast >= previous.slow;
		var crossUp = current.fast > current.slow && previous.fast <= previous.slow;

		// Cover a short when the fast line rises above the slow line again.
		if (ShortAllowClose && crossUp && Position < 0m)
		{
			BuyMarket(-Position);
			_shortEntryPrice = null;
		}

		if (!ShortAllowOpen || !crossDown)
			return;

		// Close existing longs before flipping into a short position.
		if (Position > 0m)
		{
			if (!LongAllowClose)
				return;

			SellMarket(Position);
			_longEntryPrice = null;
		}

		// Enter the new short once the direction is clear.
		if (Position >= 0m)
		{
			SellMarket(ShortVolume);
			_shortEntryPrice = candle.ClosePrice;
		}
	}

	private bool HandleShortRisk(ICandleMessage candle)
	{
		if (!ShortAllowClose || Position >= 0m || _shortEntryPrice is not decimal entry)
			return false;

		// Stop loss for the short side is triggered by a move above the entry price.
		if (ShortStopLoss > 0m && candle.HighPrice >= entry + ShortStopLoss)
		{
			BuyMarket(-Position);
			_shortEntryPrice = null;
			return true;
		}

		// Take profit for shorts fires when the low pierces the target distance.
		if (ShortTakeProfit > 0m && candle.LowPrice <= entry - ShortTakeProfit)
		{
			BuyMarket(-Position);
			_shortEntryPrice = null;
			return true;
		}

		return false;
	}

	private sealed class XFatlXSatlCloudIndicator : BaseIndicator
	{
		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 static readonly decimal[] SatlCoefficients =
		{
	0.0982862174m,
	0.0975682269m,
	0.0961401078m,
	0.0940230544m,
	0.0912437090m,
	0.0878391006m,
	0.0838544303m,
	0.0793406350m,
	0.0743569346m,
	0.0689666682m,
	0.0632381578m,
	0.0572428925m,
	0.0510534242m,
	0.0447468229m,
	0.0383959950m,
	0.0320735368m,
	0.0258537721m,
	0.0198005183m,
	0.0139807863m,
	0.0084512448m,
	0.0032639979m,
	-0.0015350359m,
	-0.0059060082m,
	-0.0098190256m,
	-0.0132507215m,
	-0.0161875265m,
	-0.0186164872m,
	-0.0205446727m,
	-0.0219739146m,
	-0.0229204861m,
	-0.0234080863m,
	-0.0234566315m,
	-0.0231017777m,
	-0.0223796900m,
	-0.0213300463m,
	-0.0199924534m,
	-0.0184126992m,
	-0.0166377699m,
	-0.0147139428m,
	-0.0126796776m,
	-0.0105938331m,
	-0.0084736770m,
	-0.0063841850m,
	-0.0043466731m,
	-0.0023956944m,
	-0.0005535180m,
	0.0011421469m,
	0.0026845693m,
	0.0040471369m,
	0.0052380201m,
	0.0062194591m,
	0.0070340085m,
	0.0076266453m,
	0.0080376628m,
	0.0083037666m,
	0.0083694798m,
	0.0082901022m,
	0.0080741359m,
	0.0077543820m,
	0.0073260526m,
	0.0068163569m,
	0.0062325477m,
	0.0056078229m,
	0.0049516078m,
	0.0161380976m,
	};

		private readonly IIndicator _fastSmoother;
		private readonly IIndicator _slowSmoother;
		private readonly AppliedPrices _appliedPrice;
		private readonly decimal[] _priceBuffer = new decimal[SatlCoefficients.Length];
		private int _bufferIndex;
		private int _bufferCount;

		public XFatlXSatlCloudIndicator(XmaMethods fastMethod, int fastLength, int fastPhase, XmaMethods slowMethod, int slowLength, int slowPhase, AppliedPrices appliedPrice)
		{
			_fastSmoother = CreateSmoother(fastMethod, fastLength, fastPhase);
			_slowSmoother = CreateSmoother(slowMethod, slowLength, slowPhase);
			_appliedPrice = appliedPrice;
		}

		protected override IIndicatorValue OnProcess(IIndicatorValue input)
		{
			var candle = input.GetValue<ICandleMessage>();
			var price = SelectPrice(candle, _appliedPrice);
			// Feed the latest price into the circular buffer used by the FIR filters.
			_priceBuffer[_bufferIndex] = price;
			_bufferIndex = (_bufferIndex + 1) % _priceBuffer.Length;
			if (_bufferCount < _priceBuffer.Length)
				_bufferCount++;

			var fastRaw = ComputeFilter(FatlCoefficients);
			var slowRaw = ComputeFilter(SatlCoefficients);

			// Smooth both raw filters with the configured moving averages.
			var fastValue = _fastSmoother.Process(new DecimalIndicatorValue(_fastSmoother, fastRaw, input.Time) { IsFinal = input.IsFinal });
			var slowValue = _slowSmoother.Process(new DecimalIndicatorValue(_slowSmoother, slowRaw, input.Time) { IsFinal = input.IsFinal });
			var fast = fastValue.ToDecimal();
			var slow = slowValue.ToDecimal();

			IsFormed = _bufferCount >= SatlCoefficients.Length && fastValue.IsFinal && slowValue.IsFinal;
			return new XFatlXSatlValue(this, input.Time, fast, slow, fastRaw, slowRaw) { IsFinal = input.IsFinal };
		}

		public override void Reset()
		{
			base.Reset();
			Array.Clear(_priceBuffer, 0, _priceBuffer.Length);
			_bufferIndex = 0;
			_bufferCount = 0;
			_fastSmoother.Reset();
			_slowSmoother.Reset();
		}

		private decimal ComputeFilter(IReadOnlyList<decimal> coefficients)
		{
			if (_bufferCount < coefficients.Count)
				return 0m;

			decimal sum = 0m;
			for (var i = 0; i < coefficients.Count; i++)
			{
				// Traverse the ring buffer backwards to align with the newest price first.
				var index = _bufferIndex - 1 - i;
				if (index < 0)
					index += _priceBuffer.Length;

				sum += coefficients[i] * _priceBuffer[index];
			}

			return sum;
		}

		private static IIndicator CreateSmoother(XmaMethods method, int length, int phase)
		{
			length = Math.Max(1, length);

			// Map the MQL smoothing options to the closest available StockSharp moving averages.
			return method switch
			{
				XmaMethods.Sma => new SimpleMovingAverage { Length = length },
				XmaMethods.Ema => new ExponentialMovingAverage { Length = length },
				XmaMethods.Smma => new SmoothedMovingAverage { Length = length },
				XmaMethods.Lwma => new WeightedMovingAverage { Length = length },
				XmaMethods.Jurik => new JurikMovingAverage { Length = length },
				XmaMethods.ZeroLag => new ZeroLagExponentialMovingAverage { Length = length },
				XmaMethods.Kaufman => new KaufmanAdaptiveMovingAverage { Length = length },
				_ => throw new ArgumentOutOfRangeException(nameof(method), method, "Unsupported smoothing method."),
			};
		}

		private static decimal SelectPrice(ICandleMessage candle, AppliedPrices price)
		{
			return price switch
			{
				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 => (candle.ClosePrice * 2m + 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 sum = candle.HighPrice + candle.LowPrice + candle.ClosePrice;
			if (candle.ClosePrice < candle.OpenPrice)
				sum = (sum + candle.LowPrice) / 2m;
			else if (candle.ClosePrice > candle.OpenPrice)
				sum = (sum + candle.HighPrice) / 2m;
			else
				sum = (sum + candle.ClosePrice) / 2m;

			return ((sum - candle.LowPrice) + (sum - candle.HighPrice)) / 2m;
		}
	}

	private sealed class XFatlXSatlValue : DecimalIndicatorValue
	{
		public XFatlXSatlValue(IIndicator indicator, DateTime time, decimal fast, decimal slow, decimal fastRaw, decimal slowRaw)
			: base(indicator, fast, time)
		{
			Fast = fast;
			Slow = slow;
			FastRaw = fastRaw;
			SlowRaw = slowRaw;
		}

		public decimal Fast { get; }
		public decimal Slow { get; }
		public decimal FastRaw { get; }
		public decimal SlowRaw { get; }
	}
}