Exp UltraFATL 双向策略
概述
Exp UltraFATL 双向策略 是对 MetaTrader 5 专家顾问 Exp_UltraFatl_Duplex 的 C# 版本。策略为多头和空头各自运行一套 UltraFATL 指标管线,计算平滑后的 FATL 梯形序列中有多少层向上或向下,从而判断买卖力量的相对强弱并生成交易信号。
交易逻辑
- 为每个方向订阅所配置的蜡烛时间框。
- 使用 39 阶 FATL 数字滤波器处理所选价格(收盘价、典型价、DeMark 价等)。
- 将滤波结果送入多个平滑器组成的梯形结构,平滑方法与长度增量可配置。
- 比较梯形中相邻两个值,统计上升票数和下降票数,并对两个计数器再次平滑。
- 在设定的信号偏移(默认上一根完整蜡烛)上评估计数器:
- 多头模块:上一根蜡烛多头票数大于空头,同时当前蜡烛票数发生下穿(多头 ≤ 空头)时开多;当上一根蜡烛空头票数占优时平多。
- 空头模块:上一根蜡烛空头票数占优,同时当前蜡烛票数上穿(多头 ≥ 空头)时开空;当上一根蜡烛多头票数占优时平空。
- 若设置了止损或止盈,按合约最小变动价位在蜡烛最高/最低价上触发。
策略采用净头寸模式:在开多前会先平掉现有空单,反之亦然,所有进出场都使用市价单。
参数
多头模块
- Long Volume – 开多时的下单数量。
- Allow Long Entries – 是否允许新的多头头寸。
- Allow Long Exits – 是否允许在反向信号上平多。
- Long Candle Type – 多头 UltraFATL 使用的时间框。
- Long Applied Price – 送入 FATL 的价格类型。
- 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 Volume、Allow Short Entries/Exits、Short Candle Type、Short Applied Price、梯形与计数器平滑设置、Short Signal Bar、以及止损/止盈点数。
实现说明
- 平滑方法映射到 StockSharp 中的指标实现。Jurik 相关选项使用
JurikMovingAverage,Parabolic与T3由于原始自定义滤波器不可用,使用指数或 Jurik 平滑近似实现。 - 止损/止盈在蜡烛数据上判断,并非向交易服务器发送真实保护单。
- 由于策略只处理已完成蜡烛,信号偏移小于 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;
}
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Strategies import Strategy
class exp_ultra_fatl_duplex_strategy(Strategy):
_FATL_COEFFICIENTS = [
0.4360409450, 0.3658689069, 0.2460452079, 0.1104506886,
-0.0054034585, -0.0760367731, -0.0933058722, -0.0670110374,
-0.0190795053, 0.0259609206, 0.0502044896, 0.0477818607,
0.0249252327, -0.0047706151, -0.0272432537, -0.0338917071,
-0.0244141482, -0.0055774838, 0.0128149838, 0.0226522218,
0.0208778257, 0.0100299086, -0.0036771622, -0.0136744850,
-0.0160483392, -0.0108597376, -0.0016060704, 0.0069480557,
0.0110573605, 0.0095711419, 0.0040444064, -0.0023824623,
-0.0067093714, -0.0072003400, -0.0047717710, 0.0005541115,
0.0007860160, 0.0130129076, 0.0040364019,
]
def __init__(self):
super(exp_ultra_fatl_duplex_strategy, self).__init__()
self._long_volume = self.Param("LongVolume", 1.0) \
.SetDisplay("Long Volume", "Order volume for long entries", "Long")
self._allow_long_entries = self.Param("AllowLongEntries", True) \
.SetDisplay("Allow Long Entries", "Enable opening long positions", "Long")
self._allow_long_exits = self.Param("AllowLongExits", True) \
.SetDisplay("Allow Long Exits", "Enable closing long positions on opposite signals", "Long")
self._long_candle_type = self.Param("LongCandleType", DataType.TimeFrame(TimeSpan.FromHours(1))) \
.SetDisplay("Long Candle Type", "Timeframe used by the long UltraFATL block", "Long")
self._long_start_length = self.Param("LongStartLength", 8) \
.SetDisplay("Long Start Length", "Initial smoothing length for the ladder", "Long")
self._long_step = self.Param("LongStep", 3) \
.SetDisplay("Long Step", "Increment between ladder lengths", "Long")
self._long_steps_total = self.Param("LongStepsTotal", 6) \
.SetDisplay("Long Steps", "Number of smoothing steps for the ladder", "Long")
self._long_smooth_length = self.Param("LongSmoothLength", 8) \
.SetDisplay("Long Counter Length", "Length used when smoothing the counters", "Long")
self._long_signal_bar = self.Param("LongSignalBar", 1) \
.SetDisplay("Long Signal Bar", "Closed-bar offset used when evaluating long signals", "Long")
self._long_stop_loss_points = self.Param("LongStopLossPoints", 0) \
.SetDisplay("Long Stop pts", "Protective stop distance in price steps for long trades", "Long")
self._long_take_profit_points = self.Param("LongTakeProfitPoints", 0) \
.SetDisplay("Long Target pts", "Take-profit distance in price steps for long trades", "Long")
self._short_volume = self.Param("ShortVolume", 1.0) \
.SetDisplay("Short Volume", "Order volume for short entries", "Short")
self._allow_short_entries = self.Param("AllowShortEntries", True) \
.SetDisplay("Allow Short Entries", "Enable opening short positions", "Short")
self._allow_short_exits = self.Param("AllowShortExits", True) \
.SetDisplay("Allow Short Exits", "Enable closing short positions on opposite signals", "Short")
self._short_candle_type = self.Param("ShortCandleType", DataType.TimeFrame(TimeSpan.FromHours(1))) \
.SetDisplay("Short Candle Type", "Timeframe used by the short UltraFATL block", "Short")
self._short_start_length = self.Param("ShortStartLength", 8) \
.SetDisplay("Short Start Length", "Initial smoothing length for the short ladder", "Short")
self._short_step = self.Param("ShortStep", 3) \
.SetDisplay("Short Step", "Increment between smoothing lengths for the short ladder", "Short")
self._short_steps_total = self.Param("ShortStepsTotal", 6) \
.SetDisplay("Short Steps", "Number of smoothing steps for the short ladder", "Short")
self._short_smooth_length = self.Param("ShortSmoothLength", 8) \
.SetDisplay("Short Counter Length", "Length used when smoothing the short counters", "Short")
self._short_signal_bar = self.Param("ShortSignalBar", 1) \
.SetDisplay("Short Signal Bar", "Closed-bar offset used when evaluating short signals", "Short")
self._short_stop_loss_points = self.Param("ShortStopLossPoints", 0) \
.SetDisplay("Short Stop pts", "Protective stop distance in price steps for short trades", "Short")
self._short_take_profit_points = self.Param("ShortTakeProfitPoints", 0) \
.SetDisplay("Short Target pts", "Take-profit distance in price steps for short trades", "Short")
self._long_entry_price = None
self._short_entry_price = None
self._price_step = 0.0
@property
def long_volume(self):
return self._long_volume.Value
@property
def allow_long_entries(self):
return self._allow_long_entries.Value
@property
def allow_long_exits(self):
return self._allow_long_exits.Value
@property
def long_candle_type(self):
return self._long_candle_type.Value
@property
def long_start_length(self):
return self._long_start_length.Value
@property
def long_step(self):
return self._long_step.Value
@property
def long_steps_total(self):
return self._long_steps_total.Value
@property
def long_smooth_length(self):
return self._long_smooth_length.Value
@property
def long_signal_bar(self):
return self._long_signal_bar.Value
@property
def long_stop_loss_points(self):
return self._long_stop_loss_points.Value
@property
def long_take_profit_points(self):
return self._long_take_profit_points.Value
@property
def short_volume(self):
return self._short_volume.Value
@property
def allow_short_entries(self):
return self._allow_short_entries.Value
@property
def allow_short_exits(self):
return self._allow_short_exits.Value
@property
def short_candle_type(self):
return self._short_candle_type.Value
@property
def short_start_length(self):
return self._short_start_length.Value
@property
def short_step(self):
return self._short_step.Value
@property
def short_steps_total(self):
return self._short_steps_total.Value
@property
def short_smooth_length(self):
return self._short_smooth_length.Value
@property
def short_signal_bar(self):
return self._short_signal_bar.Value
@property
def short_stop_loss_points(self):
return self._short_stop_loss_points.Value
@property
def short_take_profit_points(self):
return self._short_take_profit_points.Value
def OnReseted(self):
super(exp_ultra_fatl_duplex_strategy, self).OnReseted()
self._long_entry_price = None
self._short_entry_price = None
self._price_step = 0.0
self._long_ctx = None
self._short_ctx = None
def _make_context(self, start_length, step, steps_total, smooth_length):
ladder_lengths = []
for i in range(steps_total + 1):
ladder_lengths.append(max(1, start_length + i * step))
counter_len = max(1, smooth_length)
return {
'fatl_buffer': [0.0] * len(self._FATL_COEFFICIENTS),
'fatl_filled': 0,
'ladder_lengths': ladder_lengths,
'ladder_ema_vals': [None] * (steps_total + 1),
'prev_values': [None] * (steps_total + 1),
'bulls_ema_val': None,
'bears_ema_val': None,
'ladder_multipliers': [2.0 / (l + 1) for l in ladder_lengths],
'bulls_mult': 2.0 / (counter_len + 1),
'bears_mult': 2.0 / (counter_len + 1),
'history': [],
}
def _ema_process(self, prev_val, new_val, mult):
if prev_val is None:
return new_val
return prev_val + mult * (new_val - prev_val)
def OnStarted2(self, time):
super(exp_ultra_fatl_duplex_strategy, self).OnStarted2(time)
self._price_step = float(self.Security.PriceStep) if self.Security is not None and self.Security.PriceStep is not None else 0.0
self._long_ctx = self._make_context(
self.long_start_length, self.long_step, self.long_steps_total, self.long_smooth_length)
self._short_ctx = self._make_context(
self.short_start_length, self.short_step, self.short_steps_total, self.short_smooth_length)
sub_long = self.SubscribeCandles(self.long_candle_type)
sub_long.Bind(self._process_long_candle).Start()
sub_short = self.SubscribeCandles(self.short_candle_type)
sub_short.Bind(self._process_short_candle).Start()
def _fatl_process(self, ctx, value):
buf = ctx['fatl_buffer']
buf_len = len(self._FATL_COEFFICIENTS)
for i in range(buf_len - 1, 0, -1):
buf[i] = buf[i - 1]
buf[0] = value
filled = ctx['fatl_filled']
if filled < buf_len:
filled += 1
ctx['fatl_filled'] = filled
if filled < buf_len:
return None
total = 0.0
for i in range(buf_len):
total += self._FATL_COEFFICIENTS[i] * buf[i]
return total
def _process_context_candle(self, candle, is_long):
if candle.State != CandleStates.Finished:
return
close = float(candle.ClosePrice)
price_step = self._price_step
if is_long:
ctx = self._long_ctx
signal_bar = self.long_signal_bar
sl_pts = self.long_stop_loss_points
tp_pts = self.long_take_profit_points
vol = float(self.long_volume)
allow_entries = self.allow_long_entries
allow_exits = self.allow_long_exits
else:
ctx = self._short_ctx
signal_bar = self.short_signal_bar
sl_pts = self.short_stop_loss_points
tp_pts = self.short_take_profit_points
vol = float(self.short_volume)
allow_entries = self.allow_short_entries
allow_exits = self.allow_short_exits
if ctx is None:
return
# Check stops first
self._check_stops(is_long, candle, sl_pts, tp_pts, price_step)
if vol <= 0 and not allow_exits:
return
# FATL filter
fatl_val = self._fatl_process(ctx, close)
if fatl_val is None:
return
# Process ladder with manual EMA
up_count = 0.0
down_count = 0.0
ladder_count = len(ctx['ladder_lengths'])
for i in range(ladder_count):
new_ema = self._ema_process(ctx['ladder_ema_vals'][i], fatl_val, ctx['ladder_multipliers'][i])
ctx['ladder_ema_vals'][i] = new_ema
if ctx['prev_values'][i] is None:
ctx['prev_values'][i] = new_ema
return
prev_val = ctx['prev_values'][i]
if new_ema > prev_val:
up_count += 1.0
else:
down_count += 1.0
ctx['prev_values'][i] = new_ema
# Smooth counters with EMA
new_bulls = self._ema_process(ctx['bulls_ema_val'], up_count, ctx['bulls_mult'])
ctx['bulls_ema_val'] = new_bulls
new_bears = self._ema_process(ctx['bears_ema_val'], down_count, ctx['bears_mult'])
ctx['bears_ema_val'] = new_bears
bulls = new_bulls
bears = new_bears
history = ctx['history']
history.append((bulls, bears, float(candle.ClosePrice), float(candle.HighPrice), float(candle.LowPrice)))
max_hist = max(10, max(1, signal_bar) + 5)
if len(history) > max_hist:
history[:] = history[-max_hist:]
effective_shift = max(1, signal_bar)
if len(history) <= effective_shift:
return
current_index = len(history) - effective_shift
previous_index = current_index - 1
if previous_index < 0 or current_index >= len(history):
return
cur_bulls, cur_bears, cur_close, _, _ = history[current_index]
prev_bulls, prev_bears, _, _, _ = history[previous_index]
bullish_bias = cur_bulls > cur_bears
bearish_bias = cur_bears > cur_bulls
if is_long:
open_signal = bullish_bias and prev_bulls <= prev_bears
close_signal = bearish_bias
else:
open_signal = bearish_bias and prev_bulls >= prev_bears
close_signal = bullish_bias
if not open_signal and not close_signal:
return
if not allow_entries:
open_signal = False
if not allow_exits:
close_signal = False
self._process_directional_signal(is_long, open_signal, close_signal, cur_close, vol)
def _process_long_candle(self, candle):
self._process_context_candle(candle, True)
def _process_short_candle(self, candle):
self._process_context_candle(candle, False)
def _process_directional_signal(self, is_long, open_signal, close_signal, close_price, volume):
if is_long:
if close_signal and self.allow_long_exits and self.Position > 0:
self.SellMarket(self.Position)
self._long_entry_price = None
if open_signal and self.allow_long_entries and self.Position <= 0 and volume > 0:
buy_vol = volume + (-self.Position if self.Position < 0 else 0)
self.BuyMarket(buy_vol)
self._long_entry_price = close_price
else:
if close_signal and self.allow_short_exits and self.Position < 0:
self.BuyMarket(-self.Position)
self._short_entry_price = None
if open_signal and self.allow_short_entries and self.Position >= 0 and volume > 0:
sell_vol = volume + (self.Position if self.Position > 0 else 0)
self.SellMarket(sell_vol)
self._short_entry_price = close_price
def _check_stops(self, is_long, candle, stop_loss_points, take_profit_points, price_step):
if price_step <= 0:
return
if is_long:
if self.Position <= 0 or self._long_entry_price is None:
return
entry = self._long_entry_price
sl_price = entry - stop_loss_points * price_step if stop_loss_points > 0 else None
tp_price = entry + take_profit_points * price_step if take_profit_points > 0 else None
if sl_price is not None and float(candle.LowPrice) <= sl_price:
self.SellMarket()
self._long_entry_price = None
return
if tp_price is not None and float(candle.HighPrice) >= tp_price:
self.SellMarket()
self._long_entry_price = None
else:
if self.Position >= 0 or self._short_entry_price is None:
return
entry = self._short_entry_price
sl_price = entry + stop_loss_points * price_step if stop_loss_points > 0 else None
tp_price = entry - take_profit_points * price_step if take_profit_points > 0 else None
if sl_price is not None and float(candle.HighPrice) >= sl_price:
self.BuyMarket()
self._short_entry_price = None
return
if tp_price is not None and float(candle.LowPrice) <= tp_price:
self.BuyMarket()
self._short_entry_price = None
def CreateClone(self):
return exp_ultra_fatl_duplex_strategy()