Skip to content
GetProfitable
Search

Project: a VWAP plus opening-range breakout indicator with alerts

Lesson 22 · about 13 min

This project assembles modules 1 to 5 into one indicator you can put on an intraday chart tomorrow. It tracks the opening range (the high and low of the first thirty minutes of the session), draws session VWAP, marks the first breakout of the range in each direction, optionally requires the breakout to be on the VWAP's side, and exposes both breakouts as alert conditions. Read the code once top to bottom, then the walkthrough.

The script

//@version=6
indicator("VWAP + Opening Range Breakout", shorttitle="VWAP ORB", overlay=true)

g1 = "Opening range"
orSession = input.session("0930-1000", "Opening range session", group=g1,
     tooltip="Bars inside this window define the range.")
tz        = input.string("America/New_York", "Time zone", group=g1)
g2 = "Filters"
useVwap   = input.bool(true, "Require breakout on VWAP's side", group=g2)
g3 = "Style"
upCol     = input.color(color.green, "Long colour", group=g3, inline="c")
dnCol     = input.color(color.red, "Short colour", group=g3, inline="c")

if not timeframe.isintraday
    runtime.error("Use an intraday chart for an opening-range indicator.")

inOR   = not na(time(timeframe.period, orSession, tz))
newDay = timeframe.change("D")

var float orHigh = na
var float orLow  = na
var bool  orDone = false
var bool  longFired  = false
var bool  shortFired = false

if newDay
    orHigh := na
    orLow  := na
    orDone := false
    longFired  := false
    shortFired := false

if inOR
    orHigh := na(orHigh) ? high : math.max(orHigh, high)
    orLow  := na(orLow)  ? low  : math.min(orLow, low)

if not inOR and not na(orHigh)
    orDone := true

vwap = ta.vwap(hlc3)

longBreak  = orDone and not longFired  and ta.crossover(close, orHigh)  and (not useVwap or close > vwap)
shortBreak = orDone and not shortFired and ta.crossunder(close, orLow) and (not useVwap or close < vwap)

if longBreak
    longFired := true
if shortBreak
    shortFired := true

plot(orDone ? orHigh : na, "OR high", color=upCol, style=plot.style_linebr, linewidth=2)
plot(orDone ? orLow : na,  "OR low",  color=dnCol, style=plot.style_linebr, linewidth=2)
plot(vwap, "VWAP", color=color.purple, linewidth=1)
bgcolor(inOR ? color.new(color.blue, 92) : na)
plotshape(longBreak,  "Long break",  style=shape.triangleup,   location=location.belowbar, color=upCol, size=size.small)
plotshape(shortBreak, "Short break", style=shape.triangledown, location=location.abovebar, color=dnCol, size=size.small)

alertcondition(longBreak,  "ORB long",  "{{ticker}} {{interval}}: broke above the opening range at {{close}}")
alertcondition(shortBreak, "ORB short", "{{ticker}} {{interval}}: broke below the opening range at {{close}}")

Walkthrough

Inputs (module 4). The session, time zone and filter are the things a user would change. Colours are inline on one row. The default session is the first 30 minutes of the US cash session; futures traders will change it, and forex traders will pick the window that matters to them.

Guard (module 3). An opening range on a daily chart is meaningless, so runtime.error refuses it with a readable message.

Session detection (module 3). inOR is true on bars inside the window. newDay from timeframe.change("D") resets the state. On 24-hour symbols check where the day boundary falls for your exchange; a session string with an explicit time zone is what keeps the range consistent when you travel or change chart settings.

State (module 2). Five var variables carry the range and the fired flags across bars. They are reset explicitly on each new day. orHigh and orLow are float so they can start as na; the booleans start as false because v6 booleans cannot be na.

Building the range. During the window, each bar extends the high and low. The na(orHigh) ? high : math.max(orHigh, high) form handles the first bar, when there is nothing to compare with. When the first bar after the window arrives, orDone flips to true and stays true for the day.

Signals (module 3). ta.crossover(close, orHigh) fires on the single bar where close moves from at or below the range high to above it. The longFired flag makes it once per day. The VWAP clause is written as not useVwap or close > vwap, which reads as "either the filter is off, or it passes".

Visuals (module 4). plot.style_linebr with orDone ? orHigh : na draws the range only after it is complete and leaves gaps between days instead of diagonal joins. The background tint shows the window itself.

Alerts (module 5). Two alertcondition() calls with placeholders. Because longBreak uses close, it can be true intrabar and false at the close, so create the alert with "Once per bar close". If you would rather have the confirmation in code, add and barstate.isconfirmed to both signals; the plotted history will not change, because history is always confirmed.

Things to try

  • Add a bool input for "confirmed bars only" that appends barstate.isconfirmed to the signals.
  • Add a table showing the range height in ticks and as a percent of price, updated on barstate.islast.
  • Replace alertcondition with alert() and build a JSON message that includes the range high and low, so a journal receives the levels along with the signal.
  • Plot the previous day's range as dotted lines using ta.valuewhen(newDay, orHigh[1], 0).

What this indicator is and is not

It is a clean implementation of a common intraday setup with honest signals. It is not evidence that opening-range breakouts make money on your symbol; that question is for module 6's strategy conversion, with costs, and module 7's reading of the result. Many breakouts fail, and this script will faithfully mark every one of them.

Key idea: A complete indicator is inputs, a chart guard, session logic, var state with explicit resets, once-per-day signals, linebr plots, and alert conditions that fire on bar close.

Try it: Put the script on a 5-minute chart of a liquid index future or ETF. For ten sessions, note whether the first breakout reached a distance equal to the range height before returning to the range. Keep the tally; it is the beginning of an expectancy estimate, and it will tell you whether converting to a strategy is worth the effort.

Recap

  • Session membership with time(timeframe.period, session, tz) and a timeframe.change("D") reset define the range.
  • var state for high, low, done and fired flags, reset explicitly each day.
  • ta.crossover on the range levels, gated by fired flags and an optional VWAP-side filter, gives once-per-day signals.
  • plot.style_linebr and cond ? value : na draw the range only after completion.
  • alertcondition with placeholders, created with once-per-bar-close, avoids intrabar flicker.

See it drawn

Original diagrams for the ideas on this page. Illustrative, not real market data.

Intraday price swinging around VWAPA price line for one trading day weaving above and below a smoother VWAP line, with a band drawn one standard deviation either side of it.INTRADAY PRICE AND VWAPprice9:3012:4516:00+1 SD bandVWAP−1 SD bandIllustrative session. VWAP starts fresh at the open and firms up as the day fills in.
VWAP and its standard-deviation bands. VWAP is the day's average price weighted by how much volume traded at each price, so it shows where the bulk of the day's business was done. The bands sit one standard deviation either side, and price here swings between them all session.
A range beside a trendOne chart swinging between a flat floor and ceiling, another stepping upwards inside a pair of sloping lines.Range-boundresistancesupportprice bounces between two levelsTrendingthe trend channelhigher highs and higher lowsA range has two flat edges; a trend has two sloping ones.
Range versus trend. On the left price keeps bouncing between the same floor and ceiling, which is a range. On the right each high and each low is higher than the last, inside a pair of sloping lines called a channel.
One daily candle broken into four six-hour candlesA tall daily candle on the left and the four six-hour candles that make it up on the right, with dashed lines linking the day's open to the first candle and the day's close to the last.ONE DAILY CANDLEFOUR 6-HOUR CANDLEScloseopenhighlow=00:0006:0012:0018:00one dayThe same trading, summed up in one bar or spelled out in four.
How timeframes stack up. A daily candle is not different data, only coarser data: it opens where the first six-hour candle opened, closes where the last one closed, and its wicks reach the highest and lowest prices any of the four touched.