Открыть на GitHub

Контртрендовая стратегия XFatl XSatl Cloud

Эта стратегия StockSharp повторяет логику MT5-советника Exp_XFatlXSatlCloud. Она отслеживает сглаженное облако FATL/SATL и торгует против направления его пересечения. Если быстрая линия (XFATL) после пребывания выше медленной (XSATL) возвращается ниже, открывается длинная позиция. Когда быстрый фильтр поднимается выше после нахождения под медленным, открывается короткая позиция. Стоп-лосс и тейк-профит задаются в шагах цены инструмента.

Торговая логика

  • По умолчанию используется таймфрейм 8 часов. Параметр CandleType позволяет выбрать другой тип свечей.
  • Две линии индикатора строятся на базе сглаживаний StockSharp. По умолчанию применяются Jurik MA с настраиваемыми длиной и фазой. Доступны альтернативы (SMA, EMA, SMMA, WMA).
  • Сигналы рассчитываются на свече с номером SignalBar (смещение относительно последней закрытой). Значения индикаторов сохраняются в небольшой очереди, что позволяет сравнить текущий и предыдущий бар, как в оригинале MT5.
  • Правила входа (контртренд):
    • Покупка — ранее быстрая линия была выше медленной и теперь опустилась на её уровень или ниже.
    • Продажа — ранее быстрая линия была ниже медленной и теперь поднялась на её уровень или выше.
  • Правила выхода:
    • Лонг закрывается, когда предыдущий бар показал медвежье облако (быстрая ниже медленной) и включён параметр AllowLongExit.
    • Шорт закрывается, когда предыдущий бар показал бычье облако (быстрая выше медленной) и разрешён AllowShortExit.
  • Новая позиция открывается только после полного закрытия предыдущей, что полностью соответствует поведению советника из MT5.

Управление рисками

  • Параметр TradeVolume задаёт объём рыночной заявки. Стратегия не усредняется — каждая сделка открывается одним и тем же количеством.
  • TakeProfitTicks и StopLossTicks переводятся в расстояние в шагах цены и подключаются к модулю StartProtection. Нулевое значение отключает соответствующий защитный приказ.
  • Поскольку в MT5 расчёт размера позиции зависел от брокерских параметров, здесь он заменён явным выбором объёма и уровней защиты.

Параметры

Параметр Описание
CandleType Тип свечей для расчёта индикаторов.
FastMethod / SlowMethod Семейство сглаживания для XFATL и XSATL (по умолчанию Jurik).
FastLength / SlowLength Периоды быстрого и медленного фильтров.
FastPhase / SlowPhase Параметр фазы для Jurik MA, если он поддерживается.
SignalBar Смещение бара для оценки пересечений (1 = предыдущий бар).
TradeVolume Объём входа в позицию.
AllowLongEntry / AllowShortEntry Включение контртрендовых входов в каждую сторону.
AllowLongExit / AllowShortExit Разрешение закрытия позиций по противоположному сигналу.
TakeProfitTicks Дистанция до тейк-профита в шагах цены.
StopLossTicks Дистанция до стоп-лосса в шагах цены.

Особенности реализации

  • Стратегия хранит только короткие очереди последних значений индикаторов и обрезает их до длины, необходимой для выбранного SignalBar.
  • Фаза Jurik настраивается через reflection, чтобы сохранить совместимость с разными версиями StockSharp. Если свойство отсутствует, значение просто игнорируется.
  • Используется только цена закрытия свечи — наиболее популярный вариант в оригинальном эксперте. Добавление альтернативных типов цен потребует расширения кода.
  • Применяются высокоуровневые методы (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
	}
}