Opening Range Breakout

Define the high and low of the first N minutes of a session and trade directional breakouts from that range with objective entries, stops, and targets.

The Opening Range Breakout (ORB) is built on a simple observation: the first few minutes of a trading session often establish a high and low that the rest of the day's price action will react to. A break above that range with follow-through suggests the session will trend higher. A break below suggests it will trend lower.

The appeal of ORB as a strategy is its clarity. The range defines itself automatically — you just track the highest high and lowest low during the first N minutes after the open. Once the range is set, the rules are fully objective: enter on a breakout, stop at the other side of the range, and target a projection equal to the range size. There is no discretion involved and no indicators to interpret.

This makes ORB one of the most durable intraday setups across different instruments and time periods. The logic reflects genuine market microstructure — institutional participants often use the opening period to gauge supply/demand before committing size, and a clean breakout out of that consolidation frequently attracts further momentum in the same direction.

Why the opening range works

The opening minutes of a trading session are unlike any other time of day. Several forces converge simultaneously:

This creates a period of price discovery — the market is working out where fair value is for this session. The resulting high and low represent a temporary equilibrium range. When price breaks decisively above or below that range, it signals that the price discovery phase is over and a directional move is beginning.

The academic and empirical research on this setup is substantial. Studies of US equity indices consistently show that the first 30-minute high or low is violated and followed-through on with above-random frequency, particularly on days with above-average pre-market volume or news catalysts.

How the code constructs the range

The range builds bar by bar during the ORB window. Three key variables manage the state:

The orange background shading in the chart visualises the building phase. Once `orbReady` becomes true, the green (high) and red (low) horizontal lines appear and persist for the rest of the session.

Using `time` comparisons rather than bar-counting makes the range construction timeframe-independent. A 30-minute ORB on a 1-minute chart takes 30 bars; on a 5-minute chart it takes 6 bars. The millisecond comparison handles both identically.

Choosing your ORB duration

The optimal ORB window depends on the instrument and the trading session. There is no universally correct answer, but these are common starting points:

| Duration | Typical use | Characteristics | |---|---|---| | 5 minutes | Scalping futures / high-frequency | Very tight range, many false breakouts | | 15 minutes | Intraday index futures (NQ, ES) | Balances range quality vs signal frequency | | 30 minutes | Equity indices, liquid stocks | Most commonly backtested ORB window | | 60 minutes | Slower-moving instruments | Wider range, fewer signals, stronger conviction |

The 30-minute ORB is widely used because it captures the initial volatility surge at the open while still leaving sufficient session time for the breakout to develop. On the Nasdaq and S&P 500 futures, the first 30 minutes often represent 25–40% of the day's entire range.

Stops and targets: the natural geometry

The ORB provides a natural framework for stops and targets that requires no external indicators:

This geometry means you never need to ask "where's my stop?" or "how far should this go?" The range answers both questions directly.

Filters that improve the setup

Raw ORB signals fire on every session, but not every breakout is equal. These filters improve selectivity:

### ATR expansion The code includes an optional ATR filter: the breakout bar's ATR must be above 90% of its 20-bar average. Breakouts on contracting ATR tend to be low-momentum and more prone to fading back into the range. ATR expansion confirms that genuine volatility is driving the move.

### VWAP bias A common enhancement: only take long ORB breakouts when the current price is above VWAP, and only short breakouts below VWAP. VWAP represents the session's volume-weighted fair value, so trading in its direction adds confluence.

### First breakout only The code uses `longFired` and `shortFired` flags to ensure only the first breakout signal per session fires. After a failed breakout and retest, the ORB setup has been compromised — taking a second breakout in the same session typically produces worse results.

//@version=6
strategy("Opening Range Breakout", overlay=true,
         margin_long=100, margin_short=100)

// ── Inputs ───────────────────────────────────────────────────────────
orbMinutes = input.int(30,   "ORB Duration (minutes)", minval=5, maxval=120, step=5)
sess       = input.session("0930-1600", "Session (Exchange Timezone)")
useAtrFilt = input.bool(true, "Filter: require ATR expansion on breakout")
atrLen     = input.int(14,  "ATR Length")
tpMult     = input.float(1.0, "TP = N × ORB Range", minval=0.5, step=0.25)

// ── Session & range construction ─────────────────────────────────────
inSess    = not na(time(timeframe.period, sess))
sessStart = inSess and not inSess[1]

var float orbHigh    = na
var float orbLow     = na
var bool  orbReady   = false
var int   orbEndTime = na

if sessStart
    orbHigh    := high
    orbLow     := low
    orbReady   := false
    orbEndTime := time + orbMinutes * 60 * 1000

if inSess and not orbReady
    if time < orbEndTime
        orbHigh := math.max(orbHigh, high)
        orbLow  := math.min(orbLow,  low)
    else
        orbReady := true

// ── Breakout signals (first per session only) ─────────────────────────
var bool longFired  = false
var bool shortFired = false

if sessStart
    longFired  := false
    shortFired := false

atrValue  = ta.atr(atrLen)
atrOk     = not useAtrFilt or atrValue > ta.sma(atrValue, 20) * 0.9
orbRange  = nz(orbHigh) - nz(orbLow)

bullBreak = orbReady and ta.crossover(close, orbHigh) and not longFired  and atrOk
bearBreak = orbReady and ta.crossunder(close, orbLow)  and not shortFired and atrOk

if bullBreak
    longFired := true
    strategy.entry("Long",  strategy.long)
    strategy.exit("Long Exit",  "Long",  stop=orbLow,   limit=orbHigh + orbRange * tpMult)

if bearBreak
    shortFired := true
    strategy.entry("Short", strategy.short)
    strategy.exit("Short Exit", "Short", stop=orbHigh,  limit=orbLow  - orbRange * tpMult)

// ── Visuals ───────────────────────────────────────────────────────────
plot(orbReady ? orbHigh : na, "ORB High",
     color=color.green, linewidth=2, style=plot.style_linebr)
plot(orbReady ? orbLow  : na, "ORB Low",
     color=color.red,   linewidth=2, style=plot.style_linebr)

bgcolor(inSess and not orbReady and not na(orbEndTime)
        ? color.new(color.orange, 93) : na, title="Building ORB")

plotshape(bullBreak, style=shape.triangleup,
          location=location.belowbar, color=color.green, size=size.small)
plotshape(bearBreak, style=shape.triangledown,
          location=location.abovebar, color=color.red,   size=size.small)