View on GitHub

Support Resist Trade Strategy

Breakout strategy ported from MetaTrader that combines a long-term EMA trend filter with dynamic support and resistance levels. It looks back over the recent swing range, waits for price to break the prior ceiling or floor in the direction of the trend, and manages positions with staged pip-based trailing stops.

Details

  • Entry Criteria: price closes beyond the previous Lookback-period high (long) or low (short) and the bar opens above/below the EMA MaPeriod
  • Long/Short: Both
  • Exit Criteria: trailing stop hits or a profitable position crosses back through the refreshed support/resistance band
  • Stops: initial stop at the opposite band, trail after +20/+40/+60 pip moves (locking 10/20/30 pips respectively)
  • Default Values:
    • Lookback = 55
    • MaPeriod = 500
    • CandleType = 1 minute
    • OrderVolume = 0.1
  • Filters:
    • Category: Breakout
    • Direction: Both
    • Indicators: EMA, Highest, Lowest
    • Stops: Trailing
    • Complexity: Intermediate
    • Timeframe: Intraday
    • Seasonality: No
    • Neural networks: No
    • Divergence: No
    • Risk level: Medium
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,
	}
}