Открыть на GitHub

Стратегия Support Resist Trade

Пробойная стратегия, перенесённая из MetaTrader, сочетает долгосрочный EMA-фильтр тренда с динамическими уровнями поддержки и сопротивления. Алгоритм отслеживает диапазон за последние Lookback свечей, ждёт пробоя прошлых максимумов или минимумов по направлению тренда и сопровождает позицию ступенчатым трейлинг-стопом по пипсам.

Детали

  • Критерий входа: закрытие выше предыдущего максимума за Lookback баров (лонг) или ниже минимума (шорт) при открытии свечи выше/ниже EMA MaPeriod
  • Длин/Шорт: оба направления
  • Критерий выхода: срабатывание трейлинг-стопа либо возврат прибыльной позиции за обновлённый уровень поддержки/сопротивления
  • Стопы: стартовый стоп по противоположному уровню, перевод после +20/+40/+60 пипсов (фиксируя 10/20/30 пипсов соответственно)
  • Значения по умолчанию:
    • Lookback = 55
    • MaPeriod = 500
    • CandleType = 1 минута
    • OrderVolume = 0.1
  • Фильтры:
    • Категория: Breakout
    • Направление: Оба
    • Индикаторы: EMA, Highest, Lowest
    • Стопы: Трейлинг
    • Сложность: Средняя
    • Таймфрейм: Внутридневной
    • Сезонность: Нет
    • Нейросети: Нет
    • Дивергенция: Нет
    • Уровень риска: Средний
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>
/// Breakout strategy that trades when price moves beyond recent support/resistance with EMA trend filter and pip-based trailing stops.
/// </summary>
public class SupportResistTradeStrategy : Strategy
{
	private readonly StrategyParam<int> _lookback;
	private readonly StrategyParam<int> _maPeriod;
	private readonly StrategyParam<int> _signalCooldownBars;
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<decimal> _orderVolume;

	private ExponentialMovingAverage _ema;
	private Highest _highest;
	private Lowest _lowest;

	private decimal? _prevSupport;
	private decimal? _prevResistance;

	private decimal? _longStop;
	private decimal? _shortStop;

	private TrendDirections _trend = TrendDirections.None;
	private decimal _pipSize;
	private bool _levelsInitialized;
	private decimal _entryPrice;
	private int _cooldownRemaining;

	/// <summary>
	/// Number of candles used to build swing levels.
	/// </summary>
	public int Lookback
	{
		get => _lookback.Value;
		set => _lookback.Value = value;
	}

	/// <summary>
	/// Exponential moving average length for trend detection.
	/// </summary>
	public int MaPeriod
	{
		get => _maPeriod.Value;
		set => _maPeriod.Value = value;
	}

	/// <summary>
	/// Candle type to process.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Bars to wait after an entry or exit.
	/// </summary>
	public int SignalCooldownBars
	{
		get => _signalCooldownBars.Value;
		set => _signalCooldownBars.Value = value;
	}

	/// <summary>
	/// Default order volume.
	/// </summary>
	public decimal OrderVolume
	{
		get => _orderVolume.Value;
		set => _orderVolume.Value = value;
	}

	/// <summary>
	/// Initializes a new instance of <see cref="SupportResistTradeStrategy"/>.
	/// </summary>
	public SupportResistTradeStrategy()
	{
		_lookback = Param(nameof(Lookback), 55)
		.SetGreaterThanZero()
		.SetDisplay("Lookback", "Candles used for support and resistance", "Parameters")
		;

		_maPeriod = Param(nameof(MaPeriod), 500)
		.SetGreaterThanZero()
		.SetDisplay("EMA Period", "Length of EMA trend filter", "Indicators")
		;

		_signalCooldownBars = Param(nameof(SignalCooldownBars), 12)
			.SetGreaterThanZero()
			.SetDisplay("Signal Cooldown", "Bars to wait after an entry or exit", "Trading");

		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
		.SetDisplay("Candle Type", "Type of candles", "General");

		_orderVolume = Param(nameof(OrderVolume), 0.1m)
		.SetGreaterThanZero()
		.SetDisplay("Order Volume", "Default order volume", "Trading");
	}

	/// <inheritdoc />
	public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
	{
		return [(Security, CandleType)];
	}

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();

		_ema = default;
		_highest = default;
		_lowest = default;

		_prevSupport = default;
		_prevResistance = default;
		_longStop = default;
		_shortStop = default;

		_trend = TrendDirections.None;
		_pipSize = 0m;
		_levelsInitialized = false;
		_entryPrice = 0m;
		_cooldownRemaining = 0;
	}

	/// <inheritdoc />
	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);

		Volume = OrderVolume;

		// Prepare indicators for EMA trend and swing levels.
		_ema = new EMA { Length = MaPeriod };
		_highest = new Highest { Length = Lookback };
		_lowest = new Lowest { Length = Lookback };
		_cooldownRemaining = 0;

		var subscription = SubscribeCandles(CandleType);
		subscription
			.Bind(_ema, ProcessCandle)
			.Start();

		// Calculate pip size similar to MetaTrader adjustment for 3/5 digit quotes.
		_pipSize = Security?.PriceStep ?? 0.0001m;
		if (Security?.Decimals is 3 or 5)
		_pipSize *= 10m;

		var area = CreateChartArea();
		if (area != null)
		{
			DrawCandles(area, subscription);
			DrawIndicator(area, _ema);
			DrawIndicator(area, _highest);
			DrawIndicator(area, _lowest);
			DrawOwnTrades(area);
		}
	}

	private void ProcessCandle(ICandleMessage candle, decimal emaValue)
	{
		// Use only completed candles for trading decisions.
		if (candle.State != CandleStates.Finished)
		return;

		var highestValue = _highest.Process(candle.HighPrice, candle.ServerTime, true);
		var lowestValue = _lowest.Process(candle.LowPrice, candle.ServerTime, true);

		if (!_ema.IsFormed || !highestValue.IsFormed || !lowestValue.IsFormed)
		return;

		if (_cooldownRemaining > 0)
			_cooldownRemaining--;

		var support = lowestValue.ToDecimal();
		var resistance = highestValue.ToDecimal();

		if (!_levelsInitialized)
		{
			_prevSupport = support;
			_prevResistance = resistance;
			_levelsInitialized = true;
			return;
		}

		// Update trend direction using candle open price against EMA.
		if (candle.OpenPrice > emaValue)
		{
			_trend = TrendDirections.Bullish;
		}
		else if (candle.OpenPrice < emaValue)
		{
			_trend = TrendDirections.Bearish;
		}

		var exitPlaced = ManagePosition(candle);

		if (!exitPlaced && _cooldownRemaining == 0 && Position == 0)
		{
			if (_trend == TrendDirections.Bullish && _prevResistance.HasValue && candle.ClosePrice > _prevResistance.Value)
			{
				// Breakout above resistance in bullish trend opens long position.
				BuyMarket();
				_entryPrice = candle.ClosePrice;
				_longStop = _prevSupport;
				_shortStop = null;
				_cooldownRemaining = SignalCooldownBars;
			}
			else if (_trend == TrendDirections.Bearish && _prevSupport.HasValue && candle.ClosePrice < _prevSupport.Value)
			{
				// Breakout below support in bearish trend opens short position.
				SellMarket();
				_entryPrice = candle.ClosePrice;
				_shortStop = _prevResistance;
				_longStop = null;
				_cooldownRemaining = SignalCooldownBars;
			}
		}

		_prevSupport = support;
		_prevResistance = resistance;
	}

	private bool ManagePosition(ICandleMessage candle)
	{
		if (Position > 0)
		{
			if (_longStop.HasValue && candle.ClosePrice <= _longStop.Value)
			{
				// Close long when trailing stop level is breached.
				SellMarket();
				_longStop = null;
				_cooldownRemaining = SignalCooldownBars;
				return true;
			}

			var entry = _entryPrice;
			var profitPerUnit = candle.ClosePrice - entry;

			if (profitPerUnit > 0m && _prevSupport.HasValue && candle.ClosePrice < _prevSupport.Value)
			{
				// Exit profitable long on drop below refreshed support.
				SellMarket();
				_longStop = null;
				_cooldownRemaining = SignalCooldownBars;
				return true;
			}

			UpdateLongTrailing(candle.ClosePrice, entry);
		}
		else if (Position < 0)
		{
			if (_shortStop.HasValue && candle.ClosePrice >= _shortStop.Value)
			{
				// Close short when trailing stop level is breached.
				BuyMarket();
				_shortStop = null;
				_cooldownRemaining = SignalCooldownBars;
				return true;
			}

			var entry = _entryPrice;
			var profitPerUnit = entry - candle.ClosePrice;

			if (profitPerUnit > 0m && _prevResistance.HasValue && candle.ClosePrice > _prevResistance.Value)
			{
				// Exit profitable short on rally above refreshed resistance.
				BuyMarket();
				_shortStop = null;
				_cooldownRemaining = SignalCooldownBars;
				return true;
			}

			UpdateShortTrailing(candle.ClosePrice, entry);
		}
		else
		{
			_longStop = null;
			_shortStop = null;
		}

		return false;
	}

	private void UpdateLongTrailing(decimal closePrice, decimal entry)
	{
		if (_pipSize <= 0m)
		return;

		var firstTrigger = entry + 20m * _pipSize;
		var secondTrigger = entry + 40m * _pipSize;
		var thirdTrigger = entry + 60m * _pipSize;

		var firstStop = entry + 10m * _pipSize;
		var secondStop = entry + 20m * _pipSize;
		var thirdStop = entry + 30m * _pipSize;

		if (closePrice > thirdTrigger && (!_longStop.HasValue || _longStop.Value < thirdStop))
		{
			// Lock in additional profit after strong bullish move.
			_longStop = thirdStop;
		}
		else if (closePrice > secondTrigger && (!_longStop.HasValue || _longStop.Value < secondStop))
		{
			_longStop = secondStop;
		}
		else if (closePrice > firstTrigger && (!_longStop.HasValue || _longStop.Value < firstStop))
		{
			_longStop = firstStop;
		}
	}

	private void UpdateShortTrailing(decimal closePrice, decimal entry)
	{
		if (_pipSize <= 0m)
		return;

		var firstTrigger = entry - 20m * _pipSize;
		var secondTrigger = entry - 40m * _pipSize;
		var thirdTrigger = entry - 60m * _pipSize;

		var firstStop = entry - 10m * _pipSize;
		var secondStop = entry - 20m * _pipSize;
		var thirdStop = entry - 30m * _pipSize;

		if (closePrice < thirdTrigger && (!_shortStop.HasValue || _shortStop.Value > thirdStop))
		{
			// Lock in additional profit after strong bearish move.
			_shortStop = thirdStop;
		}
		else if (closePrice < secondTrigger && (!_shortStop.HasValue || _shortStop.Value > secondStop))
		{
			_shortStop = secondStop;
		}
		else if (closePrice < firstTrigger && (!_shortStop.HasValue || _shortStop.Value > firstStop))
		{
			_shortStop = firstStop;
		}
	}

	private enum TrendDirections
	{
		None,
		Bullish,
		Bearish,
	}
}