Long-Only Opening Range Breakout (ORB) with Pivot Points
The strategy buys when price breaks above the opening range high and the first resistance (R1) from daily pivot points sits above that high. A trailing stop follows pivot levels.
Details
- Entry Criteria:
- After the opening range, enter long on a breakout above the session high if R1 is higher.
- Long/Short: Long only
- Exit Criteria:
- Trailing stop adjusted to pivot levels and daily close.
- Stops: Yes
- Default Values:
RangeMinutes= 15SessionStart= 09:30MaxTradesPerDay= 1StopLossPercent= 3InitialSlType= Percentage
- Filters:
- Category: Breakout
- Direction: Long
- Indicators: Pivot Points
- Stops: Yes
- Complexity: Medium
- Timeframe: Any
- Seasonality: No
- Neural networks: No
- Divergence: No
- Risk level: Medium
using System;
using System.Collections.Generic;
using Ecng.Common;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// Opening range breakout with pivot point trailing stop.
/// Uses a rolling high/low channel for breakout entry and pivot-based trailing stop for exits.
/// </summary>
public class LongOnlyOpeningRangeBreakoutWithPivotPointsStrategy : Strategy
{
public enum SlTypes
{
Percentage,
PreviousLow
}
private readonly StrategyParam<DataType> _candleType;
private readonly StrategyParam<int> _rangeBars;
private readonly StrategyParam<decimal> _stopLossPercent;
private readonly StrategyParam<SlTypes> _initialSlType;
private readonly StrategyParam<int> _pivotLength;
private Highest _highest;
private Lowest _lowest;
private decimal _entryPrice;
private decimal _sl0;
private decimal _trailStop;
private int _cooldown;
private bool _prevReady;
private decimal _prevHighest;
private decimal _prevLowest;
// Pivot levels
private decimal _r1;
private decimal _r2;
private decimal _s1;
private decimal _s2;
// For pivot calc
private decimal _pivotHigh;
private decimal _pivotLow;
private decimal _pivotClose;
private int _pivotBarCount;
public DataType CandleType { get => _candleType.Value; set => _candleType.Value = value; }
public int RangeBars { get => _rangeBars.Value; set => _rangeBars.Value = value; }
public decimal StopLossPercent { get => _stopLossPercent.Value; set => _stopLossPercent.Value = value; }
public SlTypes InitialSlType { get => _initialSlType.Value; set => _initialSlType.Value = value; }
public int PivotLength { get => _pivotLength.Value; set => _pivotLength.Value = value; }
public LongOnlyOpeningRangeBreakoutWithPivotPointsStrategy()
{
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(30).TimeFrame())
.SetDisplay("Candle Type", "Working candle type", "General");
_rangeBars = Param(nameof(RangeBars), 20)
.SetGreaterThanZero()
.SetDisplay("Range Bars", "Lookback bars for channel", "General");
_stopLossPercent = Param(nameof(StopLossPercent), 5m)
.SetDisplay("Stop Loss %", "Initial stop loss percent", "Risk");
_initialSlType = Param(nameof(InitialSlType), SlTypes.Percentage)
.SetDisplay("Initial SL Type", "Initial stop loss type", "Risk");
_pivotLength = Param(nameof(PivotLength), 20)
.SetGreaterThanZero()
.SetDisplay("Pivot Length", "Bars for pivot calculation", "Indicators");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_entryPrice = default;
_sl0 = default;
_trailStop = default;
_cooldown = default;
_prevReady = false;
_prevHighest = default;
_prevLowest = default;
_r1 = default;
_r2 = default;
_s1 = default;
_s2 = default;
_pivotHigh = default;
_pivotLow = default;
_pivotClose = default;
_pivotBarCount = default;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_highest = new Highest { Length = RangeBars };
_lowest = new Lowest { Length = RangeBars };
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(_highest, _lowest, ProcessCandle)
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawIndicator(area, _highest);
DrawIndicator(area, _lowest);
DrawOwnTrades(area);
}
}
private void ProcessCandle(ICandleMessage candle, decimal highestValue, decimal lowestValue)
{
if (candle.State != CandleStates.Finished)
return;
if (!_highest.IsFormed || !_lowest.IsFormed)
return;
// Update pivot levels every PivotLength bars
_pivotBarCount++;
_pivotHigh = _pivotHigh == 0 ? candle.HighPrice : Math.Max(_pivotHigh, candle.HighPrice);
_pivotLow = _pivotLow == 0 ? candle.LowPrice : Math.Min(_pivotLow, candle.LowPrice);
_pivotClose = candle.ClosePrice;
if (_pivotBarCount >= PivotLength)
{
var pivot = (_pivotHigh + _pivotLow + _pivotClose) / 3m;
_r1 = pivot + pivot - _pivotLow;
_r2 = pivot + (_pivotHigh - _pivotLow);
_s1 = pivot + pivot - _pivotHigh;
_s2 = pivot - (_pivotHigh - _pivotLow);
_pivotHigh = 0;
_pivotLow = 0;
_pivotBarCount = 0;
}
if (_cooldown > 0)
{
_cooldown--;
_prevHighest = highestValue;
_prevLowest = lowestValue;
_prevReady = true;
return;
}
// Long breakout entry
if (_prevReady && Position <= 0 && candle.ClosePrice > _prevHighest && _r1 > 0)
{
if (Position < 0)
BuyMarket();
_entryPrice = candle.ClosePrice;
_sl0 = _entryPrice * (1m - StopLossPercent / 100m);
_trailStop = 0m;
BuyMarket();
_cooldown = 40;
}
// Short breakdown entry
else if (_prevReady && Position >= 0 && candle.ClosePrice < _prevLowest && _s1 > 0)
{
if (Position > 0)
SellMarket();
_entryPrice = candle.ClosePrice;
_sl0 = _entryPrice * (1m + StopLossPercent / 100m);
_trailStop = 0m;
SellMarket();
_cooldown = 40;
}
// Trailing stop for long position
if (Position > 0 && _r1 > 0)
{
if (candle.HighPrice > _r2)
_trailStop = Math.Max(_trailStop, _r1);
else if (candle.HighPrice > _r1)
_trailStop = Math.Max(_trailStop, highestValue);
var sl = Math.Max(_sl0, _trailStop);
if (candle.LowPrice <= sl)
{
SellMarket();
_cooldown = 40;
}
}
// Trailing stop for short position
if (Position < 0 && _s1 > 0)
{
if (candle.LowPrice < _s2)
_trailStop = _trailStop == 0 ? _s1 : Math.Min(_trailStop, _s1);
else if (candle.LowPrice < _s1)
_trailStop = _trailStop == 0 ? lowestValue : Math.Min(_trailStop, lowestValue);
var sl = _trailStop > 0 ? Math.Min(_sl0, _trailStop) : _sl0;
if (candle.HighPrice >= sl)
{
BuyMarket();
_cooldown = 40;
}
}
_prevHighest = highestValue;
_prevLowest = lowestValue;
_prevReady = true;
}
}
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.Indicators import Highest, Lowest
from StockSharp.Algo.Strategies import Strategy
class long_only_opening_range_breakout_with_pivot_points_strategy(Strategy):
def __init__(self):
super(long_only_opening_range_breakout_with_pivot_points_strategy, self).__init__()
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(30))) \
.SetDisplay("Candle Type", "Working candle type", "General")
self._range_bars = self.Param("RangeBars", 20) \
.SetGreaterThanZero() \
.SetDisplay("Range Bars", "Lookback bars for channel", "General")
self._stop_loss_percent = self.Param("StopLossPercent", 5.0) \
.SetDisplay("Stop Loss %", "Initial stop loss percent", "Risk")
self._pivot_length = self.Param("PivotLength", 20) \
.SetGreaterThanZero() \
.SetDisplay("Pivot Length", "Bars for pivot calculation", "Indicators")
self._entry_price = 0.0
self._sl0 = 0.0
self._trail_stop = 0.0
self._cooldown = 0
self._prev_ready = False
self._prev_highest = 0.0
self._prev_lowest = 0.0
self._r1 = 0.0
self._r2 = 0.0
self._s1 = 0.0
self._s2 = 0.0
self._pivot_high = 0.0
self._pivot_low = 0.0
self._pivot_close = 0.0
self._pivot_bar_count = 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(long_only_opening_range_breakout_with_pivot_points_strategy, self).OnReseted()
self._entry_price = 0.0
self._sl0 = 0.0
self._trail_stop = 0.0
self._cooldown = 0
self._prev_ready = False
self._prev_highest = 0.0
self._prev_lowest = 0.0
self._r1 = 0.0
self._r2 = 0.0
self._s1 = 0.0
self._s2 = 0.0
self._pivot_high = 0.0
self._pivot_low = 0.0
self._pivot_close = 0.0
self._pivot_bar_count = 0
def OnStarted2(self, time):
super(long_only_opening_range_breakout_with_pivot_points_strategy, self).OnStarted2(time)
self._entry_price = 0.0
self._sl0 = 0.0
self._trail_stop = 0.0
self._cooldown = 0
self._prev_ready = False
self._prev_highest = 0.0
self._prev_lowest = 0.0
self._r1 = 0.0
self._r2 = 0.0
self._s1 = 0.0
self._s2 = 0.0
self._pivot_high = 0.0
self._pivot_low = 0.0
self._pivot_close = 0.0
self._pivot_bar_count = 0
self._highest = Highest()
self._highest.Length = self._range_bars.Value
self._lowest = Lowest()
self._lowest.Length = self._range_bars.Value
subscription = self.SubscribeCandles(self.candle_type)
subscription.Bind(self._highest, self._lowest, self.OnProcess).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawIndicator(area, self._highest)
self.DrawIndicator(area, self._lowest)
self.DrawOwnTrades(area)
def OnProcess(self, candle, highest_val, lowest_val):
if candle.State != CandleStates.Finished:
return
if not self._highest.IsFormed or not self._lowest.IsFormed:
return
hv = float(highest_val)
lv = float(lowest_val)
high = float(candle.HighPrice)
low = float(candle.LowPrice)
close = float(candle.ClosePrice)
self._pivot_bar_count += 1
if self._pivot_high == 0.0:
self._pivot_high = high
else:
self._pivot_high = max(self._pivot_high, high)
if self._pivot_low == 0.0:
self._pivot_low = low
else:
self._pivot_low = min(self._pivot_low, low)
self._pivot_close = close
if self._pivot_bar_count >= self._pivot_length.Value:
pivot = (self._pivot_high + self._pivot_low + self._pivot_close) / 3.0
self._r1 = pivot + pivot - self._pivot_low
self._r2 = pivot + (self._pivot_high - self._pivot_low)
self._s1 = pivot + pivot - self._pivot_high
self._s2 = pivot - (self._pivot_high - self._pivot_low)
self._pivot_high = 0.0
self._pivot_low = 0.0
self._pivot_bar_count = 0
if self._cooldown > 0:
self._cooldown -= 1
self._prev_highest = hv
self._prev_lowest = lv
self._prev_ready = True
return
sl_pct = float(self._stop_loss_percent.Value) / 100.0
if self._prev_ready and self.Position <= 0 and close > self._prev_highest and self._r1 > 0.0:
if self.Position < 0:
self.BuyMarket()
self._entry_price = close
self._sl0 = self._entry_price * (1.0 - sl_pct)
self._trail_stop = 0.0
self.BuyMarket()
self._cooldown = 40
elif self._prev_ready and self.Position >= 0 and close < self._prev_lowest and self._s1 > 0.0:
if self.Position > 0:
self.SellMarket()
self._entry_price = close
self._sl0 = self._entry_price * (1.0 + sl_pct)
self._trail_stop = 0.0
self.SellMarket()
self._cooldown = 40
if self.Position > 0 and self._r1 > 0.0:
if high > self._r2:
self._trail_stop = max(self._trail_stop, self._r1)
elif high > self._r1:
self._trail_stop = max(self._trail_stop, hv)
sl = max(self._sl0, self._trail_stop)
if low <= sl:
self.SellMarket()
self._cooldown = 40
if self.Position < 0 and self._s1 > 0.0:
if low < self._s2:
if self._trail_stop == 0.0:
self._trail_stop = self._s1
else:
self._trail_stop = min(self._trail_stop, self._s1)
elif low < self._s1:
if self._trail_stop == 0.0:
self._trail_stop = lv
else:
self._trail_stop = min(self._trail_stop, lv)
sl = min(self._sl0, self._trail_stop) if self._trail_stop > 0.0 else self._sl0
if high >= sl:
self.BuyMarket()
self._cooldown = 40
self._prev_highest = hv
self._prev_lowest = lv
self._prev_ready = True
def CreateClone(self):
return long_only_opening_range_breakout_with_pivot_points_strategy()