XFatlXSatlCloud Duplex
概述
XFatlXSatlCloud Duplex 是从原始 MQL5 专家顾问移植而来的双向策略。策略使用 XFatlXSatlCloud 指标,该指标将快速的 FATL 数字滤波器与较慢的 SATL 滤波器结合,并使用可配置的移动平均线对两条曲线进行平滑。多头与空头可以分别设置不同的时间框架、平滑方法以及价格来源。
交易逻辑
策略仅在蜡烛收盘后进行计算。多头与空头逻辑由两个独立的订阅驱动,每个订阅都会调用 C# 实现的 XFatlXSatlCloud 指标,并遵循以下规则:
- 多头入场:当
LongSignalBar指定的柱子上快线向上穿越慢线时触发。如果当前持有空头,并且ShortAllowClose允许平仓,则会先平掉空头,再按LongVolume下市价买单,并记录入场价用于风控。 - 多头离场:当快线在相同偏移柱子下穿慢线时触发。若设置了
LongStopLoss或LongTakeProfit,当当前蜡烛的最高价/最低价触发这些绝对距离时,也会提前平仓。 - 空头入场:当
ShortSignalBar指定的柱子上快线向下穿越慢线时触发。若存在多头并且LongAllowClose允许平仓,则先平掉多头,然后按ShortVolume提交市价卖单。 - 空头离场:当快线在相同偏移柱子上穿慢线时触发。
ShortStopLoss和ShortTakeProfit会监控当前蜡烛的极值,满足条件时立即离场。
所有信号均基于收盘完成的数据,从而重现原版 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 系数计算,再交由所选平滑指标处理。
- 目前提供的平滑方法包括
Sma、Ema、Smma、Lwma、Jurik、ZeroLag、Kaufman。MQL 中的其他选项(如 Parabolic、T3)暂不支持。 LongSignalBar与ShortSignalBar与原版参数含义一致,取值 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; }
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Indicators import (
SimpleMovingAverage, ExponentialMovingAverage, SmoothedMovingAverage,
WeightedMovingAverage, JurikMovingAverage, ZeroLagExponentialMovingAverage,
KaufmanAdaptiveMovingAverage
)
from StockSharp.Algo.Strategies import Strategy
from indicator_extensions import *
_FATL_COEFF = [
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,
]
_SATL_COEFF = [
0.0982862174, 0.0975682269, 0.0961401078, 0.0940230544,
0.0912437090, 0.0878391006, 0.0838544303, 0.0793406350,
0.0743569346, 0.0689666682, 0.0632381578, 0.0572428925,
0.0510534242, 0.0447468229, 0.0383959950, 0.0320735368,
0.0258537721, 0.0198005183, 0.0139807863, 0.0084512448,
0.0032639979, -0.0015350359, -0.0059060082, -0.0098190256,
-0.0132507215, -0.0161875265, -0.0186164872, -0.0205446727,
-0.0219739146, -0.0229204861, -0.0234080863, -0.0234566315,
-0.0231017777, -0.0223796900, -0.0213300463, -0.0199924534,
-0.0184126992, -0.0166377699, -0.0147139428, -0.0126796776,
-0.0105938331, -0.0084736770, -0.0063841850, -0.0043466731,
-0.0023956944, -0.0005535180, 0.0011421469, 0.0026845693,
0.0040471369, 0.0052380201, 0.0062194591, 0.0070340085,
0.0076266453, 0.0080376628, 0.0083037666, 0.0083694798,
0.0082901022, 0.0080741359, 0.0077543820, 0.0073260526,
0.0068163569, 0.0062325477, 0.0056078229, 0.0049516078,
0.0161380976,
]
class x_fatl_x_satl_cloud_duplex_strategy(Strategy):
def __init__(self):
super(x_fatl_x_satl_cloud_duplex_strategy, self).__init__()
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(4)))
self._long_length1 = self.Param("LongLength1", 3)
self._long_length2 = self.Param("LongLength2", 5)
self._long_signal_bar = self.Param("LongSignalBar", 1)
self._long_stop_loss = self.Param("LongStopLoss", 0.0)
self._long_take_profit = self.Param("LongTakeProfit", 0.0)
self._long_history = []
self._long_entry_price = None
self._price_buffer = [0.0] * len(_SATL_COEFF)
self._buf_idx = 0
self._buf_count = 0
self._fast_smoother = None
self._slow_smoother = None
@property
def CandleType(self):
return self._candle_type.Value
@CandleType.setter
def CandleType(self, value):
self._candle_type.Value = value
@property
def LongLength1(self):
return self._long_length1.Value
@property
def LongLength2(self):
return self._long_length2.Value
@property
def LongSignalBar(self):
return self._long_signal_bar.Value
@property
def LongStopLoss(self):
return self._long_stop_loss.Value
@property
def LongTakeProfit(self):
return self._long_take_profit.Value
def OnStarted2(self, time):
super(x_fatl_x_satl_cloud_duplex_strategy, self).OnStarted2(time)
self._fast_smoother = JurikMovingAverage()
self._fast_smoother.Length = max(1, self.LongLength1)
self._slow_smoother = JurikMovingAverage()
self._slow_smoother.Length = max(1, self.LongLength2)
subscription = self.SubscribeCandles(self.CandleType)
subscription.Bind(self._process_candle).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawOwnTrades(area)
def _process_candle(self, candle):
if candle.State != CandleStates.Finished:
return
price = float(candle.ClosePrice)
buf_len = len(self._price_buffer)
self._price_buffer[self._buf_idx] = price
self._buf_idx = (self._buf_idx + 1) % buf_len
if self._buf_count < buf_len:
self._buf_count += 1
fast_raw = self._compute_filter(_FATL_COEFF)
slow_raw = self._compute_filter(_SATL_COEFF)
t = candle.OpenTime
fv = process_float(self._fast_smoother, fast_raw, t, True)
sv = process_float(self._slow_smoother, slow_raw, t, True)
fast = float(fv)
slow = float(sv)
self._long_history.insert(0, (fast, slow))
max_size = max(self.LongSignalBar + 2, 2)
if len(self._long_history) > max_size:
self._long_history.pop()
if self._handle_long_risk(candle):
return
if len(self._long_history) <= self.LongSignalBar + 1:
return
current = self._long_history[self.LongSignalBar]
previous = self._long_history[self.LongSignalBar + 1]
cross_up = current[0] > current[1] and previous[0] <= previous[1]
cross_down = current[0] < current[1] and previous[0] >= previous[1]
if cross_down and self.Position > 0:
self.SellMarket()
self._long_entry_price = None
if cross_up and self.Position <= 0:
if self.Position < 0:
self.BuyMarket()
self.BuyMarket()
self._long_entry_price = float(candle.ClosePrice)
if cross_down and self.Position >= 0:
if self.Position > 0:
self.SellMarket()
self.SellMarket()
def _handle_long_risk(self, candle):
if self.Position <= 0 or self._long_entry_price is None:
return False
entry = self._long_entry_price
if self.LongStopLoss > 0 and float(candle.LowPrice) <= entry - self.LongStopLoss:
self.SellMarket()
self._long_entry_price = None
return True
if self.LongTakeProfit > 0 and float(candle.HighPrice) >= entry + self.LongTakeProfit:
self.SellMarket()
self._long_entry_price = None
return True
return False
def _compute_filter(self, coefficients):
if self._buf_count < len(coefficients):
return 0.0
total = 0.0
for i in range(len(coefficients)):
idx = self._buf_idx - 1 - i
if idx < 0:
idx += len(self._price_buffer)
total += coefficients[i] * self._price_buffer[idx]
return total
def OnReseted(self):
super(x_fatl_x_satl_cloud_duplex_strategy, self).OnReseted()
self._long_history = []
self._long_entry_price = None
self._price_buffer = [0.0] * len(_SATL_COEFF)
self._buf_idx = 0
self._buf_count = 0
def CreateClone(self):
return x_fatl_x_satl_cloud_duplex_strategy()