在 GitHub 上查看

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 决定每次下单的数量,策略不会分批加仓。
  • TakeProfitTicksStopLossTicks 会转换为价格步长,交由 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(SubscribeCandlesBindStartProtection),方便在 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
	}
}