在 GitHub 上查看

XFatlXSatlCloud Duplex

概述

XFatlXSatlCloud Duplex 是从原始 MQL5 专家顾问移植而来的双向策略。策略使用 XFatlXSatlCloud 指标,该指标将快速的 FATL 数字滤波器与较慢的 SATL 滤波器结合,并使用可配置的移动平均线对两条曲线进行平滑。多头与空头可以分别设置不同的时间框架、平滑方法以及价格来源。

交易逻辑

策略仅在蜡烛收盘后进行计算。多头与空头逻辑由两个独立的订阅驱动,每个订阅都会调用 C# 实现的 XFatlXSatlCloud 指标,并遵循以下规则:

  • 多头入场:当 LongSignalBar 指定的柱子上快线向上穿越慢线时触发。如果当前持有空头,并且 ShortAllowClose 允许平仓,则会先平掉空头,再按 LongVolume 下市价买单,并记录入场价用于风控。
  • 多头离场:当快线在相同偏移柱子下穿慢线时触发。若设置了 LongStopLossLongTakeProfit,当当前蜡烛的最高价/最低价触发这些绝对距离时,也会提前平仓。
  • 空头入场:当 ShortSignalBar 指定的柱子上快线向下穿越慢线时触发。若存在多头并且 LongAllowClose 允许平仓,则先平掉多头,然后按 ShortVolume 提交市价卖单。
  • 空头离场:当快线在相同偏移柱子上穿慢线时触发。ShortStopLossShortTakeProfit 会监控当前蜡烛的极值,满足条件时立即离场。

所有信号均基于收盘完成的数据,从而重现原版 MQL EA 的运行方式。

风险控制

策略分别跟踪多头和空头的入场价。只要启用了相应的 AllowClose 参数,当当前蜡烛的高/低价突破设定的止损或止盈绝对距离时,将立即平仓。所有距离都以标的资产的绝对价格单位表示。

参数说明

组别 参数 说明
Trading LongVolume 多头建仓数量(必须为正数)。
Trading ShortVolume 空头建仓数量(必须为正数)。
Trading LongAllowOpen 是否允许开多。
Trading LongAllowClose 是否允许多头平仓(止损/止盈与金叉离场均依赖该设置)。
Trading ShortAllowOpen 是否允许开空。
Trading ShortAllowClose 是否允许空头平仓。
Signals LongSignalBar 检查多头信号时回看完成柱的数量。
Signals ShortSignalBar 检查空头信号时回看完成柱的数量。
Data LongCandleType 多头指标订阅使用的蜡烛类型(时间框架)。
Data ShortCandleType 空头指标订阅使用的蜡烛类型。
Indicators LongMethod1 多头 FATL 输出的平滑方法(支持 SMA、EMA、SMMA、LWMA、Jurik、ZeroLag、Kaufman)。
Indicators LongLength1 多头快线平滑长度。
Indicators LongPhase1 多头快线平滑的相位参数(为兼容保留,主要用于 Jurik)。
Indicators LongMethod2 多头 SATL 输出的平滑方法。
Indicators LongLength2 多头慢线平滑长度。
Indicators LongPhase2 多头慢线平滑的相位参数。
Indicators LongAppliedPrice 多头指标使用的价格类型(收盘、开盘、中值、典型价、加权价、平均价、四价、趋势跟随、Demark 等)。
Indicators ShortMethod1 空头快线平滑方法。
Indicators ShortLength1 空头快线平滑长度。
Indicators ShortPhase1 空头快线相位参数。
Indicators ShortMethod2 空头慢线平滑方法。
Indicators ShortLength2 空头慢线平滑长度。
Indicators ShortPhase2 空头慢线相位参数。
Indicators ShortAppliedPrice 空头指标使用的价格类型。
Risk LongStopLoss 多头止损的绝对价格距离(0 表示禁用)。
Risk LongTakeProfit 多头止盈的绝对价格距离(0 表示禁用)。
Risk ShortStopLoss 空头止损的绝对价格距离(0 表示禁用)。
Risk ShortTakeProfit 空头止盈的绝对价格距离(0 表示禁用)。

实现细节

  • XFatlXSatlCloud 指标在 C# 中以高层 API 重新实现,先使用原始 FATL/SATL FIR 系数计算,再交由所选平滑指标处理。
  • 目前提供的平滑方法包括 SmaEmaSmmaLwmaJurikZeroLagKaufman。MQL 中的其他选项(如 Parabolic、T3)暂不支持。
  • LongSignalBarShortSignalBar 与原版参数含义一致,取值 1 表示“使用上一根完成蜡烛”来判断交叉。
  • 止损/止盈距离以绝对价格计量,基于当前蜡烛的高低价与记录的入场价比较,不依赖经纪商的点值设置。
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; }
	}
}