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 Ecng.Common;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// Breakout and retest strategy with trailing stop.
/// </summary>
public class BreaksAndRetestsStrategy : Strategy
{
private readonly StrategyParam<int> _lookbackPeriod;
private readonly StrategyParam<decimal> _stopLossPercent;
private readonly StrategyParam<decimal> _profitThresholdPercent;
private readonly StrategyParam<decimal> _trailingStopGapPercent;
private readonly StrategyParam<int> _maxHoldBars;
private readonly StrategyParam<int> _cooldownBars;
private readonly StrategyParam<DataType> _candleType;
private readonly List<decimal> _highs = new();
private readonly List<decimal> _lows = new();
private decimal _prevHighest;
private decimal _prevLowest;
private decimal _entryPrice;
private bool _trailingStopActive;
private decimal _highestSinceTrailing;
private decimal _lowestSinceTrailing;
private int _barsInPosition;
private int _barsSinceExit;
public int LookbackPeriod { get => _lookbackPeriod.Value; set => _lookbackPeriod.Value = value; }
public decimal StopLossPercent { get => _stopLossPercent.Value; set => _stopLossPercent.Value = value; }
public decimal ProfitThresholdPercent { get => _profitThresholdPercent.Value; set => _profitThresholdPercent.Value = value; }
public decimal TrailingStopGapPercent { get => _trailingStopGapPercent.Value; set => _trailingStopGapPercent.Value = value; }
public int MaxHoldBars { get => _maxHoldBars.Value; set => _maxHoldBars.Value = value; }
public int CooldownBars { get => _cooldownBars.Value; set => _cooldownBars.Value = value; }
public DataType CandleType { get => _candleType.Value; set => _candleType.Value = value; }
public BreaksAndRetestsStrategy()
{
_lookbackPeriod = Param(nameof(LookbackPeriod), 20)
.SetGreaterThanZero()
.SetDisplay("Lookback Period", "Number of bars for support/resistance", "Levels");
_stopLossPercent = Param(nameof(StopLossPercent), 1.5m)
.SetGreaterThanZero()
.SetDisplay("Stop Loss %", "Initial stop loss", "Risk");
_profitThresholdPercent = Param(nameof(ProfitThresholdPercent), 2m)
.SetGreaterThanZero()
.SetDisplay("Profit Threshold %", "Activate trailing after profit", "Risk");
_trailingStopGapPercent = Param(nameof(TrailingStopGapPercent), 0.8m)
.SetGreaterThanZero()
.SetDisplay("Trailing Gap %", "Gap for trailing stop", "Risk");
_maxHoldBars = Param(nameof(MaxHoldBars), 25)
.SetGreaterThanZero()
.SetDisplay("Max Hold Bars", "Max bars to hold position", "Risk");
_cooldownBars = Param(nameof(CooldownBars), 3)
.SetGreaterThanZero()
.SetDisplay("Cooldown Bars", "Bars to wait after exit", "Risk");
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(30).TimeFrame())
.SetDisplay("Candle Type", "Candles for calculations", "General");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_highs.Clear();
_lows.Clear();
_prevHighest = 0m;
_prevLowest = 0m;
_entryPrice = 0m;
_trailingStopActive = false;
_highestSinceTrailing = 0m;
_lowestSinceTrailing = 0m;
_barsInPosition = 0;
_barsSinceExit = 0;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(ProcessCandle)
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawOwnTrades(area);
}
}
private void ClosePosition()
{
if (Position > 0)
SellMarket();
else if (Position < 0)
BuyMarket();
_entryPrice = 0m;
_trailingStopActive = false;
_barsInPosition = 0;
_barsSinceExit = 0;
}
private void ProcessCandle(ICandleMessage candle)
{
if (candle.State != CandleStates.Finished)
return;
_highs.Add(candle.HighPrice);
_lows.Add(candle.LowPrice);
if (_highs.Count > LookbackPeriod + 1)
_highs.RemoveAt(0);
if (_lows.Count > LookbackPeriod + 1)
_lows.RemoveAt(0);
if (_highs.Count <= LookbackPeriod)
return;
// Compute highest/lowest from previous N candles (excluding current)
var highest = decimal.MinValue;
var lowest = decimal.MaxValue;
for (var i = 0; i < _highs.Count - 1; i++)
{
if (_highs[i] > highest) highest = _highs[i];
if (_lows[i] < lowest) lowest = _lows[i];
}
if (Position != 0)
{
_barsInPosition++;
// Handle stops
HandleStop(candle);
// Max hold exit
if (Position != 0 && _barsInPosition >= MaxHoldBars)
ClosePosition();
}
else
{
_barsSinceExit++;
// Breakout detection with cooldown
if (_barsSinceExit >= CooldownBars && _prevHighest > 0 && _prevLowest > 0)
{
if (candle.ClosePrice > _prevHighest)
{
BuyMarket();
_entryPrice = candle.ClosePrice;
_trailingStopActive = false;
_barsInPosition = 0;
}
else if (candle.ClosePrice < _prevLowest)
{
SellMarket();
_entryPrice = candle.ClosePrice;
_trailingStopActive = false;
_barsInPosition = 0;
}
}
}
_prevHighest = highest;
_prevLowest = lowest;
}
private void HandleStop(ICandleMessage candle)
{
if (Position > 0 && _entryPrice > 0)
{
var profitPercent = (candle.ClosePrice - _entryPrice) / _entryPrice * 100m;
if (!_trailingStopActive && profitPercent >= ProfitThresholdPercent)
{
_trailingStopActive = true;
_highestSinceTrailing = candle.ClosePrice;
}
if (_trailingStopActive)
{
_highestSinceTrailing = Math.Max(_highestSinceTrailing, candle.ClosePrice);
var stop = _highestSinceTrailing * (1 - TrailingStopGapPercent / 100m);
if (candle.ClosePrice <= stop)
ClosePosition();
}
else
{
var stop = _entryPrice * (1 - StopLossPercent / 100m);
if (candle.ClosePrice <= stop)
ClosePosition();
}
}
else if (Position < 0 && _entryPrice > 0)
{
var profitPercent = (_entryPrice - candle.ClosePrice) / _entryPrice * 100m;
if (!_trailingStopActive && profitPercent >= ProfitThresholdPercent)
{
_trailingStopActive = true;
_lowestSinceTrailing = candle.ClosePrice;
}
if (_trailingStopActive)
{
_lowestSinceTrailing = Math.Min(_lowestSinceTrailing, candle.ClosePrice);
var stop = _lowestSinceTrailing * (1 + TrailingStopGapPercent / 100m);
if (candle.ClosePrice >= stop)
ClosePosition();
}
else
{
var stop = _entryPrice * (1 + StopLossPercent / 100m);
if (candle.ClosePrice >= stop)
ClosePosition();
}
}
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Strategies import Strategy
class breaks_and_retests_strategy(Strategy):
def __init__(self):
super(breaks_and_retests_strategy, self).__init__()
self._lookback_period = self.Param("LookbackPeriod", 20) \
.SetGreaterThanZero() \
.SetDisplay("Lookback Period", "Number of bars for support/resistance", "Levels")
self._stop_loss_percent = self.Param("StopLossPercent", 1.5) \
.SetGreaterThanZero() \
.SetDisplay("Stop Loss %", "Initial stop loss", "Risk")
self._profit_threshold_percent = self.Param("ProfitThresholdPercent", 2.0) \
.SetGreaterThanZero() \
.SetDisplay("Profit Threshold %", "Activate trailing after profit", "Risk")
self._trailing_stop_gap_percent = self.Param("TrailingStopGapPercent", 0.8) \
.SetGreaterThanZero() \
.SetDisplay("Trailing Gap %", "Gap for trailing stop", "Risk")
self._max_hold_bars = self.Param("MaxHoldBars", 25) \
.SetGreaterThanZero() \
.SetDisplay("Max Hold Bars", "Max bars to hold position", "Risk")
self._cooldown_bars = self.Param("CooldownBars", 3) \
.SetGreaterThanZero() \
.SetDisplay("Cooldown Bars", "Bars to wait after exit", "Risk")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(30))) \
.SetDisplay("Candle Type", "Candles for calculations", "General")
self._highs = []
self._lows = []
self._prev_highest = 0.0
self._prev_lowest = 0.0
self._entry_price = 0.0
self._trailing_stop_active = False
self._highest_since_trailing = 0.0
self._lowest_since_trailing = 0.0
self._bars_in_position = 0
self._bars_since_exit = 0
@property
def candle_type(self):
return self._candle_type.Value
@candle_type.setter
def candle_type(self, value):
self._candle_type.Value = value
def OnReseted(self):
super(breaks_and_retests_strategy, self).OnReseted()
self._highs = []
self._lows = []
self._prev_highest = 0.0
self._prev_lowest = 0.0
self._entry_price = 0.0
self._trailing_stop_active = False
self._highest_since_trailing = 0.0
self._lowest_since_trailing = 0.0
self._bars_in_position = 0
self._bars_since_exit = 0
def OnStarted2(self, time):
super(breaks_and_retests_strategy, self).OnStarted2(time)
subscription = self.SubscribeCandles(self.candle_type)
subscription.Bind(self.OnProcess).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawOwnTrades(area)
def _close_position(self):
if self.Position > 0:
self.SellMarket()
elif self.Position < 0:
self.BuyMarket()
self._entry_price = 0.0
self._trailing_stop_active = False
self._bars_in_position = 0
self._bars_since_exit = 0
def OnProcess(self, candle):
if candle.State != CandleStates.Finished:
return
lookback = self._lookback_period.Value
self._highs.append(float(candle.HighPrice))
self._lows.append(float(candle.LowPrice))
if len(self._highs) > lookback + 1:
self._highs = self._highs[1:]
if len(self._lows) > lookback + 1:
self._lows = self._lows[1:]
if len(self._highs) <= lookback:
return
highest = max(self._highs[:-1])
lowest = min(self._lows[:-1])
close = float(candle.ClosePrice)
if self.Position != 0:
self._bars_in_position += 1
self._handle_stop(candle)
if self.Position != 0 and self._bars_in_position >= self._max_hold_bars.Value:
self._close_position()
else:
self._bars_since_exit += 1
if self._bars_since_exit >= self._cooldown_bars.Value and self._prev_highest > 0 and self._prev_lowest > 0:
if close > self._prev_highest:
self.BuyMarket()
self._entry_price = close
self._trailing_stop_active = False
self._bars_in_position = 0
elif close < self._prev_lowest:
self.SellMarket()
self._entry_price = close
self._trailing_stop_active = False
self._bars_in_position = 0
self._prev_highest = highest
self._prev_lowest = lowest
def _handle_stop(self, candle):
close = float(candle.ClosePrice)
sl_pct = float(self._stop_loss_percent.Value)
pt_pct = float(self._profit_threshold_percent.Value)
tg_pct = float(self._trailing_stop_gap_percent.Value)
if self.Position > 0 and self._entry_price > 0:
profit_pct = (close - self._entry_price) / self._entry_price * 100.0
if not self._trailing_stop_active and profit_pct >= pt_pct:
self._trailing_stop_active = True
self._highest_since_trailing = close
if self._trailing_stop_active:
if close > self._highest_since_trailing:
self._highest_since_trailing = close
stop = self._highest_since_trailing * (1.0 - tg_pct / 100.0)
if close <= stop:
self._close_position()
else:
stop = self._entry_price * (1.0 - sl_pct / 100.0)
if close <= stop:
self._close_position()
elif self.Position < 0 and self._entry_price > 0:
profit_pct = (self._entry_price - close) / self._entry_price * 100.0
if not self._trailing_stop_active and profit_pct >= pt_pct:
self._trailing_stop_active = True
self._lowest_since_trailing = close
if self._trailing_stop_active:
if close < self._lowest_since_trailing:
self._lowest_since_trailing = close
stop = self._lowest_since_trailing * (1.0 + tg_pct / 100.0)
if close >= stop:
self._close_position()
else:
stop = self._entry_price * (1.0 + sl_pct / 100.0)
if close >= stop:
self._close_position()
def CreateClone(self):
return breaks_and_retests_strategy()