XFatl XSatl Cloud 逆势策略
本策略在 StockSharp 中复刻了 MT5 专家顾问 Exp_XFatlXSatlCloud。它跟踪经过平滑处理的 FATL/SATL 云图,并在交叉发生后执行逆势交易:当快速线(XFATL)先位于慢速线(XSATL)之上、随后回落到其下方时开多;当快速线先位于慢速线之下、随后重新上穿时开空。止盈与止损使用合约的最小价格步长表示。
交易逻辑
- 默认时间框架为 8 小时,可通过
CandleType参数选择其他蜡烛类型。 - 两条云线由 StockSharp 的移动平均组合构成。默认使用 Jurik 均线,可自定义长度与相位;同时提供 SMA、EMA、SMMA、WMA 等替代方案。
SignalBar参数控制信号所使用的历史蜡烛(相对于最新收盘柱的偏移)。策略维护一段短历史序列,从而精确复现 MT5 中“当前值 vs. 前一个值”的比较方式。- 入场规则(逆势):
- 做多:上一根柱子快速线高于慢速线,而当前柱子回落到慢速线之下或持平。
- 做空:上一根柱子快速线低于慢速线,而当前柱子上穿慢速线或与之持平。
- 离场规则:
- 当上一根柱子呈现空头云(快速线低于慢速线)且
AllowLongExit启用时,平掉现有多单。 - 当上一根柱子呈现多头云(快速线高于慢速线)且
AllowShortExit启用时,平掉现有空单。
- 当上一根柱子呈现空头云(快速线低于慢速线)且
- 在旧仓位完全平仓之前不会建立新仓,保持与原始 MT5 策略相同的节奏。
风险控制
TradeVolume决定每次下单的数量,策略不会分批加仓。TakeProfitTicks与StopLossTicks会转换为价格步长,交由StartProtection管理。设置为 0 即可关闭相应的保护单。- MT5 版本依赖于经纪商参数计算手数,本移植版改为直接控制下单量与止盈止损距离。
参数说明
| 参数 | 说明 |
|---|---|
CandleType |
计算指标所用的蜡烛类型或时间框架。 |
FastMethod / SlowMethod |
XFATL 与 XSATL 所使用的平滑算法(默认 Jurik)。 |
FastLength / SlowLength |
快速线与慢速线的周期。 |
FastPhase / SlowPhase |
Jurik 均线的相位参数(若指标支持)。 |
SignalBar |
评估信号时所使用的历史偏移(1 = 前一根已完成蜡烛)。 |
TradeVolume |
每次建仓的数量。 |
AllowLongEntry / AllowShortEntry |
是否允许做多 / 做空的逆势入场。 |
AllowLongExit / AllowShortExit |
是否允许根据相反信号平掉现有多单 / 空单。 |
TakeProfitTicks |
止盈距离(单位:价格步长)。 |
StopLossTicks |
止损距离(单位:价格步长)。 |
实现细节
- 仅保存满足
SignalBar需要的少量指标历史值,避免额外的数据缓冲区。 - 通过反射设置 Jurik 指标的相位参数,以兼容不同版本的 StockSharp;若指标不支持该属性则忽略。
- 当前实现使用蜡烛收盘价作为输入,与多数原始 EA 配置一致。如需其它价格类型需进一步扩展代码。
- 整个策略使用高层 API(
SubscribeCandles、Bind、StartProtection),方便在 Designer、Runner 等 StockSharp 产品中直接复用。
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>
/// Contrarian strategy based on the XFatl and XSatl cloud crossovers.
/// </summary>
public class XFatlXSatlCloudStrategy : Strategy
{
private readonly StrategyParam<DataType> _candleType;
private readonly StrategyParam<SmoothMethods> _fastMethod;
private readonly StrategyParam<int> _fastLength;
private readonly StrategyParam<int> _fastPhase;
private readonly StrategyParam<SmoothMethods> _slowMethod;
private readonly StrategyParam<int> _slowLength;
private readonly StrategyParam<int> _slowPhase;
private readonly StrategyParam<int> _signalBar;
private readonly StrategyParam<decimal> _tradeVolume;
private readonly StrategyParam<bool> _allowLongEntry;
private readonly StrategyParam<bool> _allowShortEntry;
private readonly StrategyParam<bool> _allowLongExit;
private readonly StrategyParam<bool> _allowShortExit;
private readonly StrategyParam<int> _takeProfitTicks;
private readonly StrategyParam<int> _stopLossTicks;
private readonly List<decimal> _fastHistory = new();
private readonly List<decimal> _slowHistory = new();
public XFatlXSatlCloudStrategy()
{
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
.SetDisplay("Candle Type", "Time frame for indicator calculations", "General");
_fastMethod = Param(nameof(FastMethod), SmoothMethods.Ema)
.SetDisplay("Fast Method", "Smoothing algorithm for the fast line", "Indicators");
_fastLength = Param(nameof(FastLength), 3)
.SetGreaterThanZero()
.SetDisplay("Fast Length", "Length of the fast filter", "Indicators");
_fastPhase = Param(nameof(FastPhase), 15)
.SetDisplay("Fast Phase", "Phase parameter for Jurik smoothing", "Indicators");
_slowMethod = Param(nameof(SlowMethod), SmoothMethods.Ema)
.SetDisplay("Slow Method", "Smoothing algorithm for the slow line", "Indicators");
_slowLength = Param(nameof(SlowLength), 5)
.SetGreaterThanZero()
.SetDisplay("Slow Length", "Length of the slow filter", "Indicators");
_slowPhase = Param(nameof(SlowPhase), 15)
.SetDisplay("Slow Phase", "Phase parameter for Jurik smoothing", "Indicators");
_signalBar = Param(nameof(SignalBar), 1)
.SetNotNegative()
.SetDisplay("Signal Bar", "Index of the bar used for signals", "Logic");
_tradeVolume = Param(nameof(TradeVolume), 1m)
.SetGreaterThanZero()
.SetDisplay("Trade Volume", "Order size in lots", "Risk");
_allowLongEntry = Param(nameof(AllowLongEntry), true)
.SetDisplay("Allow Long Entry", "Enable contrarian long trades", "Logic");
_allowShortEntry = Param(nameof(AllowShortEntry), true)
.SetDisplay("Allow Short Entry", "Enable contrarian short trades", "Logic");
_allowLongExit = Param(nameof(AllowLongExit), true)
.SetDisplay("Allow Long Exit", "Allow indicator to close long trades", "Logic");
_allowShortExit = Param(nameof(AllowShortExit), true)
.SetDisplay("Allow Short Exit", "Allow indicator to close short trades", "Logic");
_takeProfitTicks = Param(nameof(TakeProfitTicks), 2000)
.SetNotNegative()
.SetDisplay("Take Profit Ticks", "Distance to take profit in price steps", "Risk");
_stopLossTicks = Param(nameof(StopLossTicks), 1000)
.SetNotNegative()
.SetDisplay("Stop Loss Ticks", "Distance to stop loss in price steps", "Risk");
}
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
public SmoothMethods FastMethod
{
get => _fastMethod.Value;
set => _fastMethod.Value = value;
}
public int FastLength
{
get => _fastLength.Value;
set => _fastLength.Value = value;
}
public int FastPhase
{
get => _fastPhase.Value;
set => _fastPhase.Value = value;
}
public SmoothMethods SlowMethod
{
get => _slowMethod.Value;
set => _slowMethod.Value = value;
}
public int SlowLength
{
get => _slowLength.Value;
set => _slowLength.Value = value;
}
public int SlowPhase
{
get => _slowPhase.Value;
set => _slowPhase.Value = value;
}
public int SignalBar
{
get => _signalBar.Value;
set => _signalBar.Value = value;
}
public decimal TradeVolume
{
get => _tradeVolume.Value;
set => _tradeVolume.Value = value;
}
public bool AllowLongEntry
{
get => _allowLongEntry.Value;
set => _allowLongEntry.Value = value;
}
public bool AllowShortEntry
{
get => _allowShortEntry.Value;
set => _allowShortEntry.Value = value;
}
public bool AllowLongExit
{
get => _allowLongExit.Value;
set => _allowLongExit.Value = value;
}
public bool AllowShortExit
{
get => _allowShortExit.Value;
set => _allowShortExit.Value = value;
}
public int TakeProfitTicks
{
get => _takeProfitTicks.Value;
set => _takeProfitTicks.Value = value;
}
public int StopLossTicks
{
get => _stopLossTicks.Value;
set => _stopLossTicks.Value = value;
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_fastHistory.Clear();
_slowHistory.Clear();
}
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
var fastIndicator = CreateIndicator(FastMethod, FastLength, FastPhase);
var slowIndicator = CreateIndicator(SlowMethod, SlowLength, SlowPhase);
var subscription = SubscribeCandles(CandleType);
subscription.Bind(fastIndicator, slowIndicator, ProcessCandle).Start();
var step = Security?.PriceStep ?? 1m;
Unit takeProfit = null;
if (TakeProfitTicks > 0)
takeProfit = new Unit(TakeProfitTicks * step, UnitTypes.Absolute);
Unit stopLoss = null;
if (StopLossTicks > 0)
stopLoss = new Unit(StopLossTicks * step, UnitTypes.Absolute);
if (takeProfit != null || stopLoss != null)
StartProtection(takeProfit: takeProfit, stopLoss: stopLoss);
}
private void ProcessCandle(ICandleMessage candle, decimal fastValue, decimal slowValue)
{
if (candle.State != CandleStates.Finished)
return;
UpdateHistory(_fastHistory, fastValue);
UpdateHistory(_slowHistory, slowValue);
var required = SignalBar + 2;
if (_fastHistory.Count < required || _slowHistory.Count < required)
return;
var fastCurrent = GetShiftedValue(_fastHistory, SignalBar);
var fastPrevious = GetShiftedValue(_fastHistory, SignalBar + 1);
var slowCurrent = GetShiftedValue(_slowHistory, SignalBar);
var slowPrevious = GetShiftedValue(_slowHistory, SignalBar + 1);
// The cloud is considered bullish when the fast line was above the slow line on the prior bar.
var fastWasAbove = fastPrevious > slowPrevious;
var fastWasBelow = fastPrevious < slowPrevious;
var closeShort = AllowShortExit && fastWasAbove && Position < 0;
if (closeShort)
{
BuyMarket();
}
var closeLong = AllowLongExit && fastWasBelow && Position > 0;
if (closeLong)
{
SellMarket();
}
var enterLong = AllowLongEntry && fastWasAbove && fastCurrent <= slowCurrent;
var enterShort = AllowShortEntry && fastWasBelow && fastCurrent >= slowCurrent;
// Wait for the portfolio to flatten before issuing a new entry order.
if (Position != 0)
return;
if (enterLong)
{
BuyMarket();
}
else if (enterShort)
{
SellMarket();
}
}
private void UpdateHistory(List<decimal> history, decimal value)
{
history.Add(value);
var maxSize = SignalBar + 2;
while (history.Count > maxSize)
history.RemoveAt(0);
}
private static decimal GetShiftedValue(List<decimal> history, int shift)
{
var index = history.Count - shift - 1;
if (index >= 0 && index < history.Count)
return history[index];
return 0m;
}
private static IIndicator CreateIndicator(SmoothMethods method, int length, int phase)
{
return method switch
{
SmoothMethods.Sma => new SimpleMovingAverage { Length = length },
SmoothMethods.Ema => new ExponentialMovingAverage { Length = length },
SmoothMethods.Smma => new SmoothedMovingAverage { Length = length },
SmoothMethods.Wma => new WeightedMovingAverage { Length = length },
_ => CreateJurikIndicator(length, phase),
};
}
private static IIndicator CreateJurikIndicator(int length, int phase)
{
var jurik = new JurikMovingAverage { Length = length };
// Configure the Jurik phase through reflection because the property is optional across versions.
var phaseProperty = jurik.GetType().GetProperty("Phase");
if (phaseProperty != null && phaseProperty.CanWrite)
{
var converted = Convert.ChangeType(phase, phaseProperty.PropertyType);
phaseProperty.SetValue(jurik, converted);
}
return jurik;
}
public enum SmoothMethods
{
Sma = 1,
Ema,
Smma,
Wma,
Jurik
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan, Math
from StockSharp.Messages import DataType, CandleStates, Unit, UnitTypes
from StockSharp.Algo.Indicators import (SimpleMovingAverage, ExponentialMovingAverage,
SmoothedMovingAverage, WeightedMovingAverage, JurikMovingAverage)
from StockSharp.Algo.Strategies import Strategy
SMOOTH_SMA = 1
SMOOTH_EMA = 2
SMOOTH_SMMA = 3
SMOOTH_WMA = 4
SMOOTH_JURIK = 5
class x_fatl_x_satl_cloud_strategy(Strategy):
def __init__(self):
super(x_fatl_x_satl_cloud_strategy, self).__init__()
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(4)))
self._fast_method = self.Param("FastMethod", SMOOTH_EMA)
self._fast_length = self.Param("FastLength", 3)
self._fast_phase = self.Param("FastPhase", 15)
self._slow_method = self.Param("SlowMethod", SMOOTH_EMA)
self._slow_length = self.Param("SlowLength", 5)
self._slow_phase = self.Param("SlowPhase", 15)
self._signal_bar = self.Param("SignalBar", 1)
self._trade_volume = self.Param("TradeVolume", 1.0)
self._allow_long_entry = self.Param("AllowLongEntry", True)
self._allow_short_entry = self.Param("AllowShortEntry", True)
self._allow_long_exit = self.Param("AllowLongExit", True)
self._allow_short_exit = self.Param("AllowShortExit", True)
self._take_profit_ticks = self.Param("TakeProfitTicks", 2000)
self._stop_loss_ticks = self.Param("StopLossTicks", 1000)
self._fast_history = []
self._slow_history = []
@property
def CandleType(self):
return self._candle_type.Value
@CandleType.setter
def CandleType(self, value):
self._candle_type.Value = value
@property
def FastMethod(self):
return self._fast_method.Value
@FastMethod.setter
def FastMethod(self, value):
self._fast_method.Value = value
@property
def FastLength(self):
return self._fast_length.Value
@FastLength.setter
def FastLength(self, value):
self._fast_length.Value = value
@property
def FastPhase(self):
return self._fast_phase.Value
@FastPhase.setter
def FastPhase(self, value):
self._fast_phase.Value = value
@property
def SlowMethod(self):
return self._slow_method.Value
@SlowMethod.setter
def SlowMethod(self, value):
self._slow_method.Value = value
@property
def SlowLength(self):
return self._slow_length.Value
@SlowLength.setter
def SlowLength(self, value):
self._slow_length.Value = value
@property
def SlowPhase(self):
return self._slow_phase.Value
@SlowPhase.setter
def SlowPhase(self, value):
self._slow_phase.Value = value
@property
def SignalBar(self):
return self._signal_bar.Value
@SignalBar.setter
def SignalBar(self, value):
self._signal_bar.Value = value
@property
def TradeVolume(self):
return self._trade_volume.Value
@TradeVolume.setter
def TradeVolume(self, value):
self._trade_volume.Value = value
@property
def AllowLongEntry(self):
return self._allow_long_entry.Value
@AllowLongEntry.setter
def AllowLongEntry(self, value):
self._allow_long_entry.Value = value
@property
def AllowShortEntry(self):
return self._allow_short_entry.Value
@AllowShortEntry.setter
def AllowShortEntry(self, value):
self._allow_short_entry.Value = value
@property
def AllowLongExit(self):
return self._allow_long_exit.Value
@AllowLongExit.setter
def AllowLongExit(self, value):
self._allow_long_exit.Value = value
@property
def AllowShortExit(self):
return self._allow_short_exit.Value
@AllowShortExit.setter
def AllowShortExit(self, value):
self._allow_short_exit.Value = value
@property
def TakeProfitTicks(self):
return self._take_profit_ticks.Value
@TakeProfitTicks.setter
def TakeProfitTicks(self, value):
self._take_profit_ticks.Value = value
@property
def StopLossTicks(self):
return self._stop_loss_ticks.Value
@StopLossTicks.setter
def StopLossTicks(self, value):
self._stop_loss_ticks.Value = value
def _create_indicator(self, method, length):
m = int(method)
if m == SMOOTH_SMA:
ind = SimpleMovingAverage()
ind.Length = length
return ind
elif m == SMOOTH_EMA:
ind = ExponentialMovingAverage()
ind.Length = length
return ind
elif m == SMOOTH_SMMA:
ind = SmoothedMovingAverage()
ind.Length = length
return ind
elif m == SMOOTH_WMA:
ind = WeightedMovingAverage()
ind.Length = length
return ind
else:
ind = JurikMovingAverage()
ind.Length = length
return ind
def OnStarted2(self, time):
super(x_fatl_x_satl_cloud_strategy, self).OnStarted2(time)
self._fast_history = []
self._slow_history = []
fast_ind = self._create_indicator(self.FastMethod, int(self.FastLength))
slow_ind = self._create_indicator(self.SlowMethod, int(self.SlowLength))
subscription = self.SubscribeCandles(self.CandleType)
subscription.Bind(fast_ind, slow_ind, self.ProcessCandle).Start()
step = float(self.Security.PriceStep) if self.Security is not None and self.Security.PriceStep is not None else 1.0
if step <= 0.0:
step = 1.0
tp = int(self.TakeProfitTicks)
sl = int(self.StopLossTicks)
tp_unit = Unit(tp * step, UnitTypes.Absolute) if tp > 0 else None
sl_unit = Unit(sl * step, UnitTypes.Absolute) if sl > 0 else None
if tp_unit is not None or sl_unit is not None:
self.StartProtection(tp_unit, sl_unit)
def ProcessCandle(self, candle, fast_value, slow_value):
if candle.State != CandleStates.Finished:
return
fv = float(fast_value)
sv = float(slow_value)
self._update_history(self._fast_history, fv)
self._update_history(self._slow_history, sv)
signal_bar = int(self.SignalBar)
required = signal_bar + 2
if len(self._fast_history) < required or len(self._slow_history) < required:
return
fast_current = self._get_shifted(self._fast_history, signal_bar)
fast_previous = self._get_shifted(self._fast_history, signal_bar + 1)
slow_current = self._get_shifted(self._slow_history, signal_bar)
slow_previous = self._get_shifted(self._slow_history, signal_bar + 1)
fast_was_above = fast_previous > slow_previous
fast_was_below = fast_previous < slow_previous
close_short = self.AllowShortExit and fast_was_above and self.Position < 0
if close_short:
self.BuyMarket()
close_long = self.AllowLongExit and fast_was_below and self.Position > 0
if close_long:
self.SellMarket()
enter_long = self.AllowLongEntry and fast_was_above and fast_current <= slow_current
enter_short = self.AllowShortEntry and fast_was_below and fast_current >= slow_current
if self.Position != 0:
return
if enter_long:
self.BuyMarket()
elif enter_short:
self.SellMarket()
def _update_history(self, history, value):
history.append(value)
max_size = int(self.SignalBar) + 2
while len(history) > max_size:
history.pop(0)
def _get_shifted(self, history, shift):
index = len(history) - shift - 1
if 0 <= index < len(history):
return history[index]
return 0.0
def OnReseted(self):
super(x_fatl_x_satl_cloud_strategy, self).OnReseted()
self._fast_history = []
self._slow_history = []
def CreateClone(self):
return x_fatl_x_satl_cloud_strategy()