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 EMAMaPeriod - 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= 55MaPeriod= 500CandleType= 1 minuteOrderVolume= 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,
}
}
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, Decimal
from StockSharp.Messages import DataType, CandleStates, Unit, UnitTypes
from StockSharp.Algo.Indicators import ExponentialMovingAverage, Highest, Lowest
from StockSharp.Algo.Strategies import Strategy
from indicator_extensions import *
TREND_NONE = 0
TREND_BULLISH = 1
TREND_BEARISH = 2
class support_resist_trade_strategy(Strategy):
def __init__(self):
super(support_resist_trade_strategy, self).__init__()
self._lookback = self.Param("Lookback", 55)
self._ma_period = self.Param("MaPeriod", 500)
self._signal_cooldown_bars = self.Param("SignalCooldownBars", 12)
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(5)))
self._order_volume = self.Param("OrderVolume", 0.1)
self._prev_support = None
self._prev_resistance = None
self._long_stop = None
self._short_stop = None
self._trend = TREND_NONE
self._pip_size = 0.0
self._levels_initialized = False
self._entry_price = 0.0
self._cooldown_remaining = 0
@property
def Lookback(self):
return self._lookback.Value
@Lookback.setter
def Lookback(self, value):
self._lookback.Value = value
@property
def MaPeriod(self):
return self._ma_period.Value
@MaPeriod.setter
def MaPeriod(self, value):
self._ma_period.Value = value
@property
def SignalCooldownBars(self):
return self._signal_cooldown_bars.Value
@SignalCooldownBars.setter
def SignalCooldownBars(self, value):
self._signal_cooldown_bars.Value = value
@property
def CandleType(self):
return self._candle_type.Value
@CandleType.setter
def CandleType(self, value):
self._candle_type.Value = value
@property
def OrderVolume(self):
return self._order_volume.Value
@OrderVolume.setter
def OrderVolume(self, value):
self._order_volume.Value = value
def OnStarted2(self, time):
super(support_resist_trade_strategy, self).OnStarted2(time)
self._ema = ExponentialMovingAverage()
self._ema.Length = self.MaPeriod
self._highest = Highest()
self._highest.Length = self.Lookback
self._lowest = Lowest()
self._lowest.Length = self.Lookback
self._cooldown_remaining = 0
subscription = self.SubscribeCandles(self.CandleType)
subscription.Bind(self._ema, self.ProcessCandle).Start()
ps = float(self.Security.PriceStep) if self.Security is not None and self.Security.PriceStep is not None else 0.0001
self._pip_size = ps
if self.Security is not None and self.Security.Decimals is not None:
d = self.Security.Decimals
if d == 3 or d == 5:
self._pip_size = ps * 10.0
self.StartProtection(
Unit(2000.0, UnitTypes.Absolute),
Unit(1000.0, UnitTypes.Absolute))
def ProcessCandle(self, candle, ema_value):
if candle.State != CandleStates.Finished:
return
high_result = process_float(self._highest, Decimal(float(candle.HighPrice)), candle.ServerTime, True)
low_result = process_float(self._lowest, Decimal(float(candle.LowPrice)), candle.ServerTime, True)
if not self._ema.IsFormed or not high_result.IsFormed or not low_result.IsFormed:
return
if self._cooldown_remaining > 0:
self._cooldown_remaining -= 1
support = float(low_result)
resistance = float(high_result)
ema = float(ema_value)
close = float(candle.ClosePrice)
open_price = float(candle.OpenPrice)
if not self._levels_initialized:
self._prev_support = support
self._prev_resistance = resistance
self._levels_initialized = True
return
if open_price > ema:
self._trend = TREND_BULLISH
elif open_price < ema:
self._trend = TREND_BEARISH
exit_placed = self._manage_position(candle)
if not exit_placed and self._cooldown_remaining == 0 and self.Position == 0:
if self._trend == TREND_BULLISH and self._prev_resistance is not None and close > self._prev_resistance:
self.BuyMarket()
self._entry_price = close
self._long_stop = self._prev_support
self._short_stop = None
self._cooldown_remaining = int(self.SignalCooldownBars)
elif self._trend == TREND_BEARISH and self._prev_support is not None and close < self._prev_support:
self.SellMarket()
self._entry_price = close
self._short_stop = self._prev_resistance
self._long_stop = None
self._cooldown_remaining = int(self.SignalCooldownBars)
self._prev_support = support
self._prev_resistance = resistance
def _manage_position(self, candle):
close = float(candle.ClosePrice)
if self.Position > 0:
if self._long_stop is not None and close <= self._long_stop:
self.SellMarket()
self._long_stop = None
self._cooldown_remaining = int(self.SignalCooldownBars)
return True
entry = self._entry_price
profit_per_unit = close - entry
if profit_per_unit > 0.0 and self._prev_support is not None and close < self._prev_support:
self.SellMarket()
self._long_stop = None
self._cooldown_remaining = int(self.SignalCooldownBars)
return True
self._update_long_trailing(close, entry)
elif self.Position < 0:
if self._short_stop is not None and close >= self._short_stop:
self.BuyMarket()
self._short_stop = None
self._cooldown_remaining = int(self.SignalCooldownBars)
return True
entry = self._entry_price
profit_per_unit = entry - close
if profit_per_unit > 0.0 and self._prev_resistance is not None and close > self._prev_resistance:
self.BuyMarket()
self._short_stop = None
self._cooldown_remaining = int(self.SignalCooldownBars)
return True
self._update_short_trailing(close, entry)
else:
self._long_stop = None
self._short_stop = None
return False
def _update_long_trailing(self, close_price, entry):
if self._pip_size <= 0.0:
return
first_trigger = entry + 20.0 * self._pip_size
second_trigger = entry + 40.0 * self._pip_size
third_trigger = entry + 60.0 * self._pip_size
first_stop = entry + 10.0 * self._pip_size
second_stop = entry + 20.0 * self._pip_size
third_stop = entry + 30.0 * self._pip_size
if close_price > third_trigger and (self._long_stop is None or self._long_stop < third_stop):
self._long_stop = third_stop
elif close_price > second_trigger and (self._long_stop is None or self._long_stop < second_stop):
self._long_stop = second_stop
elif close_price > first_trigger and (self._long_stop is None or self._long_stop < first_stop):
self._long_stop = first_stop
def _update_short_trailing(self, close_price, entry):
if self._pip_size <= 0.0:
return
first_trigger = entry - 20.0 * self._pip_size
second_trigger = entry - 40.0 * self._pip_size
third_trigger = entry - 60.0 * self._pip_size
first_stop = entry - 10.0 * self._pip_size
second_stop = entry - 20.0 * self._pip_size
third_stop = entry - 30.0 * self._pip_size
if close_price < third_trigger and (self._short_stop is None or self._short_stop > third_stop):
self._short_stop = third_stop
elif close_price < second_trigger and (self._short_stop is None or self._short_stop > second_stop):
self._short_stop = second_stop
elif close_price < first_trigger and (self._short_stop is None or self._short_stop > first_stop):
self._short_stop = first_stop
def OnReseted(self):
super(support_resist_trade_strategy, self).OnReseted()
self._prev_support = None
self._prev_resistance = None
self._long_stop = None
self._short_stop = None
self._trend = TREND_NONE
self._pip_size = 0.0
self._levels_initialized = False
self._entry_price = 0.0
self._cooldown_remaining = 0
def CreateClone(self):
return support_resist_trade_strategy()