Breaks and Retests
Strategy entering on breakouts of recent highs or lows and optional retests with trailing stop management.
The approach first tracks support and resistance defined by the highest and lowest closes over a lookback window. Breakouts open positions in the breakout direction or wait for a retest of the broken level. Exits use an initial stop loss that turns into a trailing stop once profit reaches a threshold.
Details
- Entry Criteria: Breakout above resistance or below support, optional retest.
- Long/Short: Configurable.
- Exit Criteria: Trailing stop or opposite breakout.
- Stops: Initial stop loss and trailing stop.
- Default Values:
LookbackPeriod= 20RetestBarsSinceBreakout= 2RetestDetectionLimit= 2ProfitThresholdPercent= 5mTrailingStopGapPercent= 1mStopLossPercent= 2mCandleType= TimeSpan.FromMinutes(5)
- Filters:
- Category: Breakout
- Direction: Both
- Indicators: Highest, Lowest
- Stops: Yes
- Complexity: Intermediate
- Timeframe: Intraday (5m)
- Seasonality: No
- Neural Networks: No
- Divergence: No
- Risk Level: Medium
using System;
using System.Collections.Generic;
using System.Linq;
using Ecng.Common;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// Breaks and retests strategy.
/// Resistance and support are the highest and lowest closes of the previous LookbackPeriod candles. A close above resistance goes long
/// and a close below support goes short, reversing an opposite position. When not already in the breakout direction, a retest of the
/// broken level between RetestBarsSinceBreakout and RetestBarsSinceBreakout + RetestDetectionLimit bars after the breakout (the
/// candle touches the level and closes back on the breakout side) also enters. A StopLossPercent stop protects the trade until the
/// close is ProfitThresholdPercent in profit, after which a trailing stop TrailingStopGapPercent from the best close takes over.
/// </summary>
public class BreaksAndRetestsStrategy : Strategy
{
private readonly StrategyParam<int> _lookbackPeriod;
private readonly StrategyParam<int> _retestBarsSinceBreakout;
private readonly StrategyParam<int> _retestDetectionLimit;
private readonly StrategyParam<decimal> _profitThresholdPercent;
private readonly StrategyParam<decimal> _trailingStopGapPercent;
private readonly StrategyParam<decimal> _stopLossPercent;
private readonly StrategyParam<DataType> _candleType;
private readonly Queue<decimal> _closes = new();
private decimal? _brokenResistance;
private int _barsSinceBullBreak;
private decimal? _brokenSupport;
private int _barsSinceBearBreak;
private decimal _entryPrice;
private decimal _bestPrice;
private bool _trailingActive;
/// <summary>
/// Previous candles whose closes define support and resistance.
/// </summary>
public int LookbackPeriod
{
get => _lookbackPeriod.Value;
set => _lookbackPeriod.Value = value;
}
/// <summary>
/// Bars after a breakout before a retest can be detected.
/// </summary>
public int RetestBarsSinceBreakout
{
get => _retestBarsSinceBreakout.Value;
set => _retestBarsSinceBreakout.Value = value;
}
/// <summary>
/// Additional bars during which a retest is still detected.
/// </summary>
public int RetestDetectionLimit
{
get => _retestDetectionLimit.Value;
set => _retestDetectionLimit.Value = value;
}
/// <summary>
/// Profit in percent that switches to the trailing stop.
/// </summary>
public decimal ProfitThresholdPercent
{
get => _profitThresholdPercent.Value;
set => _profitThresholdPercent.Value = value;
}
/// <summary>
/// Trailing stop distance from the best close in percent.
/// </summary>
public decimal TrailingStopGapPercent
{
get => _trailingStopGapPercent.Value;
set => _trailingStopGapPercent.Value = value;
}
/// <summary>
/// Initial stop loss in percent from the entry price.
/// </summary>
public decimal StopLossPercent
{
get => _stopLossPercent.Value;
set => _stopLossPercent.Value = value;
}
/// <summary>
/// Candle type.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Constructor.
/// </summary>
public BreaksAndRetestsStrategy()
{
_lookbackPeriod = Param(nameof(LookbackPeriod), 20)
.SetGreaterThanZero()
.SetDisplay("Lookback Period", "Previous candles whose closes define support and resistance", "Levels");
_retestBarsSinceBreakout = Param(nameof(RetestBarsSinceBreakout), 2)
.SetNotNegative()
.SetDisplay("Retest Bars Since Breakout", "Bars after a breakout before a retest can be detected", "Retest");
_retestDetectionLimit = Param(nameof(RetestDetectionLimit), 2)
.SetNotNegative()
.SetDisplay("Retest Detection Limit", "Additional bars during which a retest is still detected", "Retest");
_profitThresholdPercent = Param(nameof(ProfitThresholdPercent), 5m)
.SetGreaterThanZero()
.SetDisplay("Profit Threshold %", "Profit that switches to the trailing stop", "Risk");
_trailingStopGapPercent = Param(nameof(TrailingStopGapPercent), 1m)
.SetGreaterThanZero()
.SetDisplay("Trailing Gap %", "Trailing stop distance from the best close", "Risk");
_stopLossPercent = Param(nameof(StopLossPercent), 2m)
.SetGreaterThanZero()
.SetDisplay("Stop Loss %", "Initial stop loss from the entry price", "Risk");
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
.SetDisplay("Candle Type", "Type of candles to use", "General");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
ResetState();
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
ResetState();
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(ProcessCandle)
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawOwnTrades(area);
}
}
private void ResetState()
{
_closes.Clear();
_brokenResistance = null;
_brokenSupport = null;
_barsSinceBullBreak = 0;
_barsSinceBearBreak = 0;
_entryPrice = 0;
_bestPrice = 0;
_trailingActive = false;
}
private void ProcessCandle(ICandleMessage candle)
{
if (candle.State != CandleStates.Finished)
return;
var close = candle.ClosePrice;
// Levels come from the closes before this candle.
decimal? resistance = null;
decimal? support = null;
if (_closes.Count == LookbackPeriod)
{
resistance = _closes.Max();
support = _closes.Min();
}
_closes.Enqueue(close);
while (_closes.Count > LookbackPeriod)
_closes.Dequeue();
if (_brokenResistance != null)
_barsSinceBullBreak++;
if (_brokenSupport != null)
_barsSinceBearBreak++;
if (!IsFormedAndOnlineAndAllowTrading())
return;
if (Position != 0 && CheckStops(candle))
return;
if (resistance is not decimal res || support is not decimal sup)
return;
var bullBreak = close > res;
var bearBreak = close < sup;
var maxRetestBars = RetestBarsSinceBreakout + RetestDetectionLimit;
var bullRetest = !bullBreak && _brokenResistance is decimal brokenRes
&& _barsSinceBullBreak >= RetestBarsSinceBreakout && _barsSinceBullBreak <= maxRetestBars
&& candle.LowPrice <= brokenRes && close > brokenRes;
var bearRetest = !bearBreak && _brokenSupport is decimal brokenSup
&& _barsSinceBearBreak >= RetestBarsSinceBreakout && _barsSinceBearBreak <= maxRetestBars
&& candle.HighPrice >= brokenSup && close < brokenSup;
if (bullBreak)
{
_brokenResistance = res;
_barsSinceBullBreak = 0;
}
else if (_brokenResistance != null && _barsSinceBullBreak > maxRetestBars)
{
_brokenResistance = null;
}
if (bearBreak)
{
_brokenSupport = sup;
_barsSinceBearBreak = 0;
}
else if (_brokenSupport != null && _barsSinceBearBreak > maxRetestBars)
{
_brokenSupport = null;
}
if ((bullBreak || bullRetest) && Position <= 0)
{
if (bullRetest)
_brokenResistance = null;
Enter(true, close);
}
else if ((bearBreak || bearRetest) && Position >= 0)
{
if (bearRetest)
_brokenSupport = null;
Enter(false, close);
}
}
private void Enter(bool isLong, decimal price)
{
var volume = Volume + Math.Abs(Position);
if (isLong)
BuyMarket(volume);
else
SellMarket(volume);
_entryPrice = price;
_bestPrice = price;
_trailingActive = false;
}
private bool CheckStops(ICandleMessage candle)
{
if (_entryPrice <= 0)
return false;
var close = candle.ClosePrice;
if (Position > 0)
{
_bestPrice = Math.Max(_bestPrice, close);
if (!_trailingActive && (close - _entryPrice) / _entryPrice * 100m >= ProfitThresholdPercent)
_trailingActive = true;
var stop = _trailingActive
? _bestPrice * (1m - TrailingStopGapPercent / 100m)
: _entryPrice * (1m - StopLossPercent / 100m);
if (candle.LowPrice <= stop)
{
SellMarket(Position);
_entryPrice = 0;
return true;
}
}
else if (Position < 0)
{
_bestPrice = Math.Min(_bestPrice, close);
if (!_trailingActive && (_entryPrice - close) / _entryPrice * 100m >= ProfitThresholdPercent)
_trailingActive = true;
var stop = _trailingActive
? _bestPrice * (1m + TrailingStopGapPercent / 100m)
: _entryPrice * (1m + StopLossPercent / 100m);
if (candle.HighPrice >= stop)
{
BuyMarket(-Position);
_entryPrice = 0;
return true;
}
}
return false;
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Strategies")
from collections import deque
from System import TimeSpan, Decimal
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Strategies import Strategy
class breaks_and_retests_strategy(Strategy):
"""
Breaks and retests strategy.
Resistance and support are the highest and lowest closes of the previous LookbackPeriod candles. A close above resistance goes long
and a close below support goes short, reversing an opposite position. When not already in the breakout direction, a retest of the
broken level between RetestBarsSinceBreakout and RetestBarsSinceBreakout + RetestDetectionLimit bars after the breakout (the
candle touches the level and closes back on the breakout side) also enters. A StopLossPercent stop protects the trade until the
close is ProfitThresholdPercent in profit, after which a trailing stop TrailingStopGapPercent from the best close takes over.
"""
def __init__(self):
super(breaks_and_retests_strategy, self).__init__()
self._lookback_period = self.Param("LookbackPeriod", 20).SetGreaterThanZero().SetDisplay("Lookback Period", "Previous candles whose closes define support and resistance", "Levels")
self._retest_bars_since_breakout = self.Param("RetestBarsSinceBreakout", 2).SetNotNegative().SetDisplay("Retest Bars Since Breakout", "Bars after a breakout before a retest can be detected", "Retest")
self._retest_detection_limit = self.Param("RetestDetectionLimit", 2).SetNotNegative().SetDisplay("Retest Detection Limit", "Additional bars during which a retest is still detected", "Retest")
self._profit_threshold_percent = self.Param("ProfitThresholdPercent", 5.0).SetGreaterThanZero().SetDisplay("Profit Threshold %", "Profit that switches to the trailing stop", "Risk")
self._trailing_stop_gap_percent = self.Param("TrailingStopGapPercent", 1.0).SetGreaterThanZero().SetDisplay("Trailing Gap %", "Trailing stop distance from the best close", "Risk")
self._stop_loss_percent = self.Param("StopLossPercent", 2.0).SetGreaterThanZero().SetDisplay("Stop Loss %", "Initial stop loss from the entry price", "Risk")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(5))).SetDisplay("Candle Type", "Type of candles to use", "General")
self._reset_state()
@property
def candle_type(self):
return self._candle_type.Value
def _reset_state(self):
self._closes = deque()
self._broken_resistance = None
self._broken_support = None
self._bars_since_bull_break = 0
self._bars_since_bear_break = 0
self._entry_price = Decimal(0)
self._best_price = Decimal(0)
self._trailing_active = False
def OnReseted(self):
super(breaks_and_retests_strategy, self).OnReseted()
self._reset_state()
def OnStarted2(self, time):
super(breaks_and_retests_strategy, self).OnStarted2(time)
self._reset_state()
subscription = self.SubscribeCandles(self.candle_type)
subscription.Bind(self._process_candle).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawOwnTrades(area)
def _process_candle(self, candle):
if candle.State != CandleStates.Finished:
return
close = candle.ClosePrice
lookback = self._lookback_period.Value
# Levels come from the closes before this candle.
resistance = None
support = None
if len(self._closes) == lookback:
resistance = max(self._closes)
support = min(self._closes)
self._closes.append(close)
while len(self._closes) > lookback:
self._closes.popleft()
if self._broken_resistance is not None:
self._bars_since_bull_break += 1
if self._broken_support is not None:
self._bars_since_bear_break += 1
if not self.IsFormedAndOnlineAndAllowTrading():
return
if self.Position != 0 and self._check_stops(candle):
return
if resistance is None or support is None:
return
bull_break = close > resistance
bear_break = close < support
min_bars = self._retest_bars_since_breakout.Value
max_bars = min_bars + self._retest_detection_limit.Value
bull_retest = (not bull_break and self._broken_resistance is not None
and min_bars <= self._bars_since_bull_break <= max_bars
and candle.LowPrice <= self._broken_resistance and close > self._broken_resistance)
bear_retest = (not bear_break and self._broken_support is not None
and min_bars <= self._bars_since_bear_break <= max_bars
and candle.HighPrice >= self._broken_support and close < self._broken_support)
if bull_break:
self._broken_resistance = resistance
self._bars_since_bull_break = 0
elif self._broken_resistance is not None and self._bars_since_bull_break > max_bars:
self._broken_resistance = None
if bear_break:
self._broken_support = support
self._bars_since_bear_break = 0
elif self._broken_support is not None and self._bars_since_bear_break > max_bars:
self._broken_support = None
if (bull_break or bull_retest) and self.Position <= 0:
if bull_retest:
self._broken_resistance = None
self._enter(True, close)
elif (bear_break or bear_retest) and self.Position >= 0:
if bear_retest:
self._broken_support = None
self._enter(False, close)
def _enter(self, is_long, price):
volume = self.Volume + abs(self.Position)
if is_long:
self.BuyMarket(volume)
else:
self.SellMarket(volume)
self._entry_price = price
self._best_price = price
self._trailing_active = False
def _check_stops(self, candle):
if self._entry_price <= 0:
return False
close = candle.ClosePrice
hundred = Decimal(100)
one = Decimal(1)
threshold = Decimal(self._profit_threshold_percent.Value)
gap = Decimal(self._trailing_stop_gap_percent.Value)
stop_loss = Decimal(self._stop_loss_percent.Value)
if self.Position > 0:
self._best_price = max(self._best_price, close)
if not self._trailing_active and (close - self._entry_price) / self._entry_price * hundred >= threshold:
self._trailing_active = True
if self._trailing_active:
stop = self._best_price * (one - gap / hundred)
else:
stop = self._entry_price * (one - stop_loss / hundred)
if candle.LowPrice <= stop:
self.SellMarket(self.Position)
self._entry_price = Decimal(0)
return True
elif self.Position < 0:
self._best_price = min(self._best_price, close)
if not self._trailing_active and (self._entry_price - close) / self._entry_price * hundred >= threshold:
self._trailing_active = True
if self._trailing_active:
stop = self._best_price * (one + gap / hundred)
else:
stop = self._entry_price * (one + stop_loss / hundred)
if candle.HighPrice >= stop:
self.BuyMarket(-self.Position)
self._entry_price = Decimal(0)
return True
return False
def CreateClone(self):
return breaks_and_retests_strategy()