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
boolinput for "confirmed bars only" that appendsbarstate.isconfirmedto the signals. - Add a table showing the range height in ticks and as a percent of price, updated on
barstate.islast. - Replace
alertconditionwithalert()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,
varstate with explicit resets, once-per-day signals,linebrplots, 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 atimeframe.change("D")reset define the range. varstate for high, low, done and fired flags, reset explicitly each day.ta.crossoveron the range levels, gated by fired flags and an optional VWAP-side filter, gives once-per-day signals.plot.style_linebrandcond ? value : nadraw the range only after completion.alertconditionwith 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.