GitHub で見る

Exp UltraFATL Duplex 戦略

概要

Exp UltraFATL Duplex 戦略は、MetaTrader 5のエキスパートアドバイザー Exp_UltraFatl_Duplex をC#に変換したものです。システムは2つの独立したUltraFATLインジケーターパイプラインを実行します:1つはロングの機会専用、もう1つはショートのセットアップ用に調整されています。各パイプラインは平滑化されたFATL値の階段を評価し、何段階が上昇または下降しているかをカウントします。強気と弱気のカウンターのバランスが次のトレードの方向を定義します。

トレードロジック

  1. 各方向ブロックの設定されたローソク足の時間軸にサブスクライブします。
  2. FATLカーネル(39タップデジタルフィルター)で適用価格をフィルタリングします。
  3. フィルタリングされたシリーズを、設定されたステップで長さが増加する移動平均の階段に通します。階段はユーザーが指定した平滑化方法を使用します。
  4. 階段内の連続した値を比較して強気と弱気の票数をカウントします。両方のカウンターを2番目の移動平均で平滑化します。
  5. 選択したシグナルシフト(デフォルト:完全に閉じた1本のローソク足)でカウンターを評価します:
    • ロングブロックは前のローソク足が強気の優位を示したが、現在のローソク足でカウンターが下向きにクロス(強気 ≤ 弱気)した場合にポジションを開きます。前のローソク足で弱気が強気を上回ったときにロングポジションを閉じます。
    • ショートブロックは逆方向で動作します:前のローソク足が弱気に支配され、現在のローソク足が上向きにクロス(強気 ≥ 弱気)した場合にショートを開きます。前のローソク足で強気がリードしたときにショートを閉じます。
  6. オプションのストップロスとテイクプロフィットレベルは、銘柄の価格ステップを使用してローソク足データで評価されます。

戦略はネットポジションを強制します:ショートシグナルは開く前に既存のロングを閉じ、その逆も同様です。エントリーとエグジットには市場注文が使用されます。

パラメーター

ロングブロック

  • Long Volume – ロングトレードを開くときの注文サイズ。
  • Allow Long Entries – 新しいロングポジションを有効または無効にします。
  • Allow Long Exits – 反対のシグナルでロングを閉じることを許可します。
  • Long Candle Type – ロングUltraFATLパイプラインに使用する時間軸。
  • Long Applied Price – FATLカーネルに供給する価格ソース(終値、典型値、DeMark など)。
  • Long Trend Method / Start Length / Phase / Step / Steps – 階段の平滑化設定。
  • Long Counter Method / Counter Length / Counter Phase – 強気/弱気カウンターの平滑化設定。
  • Long Signal Bar – シグナルオフセットとして使用する完了ローソク足の数(1未満の値は1として扱われます)。
  • Long Stop (pts) – 価格ステップ単位のオプションのストップロス距離。
  • Long Target (pts) – 価格ステップ単位のオプションのテイクプロフィット距離。

ショートブロック

ショートパイプラインの対称的な設定:Short VolumeAllow Short EntriesAllow Short ExitsShort Candle TypeShort Applied PriceShort Trend Method / Start Length / Phase / Step / StepsShort Counter Method / Counter Length / Counter PhaseShort Signal BarShort Stop (pts)Short Target (pts)

実装上の注意

  • 平滑化方法はStockSharpインジケーターにマッピングされます。Jurikベースのオプションは JurikMovingAverage を再利用します;ParabolicT3 などのメソッドは、元のカスタムカーネルが利用できないため、指数移動平均またはJurik移動平均で近似されます。
  • ストップロスとテイクプロフィットレベルはローソク足の高値/安値で評価されます;サーバーサイドの保護注文ではありません。
  • StockSharpポートは完了したローソク足にのみ反応するため、1バー未満のシグナルオフセットは再現できません。したがって、シグナルバーをゼロに設定すると、シフト1と同じように動作します。
  • 両方のインジケーターパイプラインは、視覚的な検査のために専用のチャートエリアに平滑化されたカウンターを描画します。

使用方法

戦略をStockSharpソリューションに追加し、トレーディングプランに従って方向ブロックを設定し、Designer、Shell、またはRunnerで実行します。銘柄が必要なローソク足シリーズを提供し、LongVolume/ShortVolume パラメーターが目的の注文サイズに設定されていることを確認してください。

namespace StockSharp.Samples.Strategies;

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 StockSharp.Algo;

/// <summary>
/// Conversion of the MetaTrader strategy "Exp_UltraFatl_Duplex".
/// The logic runs the UltraFATL histogram twice with separate parameter blocks for long and short trades.
/// Signals are generated from the balance between smoothed bullish and bearish counters.
/// </summary>
public class ExpUltraFatlDuplexStrategy : Strategy
{
	public enum AppliedPrices
	{
		Close,
		Open,
		High,
		Low,
		Median,
		Typical,
		Weighted,
		Simplified,
		Quarter,
		TrendFollow0,
		TrendFollow1,
		DeMark
	}

	public enum SmoothMethods
	{
		Sma,
		Ema,
		Smma,
		Lwma,
		Jurik,
		JurX,
		Parabolic,
		T3,
		Vidya,
		Ama
	}

	private readonly StrategyParam<decimal> _longVolume;
	private readonly StrategyParam<bool> _allowLongEntries;
	private readonly StrategyParam<bool> _allowLongExits;
	private readonly StrategyParam<DataType> _longCandleType;
	private readonly StrategyParam<AppliedPrices> _longAppliedPrice;
	private readonly StrategyParam<SmoothMethods> _longTrendMethod;
	private readonly StrategyParam<int> _longStartLength;
	private readonly StrategyParam<int> _longPhase;
	private readonly StrategyParam<int> _longStep;
	private readonly StrategyParam<int> _longStepsTotal;
	private readonly StrategyParam<SmoothMethods> _longSmoothMethod;
	private readonly StrategyParam<int> _longSmoothLength;
	private readonly StrategyParam<int> _longSmoothPhase;
	private readonly StrategyParam<int> _longSignalBar;
	private readonly StrategyParam<int> _longStopLossPoints;
	private readonly StrategyParam<int> _longTakeProfitPoints;

	private readonly StrategyParam<decimal> _shortVolume;
	private readonly StrategyParam<bool> _allowShortEntries;
	private readonly StrategyParam<bool> _allowShortExits;
	private readonly StrategyParam<DataType> _shortCandleType;
	private readonly StrategyParam<AppliedPrices> _shortAppliedPrice;
	private readonly StrategyParam<SmoothMethods> _shortTrendMethod;
	private readonly StrategyParam<int> _shortStartLength;
	private readonly StrategyParam<int> _shortPhase;
	private readonly StrategyParam<int> _shortStep;
	private readonly StrategyParam<int> _shortStepsTotal;
	private readonly StrategyParam<SmoothMethods> _shortSmoothMethod;
	private readonly StrategyParam<int> _shortSmoothLength;
	private readonly StrategyParam<int> _shortSmoothPhase;
	private readonly StrategyParam<int> _shortSignalBar;
	private readonly StrategyParam<int> _shortStopLossPoints;
	private readonly StrategyParam<int> _shortTakeProfitPoints;

	private UltraFatlContext _longContext;
	private UltraFatlContext _shortContext;
	private decimal? _longEntryPrice;
	private decimal? _shortEntryPrice;
	private decimal _priceStep;
	private bool _priceChartInitialized;

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

		_allowLongEntries = Param(nameof(AllowLongEntries), true)
			.SetDisplay("Allow Long Entries", "Enable opening long positions.", "Long");

		_allowLongExits = Param(nameof(AllowLongExits), true)
			.SetDisplay("Allow Long Exits", "Enable closing long positions on opposite signals.", "Long");

		_longCandleType = Param(nameof(LongCandleType), TimeSpan.FromHours(1).TimeFrame())
			.SetDisplay("Long Candle Type", "Timeframe used by the long UltraFATL block.", "Long");

		_longAppliedPrice = Param(nameof(LongAppliedPrice), AppliedPrices.Close)
			.SetDisplay("Long Applied Price", "Price source fed into the long UltraFATL filter.", "Long");

		_longTrendMethod = Param(nameof(LongTrendMethod), SmoothMethods.Ema)
			.SetDisplay("Long Trend Method", "Smoothing method for the long FATL ladder.", "Long");

		_longStartLength = Param(nameof(LongStartLength), 8)
			.SetGreaterThanZero()
			.SetDisplay("Long Start Length", "Initial smoothing length for the ladder.", "Long");

		_longPhase = Param(nameof(LongPhase), 100)
			.SetDisplay("Long Phase", "Phase parameter applied to Jurik-based smoothers.", "Long");

		_longStep = Param(nameof(LongStep), 3)
			.SetGreaterThanZero()
			.SetDisplay("Long Step", "Increment between ladder lengths.", "Long");

		_longStepsTotal = Param(nameof(LongStepsTotal), 6)
			.SetGreaterThanZero()
			.SetDisplay("Long Steps", "Number of smoothing steps for the ladder.", "Long");

		_longSmoothMethod = Param(nameof(LongSmoothMethod), SmoothMethods.Ema)
			.SetDisplay("Long Counter Method", "Method applied to the bullish/bearish counters.", "Long");

		_longSmoothLength = Param(nameof(LongSmoothLength), 8)
			.SetGreaterThanZero()
			.SetDisplay("Long Counter Length", "Length used when smoothing the counters.", "Long");

		_longSmoothPhase = Param(nameof(LongSmoothPhase), 100)
			.SetDisplay("Long Counter Phase", "Phase parameter for the counter smoother.", "Long");

		_longSignalBar = Param(nameof(LongSignalBar), 1)
			.SetNotNegative()
			.SetDisplay("Long Signal Bar", "Closed-bar offset used when evaluating long signals.", "Long");

		_longStopLossPoints = Param(nameof(LongStopLossPoints), 0)
			.SetNotNegative()
			.SetDisplay("Long Stop (pts)", "Protective stop distance in price steps for long trades.", "Long");

		_longTakeProfitPoints = Param(nameof(LongTakeProfitPoints), 0)
			.SetNotNegative()
			.SetDisplay("Long Target (pts)", "Take-profit distance in price steps for long trades.", "Long");

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

		_allowShortEntries = Param(nameof(AllowShortEntries), true)
			.SetDisplay("Allow Short Entries", "Enable opening short positions.", "Short");

		_allowShortExits = Param(nameof(AllowShortExits), true)
			.SetDisplay("Allow Short Exits", "Enable closing short positions on opposite signals.", "Short");

		_shortCandleType = Param(nameof(ShortCandleType), TimeSpan.FromHours(1).TimeFrame())
			.SetDisplay("Short Candle Type", "Timeframe used by the short UltraFATL block.", "Short");

		_shortAppliedPrice = Param(nameof(ShortAppliedPrice), AppliedPrices.Close)
			.SetDisplay("Short Applied Price", "Price source fed into the short UltraFATL filter.", "Short");

		_shortTrendMethod = Param(nameof(ShortTrendMethod), SmoothMethods.Ema)
			.SetDisplay("Short Trend Method", "Smoothing method for the short FATL ladder.", "Short");

		_shortStartLength = Param(nameof(ShortStartLength), 8)
			.SetGreaterThanZero()
			.SetDisplay("Short Start Length", "Initial smoothing length for the short ladder.", "Short");

		_shortPhase = Param(nameof(ShortPhase), 100)
			.SetDisplay("Short Phase", "Phase parameter applied to the short Jurik-based smoothers.", "Short");

		_shortStep = Param(nameof(ShortStep), 3)
			.SetGreaterThanZero()
			.SetDisplay("Short Step", "Increment between smoothing lengths for the short ladder.", "Short");

		_shortStepsTotal = Param(nameof(ShortStepsTotal), 6)
			.SetGreaterThanZero()
			.SetDisplay("Short Steps", "Number of smoothing steps for the short ladder.", "Short");

		_shortSmoothMethod = Param(nameof(ShortSmoothMethod), SmoothMethods.Ema)
			.SetDisplay("Short Counter Method", "Method applied to the bearish counters.", "Short");

		_shortSmoothLength = Param(nameof(ShortSmoothLength), 8)
			.SetGreaterThanZero()
			.SetDisplay("Short Counter Length", "Length used when smoothing the short counters.", "Short");

		_shortSmoothPhase = Param(nameof(ShortSmoothPhase), 100)
			.SetDisplay("Short Counter Phase", "Phase parameter for the short counter smoother.", "Short");

		_shortSignalBar = Param(nameof(ShortSignalBar), 1)
			.SetNotNegative()
			.SetDisplay("Short Signal Bar", "Closed-bar offset used when evaluating short signals.", "Short");

		_shortStopLossPoints = Param(nameof(ShortStopLossPoints), 0)
			.SetNotNegative()
			.SetDisplay("Short Stop (pts)", "Protective stop distance in price steps for short trades.", "Short");

		_shortTakeProfitPoints = Param(nameof(ShortTakeProfitPoints), 0)
			.SetNotNegative()
			.SetDisplay("Short Target (pts)", "Take-profit distance in price steps for short trades.", "Short");
	}

	/// <summary>Volume used for long entries.</summary>
	public decimal LongVolume { get => _longVolume.Value; set => _longVolume.Value = value; }

	/// <summary>Enable long-side entries.</summary>
	public bool AllowLongEntries { get => _allowLongEntries.Value; set => _allowLongEntries.Value = value; }

	/// <summary>Enable long-side exits.</summary>
	public bool AllowLongExits { get => _allowLongExits.Value; set => _allowLongExits.Value = value; }

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

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

	/// <summary>Smoothing method for the long ladder.</summary>
	public SmoothMethods LongTrendMethod { get => _longTrendMethod.Value; set => _longTrendMethod.Value = value; }

	/// <summary>Initial length for the long ladder.</summary>
	public int LongStartLength { get => _longStartLength.Value; set => _longStartLength.Value = value; }

	/// <summary>Phase parameter for the long ladder.</summary>
	public int LongPhase { get => _longPhase.Value; set => _longPhase.Value = value; }

	/// <summary>Increment between smoothing lengths for the long ladder.</summary>
	public int LongStep { get => _longStep.Value; set => _longStep.Value = value; }

	/// <summary>Total number of smoothing steps for the long ladder.</summary>
	public int LongStepsTotal { get => _longStepsTotal.Value; set => _longStepsTotal.Value = value; }

	/// <summary>Smoothing method for the long counters.</summary>
	public SmoothMethods LongSmoothMethod { get => _longSmoothMethod.Value; set => _longSmoothMethod.Value = value; }

	/// <summary>Length applied to the long counters.</summary>
	public int LongSmoothLength { get => _longSmoothLength.Value; set => _longSmoothLength.Value = value; }

	/// <summary>Phase parameter for the long counter smoother.</summary>
	public int LongSmoothPhase { get => _longSmoothPhase.Value; set => _longSmoothPhase.Value = value; }

	/// <summary>Closed-bar offset when checking long signals.</summary>
	public int LongSignalBar { get => _longSignalBar.Value; set => _longSignalBar.Value = value; }

	/// <summary>Stop-loss distance for long trades measured in price steps.</summary>
	public int LongStopLossPoints { get => _longStopLossPoints.Value; set => _longStopLossPoints.Value = value; }

	/// <summary>Take-profit distance for long trades measured in price steps.</summary>
	public int LongTakeProfitPoints { get => _longTakeProfitPoints.Value; set => _longTakeProfitPoints.Value = value; }

	/// <summary>Volume used for short entries.</summary>
	public decimal ShortVolume { get => _shortVolume.Value; set => _shortVolume.Value = value; }

	/// <summary>Enable short-side entries.</summary>
	public bool AllowShortEntries { get => _allowShortEntries.Value; set => _allowShortEntries.Value = value; }

	/// <summary>Enable short-side exits.</summary>
	public bool AllowShortExits { get => _allowShortExits.Value; set => _allowShortExits.Value = value; }

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

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

	/// <summary>Smoothing method for the short ladder.</summary>
	public SmoothMethods ShortTrendMethod { get => _shortTrendMethod.Value; set => _shortTrendMethod.Value = value; }

	/// <summary>Initial length for the short ladder.</summary>
	public int ShortStartLength { get => _shortStartLength.Value; set => _shortStartLength.Value = value; }

	/// <summary>Phase parameter for the short ladder.</summary>
	public int ShortPhase { get => _shortPhase.Value; set => _shortPhase.Value = value; }

	/// <summary>Increment between smoothing lengths for the short ladder.</summary>
	public int ShortStep { get => _shortStep.Value; set => _shortStep.Value = value; }

	/// <summary>Total number of smoothing steps for the short ladder.</summary>
	public int ShortStepsTotal { get => _shortStepsTotal.Value; set => _shortStepsTotal.Value = value; }

	/// <summary>Smoothing method for the short counters.</summary>
	public SmoothMethods ShortSmoothMethod { get => _shortSmoothMethod.Value; set => _shortSmoothMethod.Value = value; }

	/// <summary>Length applied to the short counters.</summary>
	public int ShortSmoothLength { get => _shortSmoothLength.Value; set => _shortSmoothLength.Value = value; }

	/// <summary>Phase parameter for the short counter smoother.</summary>
	public int ShortSmoothPhase { get => _shortSmoothPhase.Value; set => _shortSmoothPhase.Value = value; }

	/// <summary>Closed-bar offset when checking short signals.</summary>
	public int ShortSignalBar { get => _shortSignalBar.Value; set => _shortSignalBar.Value = value; }

	/// <summary>Stop-loss distance for short trades measured in price steps.</summary>
	public int ShortStopLossPoints { get => _shortStopLossPoints.Value; set => _shortStopLossPoints.Value = value; }

	/// <summary>Take-profit distance for short trades measured in price steps.</summary>
	public int ShortTakeProfitPoints { get => _shortTakeProfitPoints.Value; set => _shortTakeProfitPoints.Value = value; }

	/// <inheritdoc />
	public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
	{
		if (Security is null)
			yield break;

		yield return (Security, LongCandleType);

		if (!Equals(LongCandleType, ShortCandleType))
			yield return (Security, ShortCandleType);
	}

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();

		_longContext?.Dispose();
		_shortContext?.Dispose();
		_longContext = null;
		_shortContext = null;
		_longEntryPrice = null;
		_shortEntryPrice = null;
		_priceStep = 0m;
		_priceChartInitialized = false;
	}

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

		_priceStep = Security?.PriceStep ?? 0m;
		Volume = AdjustOrderVolume(Math.Max(LongVolume, ShortVolume));

		_longContext = new UltraFatlContext(this, true, LongCandleType, LongAppliedPrice, LongTrendMethod,
			LongStartLength, LongPhase, LongStep, LongStepsTotal, LongSmoothMethod, LongSmoothLength,
			LongSmoothPhase, LongSignalBar, LongVolume, AllowLongEntries, AllowLongExits,
			LongStopLossPoints, LongTakeProfitPoints, _priceStep);

		_shortContext = new UltraFatlContext(this, false, ShortCandleType, ShortAppliedPrice, ShortTrendMethod,
			ShortStartLength, ShortPhase, ShortStep, ShortStepsTotal, ShortSmoothMethod, ShortSmoothLength,
			ShortSmoothPhase, ShortSignalBar, ShortVolume, AllowShortEntries, AllowShortExits,
			ShortStopLossPoints, ShortTakeProfitPoints, _priceStep);

		_longContext.Start();
		_shortContext.Start();
	}

	/// <inheritdoc />
	protected override void OnOwnTradeReceived(MyTrade trade)
	{
		base.OnOwnTradeReceived(trade);

		var price = trade.Trade?.Price ?? 0m;

		if (trade.Order.Side == Sides.Buy)
		{
			if (Position > 0m)
				_longEntryPrice = price;

			if (Position >= 0m)
				_shortEntryPrice = Position == 0m ? null : _shortEntryPrice;
		}
		else if (trade.Order.Side == Sides.Sell)
		{
			if (Position < 0m)
				_shortEntryPrice = price;

			if (Position <= 0m)
				_longEntryPrice = Position == 0m ? null : _longEntryPrice;
		}
	}

	private void ProcessDirectionalSignal(bool isLong, bool openSignal, bool closeSignal, UltraFatlSnapshot snapshot, decimal volume)
	{
		var normalizedVolume = AdjustOrderVolume(volume);

		if (isLong)
		{
			if (closeSignal && AllowLongExits && Position > 0m)
			{
				SellMarket(Position);
				_longEntryPrice = null;
			}

			if (openSignal && AllowLongEntries && Position <= 0m && normalizedVolume > 0m)
			{
				BuyMarket(normalizedVolume + (Position < 0m ? -Position : 0m));
				_longEntryPrice = snapshot.ClosePrice;
			}
		}
		else
		{
			if (closeSignal && AllowShortExits && Position < 0m)
			{
				BuyMarket(-Position);
				_shortEntryPrice = null;
			}

			if (openSignal && AllowShortEntries && Position >= 0m && normalizedVolume > 0m)
			{
				SellMarket(normalizedVolume + (Position > 0m ? Position : 0m));
				_shortEntryPrice = snapshot.ClosePrice;
			}
		}
	}

	private void CheckStops(bool isLong, ICandleMessage candle, int stopLossPoints, int takeProfitPoints, decimal priceStep)
	{
		if (priceStep <= 0m)
			return;

		if (isLong)
		{
			if (Position <= 0m || _longEntryPrice is null)
				return;

			var stopLossPrice = stopLossPoints > 0 ? _longEntryPrice.Value - stopLossPoints * priceStep : (decimal?)null;
			var takeProfitPrice = takeProfitPoints > 0 ? _longEntryPrice.Value + takeProfitPoints * priceStep : (decimal?)null;

			if (stopLossPrice.HasValue && candle.LowPrice <= stopLossPrice.Value)
			{
				SellMarket();
				_longEntryPrice = null;
				return;
			}

			if (takeProfitPrice.HasValue && candle.HighPrice >= takeProfitPrice.Value)
			{
				SellMarket();
				_longEntryPrice = null;
			}
		}
		else
		{
			if (Position >= 0m || _shortEntryPrice is null)
				return;

			var stopLossPrice = stopLossPoints > 0 ? _shortEntryPrice.Value + stopLossPoints * priceStep : (decimal?)null;
			var takeProfitPrice = takeProfitPoints > 0 ? _shortEntryPrice.Value - takeProfitPoints * priceStep : (decimal?)null;

			if (stopLossPrice.HasValue && candle.HighPrice >= stopLossPrice.Value)
			{
				BuyMarket();
				_shortEntryPrice = null;
				return;
			}

			if (takeProfitPrice.HasValue && candle.LowPrice <= takeProfitPrice.Value)
			{
				BuyMarket();
				_shortEntryPrice = null;
			}
		}
	}

	private static decimal GetAppliedPrice(ICandleMessage candle, AppliedPrices priceMode)
	{
		return priceMode 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.ClosePrice + candle.HighPrice + candle.LowPrice) / 3m,
			AppliedPrices.Weighted => (2m * candle.ClosePrice + candle.HighPrice + candle.LowPrice) / 4m,
			AppliedPrices.Simplified => (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.LowPrice,
			AppliedPrices.TrendFollow1 => candle.ClosePrice >= candle.OpenPrice
				? (candle.HighPrice + candle.ClosePrice) / 2m
				: (candle.LowPrice + candle.ClosePrice) / 2m,
			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 decimal AdjustOrderVolume(decimal volume)
	{
		if (volume <= 0m)
			return 0m;

		var step = Security?.VolumeStep ?? 0m;
		if (step > 0m)
			volume = decimal.Floor(volume / step) * step;

		var minVolume = Security?.MinVolume ?? 0m;
		if (minVolume > 0m && volume < minVolume)
			volume = minVolume;

		var maxVolume = Security?.MaxVolume ?? 0m;
		if (maxVolume > 0m && volume > maxVolume)
			volume = maxVolume;

		return volume;
	}

	private static DecimalLengthIndicator CreateMovingAverage(SmoothMethods method, int length, int phase)
	{
		var normalizedLength = Math.Max(1, length);

		return method switch
		{
			SmoothMethods.Sma => new SMA { Length = normalizedLength },
			SmoothMethods.Ema => new EMA { Length = normalizedLength },
			SmoothMethods.Smma => new SmoothedMovingAverage { Length = normalizedLength },
			SmoothMethods.Lwma => new WeightedMovingAverage { Length = normalizedLength },
			SmoothMethods.Jurik => new JurikMovingAverage { Length = normalizedLength, Phase = phase },
			SmoothMethods.JurX => new JurikMovingAverage { Length = normalizedLength, Phase = phase },
			SmoothMethods.Parabolic => new EMA { Length = normalizedLength },
			SmoothMethods.T3 => new JurikMovingAverage { Length = normalizedLength, Phase = phase },
			SmoothMethods.Vidya => new EMA { Length = normalizedLength },
			SmoothMethods.Ama => new KaufmanAdaptiveMovingAverage { Length = normalizedLength },
			_ => new EMA { Length = normalizedLength },
		};
	}

	private void RegisterPriceChartOnce(ISubscriptionHandler<ICandleMessage> subscription)
	{
		if (_priceChartInitialized)
			return;

		var priceArea = CreateChartArea();
		if (priceArea != null)
		{
			DrawCandles(priceArea, subscription);
			DrawOwnTrades(priceArea);
			_priceChartInitialized = true;
		}
	}

	private readonly record struct UltraFatlSnapshot(DateTimeOffset Time, decimal Bulls, decimal Bears, decimal ClosePrice, decimal HighPrice, decimal LowPrice);

	private sealed class UltraFatlContext : IDisposable
	{
		private readonly ExpUltraFatlDuplexStrategy _strategy;
		private readonly bool _isLong;
		private readonly DataType _candleType;
		private readonly AppliedPrices _appliedPrice;
		private readonly SmoothMethods _trendMethod;
		private readonly int _startLength;
		private readonly int _phase;
		private readonly int _step;
		private readonly int _stepsTotal;
		private readonly SmoothMethods _smoothMethod;
		private readonly int _smoothLength;
		private readonly int _smoothPhase;
		private readonly int _signalBar;
		private readonly decimal _volume;
		private readonly bool _allowEntries;
		private readonly bool _allowExits;
		private readonly int _stopLossPoints;
		private readonly int _takeProfitPoints;
		private readonly decimal _priceStep;

		private readonly List<DecimalLengthIndicator> _ladder = new();
		private readonly List<decimal?> _previousValues = new();
		private DecimalLengthIndicator _bullsSmoother;
		private DecimalLengthIndicator _bearsSmoother;
		private readonly List<UltraFatlSnapshot> _history = new();
		private readonly FatlFilter _fatl = new();
		private ISubscriptionHandler<ICandleMessage> _subscription;

		public UltraFatlContext(
			ExpUltraFatlDuplexStrategy strategy,
			bool isLong,
			DataType candleType,
			AppliedPrices appliedPrice,
			SmoothMethods trendMethod,
			int startLength,
			int phase,
			int step,
			int stepsTotal,
			SmoothMethods smoothMethod,
			int smoothLength,
			int smoothPhase,
			int signalBar,
			decimal volume,
			bool allowEntries,
			bool allowExits,
			int stopLossPoints,
			int takeProfitPoints,
			decimal priceStep)
		{
			_strategy = strategy;
			_isLong = isLong;
			_candleType = candleType;
			_appliedPrice = appliedPrice;
			_trendMethod = trendMethod;
			_startLength = startLength;
			_phase = phase;
			_step = step;
			_stepsTotal = stepsTotal;
			_smoothMethod = smoothMethod;
			_smoothLength = smoothLength;
			_smoothPhase = smoothPhase;
			_signalBar = signalBar;
			_volume = volume;
			_allowEntries = allowEntries;
			_allowExits = allowExits;
			_stopLossPoints = stopLossPoints;
			_takeProfitPoints = takeProfitPoints;
			_priceStep = priceStep;
		}

		public void Start()
		{
			_ladder.Clear();
			_previousValues.Clear();
			_history.Clear();
			_fatl.Reset();

			for (var i = 0; i <= _stepsTotal; i++)
			{
				var length = Math.Max(1, _startLength + i * _step);
				var indicator = CreateMovingAverage(_trendMethod, length, _phase);
				_ladder.Add(indicator);
				_previousValues.Add(null);
			}

			var counterLength = Math.Max(1, _smoothLength);
			_bullsSmoother = CreateMovingAverage(_smoothMethod, counterLength, _smoothPhase);
			_bearsSmoother = CreateMovingAverage(_smoothMethod, counterLength, _smoothPhase);

			_subscription = _strategy.SubscribeCandles(_candleType);
			_subscription.Bind(ProcessCandle).Start();

			_strategy.RegisterPriceChartOnce(_subscription);

			var indicatorArea = _strategy.CreateChartArea();
			if (indicatorArea != null)
			{
				if (_bullsSmoother != null)
					_strategy.DrawIndicator(indicatorArea, _bullsSmoother);
				if (_bearsSmoother != null)
					_strategy.DrawIndicator(indicatorArea, _bearsSmoother);
			}
		}

		private void ProcessCandle(ICandleMessage candle)
		{
			if (candle.State != CandleStates.Finished)
				return;

			if (!_allowEntries && !_allowExits && _stopLossPoints <= 0 && _takeProfitPoints <= 0)
				return;

			_strategy.CheckStops(_isLong, candle, _stopLossPoints, _takeProfitPoints, _priceStep);

			if (_volume <= 0m && !_allowExits)
				return;

			var price = GetAppliedPrice(candle, _appliedPrice);
			var fatlValue = _fatl.Process(price);
			if (fatlValue is null)
				return;

			decimal upCount = 0m;
			decimal downCount = 0m;

			for (var i = 0; i < _ladder.Count; i++)
			{
				var indicatorValue = _ladder[i].Process(new DecimalIndicatorValue(_ladder[i], fatlValue.Value, candle.OpenTime) { IsFinal = true });
				if (!indicatorValue.IsFinal)
					return;

				var curVal = indicatorValue.GetValue<decimal>();

				if (_previousValues[i] is not decimal prevVal)
				{
					_previousValues[i] = curVal;
					return;
				}

				if (curVal > prevVal)
					upCount += 1m;
				else
					downCount += 1m;

				_previousValues[i] = curVal;
			}

			if (_bullsSmoother is null || _bearsSmoother is null)
				return;

			var bullsValue = _bullsSmoother.Process(new DecimalIndicatorValue(_bullsSmoother, upCount, candle.OpenTime) { IsFinal = true });
			var bearsValue = _bearsSmoother.Process(new DecimalIndicatorValue(_bearsSmoother, downCount, candle.OpenTime) { IsFinal = true });

			if (!bullsValue.IsFinal || !bearsValue.IsFinal)
				return;

			var bulls = bullsValue.GetValue<decimal>();
			var bears = bearsValue.GetValue<decimal>();

			_history.Add(new UltraFatlSnapshot(candle.CloseTime, bulls, bears, candle.ClosePrice, candle.HighPrice, candle.LowPrice));

			var maxHistory = Math.Max(10, Math.Max(_signalBar, 1) + 5);
			if (_history.Count > maxHistory)
				_history.RemoveRange(0, _history.Count - maxHistory);

			var effectiveShift = Math.Max(1, _signalBar);
			if (_history.Count <= effectiveShift)
				return;

			var currentIndex = _history.Count - effectiveShift;
			var previousIndex = currentIndex - 1;
			if (previousIndex < 0 || currentIndex >= _history.Count)
				return;

			var current = _history[currentIndex];
			var previous = _history[previousIndex];
			var bullishBias = current.Bulls > current.Bears;
			var bearishBias = current.Bears > current.Bulls;

			bool closeSignal;
			bool openSignal;

			if (_isLong)
			{
				openSignal = bullishBias && previous.Bulls <= previous.Bears;
				closeSignal = bearishBias;
			}
			else
			{
				openSignal = bearishBias && previous.Bulls >= previous.Bears;
				closeSignal = bullishBias;
			}

			if (!openSignal && !closeSignal)
				return;

			if (!_allowEntries)
				openSignal = false;

			if (!_allowExits)
				closeSignal = false;

			_strategy.ProcessDirectionalSignal(_isLong, openSignal, closeSignal, current, _volume);
		}

		public void Dispose()
		{
			_subscription?.Dispose();
		}
	}

	private sealed class FatlFilter
	{
		private static readonly decimal[] _coefficients =
		{
			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 decimal[] _buffer = new decimal[_coefficients.Length];
		private int _filled;

		public void Reset()
		{
			Array.Clear(_buffer, 0, _buffer.Length);
			_filled = 0;
		}

		public decimal? Process(decimal value)
		{
			for (var i = _buffer.Length - 1; i > 0; i--)
				_buffer[i] = _buffer[i - 1];

			_buffer[0] = value;

			if (_filled < _buffer.Length)
				_filled++;

			if (_filled < _buffer.Length)
				return null;

			decimal sum = 0m;
			for (var i = 0; i < _coefficients.Length; i++)
				sum += _coefficients[i] * _buffer[i];

			return sum;
		}
	}
}