Opening Range Desk · Free & open-source · MIT
Give the market’s opening move a frame.
Box the opening high and low. Then watch where later candles close.
Draws on the price chart. The indicator does not place trades.

- Box the opening
Capture the opening high and low.
- Wait for confirmation
Check a breakout or retest; flag a return inside.
- Draw the plan
Entry, stop and target levels.
What is Opening Range Desk?
Opening Range Desk is a free TradingView indicator for opening range breakouts. It draws the opening high and low, checks later candle closes against that range, and displays entry, stop and target rules. A separate strategy script lets you test the configured rules with TradingView's Strategy Tester.
Why is the opening range box missing?
Check that your chart includes the selected opening window and timezone. Five-minute candles fit the default fifteen-minute window; larger candles cannot reconstruct that smaller range. Allow the opening window to finish before expecting a locked box. Read chart setup and installation
Does a wick outside the range count as a breakout?
A wick alone does not meet the default rule. Opening Range Desk requires two consecutive candle closes strictly beyond the range edge and buffer. A close on or inside that boundary resets the count. Read the breakout checks
How does the retest entry differ from a breakout entry?
The retest option first arms after the required breakout closes. A later candle must revisit the edge within the configured tolerance and close beyond the raw edge. The arming candle cannot also complete that retest. Read the retest entry rules
Where does the box come from?
Can price move beyond the opening high or low? First, build those two edges.
Keep the first candle’s high and low.
The first five-minute candle traded between 101 and 104. These prices start our box.
Fictional five-minute candles · New York close times. Candles start at 09:30, 09:35, 09:40 and 09:45.
How could this help a trade?
The idea: follow a move beyond the box. Test its entry, stop and targets. Price can reverse after entry; profit depends on what follows, trade size and costs.
Why did we build this tool?
A reader asked for an “ORB strategy.” ORB means opening-range breakout. There are many versions, so we made the window, entry and exit rules editable, with a checklist that explains what is still missing.
Did it close outside—or only visit?
Play or step through. The default needs two consecutive closes beyond an edge.
Keep the first candle’s high and low.
The first five-minute candle traded between 101 and 104. These prices start our box.
Fictional candles · ATR fixed at 8. Default entries; other checks held satisfied, optional filters and exits off. Shorts mirror the rule below the box.
What is a candle’s close?
The final price when that candle’s time ends. Its thin wick shows the highest and lowest prices visited. A visit beyond the box alone does not meet the default entry rule.
Find the same three things on your chart.
Box first. Candle closes second. Read the checklist when something is missing.

1 The opening box
Top = opening high. Bottom = opening low. Locked edges stay fixed.
2 Closes and plan labels
Entry marks a completed rule. Stop and target lines are a plan, with no promised outcome.
3 The Missing row
Start here in the dashboard: it names what the tool is waiting for.
Try changing the price, wick and confirmation count
This extra simulator uses fictional candles and the same entry logic. Change one control at a time; an earlier entry keeps its original plan.
Does this close complete the rule?
The range is fixed. Move the final candle to recalculate the checklist.
How the calculation works & more controls
Retest needs an earlier armed breakout. Try two earlier closes, then move this candle's wick toward the edge. A close exactly on the edge waits. The plan uses the opposite edge plus its stop buffer; the first target moves the stop to entry.
Illustrative inputs · changing a slider recalculates this example · no market feed
Paste it into TradingView.
Use an intraday chart, such as five minutes. Both complete files are free · Pine v6 · MIT.
- 1
Open Pine Editor
On your chart, create a new indicator.
- 2
Paste the full code
Copy below and replace the sample code.
- 3
Add to chart
Save it. Start with the default settings.
The indicator
Draws the range, checklist, plan and alerts.
Copy the code as it is. No coding needed.
View the full code
// ═══════════════════════════════════════════════════════════════════════════════════════════════════════
// OPENING RANGE DESK — ORB · close-through or retest · VWAP / EMA / volume filters · checklist · Co-Pilot · Pine Script v6 · v1.2.0
// Built for a r/pinescript thread reply ("Orb strategy") — every variation the thread asked about is an input:
// the range window (5 / 15 / 30 min or any session), direct breakout closes OR retests, VWAP and EMA trend
// filters, plus the things the textbook leaves out: a range-size gate, a volume gate, a trade-window and
// flat-at-the-close rule, one trade per day (or more), and a panel that says which step is still missing.
//
// HOW IT READS THE MARKET (every decision commits on the CLOSED bar — nothing repaints):
// 1 RANGE the high / low of every bar inside the opening-range window; final on the first bar after it
// 2 WINDOW entries only inside the trade window; flat at its end (switch)
// 3 FILTERS VWAP side · EMA side · breakout-bar volume · range size vs the daily ATR — all off by default
// 4 BREAKOUT close-through: a bar CLOSES beyond the range edge (+ buffer) → entry at that close
// retest: after the close-through, price comes back to the edge and a bar closes back beyond it
// EXIT stop at the opposite side of the range (or the midpoint / ATR), TP1 → breakeven, TP2 final,
// flat at the end of the trade window
//
// Paper ledger has NO costs — a description of the past on your chart. The strategy twin measures it with costs.
// License: MIT — share freely, keep this header. © 2026 ProEA Lab · built for r/pinescript
// ═══════════════════════════════════════════════════════════════════════════════════════════════════════
//@version=6
indicator("Opening Range Desk — ORB · close-through or retest · filters · Checklist", "ORB Desk", overlay = true, max_labels_count = 400, max_boxes_count = 200, max_lines_count = 300)
// ─────────────────────────────────────────────────────────────────── INPUTS
gR = "Opening range"
inRangeSess = input.session("0930-0945", "Opening-range window", group = gR, tooltip = "The bars whose highs and lows form the range. 0930-0935 = a 5-minute range, 0930-0945 = 15 minutes, 0930-1000 = 30 minutes. The range is final on the first bar after the window — that bar can already be the breakout bar. Measured on SPY 5m (profit factor, costs on): 5-minute range 0.61 · 15-minute 0.69 · 30-minute 0.60 — none of them paid this summer.", display = display.none)
inTz = input.string("America/New_York", "Timezone (IANA)", group = gR, tooltip = "Both windows are read in this timezone. New York for US stocks, indices and the NY gold open.", display = display.none)
inTradeSess = input.session("0945-1555", "Trade window (entries allowed)", group = gR, tooltip = "Entries only inside this window. With 'flat at the end' on, an open trade is closed on the first bar after it.", display = display.none)
inEodFlat = input.bool(true, "Flat at the end of the trade window", group = gR, display = display.none)
inLastEntry = input.string("", "Latest entry time — HH:MM in the timezone (blank = the whole window)", group = gR, tooltip = "Breakouts late in the day have the least room to run. With a time here, no new trade opens after it; open trades are still managed to the window's end. Measured (11:00, profit factor): SPY 5m 0.69 → 0.71 · QQQ 5m unchanged (every breakout came before 11:00 anyway) · gold 15m 0.88 → 1.01.", display = display.none)
inMinRange = input.float(0.0, "Skip the day if the range is smaller than N × daily ATR (0 = off)", minval = 0, maxval = 3, step = 0.05, group = gR, tooltip = "A tiny range breaks on noise. The range height is compared with the previous day's 14-day ATR. Measured (0.15): SPY 5m 0.69 → 0.59.", display = display.none)
inMaxRange = input.float(0.0, "Skip the day if the range is larger than N × daily ATR (0 = off)", minval = 0, maxval = 5, step = 0.05, group = gR, tooltip = "A huge range means the move already happened and the stop (the other side of the range) is far away. Measured (0.5): SPY 5m 0.69 → 0.75 · QQQ 5m 0.44 → 0.49.", display = display.none)
gE = "Entry"
inEntryMode = input.string("Close through", "Entry", options = ["Close through", "Retest"], group = gE, tooltip = "Close through: a bar closes beyond the range edge (+ buffer) → entry at that close. Retest: after that close-through, wait for price to come back to the edge (within the tolerance) and enter when a bar closes back beyond it; the setup expires if the retest does not come in time, and fails if a bar closes back inside the range. Measured (profit factor, close-through → retest): SPY 5m 0.69 → 0.58 · QQQ 5m 0.44 → 0.67 · gold 15m 0.88 → 0.55.", display = display.none)
inBreakBuf = input.float(0.0, "Breakout buffer (× ATR beyond the edge)", minval = 0, maxval = 2, step = 0.05, group = gE, tooltip = "The close must be this far beyond the edge, in ATR. Measured (0.25, on the classic 1-close rule): SPY 5m 0.69 → 0.75 · QQQ 5m 0.44 → 0.39 · gold 15m 0.88 → 0.85.", display = display.none)
inRetestTol = input.float(0.25, "Retest tolerance (× ATR around the edge)", minval = 0, maxval = 2, step = 0.05, group = gE, display = display.none)
inRetestWait = input.int(12, "Retest must come within N bars", minval = 1, maxval = 200, group = gE, display = display.none)
inMaxTrades = input.int(1, "Trades per day", minval = 1, maxval = 3, group = gE, tooltip = "1 = the first breakout only (classic). 2 = after a stop-out the next breakout — either side — can fire again. Measured (2): SPY 5m 0.69 → 0.65 with 107 trades instead of 65.", display = display.none)
gB = "False-breakout defence (all off by default — each one measured)"
inConfirmBars = input.int(2, "Closes beyond the edge before entry", minval = 1, maxval = 5, group = gB, tooltip = "1 = the first close beyond the edge (the classic rule). 2 = the breakout must hold for a second consecutive close; a close back inside resets the count. Measured (1 → 2, profit factor): SPY 5m 0.69 → 0.80 · QQQ 5m 0.44 → 0.58 · gold 15m 0.88 → 1.08 — the one false-breakout defence that helped on all three charts, so 2 is the shipped default.", display = display.none)
inStrongClose = input.bool(false, "Strong close: the breakout bar must close in the top / bottom part of its range", group = gB, tooltip = "A breakout bar that closes near its own extreme shows conviction; one that closes mid-bar after poking through is the anatomy of a fake-out. Measured (30 %, on the classic 1-close rule): SPY 5m 0.69 → 0.67 · QQQ 5m 0.44 → 0.44 · gold 15m 0.88 → 0.93 — cosmetic.", display = display.none)
inStrongPct = input.int(30, " Top / bottom part of the bar (%)", minval = 10, maxval = 50, group = gB, display = display.none)
inFailExit = input.bool(false, "Exit when a bar closes back inside the range (failed breakout)", group = gB, tooltip = "The breakout is the idea; a close back inside the range is the idea failing. This closes the trade at that close instead of waiting for the far-side stop. Measured (on the classic 1-close rule): SPY 5m 0.69 → 0.51 (win rate 34 → 17 % — the ETFs chop around the edge and the exit throws away trades that would have worked) · QQQ 5m 0.44 → 0.40 with a third less net loss · gold 15m 0.88 → 1.10 with the drawdown halved. Where breakouts either work or fail cleanly (the New York gold open) it is the right defence; where they chop, use the confirmation closes instead.", display = display.none)
inFadeFail = input.bool(false, "Trade the failure: after a close back inside, enter the opposite way", group = gB, tooltip = "The failed-breakout fade: when a close beyond an edge is followed by a close back inside the range, enter against the failed side at that close, stop at the failed extreme plus the buffer, targets in R. Counts against the trades-per-day budget (set it to 2 to fade after your own stop-out). Measured (with the failed-breakout exit and 2 trades per day): SPY 5m 0.69 → 0.59 (123 trades) · QQQ 5m 0.44 → 0.52 · gold 15m 0.88 → 1.15 (128 trades).", display = display.none)
gX = "Exit"
inStopMode = input.string("Opposite side of the range", "Stop", options = ["Opposite side of the range", "Range midpoint", "ATR from entry"], group = gX, tooltip = "Measured (midpoint): SPY 5m 0.69 → 0.64, drawdown −22 %. The classic opposite-side stop is the default.", display = display.none)
inStopAtr = input.float(1.5, " ATR stop distance (× ATR)", minval = 0.3, maxval = 10, step = 0.1, group = gX, display = display.none)
inStopBuf = input.float(0.1, " Stop buffer (× ATR beyond the level)", minval = 0, maxval = 2, step = 0.05, group = gX, display = display.none)
inTpUnit = input.string("R (risk multiples)", "Targets measured in", options = ["R (risk multiples)", "Range heights"], group = gX, tooltip = "Measured (range heights): SPY 5m 0.69 → 0.81 · QQQ 5m 0.44 → 0.56 · gold 15m 0.88 → 0.74. With the volume gate too: QQQ 1.01.", display = display.none)
inTp1 = input.float(1.0, "TP1 → stop to breakeven", minval = 0.2, maxval = 10, step = 0.1, group = gX, display = display.none)
inTp2 = input.float(2.0, "TP2 final", minval = 0.3, maxval = 20, step = 0.1, group = gX, display = display.none)
inBeAtTp1 = input.bool(true, "Move the stop to breakeven at TP1", group = gX, tooltip = "Measured (off): SPY 5m 0.69 → 0.68, win rate 34 → 42 % — more small wins, same result.", display = display.none)
inTrailOn = input.bool(false, "Trailing stop after TP1 (ATR)", group = gX, tooltip = "After TP1 the stop trails N ATR behind price instead of parking at breakeven — the classic 'let the winner run into the close'. Measured (profit factor, base → switch): SPY 5m 0.69 → 0.82 (win rate 34 → 51 %) · QQQ 5m 0.44 → 0.54 · gold 15m 0.88 → 0.89 — the only switch that helped on all three charts, still not enough on the ETFs.", display = display.none)
inTrailAtr = input.float(1.5, " Trail distance (× ATR)", minval = 0.3, maxval = 10, step = 0.1, group = gX, display = display.none)
inTimeStop = input.string("", "Time stop — HH:MM in the timezone (blank = off): exit if TP1 not reached", group = gX, tooltip = "Classic ORB discipline: if the breakout has not paid its first target by this time, it is not the day — close it and stop waiting for the bell. Measured (12:00, profit factor): SPY 5m 0.69 → 0.55 · QQQ 5m 0.44 → 0.34 · gold 15m 0.88 → 1.09 with the drawdown cut by two thirds; with the VWAP + EMA gates too, gold 1.55. Right for the New York gold open, wrong for the index ETFs.", display = display.none)
gF = "Filters (all off by default — each one measured, numbers in its tooltip)"
inVwapOn = input.bool(false, "VWAP side: longs above VWAP, shorts below", group = gF, tooltip = "The breakout bar must close on the VWAP's side of the trade. Measured (profit factor): SPY 5m and QQQ 5m — changed nothing (a close beyond the opening range at the open is on the VWAP's side anyway); gold 15m 0.88 → 1.11, and 1.36 together with the EMA filter. On a 24-hour market the VWAP has a whole session behind it — that is where this gate bites.", display = display.none)
inEmaOn = input.bool(false, "EMA trend: longs above the EMA, shorts below", group = gF, tooltip = "Measured (profit factor): SPY 5m 0.69 → 0.53 · QQQ 5m 0.44 → 0.41 · gold 15m 0.88 → 1.27 (1.36 with the VWAP gate too). Helps where the open trends with the day, hurts where the open reverses it.", display = display.none)
inEmaLen = input.int(200, " EMA length", minval = 5, maxval = 500, group = gF, display = display.none)
inEmaTf = input.timeframe("", " EMA timeframe (blank = chart)", group = gF, tooltip = "A higher timeframe is read from its last CLOSED bar — no lookahead.", display = display.none)
inVolOn = input.bool(false, "Breakout bar volume above its average", group = gF, tooltip = "Measured (profit factor): SPY 5m 0.69 → 0.78 · QQQ 5m 0.44 → 0.93 (1.01 with range-height targets) · gold 15m 0.88 → 0.90. The most useful gate on the index ETFs, still not enough to make them pay this summer.", display = display.none)
inVolMult = input.float(1.2, " Volume must be ≥ N × its average", minval = 0.5, maxval = 5, step = 0.1, group = gF, display = display.none)
inVolLen = input.int(20, " Volume average length", minval = 5, maxval = 200, group = gF, display = display.none)
inGapOn = input.bool(false, "Gap direction: longs only after a gap up, shorts only after a gap down", group = gF, tooltip = "The day's open (the first bar of the range window) against the previous daily close. 'Gap and go' — trade the breakout only when it continues the gap. Measured (profit factor): SPY 5m 0.69 → 0.91 with the drawdown more than halved · QQQ 5m 0.44 → 0.65 · gold 15m 0.88 → 0.79 (a 24-hour market barely gaps — the gate has nothing to read there).", display = display.none)
gP = "Position size (display only)"
inRiskUsd = input.float(0, "Risk per trade in account currency (0 = off)", minval = 0, step = 10, group = gP, tooltip = "Shows the size that risks this amount between entry and stop — on the ticket, the trade card and the Co-Pilot plan. Display only; the strategy twin trades one unit.", display = display.none)
gV = "Style"
inTheme = input.string("Aurora Neon", "Theme", options = ["Aurora Neon", "Royal Gold", "Colorblind-safe"], group = gV, display = display.none)
inShowOr = input.string("All days", "Opening-range boxes", options = ["All days", "Today only", "Off"], group = gV, display = display.none)
inShowLvls = input.bool(true, "Breakout levels (dotted) while the day is live", group = gV, display = display.none)
inZones = input.bool(true, "Gradient risk / reward zones", group = gV, display = display.none)
inTint = input.bool(true, "Tint candles while a trade is open", group = gV, display = display.none)
inDrawVwap = input.bool(false, "Draw VWAP (auto when its filter is on)", group = gV, display = display.none)
inDrawEma = input.bool(false, "Draw the EMA (auto when its filter is on)", group = gV, display = display.none)
inRangeTag = input.bool(true, "Range tag (height vs daily ATR) on each box", group = gV, display = display.none)
inZoneKeep = input.string("Last trade only", "Keep risk / reward zones for", options = ["Last trade only", "All trades"], group = gV, tooltip = "Zones of older trades are removed when a new trade opens, so the chart stays readable; the tickets and exit tags keep the history.", display = display.none)
inBoxResult = input.bool(true, "Colour finished days by their result (green · red · grey)", group = gV, display = display.none)
inMidline = input.bool(true, "Range midline (dotted)", group = gV, display = display.none)
inShowFake = input.bool(true, "Mark fake-outs (a wick beyond the edge that closed back inside)", group = gV, display = display.none)
gD = "Dashboard"
inDash = input.bool(true, "Show the dashboard", group = gD, display = display.none)
inDashPos = input.string("Top Right", "Position", options = ["Top Right", "Top Left", "Bottom Right", "Bottom Left", "Middle Right", "Middle Left"], group = gD, display = display.none)
inSize = input.string("Medium", "Size", options = ["Small", "Medium", "Large"], group = gD, display = display.none)
gN = "Co-Pilot (narrative)"
inNarr = input.string("Brief", "Narrative depth", options = ["Off", "Brief", "Standard", "Detailed"], group = gN, tooltip = "Brief = one sentence (NOW). Standard adds WHY and PLAN. Detailed adds the track record and a glossary.", display = display.none)
inNarrPos = input.string("Bottom Left", "Position", options = ["Bottom Left", "Bottom Right", "Top Left", "Top Right", "Middle Left", "Middle Right"], group = gN, display = display.none)
inNarrSize = input.string("Auto", "Size", options = ["Auto", "Small", "Medium", "Large"], group = gN, display = display.none)
inNarrWrap = input.int(64, "Line width (characters)", minval = 30, maxval = 140, group = gN, display = display.none)
gL = "Alerts"
inAlertJson = input.bool(true, "alert() payload as JSON (webhooks)", group = gL, tooltip = "Choose 'Any alert() function call' when creating the alert.", display = display.none)
// ─────────────────────────────────────────────────────────────────── THEME
type Theme
color bg = #0B0E14
color frame = #B8923A
color accent = #D8B14A
color accentHi = #E8C25A
color up = #2BE7C7
color down = #FF3B5C
color flat = #6B7280
color txt = #D7DCE6
color txtHi = #ECEFF5
themeOf(string name) =>
Theme t = Theme.new()
if name == "Aurora Neon"
t.bg := #0E1118
t.frame := #2C3A5A
t.accent := #7C9CFF
t.accentHi := #A9B8FF
t.up := #2AF0C8
t.down := #FF4D6D
t.flat := #5B6472
t.txt := #E6E9EF
t.txtHi := #F2F4F9
else if name == "Colorblind-safe"
t.bg := #0E1014
t.frame := #3A4150
t.accent := #56B4E9
t.accentHi := #8ED0F5
t.up := #E69F00
t.down := #0072B2
t.flat := #8C92A0
t.txt := #E8EAED
t.txtHi := #FFFFFF
t
Theme th = themeOf(inTheme)
fmt(float x) => str.tostring(x, format.mintick)
// ─────────────────────────────────────────────────────────────────── ENGINE (all ta.* at global scope)
float atr = ta.atr(14)
float atrU = math.max(atr, syminfo.mintick)
float vwap = ta.vwap(hlc3)
float emaC = ta.ema(close, inEmaLen)
emaFn() => ta.ema(close, inEmaLen)
float emaH = request.security(syminfo.tickerid, inEmaTf == "" ? timeframe.period : inEmaTf, emaFn()[1], lookahead = barmerge.lookahead_on)
float emaF = inEmaTf == "" ? emaC : emaH
float volMa = ta.sma(nz(volume, 0.0), inVolLen)
dAtrFn() => ta.atr(14)
float dAtr = request.security(syminfo.tickerid, "D", dAtrFn()[1], lookahead = barmerge.lookahead_on)
float dAtrU = math.max(nz(dAtr, atrU * 10), syminfo.mintick)
float pClose = request.security(syminfo.tickerid, "D", close[1], lookahead = barmerge.lookahead_on)
// clock helpers (all in the input timezone)
int nowMin = hour(time, inTz) * 60 + minute(time, inTz)
string hhmm = str.tostring(hour(time, inTz), "00") + ":" + str.tostring(minute(time, inTz), "00")
sessEndMin(string sess) =>
string e = str.length(sess) >= 9 ? str.substring(sess, 5, 9) : "0000"
int hh = int(nz(str.tonumber(str.substring(e, 0, 2)), 0))
int mm = int(nz(str.tonumber(str.substring(e, 2, 4)), 0))
hh * 60 + mm
clockMin(string hm) =>
array<string> parts = str.split(hm, ":")
array.size(parts) == 2 ? int(nz(str.tonumber(array.get(parts, 0)), -1)) * 60 + int(nz(str.tonumber(array.get(parts, 1)), 0)) : -1
int winEndMin = sessEndMin(inTradeSess)
int minsLeft = winEndMin - nowMin
string leftTxt = minsLeft <= 0 ? "closing" : (minsLeft >= 60 ? str.tostring(math.floor(minsLeft / 60)) + " h " : "") + str.tostring(minsLeft % 60) + " m left"
int timeStopMin = clockMin(inTimeStop)
int lastEntryMin = clockMin(inLastEntry)
bool timeStopNow = timeStopMin >= 0 and nowMin >= timeStopMin
bool inRange = not na(time(timeframe.period, inRangeSess, inTz))
bool inTrade = not na(time(timeframe.period, inTradeSess, inTz))
bool rangeStart = inRange and not inRange[1]
bool rangeEnd = not inRange and inRange[1]
bool windowEnd = not inTrade and inTrade[1]
bool entryOpen = inTrade and (lastEntryMin < 0 or nowMin <= lastEntryMin)
bool volOk = not inVolOn or (not na(volume) and volume >= inVolMult * volMa)
bool vwapUp = not inVwapOn or close > vwap
bool vwapDn = not inVwapOn or close < vwap
bool emaUp = not inEmaOn or close > emaF
bool emaDn = not inEmaOn or close < emaF
// ─────────────────────────────────────────────────────────────────── STATE
type Day
float orH = na
float orL = na
bool set = false
int setBar = na
int startBar = na
int trades = 0
int brk = 0 // retest mode: side of the close-through we are waiting on
int brkBar = na
bool touched = false
string skip = "" // "" = tradable day; otherwise the reason
float todayR = 0.0
int dayNo = 0
bool done = false
float openPx = na
string log = ""
int cntUp = 0
int cntDn = 0
int lastBrk = 0
float brkExt = na
bool fakeUpLogged = false
bool fakeDnLogged = false
type Trade
int dir = 0
float entry = na
float sl = na
float sl0 = na
float r = na
float tp1 = na
float tp2 = na
int bar = na
bool tp1Hit = false
bool retest = false
bool fade = false
float lastR = na
string lastWhy = ""
type Stat
int n = 0
int wins = 0
int flat = 0
float sumR = 0.0
int nTp = 0
int nStop = 0
int nBe = 0
int nEod = 0
int days = 0
int daysTraded = 0
int daysSkipped = 0
int nTime = 0
int nFail = 0
int nFake = 0
type Ev
bool sigL = false
bool sigS = false
bool exit = false
bool tp1 = false
bool rangeSet = false
bool brkL = false
bool brkS = false
bool retestFail = false
bool fake = false
string why = ""
float r = na
int dir = 0
var Day dy = Day.new()
var Trade t = Trade.new()
var Stat st = Stat.new()
Ev ev = Ev.new()
bool gapUp = not na(dy.openPx) and dy.openPx > pClose
bool gapDn = not na(dy.openPx) and dy.openPx < pClose
bool gapOkL = not inGapOn or gapUp
bool gapOkS = not inGapOn or gapDn
logEv(string what) =>
dy.log := dy.log + (dy.log == "" ? "" : " · ") + hhmm + " " + what
closeTrade(float px, string why) =>
float rr = (px - t.entry) * t.dir / t.r
ev.exit := true
ev.why := why
ev.r := rr
ev.dir := t.dir
st.n := st.n + 1
st.wins := st.wins + (rr >= 0.05 ? 1 : 0)
st.flat := st.flat + (math.abs(rr) < 0.05 ? 1 : 0)
st.sumR := st.sumR + rr
if why == "TP2"
st.nTp := st.nTp + 1
else if why == "EOD"
st.nEod := st.nEod + 1
else if why == "BE"
st.nBe := st.nBe + 1
else if why == "TIME"
st.nTime := st.nTime + 1
else if why == "FAIL"
st.nFail := st.nFail + 1
else
st.nStop := st.nStop + 1
dy.todayR := dy.todayR + rr
logEv((why == "TP2" ? "TP2 " : why == "EOD" ? "flat " : why == "TIME" ? "time stop " : why == "BE" ? "breakeven " : why == "TRAIL" ? "trail " : why == "FAIL" ? "failed " : "stop ") + (rr >= 0 ? "+" : "") + str.tostring(rr, "0.0") + "R")
t.lastR := rr
t.lastWhy := why
t.dir := 0
openTrade(int d, bool viaRetest, float stopOverride) =>
float rangeH = dy.orH - dy.orL
float base = inStopMode == "Range midpoint" ? (dy.orH + dy.orL) / 2 : inStopMode == "ATR from entry" ? (d == 1 ? close - inStopAtr * atrU : close + inStopAtr * atrU) : (d == 1 ? dy.orL : dy.orH)
float anchor = na(stopOverride) ? base : stopOverride
float slPx = d == 1 ? math.min(anchor - inStopBuf * atrU, close - syminfo.mintick) : math.max(anchor + inStopBuf * atrU, close + syminfo.mintick)
float unit = inTpUnit == "Range heights" ? math.max(rangeH, syminfo.mintick) : math.abs(close - slPx)
t.dir := d
t.entry := close
t.sl := slPx
t.sl0 := slPx
t.r := math.abs(close - slPx)
t.tp1 := close + d * inTp1 * unit
t.tp2 := close + d * inTp2 * unit
t.bar := bar_index
t.tp1Hit := false
t.retest := viaRetest
t.fade := not na(stopOverride)
dy.trades := dy.trades + 1
logEv((d == 1 ? "▲ long " : "▼ short ") + fmt(close) + (viaRetest ? " (retest)" : t.fade ? " (fade)" : ""))
if dy.trades == 1
st.daysTraded := st.daysTraded + 1
if d == 1
ev.sigL := true
else
ev.sigS := true
if barstate.isconfirmed and not na(atr)
// ── the day
if rangeStart
dy.orH := high
dy.orL := low
dy.set := false
dy.setBar := na
dy.startBar := bar_index
dy.trades := 0
dy.brk := 0
dy.brkBar := na
dy.touched := false
dy.skip := ""
dy.todayR := 0.0
dy.dayNo := dy.dayNo + 1
dy.done := false
dy.openPx := open
dy.log := ""
dy.cntUp := 0
dy.cntDn := 0
dy.lastBrk := 0
dy.brkExt := na
dy.fakeUpLogged := false
dy.fakeDnLogged := false
st.days := st.days + 1
else if inRange
dy.orH := math.max(nz(dy.orH, high), high)
dy.orL := math.min(nz(dy.orL, low), low)
if rangeEnd and not dy.set and not na(dy.orH)
dy.set := true
dy.setBar := bar_index
ev.rangeSet := true
float rAtr = (dy.orH - dy.orL) / dAtrU
if inMinRange > 0 and rAtr < inMinRange
dy.skip := "range " + str.tostring(rAtr, "0.00") + " × daily ATR — too small (< " + str.tostring(inMinRange, "0.00") + ")"
else if inMaxRange > 0 and rAtr > inMaxRange
dy.skip := "range " + str.tostring(rAtr, "0.00") + " × daily ATR — too large (> " + str.tostring(inMaxRange, "0.00") + ")"
if dy.skip != ""
st.daysSkipped := st.daysSkipped + 1
logEv("range set" + (dy.skip != "" ? " · skipped" : ""))
// ── manage an open trade (stop first, then TP2, TP1, end of window)
if t.dir != 0
int d = t.dir
bool stopHit = d == 1 ? low <= t.sl : high >= t.sl
if stopHit
closeTrade(t.sl, t.tp1Hit and t.sl == t.entry ? "BE" : t.tp1Hit and inTrailOn and t.sl != t.sl0 ? "TRAIL" : "SL")
else if d == 1 ? high >= t.tp2 : low <= t.tp2
closeTrade(t.tp2, "TP2")
else
if not t.tp1Hit and (d == 1 ? high >= t.tp1 : low <= t.tp1)
t.tp1Hit := true
ev.tp1 := true
logEv("TP1")
if inBeAtTp1
t.sl := t.entry
if inFailExit and not t.fade and t.dir != 0 and (d == 1 ? close < dy.orH : close > dy.orL)
closeTrade(close, "FAIL")
if inTrailOn and t.tp1Hit and t.dir != 0
float trailPx = d == 1 ? close - inTrailAtr * atrU : close + inTrailAtr * atrU
t.sl := d == 1 ? math.max(t.sl, trailPx) : math.min(t.sl, trailPx)
if timeStopNow and not t.tp1Hit and t.dir != 0
closeTrade(close, "TIME")
if inEodFlat and windowEnd and t.dir != 0
closeTrade(close, "EOD")
// ── breakout bookkeeping (every confirmed bar once the range is set): consecutive closes beyond an edge,
// the extreme reached since the last break, fake-outs (a wick beyond the edge that closed back inside)
bool failUp = false
bool failDn = false
if dy.set
float upLvlB = dy.orH + inBreakBuf * atrU
float dnLvlB = dy.orL - inBreakBuf * atrU
bool upRaw = close > upLvlB
bool dnRaw = close < dnLvlB
dy.cntUp := upRaw ? dy.cntUp + 1 : 0
dy.cntDn := dnRaw ? dy.cntDn + 1 : 0
failUp := dy.lastBrk == 1 and not upRaw and close < dy.orH
failDn := dy.lastBrk == -1 and not dnRaw and close > dy.orL
if upRaw
dy.brkExt := dy.lastBrk == 1 ? math.max(nz(dy.brkExt, high), high) : high
dy.lastBrk := 1
else if dnRaw
dy.brkExt := dy.lastBrk == -1 ? math.min(nz(dy.brkExt, low), low) : low
dy.lastBrk := -1
if t.dir == 0 and not upRaw and not dnRaw and dy.lastBrk == 0 and (high > dy.orH or low < dy.orL)
ev.fake := true
st.nFake := st.nFake + 1
bool fakeAbv = high > dy.orH
if fakeAbv and not dy.fakeUpLogged
dy.fakeUpLogged := true
logEv("fake-out ▲")
else if not fakeAbv and not dy.fakeDnLogged
dy.fakeDnLogged := true
logEv("fake-out ▼")
// ── signals (only when flat, the range is set, inside the trade window, on a tradable day)
if t.dir == 0 and dy.set and entryOpen and dy.skip == "" and dy.trades < inMaxTrades
float upLvl = dy.orH + inBreakBuf * atrU
float dnLvl = dy.orL - inBreakBuf * atrU
float barRange = math.max(high - low, syminfo.mintick)
bool strongUp = not inStrongClose or (close - low) / barRange >= 1 - inStrongPct / 100.0
bool strongDn = not inStrongClose or (high - close) / barRange >= 1 - inStrongPct / 100.0
bool brkUp = close > upLvl and dy.cntUp >= inConfirmBars and strongUp
bool brkDn = close < dnLvl and dy.cntDn >= inConfirmBars and strongDn
bool longOk = vwapUp and emaUp and volOk and gapOkL
bool shortOk = vwapDn and emaDn and volOk and gapOkS
if inFadeFail and ((failUp and shortOk) or (failDn and longOk))
openTrade(failUp ? -1 : 1, false, dy.brkExt)
dy.lastBrk := 0
else if inEntryMode == "Close through"
if brkUp and longOk
openTrade(1, false, na)
else if brkDn and shortOk
openTrade(-1, false, na)
else
if dy.brk == 0
if brkUp and longOk
dy.brk := 1
dy.brkBar := bar_index
dy.touched := false
ev.brkL := true
logEv("broke out ▲")
else if brkDn and shortOk
dy.brk := -1
dy.brkBar := bar_index
dy.touched := false
ev.brkS := true
logEv("broke out ▼")
else
int bd = dy.brk
float edge = bd == 1 ? dy.orH : dy.orL
float tol = inRetestTol * atrU
if bar_index - dy.brkBar > inRetestWait
dy.brk := 0
ev.retestFail := true
ev.why := "retest expired"
logEv("no retest")
else if bd == 1 ? close < edge - tol : close > edge + tol
dy.brk := 0
ev.retestFail := true
ev.why := "closed back inside the range"
logEv("back inside")
else if bar_index > dy.brkBar
if bd == 1 ? low <= edge + tol : high >= edge - tol
dy.touched := true
if dy.touched and (bd == 1 ? close > edge : close < edge)
openTrade(bd, true, na)
dy.brk := 0
if failUp or failDn
dy.lastBrk := 0
if dy.set and t.dir == 0 and (dy.trades >= inMaxTrades or (not inTrade and dy.trades > 0))
dy.done := true
// ─────────────────────────────────────────────────────────────────── READINESS (display only)
bool flatNow = t.dir == 0
float rangeH = dy.set ? dy.orH - dy.orL : na
float rangeAtr = dy.set ? rangeH / dAtrU : na
float upLvlD = dy.set ? dy.orH + inBreakBuf * atrU : na
float dnLvlD = dy.set ? dy.orL - inBreakBuf * atrU : na
bool s1 = dy.set
bool s2 = entryOpen
bool s3 = dy.skip == ""
bool fVwapL = vwapUp
bool fVwapS = vwapDn
bool fEmaL = emaUp
bool fEmaS = emaDn
bool filtersOn = inVwapOn or inEmaOn or inVolOn or inGapOn
bool s4L = fVwapL and fEmaL and gapOkL
bool s4S = fVwapS and fEmaS and gapOkS
bool s5 = dy.trades < inMaxTrades
int side = filtersOn and (s4L != s4S) ? (s4L ? 1 : -1) : close >= (dy.set ? (dy.orH + dy.orL) / 2 : close) ? 1 : -1 // the side the filters allow, else the nearer edge
string sideWord = side == 1 ? "long" : "short"
string s1Txt = dy.set ? fmt(dy.orL) + " – " + fmt(dy.orH) + " · " + str.tostring(rangeAtr, "0.00") + " × daily ATR" : inRange ? "forming — " + (na(dy.orH) ? "first bar" : fmt(dy.orL) + " – " + fmt(dy.orH) + " so far") : "waiting for " + inRangeSess + " (" + inTz + ")"
string s2Txt = entryOpen ? "inside " + inTradeSess + (lastEntryMin >= 0 ? " · entries until " + inLastEntry : "") + " · " + leftTxt : inTrade ? "past the latest entry time " + inLastEntry + " · " + leftTxt : "outside " + inTradeSess + (dy.set ? " · no new entries" : "")
string s3Txt = dy.skip == "" ? ((inMinRange > 0 or inMaxRange > 0) ? "range size passes" : "no size gate") : dy.skip
string fTxt = (inVwapOn ? "VWAP " + (close > vwap ? "below price" : "above price") : "") + (inEmaOn ? (inVwapOn ? " · " : "") + "EMA " + (close > emaF ? "below price" : "above price") : "") + (inVolOn ? ((inVwapOn or inEmaOn) ? " · " : "") + "volume " + str.tostring(nz(volume) / math.max(volMa, 1e-9), "0.0") + "× avg" : "") + (inGapOn ? ((inVwapOn or inEmaOn or inVolOn) ? " · " : "") + "gap " + (gapUp ? "up" : gapDn ? "down" : "flat") : "")
string s4Txt = filtersOn ? fTxt + " → " + (s4L and s4S ? "either side" : s4L ? "longs only" : s4S ? "shorts only" : "no side passes") : "no filters on — either side"
string brkTxt = not dy.set ? "needs the range first" : dy.brk != 0 ? (dy.brk == 1 ? "broke out ▲ · retest of " + fmt(dy.orH) : "broke out ▼ · retest of " + fmt(dy.orL)) + (dy.touched ? " · touched" : "") + " · " + str.tostring(math.max(0, inRetestWait - (bar_index - nz(dy.brkBar, bar_index)))) + " bars left" : "long > " + fmt(upLvlD) + " · short < " + fmt(dnLvlD) + (close > dy.orH ? " · price above" + (inConfirmBars > 1 and not dy.done ? " · " + str.tostring(math.min(dy.cntUp, inConfirmBars)) + "/" + str.tostring(inConfirmBars) + " closes" : "") : close < dy.orL ? " · price below" + (inConfirmBars > 1 and not dy.done ? " · " + str.tostring(math.min(dy.cntDn, inConfirmBars)) + "/" + str.tostring(inConfirmBars) + " closes" : "") : " · price inside")
bool allPre = s1 and s2 and s3 and s5 and (s4L or s4S)
string missing = not flatNow ? "in a trade" : dy.done ? "done for today (" + str.tostring(dy.trades) + " of " + str.tostring(inMaxTrades) + " trades)" : not s1 ? "the range" : not s2 ? "the trade window" : not s3 ? "a tradable range size" : not (s4L or s4S) ? "a filter on either side" : inEntryMode == "Retest" and dy.brk != 0 ? "the retest" : "the breakout close"
string stateWord = t.dir == 1 ? "LONG OPEN" : t.dir == -1 ? "SHORT OPEN" : not dy.set ? (inRange ? "RANGE FORMING" : "WAITING FOR THE RANGE") : dy.skip != "" ? "DAY SKIPPED" : dy.done ? "DONE FOR TODAY" : not inTrade ? "OUTSIDE THE WINDOW" : not entryOpen ? "PAST THE ENTRY CUT-OFF" : dy.brk != 0 ? (dy.brk == 1 ? "BROKE OUT ▲ · RETEST?" : "BROKE OUT ▼ · RETEST?") : "RANGE SET · WATCHING"
color stateCol = t.dir == 1 ? th.up : t.dir == -1 ? th.down : dy.brk == 1 ? th.up : dy.brk == -1 ? th.down : dy.set and entryOpen and dy.skip == "" and not dy.done ? th.accentHi : th.flat
float openR = t.dir != 0 ? (close - t.entry) * t.dir / t.r : na
string sizeTxt = inRiskUsd > 0 and t.dir != 0 and t.r > 0 ? str.tostring(math.floor(inRiskUsd / t.r)) + (syminfo.type == "stock" or syminfo.type == "fund" ? " shares" : " units") + " for " + str.tostring(inRiskUsd, "#") + " risk" : ""
string chipTxt = t.dir != 0 ? (openR >= 0 ? "+" : "") + str.tostring(openR, "0.0") + "R now" + (t.tp1Hit ? " · stop at breakeven" : "") : dy.trades > 0 ? "today " + (dy.todayR >= 0 ? "+" : "") + str.tostring(dy.todayR, "0.0") + "R" : dy.set ? str.tostring(bar_index - nz(dy.setBar, bar_index)) + " bars since the range" : "day " + str.tostring(dy.dayNo)
// ─────────────────────────────────────────────────────────────────── CHART · range boxes, levels, trade levels
var array<box> orBoxes = array.new<box>()
var array<line> orLines = array.new<line>()
var array<label> orTags = array.new<label>()
var box curBox = na
var box curExt = na
var line curHi = na
var line curLo = na
var line curMid = na
var label curTag = na
color rangeCol = dy.skip != "" ? th.flat : th.accent
if barstate.isconfirmed
if rangeEnd and dy.set and inShowOr != "Off"
if inShowOr == "Today only"
for b in orBoxes
box.delete(b)
array.clear(orBoxes)
for l in orLines
line.delete(l)
array.clear(orLines)
for g in orTags
label.delete(g)
array.clear(orTags)
int x1 = math.max(nz(dy.startBar, bar_index - 1), bar_index - 250)
float midPx = (dy.orH + dy.orL) / 2
curBox := box.new(x1, dy.orH, bar_index, dy.orL, border_color = color.new(rangeCol, 100), border_width = 1, bgcolor = color.new(rangeCol, 82))
curExt := box.new(bar_index, dy.orH, bar_index, dy.orL, border_color = color.new(rangeCol, 100), border_width = 1, bgcolor = color.new(rangeCol, 93))
curHi := line.new(x1, dy.orH, bar_index, dy.orH, color = color.new(rangeCol, 15), width = 2)
curLo := line.new(x1, dy.orL, bar_index, dy.orL, color = color.new(rangeCol, 15), width = 2)
curMid := line.new(x1, midPx, bar_index, midPx, color = color.new(rangeCol, inMidline ? 60 : 100), width = 1, style = line.style_dotted)
array.push(orBoxes, curBox)
array.push(orBoxes, curExt)
array.push(orLines, curHi)
array.push(orLines, curLo)
array.push(orLines, curMid)
if inRangeTag
curTag := label.new(bar_index, dy.orH, "OR " + str.tostring(rangeAtr, "0.00") + "× dATR" + (dy.skip != "" ? " · skipped" : ""), style = label.style_label_lower_left, color = color.new(th.bg, 20), textcolor = dy.skip != "" ? th.flat : th.accent, size = size.tiny)
array.push(orTags, curTag)
while array.size(orBoxes) > 120
box.delete(array.shift(orBoxes))
while array.size(orLines) > 180
line.delete(array.shift(orLines))
while array.size(orTags) > 60
label.delete(array.shift(orTags))
else if dy.set and inTrade and not na(curExt)
box.set_right(curExt, bar_index)
line.set_x2(curHi, bar_index)
line.set_x2(curLo, bar_index)
line.set_x2(curMid, bar_index)
if inBoxResult and not na(curBox) and dy.set and t.dir == 0 and (dy.done or (not inTrade and inTrade[1]))
color resCol = dy.trades == 0 ? th.flat : dy.todayR > 0.05 ? th.up : dy.todayR < -0.05 ? th.down : th.flat
box.set_bgcolor(curBox, color.new(resCol, 86))
box.set_bgcolor(curExt, color.new(resCol, 94))
line.set_color(curHi, color.new(resCol, 45))
line.set_color(curLo, color.new(resCol, 45))
line.set_width(curHi, 1)
line.set_width(curLo, 1)
line.set_color(curMid, color.new(resCol, inMidline ? 75 : 100))
// breakout levels (dotted) while the day is live and no trade is open
bool lvlOn = inShowLvls and dy.set and inTrade and t.dir == 0 and dy.skip == "" and not dy.done
plot(lvlOn ? upLvlD : na, "Breakout level · long", color = color.new(th.up, 45), linewidth = 1, style = plot.style_circles, display = display.pane)
plot(lvlOn ? dnLvlD : na, "Breakout level · short", color = color.new(th.down, 45), linewidth = 1, style = plot.style_circles, display = display.pane)
// filters
plot(inVwapOn or inDrawVwap ? vwap : na, "VWAP", color = color.new(close > vwap ? th.up : th.down, 35), linewidth = 1, display = display.pane)
plot(inEmaOn or inDrawEma ? emaF : na, "EMA", color = color.new(th.accent, 40), linewidth = 1, display = display.pane)
// trade levels
bool inT = t.dir != 0
plot(inT ? t.entry : na, "Entry", color = color.new(th.accentHi, 20), linewidth = 1, style = plot.style_linebr, display = display.pane)
plot(inT ? t.sl : na, "Stop", color = color.new(th.down, 20), linewidth = 1, style = plot.style_linebr, display = display.pane)
plot(inT ? t.tp1 : na, "TP1", color = color.new(th.up, 55), linewidth = 1, style = plot.style_linebr, display = display.pane)
plot(inT ? t.tp2 : na, "TP2", color = color.new(th.up, 20), linewidth = 1, style = plot.style_linebr, display = display.pane)
barcolor(inTint and inT ? color.new(t.dir == 1 ? th.up : th.down, 15) : na, title = "Trade tint")
// gradient risk / reward zones: four bands each, densest at the entry, extended while the trade is open
var array<box> zones = array.new<box>()
var array<box> liveZ = array.new<box>()
gradBand(float top, float bot, color c, int x1, int x2, bool denseTop) =>
float h = (top - bot) / 4
for k = 0 to 3
float bTop = top - h * k
float bBot = bTop - h
int alpha = denseTop ? 88 + k * 2 : 96 - k * 2
box b = box.new(x1, bTop, x2, bBot, border_color = color.new(c, 100), bgcolor = color.new(c, alpha))
array.push(zones, b)
array.push(liveZ, b)
if barstate.isconfirmed
if (ev.sigL or ev.sigS) and inZones
if inZoneKeep == "Last trade only"
for b in zones
box.delete(b)
array.clear(zones)
array.clear(liveZ)
int d = t.dir
gradBand(math.max(t.entry, t.sl), math.min(t.entry, t.sl), th.down, bar_index, bar_index + 1, d == 1)
gradBand(math.max(t.entry, t.tp2), math.min(t.entry, t.tp2), th.up, bar_index, bar_index + 1, d == -1)
while array.size(zones) > 96
box.delete(array.shift(zones))
else if (t.dir != 0 or ev.exit) and array.size(liveZ) > 0
for b in liveZ
box.set_right(b, bar_index)
// event labels
if ev.sigL
label.new(bar_index, low, "ORB ▲ " + fmt(t.entry) + (t.retest ? " · retest" : t.fade ? " · fade" : "") + "\nstop " + fmt(t.sl) + " · TP2 " + fmt(t.tp2) + (sizeTxt != "" ? "\n" + sizeTxt : ""), style = label.style_label_up, color = color.new(th.up, 12), textcolor = th.bg, size = size.small,
tooltip = "Long: a bar closed above the opening range" + (t.retest ? " and price came back to the edge and closed above it again" : "") + ". Stop " + fmt(t.sl) + " · TP1 " + fmt(t.tp1) + " · TP2 " + fmt(t.tp2))
if ev.sigS
label.new(bar_index, high, "ORB ▼ " + fmt(t.entry) + (t.retest ? " · retest" : t.fade ? " · fade" : "") + "\nstop " + fmt(t.sl) + " · TP2 " + fmt(t.tp2) + (sizeTxt != "" ? "\n" + sizeTxt : ""), style = label.style_label_down, color = color.new(th.down, 12), textcolor = th.bg, size = size.small,
tooltip = "Short: a bar closed below the opening range" + (t.retest ? " and price came back to the edge and closed below it again" : "") + ". Stop " + fmt(t.sl) + " · TP1 " + fmt(t.tp1) + " · TP2 " + fmt(t.tp2))
if ev.brkL
label.new(bar_index, high, "broke out ▲ · waiting for the retest", style = label.style_label_down, color = color.new(th.bg, 15), textcolor = th.up, size = size.tiny)
if ev.brkS
label.new(bar_index, low, "broke out ▼ · waiting for the retest", style = label.style_label_up, color = color.new(th.bg, 15), textcolor = th.down, size = size.tiny)
if ev.retestFail
label.new(bar_index, high, "no retest · " + ev.why, style = label.style_label_down, color = color.new(th.bg, 15), textcolor = th.flat, size = size.tiny)
if ev.fake and inShowFake
bool fakeAbove = high > dy.orH
label.new(bar_index, fakeAbove ? high : low, "✕", style = fakeAbove ? label.style_label_down : label.style_label_up, color = color.new(th.bg, 100), textcolor = color.new(th.flat, 25), size = size.tiny, tooltip = "Fake-out: the bar poked beyond the range and closed back inside")
if ev.exit
string rTxt = (ev.r >= 0 ? "+" : "") + str.tostring(ev.r, "0.0") + "R"
string whyTxt = ev.why == "TP2" ? "target" : ev.why == "EOD" ? "flat at the close" : ev.why == "BE" ? "breakeven" : ev.why == "TIME" ? "time stop" : ev.why == "TRAIL" ? "trail" : ev.why == "FAIL" ? "failed breakout" : "stop"
label.new(bar_index, ev.dir == 1 ? high : low, (ev.dir == 1 ? "exit long · " : "exit short · ") + whyTxt + " " + rTxt, style = ev.dir == 1 ? label.style_label_down : label.style_label_up, color = color.new(th.bg, 15), textcolor = ev.r >= 0 ? th.up : th.down, size = size.tiny)
// ─────────────────────────────────────────────────────────────────── DASHBOARD
posOf(string p) =>
p == "Top Left" ? position.top_left : p == "Bottom Right" ? position.bottom_right : p == "Bottom Left" ? position.bottom_left : p == "Middle Right" ? position.middle_right : p == "Middle Left" ? position.middle_left : position.top_right
szLead() => inSize == "Small" ? size.small : inSize == "Large" ? size.large : size.normal
szBody() => inSize == "Small" ? size.tiny : inSize == "Large" ? size.normal : size.small
szSub() => inSize == "Small" ? size.tiny : inSize == "Large" ? size.small : size.tiny
row3(table tb, int r, string k, bool ok, string why, bool showMark) =>
color stripe = r % 2 == 1 ? color.new(th.accent, 94) : th.bg
table.cell(tb, 0, r, k, text_color = th.flat, text_size = szBody(), text_halign = text.align_left, bgcolor = stripe)
table.cell(tb, 1, r, showMark ? (ok ? "✓" : "✗") : "", text_color = ok ? th.up : th.down, text_size = szBody(), text_halign = text.align_center, bgcolor = stripe)
table.cell(tb, 2, r, why, text_color = ok ? th.txt : th.txtHi, text_size = szBody(), text_halign = text.align_left, text_font_family = font.family_monospace, bgcolor = stripe)
section(table tb, int r, string title) =>
table.cell(tb, 0, r, title, text_color = th.accent, text_size = szSub(), text_halign = text.align_left, bgcolor = color.new(th.accent, 90))
table.cell(tb, 1, r, "", bgcolor = color.new(th.accent, 90))
table.cell(tb, 2, r, "", bgcolor = color.new(th.accent, 90))
table.merge_cells(tb, 0, r, 2, r)
int readyN = (s1 ? 1 : 0) + (s2 ? 1 : 0) + (s3 ? 1 : 0) + ((s4L or s4S) ? 1 : 0) + (s5 ? 1 : 0)
var table dash = table.new(posOf(inDashPos), 3, 20, bgcolor = th.bg, frame_color = th.frame, frame_width = 1, border_width = 0, force_overlay = false)
if barstate.islast and inDash
table.clear(dash, 0, 0, 2, 19)
int r = 0
table.cell(dash, 0, r, "OPENING RANGE DESK", text_color = th.accentHi, text_size = szLead(), text_halign = text.align_left, bgcolor = th.bg, tooltip = "Opening range breakout: the range window, the trade window, the filters, the breakout — as a checklist, in order. Everything commits on the closed bar.")
table.cell(dash, 1, r, "", bgcolor = th.bg)
table.cell(dash, 2, r, syminfo.ticker + " · " + timeframe.period, text_color = th.accent, text_size = szSub(), text_halign = text.align_right, bgcolor = th.bg)
r += 1
table.cell(dash, 0, r, (t.dir == 1 ? "▲ " : t.dir == -1 ? "▼ " : "● ") + stateWord, text_color = stateCol, text_size = szLead(), text_halign = text.align_left, bgcolor = th.bg, tooltip = "Where today stands: range forming → range set → breakout (or retest) → trade → done.")
table.cell(dash, 1, r, "", bgcolor = th.bg)
color chipBg = t.dir != 0 ? color.new(openR >= 0 ? th.up : th.down, 75) : dy.trades > 0 ? color.new(dy.todayR >= 0 ? th.up : th.down, 75) : color.new(color.from_gradient(readyN, 0, 5, th.down, th.up), 75)
table.cell(dash, 2, r, chipTxt, text_color = th.txtHi, text_size = szBody(), text_halign = text.align_right, bgcolor = chipBg)
r += 1
if t.dir == 0
section(dash, r, "CHECKLIST · " + str.tostring(readyN) + " OF 5 STEPS")
r += 1
row3(dash, r, "1 · Range", s1, s1Txt, true)
r += 1
row3(dash, r, "2 · Window", s2, s2Txt, true)
r += 1
row3(dash, r, "3 · Range size", s3, s3Txt, true)
r += 1
row3(dash, r, "4 · Filters", s4L or s4S, s4Txt, true)
r += 1
row3(dash, r, "5 · Budget", s5, str.tostring(dy.trades) + " of " + str.tostring(inMaxTrades) + " trades today" + (dy.trades > 0 ? " · " + (dy.todayR >= 0 ? "+" : "") + str.tostring(dy.todayR, "0.0") + "R" : ""), true)
r += 1
row3(dash, r, "Breakout", dy.brk != 0 or (dy.set and (close > dy.orH or close < dy.orL)), brkTxt, false)
r += 1
row3(dash, r, "Missing", allPre and not dy.done, missing, false)
r += 1
if dy.log != ""
row3(dash, r, "Today", true, dy.log, false)
r += 1
else
section(dash, r, "TRADE · " + (t.dir == 1 ? "LONG" : "SHORT") + (t.retest ? " · RETEST" : " · CLOSE-THROUGH") + " · " + str.tostring(bar_index - t.bar) + " BARS")
r += 1
row3(dash, r, "Entry", true, fmt(t.entry) + " · 1R = " + fmt(t.r), false)
r += 1
row3(dash, r, "Stop", true, fmt(t.sl) + (t.tp1Hit and t.sl == t.entry ? " · breakeven" : inStopMode == "Range midpoint" ? " · range midpoint" : inStopMode == "ATR from entry" ? " · ATR stop" : " · other side of the range"), false)
r += 1
row3(dash, r, "Targets", t.tp1Hit, "TP1 " + fmt(t.tp1) + (t.tp1Hit ? " ✓" : "") + " · TP2 " + fmt(t.tp2) + (inTpUnit == "Range heights" ? " (range heights)" : ""), false)
r += 1
row3(dash, r, "Exit rule", true, "stop or TP2" + (inFailExit and not t.fade ? " · out if a bar closes back inside" : "") + (inTrailOn ? " · trail " + str.tostring(inTrailAtr, "0.0") + " ATR after TP1" : "") + (timeStopMin >= 0 ? " · time stop " + inTimeStop : "") + (inEodFlat ? " · flat after " + inTradeSess : " · may hold overnight"), false)
r += 1
if sizeTxt != ""
row3(dash, r, "Size", true, sizeTxt + " · " + fmt(t.r) + " per unit", false)
r += 1
if dy.log != ""
row3(dash, r, "Today", true, dy.log, false)
r += 1
row3(dash, r, "Range", true, fmt(dy.orL) + " – " + fmt(dy.orH) + " · " + str.tostring(rangeAtr, "0.00") + " × daily ATR", false)
r += 1
section(dash, r, "RECORD · PAPER, NO COSTS")
r += 1
row3(dash, r, "Days", true, str.tostring(st.days) + " seen · " + str.tostring(st.daysTraded) + " traded" + (st.daysSkipped > 0 ? " · " + str.tostring(st.daysSkipped) + " skipped" : "") + (st.nFake > 0 ? " · " + str.tostring(st.nFake) + " fake-outs" : ""), false)
r += 1
row3(dash, r, "Closed", st.n > 0 and st.sumR >= 0, st.n == 0 ? "no closed trades yet" : str.tostring(st.n) + " · " + str.tostring(math.round(st.wins / st.n * 100)) + "% win · " + str.tostring(st.flat) + " flat · avg " + (st.sumR / st.n >= 0 ? "+" : "") + str.tostring(st.sumR / st.n, "0.00") + "R", false)
r += 1
row3(dash, r, "How they ended", true, "TP2 " + str.tostring(st.nTp) + " · stop " + str.tostring(st.nStop) + " · BE " + str.tostring(st.nBe) + " · EOD " + str.tostring(st.nEod) + (st.nTime > 0 ? " · time " + str.tostring(st.nTime) : "") + (st.nFail > 0 ? " · failed " + str.tostring(st.nFail) : "") + (na(t.lastR) ? "" : " · last " + (t.lastR >= 0 ? "+" : "") + str.tostring(t.lastR, "0.0") + "R"), false)
r += 1
string fSum = (inVwapOn ? "VWAP · " : "") + (inEmaOn ? "EMA " + str.tostring(inEmaLen) + (inEmaTf != "" ? "@" + inEmaTf : "") + " · " : "") + (inVolOn ? "volume · " : "") + (inGapOn ? "gap · " : "") + (inMinRange > 0 or inMaxRange > 0 ? "range size · " : "") + (inTrailOn ? "trail · " : "") + (timeStopMin >= 0 ? "time " + inTimeStop + " · " : "") + (inConfirmBars > 1 ? str.tostring(inConfirmBars) + " closes · " : "") + (inStrongClose ? "strong close · " : "") + (inFailExit ? "fail-exit · " : "") + (inFadeFail ? "fade · " : "") + (lastEntryMin >= 0 ? "entries until " + inLastEntry + " · " : "")
table.cell(dash, 0, r, "OR " + inRangeSess + " · " + (inEntryMode == "Retest" ? "retest" : "close-through") + " · stop " + (inStopMode == "Range midpoint" ? "midpoint" : inStopMode == "ATR from entry" ? "ATR" : "far side") + " · TP " + str.tostring(inTp1, "0.0") + "/" + str.tostring(inTp2, "0.0") + (inTpUnit == "Range heights" ? " ranges" : "R") + (inEodFlat ? " · flat at close" : ""), text_color = th.flat, text_size = szSub(), text_halign = text.align_left, bgcolor = th.bg)
table.cell(dash, 1, r, "", bgcolor = th.bg)
table.cell(dash, 2, r, fSum == "" ? "no filters · classic rule" : "filters: " + str.substring(fSum, 0, str.length(fSum) - 3), text_color = th.flat, text_size = szSub(), text_halign = text.align_right, bgcolor = th.bg)
// ─────────────────────────────────────────────────────────────────── CO-PILOT
wrap(string s, int width) =>
array<string> words = str.split(s, " ")
string out = ""
string line = ""
for w in words
if str.length(line) + str.length(w) + 1 > width and line != ""
out := out + line + "\n"
line := w
else
line := line == "" ? w : line + " " + w
out + line
narrNow() =>
string o = ""
if t.dir != 0
o := "in a " + (t.dir == 1 ? "long" : "short") + " from " + fmt(t.entry) + (t.retest ? " (retest entry)" : " (close-through entry)") + " for " + str.tostring(bar_index - t.bar) + " bars, " + (openR >= 0 ? "+" : "") + str.tostring(openR, "0.0") + "R at this close — stop " + fmt(t.sl) + (t.tp1Hit ? " (breakeven after TP1)" : " at the " + (inStopMode == "Range midpoint" ? "range midpoint" : inStopMode == "ATR from entry" ? "ATR distance" : "other side of the range")) + ", TP2 " + fmt(t.tp2) + (inEodFlat ? ", flat after " + inTradeSess : "")
else if not dy.set
o := inRange ? "the opening range is forming (" + inRangeSess + " " + inTz + ") — " + (na(dy.orH) ? "first bar" : fmt(dy.orL) + " to " + fmt(dy.orH) + " so far") + "; nothing fires until the window closes" : "waiting for the opening-range window (" + inRangeSess + " " + inTz + ")"
else if dy.skip != ""
o := "today is skipped — " + dy.skip
else if dy.done
o := "done for today: " + str.tostring(dy.trades) + " trade" + (dy.trades == 1 ? "" : "s") + ", " + (dy.todayR >= 0 ? "+" : "") + str.tostring(dy.todayR, "0.0") + "R" + (t.lastWhy != "" ? ", the last one ended on the " + (t.lastWhy == "TP2" ? "target" : t.lastWhy == "EOD" ? "close" : t.lastWhy == "BE" ? "breakeven stop" : "stop") : "")
else if not entryOpen
o := "the range is " + fmt(dy.orL) + " to " + fmt(dy.orH) + " but " + (inTrade ? "the latest entry time " + inLastEntry + " has passed" : "the trade window " + inTradeSess + " is closed") + " — no new entries"
else
o := "the range is set: " + fmt(dy.orL) + " to " + fmt(dy.orH) + " (" + str.tostring(rangeAtr, "0.00") + " × the daily ATR); " + (dy.brk != 0 ? "price broke " + (dy.brk == 1 ? "out above and a retest of " + fmt(dy.orH) : "out below and a retest of " + fmt(dy.orL)) + " would be the entry (" + str.tostring(math.max(0, inRetestWait - (bar_index - nz(dy.brkBar, bar_index)))) + " bars left)" : "a long fires on a close above " + fmt(upLvlD) + ", a short on a close below " + fmt(dnLvlD)) + (filtersOn ? "; filters allow " + (s4L and s4S ? "either side" : s4L ? "longs only" : s4S ? "shorts only" : "neither side right now") : "")
o + (dy.log != "" ? "; today so far: " + dy.log : "")
narrWhy() =>
"Opening range breakout: the first " + inRangeSess + " of the session sets the day's first balance; a close beyond it says one side has lost, so the trade goes with the winner and the other side of the range is where the idea is wrong. " + (inEntryMode == "Retest" ? "The retest variant waits for price to come back to the edge and hold, trading the confirmation instead of the first push." : "The close-through variant trades the first confirmed push.")
narrPlan() =>
string o = ""
if t.dir != 0
o := "Stop first on every closed bar. TP1 " + fmt(t.tp1) + (inBeAtTp1 ? " moves the stop to breakeven" : " is noted") + "; TP2 " + fmt(t.tp2) + " is the final target" + (inEodFlat ? "; whatever is open closes on the first bar after " + inTradeSess : "") + ". The idea is wrong beyond " + fmt(t.sl0) + (inFailExit and not t.fade ? ", and a bar closing back inside the range ends it early" : "") + "." + (sizeTxt != "" ? " Size: " + sizeTxt + "." : "")
else
o := (dy.set ? "" : "Wait for the window " + inRangeSess + " to close; then ") + "a " + sideWord + " fires when a bar closes " + (side == 1 ? "above " + fmt(upLvlD) : "below " + fmt(dnLvlD)) + (inEntryMode == "Retest" ? ", price comes back to the edge within " + str.tostring(inRetestWait) + " bars and a bar closes back beyond it" : "") + (inConfirmBars > 1 ? " and holds for " + str.tostring(inConfirmBars) + " closes" : "") + (inStrongClose ? ", closing in the top part of its bar" : "") + (inFadeFail ? "; a break that closes back inside is traded the other way" : "") + (filtersOn ? ", with the filters on that side" : "") + ". Then: entry at that close, stop at the " + (inStopMode == "Range midpoint" ? "range midpoint" : inStopMode == "ATR from entry" ? str.tostring(inStopAtr, "0.0") + " ATR distance" : "other side of the range") + " plus a buffer, TP1 " + str.tostring(inTp1, "0.0") + " and TP2 " + str.tostring(inTp2, "0.0") + (inTpUnit == "Range heights" ? " range heights" : "R") + (inEodFlat ? ", flat after " + inTradeSess : "") + ". " + str.tostring(inMaxTrades) + " trade" + (inMaxTrades == 1 ? "" : "s") + " per day."
o
narrTrack() =>
st.n < 5 ? "Too few closed trades on this chart to say anything yet." : str.tostring(st.n) + " paper trades over " + str.tostring(st.daysTraded) + " traded days: " + str.tostring(math.round(st.wins / st.n * 100)) + "% won, avg " + (st.sumR / st.n >= 0 ? "+" : "") + str.tostring(st.sumR / st.n, "0.00") + "R; " + str.tostring(st.nTp) + " reached TP2, " + str.tostring(st.nStop) + " stopped, " + str.tostring(st.nBe) + " breakeven, " + str.tostring(st.nEod) + " closed at the end of the window. No costs, small sample — a description of the past on THIS chart, not an edge."
narrPosOf(string p) =>
p == "Top Left" ? position.top_left : p == "Top Right" ? position.top_right : p == "Bottom Right" ? position.bottom_right : p == "Middle Left" ? position.middle_left : p == "Middle Right" ? position.middle_right : position.bottom_left
narrBodySz() => inNarrSize == "Small" ? size.small : inNarrSize == "Medium" ? size.normal : inNarrSize == "Large" ? size.large : szBody()
narrHeadSz() => inNarrSize == "Small" ? size.normal : inNarrSize == "Medium" ? size.large : inNarrSize == "Large" ? size.large : szLead()
var table narrT = na
if barstate.islast
if not na(narrT)
table.delete(narrT)
narrT := na
if inNarr != "Off"
array<string> labs = array.new<string>()
array<string> vals = array.new<string>()
array.push(labs, "NOW")
array.push(vals, wrap(narrNow() + " (decision support, not a trade instruction)", inNarrWrap))
bool deep = inNarr == "Standard" or inNarr == "Detailed"
if deep
array.push(labs, "WHY")
array.push(vals, wrap(narrWhy(), inNarrWrap))
array.push(labs, "PLAN")
array.push(vals, wrap(narrPlan(), inNarrWrap))
if inNarr == "Detailed"
array.push(labs, "TRACK REC")
array.push(vals, wrap(narrTrack(), inNarrWrap))
array.push(labs, "GLOSSARY")
array.push(vals, wrap("Opening range = the high and low of the first minutes of the session. Close-through = a bar closing beyond the range edge. Retest = price returning to the broken edge and holding. Daily ATR = the previous day's 14-day average true range, the yardstick for the range size. Flat at the close = no overnight risk.", inNarrWrap))
int nr = array.size(labs)
bool atBottom = inNarrPos == "Bottom Left" or inNarrPos == "Bottom Right"
int rows = nr + 2 + (atBottom ? 1 : 0)
narrT := table.new(narrPosOf(inNarrPos), 2, rows, bgcolor = th.bg, frame_color = th.frame, frame_width = 1, border_color = color.new(th.frame, 70), border_width = 1)
table.cell(narrT, 0, 0, "◆ CO-PILOT", text_color = th.accentHi, bgcolor = color.new(th.accent, 88), text_size = narrHeadSz(), text_halign = text.align_left)
table.cell(narrT, 1, 0, stateWord + " · " + str.upper(inNarr), text_color = th.accent, bgcolor = color.new(th.accent, 88), text_size = narrBodySz(), text_halign = text.align_right)
for i = 0 to nr - 1
table.cell(narrT, 0, i + 1, array.get(labs, i), text_color = th.accent, bgcolor = th.bg, text_size = narrBodySz(), text_halign = text.align_left, text_valign = text.align_top)
table.cell(narrT, 1, i + 1, array.get(vals, i), text_color = th.txt, bgcolor = th.bg, text_size = narrBodySz(), text_halign = text.align_left, text_valign = text.align_top)
table.cell(narrT, 0, nr + 1, "", bgcolor = th.bg)
table.cell(narrT, 1, nr + 1, "Honest: levels are geometry, not promises. The record has no costs — measure before you trust it.", text_color = th.flat, bgcolor = th.bg, text_size = narrBodySz(), text_halign = text.align_left)
if atBottom
table.cell(narrT, 0, nr + 2, " ", bgcolor = th.bg)
table.cell(narrT, 1, nr + 2, " ", bgcolor = th.bg)
// ─────────────────────────────────────────────────────────────────── ALERTS (confirmed bars only by construction)
payload(string evName, string why) =>
string js = '{"src":"OpeningRangeDesk","symbol":"' + syminfo.ticker + '","tf":"' + timeframe.period + '","event":"' + evName + '","side":"' + (t.dir == 1 or ev.dir == 1 ? "LONG" : t.dir == -1 or ev.dir == -1 ? "SHORT" : "NONE") + '","entry":' + fmt(nz(t.entry, close)) + ',"sl":' + fmt(nz(t.sl, 0)) + ',"tp1":' + fmt(nz(t.tp1, 0)) + ',"tp2":' + fmt(nz(t.tp2, 0)) + ',"orHigh":' + fmt(nz(dy.orH, 0)) + ',"orLow":' + fmt(nz(dy.orL, 0)) + ',"reason":"' + why + '"' + (na(ev.r) ? "" : ',"r":' + str.tostring(ev.r, "0.00")) + '}'
string tx = "Opening Range Desk · " + syminfo.ticker + " " + timeframe.period + " · " + evName + " · entry " + fmt(nz(t.entry, close)) + " · SL " + fmt(nz(t.sl, 0)) + " · TP " + fmt(nz(t.tp1, 0)) + " / " + fmt(nz(t.tp2, 0)) + " · OR " + fmt(nz(dy.orL, 0)) + "–" + fmt(nz(dy.orH, 0)) + (why != "" ? " · " + why : "")
inAlertJson ? js : tx
alertcondition(ev.rangeSet, "Range set", "Opening Range Desk: the opening range is final — watching for the breakout")
alertcondition(ev.sigL, "ORB long", "Opening Range Desk: long — a bar closed above the opening range")
alertcondition(ev.sigS, "ORB short", "Opening Range Desk: short — a bar closed below the opening range")
alertcondition(ev.brkL or ev.brkS, "Broke out — waiting for the retest", "Opening Range Desk: price closed beyond the range; the retest is the entry")
alertcondition(ev.tp1, "TP1 reached", "Opening Range Desk: TP1 reached — stop to breakeven")
alertcondition(ev.exit and ev.why == "TP2", "TP2 reached", "Opening Range Desk: final target reached")
alertcondition(ev.exit and (ev.why == "SL" or ev.why == "BE"), "Stopped", "Opening Range Desk: stop hit")
alertcondition(ev.exit and ev.why == "EOD", "Flat at the close", "Opening Range Desk: the trade window ended — position closed")
alertcondition(ev.exit and ev.why == "TIME", "Time stop", "Opening Range Desk: TP1 not reached by the time stop — position closed")
alertcondition(ev.exit and ev.why == "FAIL", "Failed breakout", "Opening Range Desk: a bar closed back inside the range — position closed")
alertcondition(ev.fake, "Fake-out", "Opening Range Desk: a wick beyond the range closed back inside")
alertcondition(ev.rangeSet and dy.skip != "", "Day skipped", "Opening Range Desk: the opening range failed the size gate — no trade today")
if ev.rangeSet
alert(payload("RANGE_SET", dy.skip), alert.freq_once_per_bar_close)
if ev.sigL
alert(payload("ORB_LONG", t.retest ? "retest" : t.fade ? "fade" : "close-through"), alert.freq_once_per_bar_close)
if ev.sigS
alert(payload("ORB_SHORT", t.retest ? "retest" : t.fade ? "fade" : "close-through"), alert.freq_once_per_bar_close)
if ev.tp1
alert(payload("TP1", ""), alert.freq_once_per_bar_close)
if ev.exit
alert(payload("EXIT", ev.why), alert.freq_once_per_bar_close)
The strategy twin · optional
Measure with fees and slippage on a second chart. Read the workflow chapter first.
Copy the code as it is. No coding needed.
View the full code
// ═══════════════════════════════════════════════════════════════════════════════════════════════════════
// OPENING RANGE DESK — STRATEGY TWIN · Pine Script v6 · v1.2.0 (generated from orb_desk.pine by tools/gen-twin.mjs)
// Built for a r/pinescript thread reply ("Orb strategy") — every variation the thread asked about is an input:
// the range window (5 / 15 / 30 min or any session), direct breakout closes OR retests, VWAP and EMA trend
// filters, plus the things the textbook leaves out: a range-size gate, a volume gate, a trade-window and
// flat-at-the-close rule, one trade per day (or more), and a panel that says which step is still missing.
//
// HOW IT READS THE MARKET (every decision commits on the CLOSED bar — nothing repaints):
// 1 RANGE the high / low of every bar inside the opening-range window; final on the first bar after it
// 2 WINDOW entries only inside the trade window; flat at its end (switch)
// 3 FILTERS VWAP side · EMA side · breakout-bar volume · range size vs the daily ATR — all off by default
// 4 BREAKOUT close-through: a bar CLOSES beyond the range edge (+ buffer) → entry at that close
// retest: after the close-through, price comes back to the edge and a bar closes back beyond it
// EXIT stop at the opposite side of the range (or the midpoint / ATR), TP1 → breakeven, TP2 final,
// flat at the end of the trade window
//
// Paper ledger has NO costs — a description of the past on your chart. The strategy twin measures it with costs.
// License: MIT — share freely, keep this header. © 2026 ProEA Lab · built for r/pinescript
// ═══════════════════════════════════════════════════════════════════════════════════════════════════════
//@version=6
strategy("Opening Range Desk — Strategy Twin", "ORBDeskS", overlay = true, initial_capital = 100000, default_qty_type = strategy.fixed, default_qty_value = 1, pyramiding = 0,
commission_type = strategy.commission.percent, commission_value = 0.02, slippage = 5, process_orders_on_close = true, calc_on_every_tick = false,
max_labels_count = 400, max_boxes_count = 200, max_lines_count = 300)
// COSTS ARE ON: 0.02% commission per order + 5 ticks slippage per fill (edit the strategy() header for your broker). Quantity = 1 unit.
// Entries fill at the signal bar's close (process_orders_on_close) — the same price the paper ledger uses.
// ─────────────────────────────────────────────────────────────────── INPUTS
gR = "Opening range"
inRangeSess = input.session("0930-0945", "Opening-range window", group = gR, tooltip = "The bars whose highs and lows form the range. 0930-0935 = a 5-minute range, 0930-0945 = 15 minutes, 0930-1000 = 30 minutes. The range is final on the first bar after the window — that bar can already be the breakout bar. Measured on SPY 5m (profit factor, costs on): 5-minute range 0.61 · 15-minute 0.69 · 30-minute 0.60 — none of them paid this summer.", display = display.none)
inTz = input.string("America/New_York", "Timezone (IANA)", group = gR, tooltip = "Both windows are read in this timezone. New York for US stocks, indices and the NY gold open.", display = display.none)
inTradeSess = input.session("0945-1555", "Trade window (entries allowed)", group = gR, tooltip = "Entries only inside this window. With 'flat at the end' on, an open trade is closed on the first bar after it.", display = display.none)
inEodFlat = input.bool(true, "Flat at the end of the trade window", group = gR, display = display.none)
inLastEntry = input.string("", "Latest entry time — HH:MM in the timezone (blank = the whole window)", group = gR, tooltip = "Breakouts late in the day have the least room to run. With a time here, no new trade opens after it; open trades are still managed to the window's end. Measured (11:00, profit factor): SPY 5m 0.69 → 0.71 · QQQ 5m unchanged (every breakout came before 11:00 anyway) · gold 15m 0.88 → 1.01.", display = display.none)
inMinRange = input.float(0.0, "Skip the day if the range is smaller than N × daily ATR (0 = off)", minval = 0, maxval = 3, step = 0.05, group = gR, tooltip = "A tiny range breaks on noise. The range height is compared with the previous day's 14-day ATR. Measured (0.15): SPY 5m 0.69 → 0.59.", display = display.none)
inMaxRange = input.float(0.0, "Skip the day if the range is larger than N × daily ATR (0 = off)", minval = 0, maxval = 5, step = 0.05, group = gR, tooltip = "A huge range means the move already happened and the stop (the other side of the range) is far away. Measured (0.5): SPY 5m 0.69 → 0.75 · QQQ 5m 0.44 → 0.49.", display = display.none)
gE = "Entry"
inEntryMode = input.string("Close through", "Entry", options = ["Close through", "Retest"], group = gE, tooltip = "Close through: a bar closes beyond the range edge (+ buffer) → entry at that close. Retest: after that close-through, wait for price to come back to the edge (within the tolerance) and enter when a bar closes back beyond it; the setup expires if the retest does not come in time, and fails if a bar closes back inside the range. Measured (profit factor, close-through → retest): SPY 5m 0.69 → 0.58 · QQQ 5m 0.44 → 0.67 · gold 15m 0.88 → 0.55.", display = display.none)
inBreakBuf = input.float(0.0, "Breakout buffer (× ATR beyond the edge)", minval = 0, maxval = 2, step = 0.05, group = gE, tooltip = "The close must be this far beyond the edge, in ATR. Measured (0.25, on the classic 1-close rule): SPY 5m 0.69 → 0.75 · QQQ 5m 0.44 → 0.39 · gold 15m 0.88 → 0.85.", display = display.none)
inRetestTol = input.float(0.25, "Retest tolerance (× ATR around the edge)", minval = 0, maxval = 2, step = 0.05, group = gE, display = display.none)
inRetestWait = input.int(12, "Retest must come within N bars", minval = 1, maxval = 200, group = gE, display = display.none)
inMaxTrades = input.int(1, "Trades per day", minval = 1, maxval = 3, group = gE, tooltip = "1 = the first breakout only (classic). 2 = after a stop-out the next breakout — either side — can fire again. Measured (2): SPY 5m 0.69 → 0.65 with 107 trades instead of 65.", display = display.none)
gB = "False-breakout defence (all off by default — each one measured)"
inConfirmBars = input.int(2, "Closes beyond the edge before entry", minval = 1, maxval = 5, group = gB, tooltip = "1 = the first close beyond the edge (the classic rule). 2 = the breakout must hold for a second consecutive close; a close back inside resets the count. Measured (1 → 2, profit factor): SPY 5m 0.69 → 0.80 · QQQ 5m 0.44 → 0.58 · gold 15m 0.88 → 1.08 — the one false-breakout defence that helped on all three charts, so 2 is the shipped default.", display = display.none)
inStrongClose = input.bool(false, "Strong close: the breakout bar must close in the top / bottom part of its range", group = gB, tooltip = "A breakout bar that closes near its own extreme shows conviction; one that closes mid-bar after poking through is the anatomy of a fake-out. Measured (30 %, on the classic 1-close rule): SPY 5m 0.69 → 0.67 · QQQ 5m 0.44 → 0.44 · gold 15m 0.88 → 0.93 — cosmetic.", display = display.none)
inStrongPct = input.int(30, " Top / bottom part of the bar (%)", minval = 10, maxval = 50, group = gB, display = display.none)
inFailExit = input.bool(false, "Exit when a bar closes back inside the range (failed breakout)", group = gB, tooltip = "The breakout is the idea; a close back inside the range is the idea failing. This closes the trade at that close instead of waiting for the far-side stop. Measured (on the classic 1-close rule): SPY 5m 0.69 → 0.51 (win rate 34 → 17 % — the ETFs chop around the edge and the exit throws away trades that would have worked) · QQQ 5m 0.44 → 0.40 with a third less net loss · gold 15m 0.88 → 1.10 with the drawdown halved. Where breakouts either work or fail cleanly (the New York gold open) it is the right defence; where they chop, use the confirmation closes instead.", display = display.none)
inFadeFail = input.bool(false, "Trade the failure: after a close back inside, enter the opposite way", group = gB, tooltip = "The failed-breakout fade: when a close beyond an edge is followed by a close back inside the range, enter against the failed side at that close, stop at the failed extreme plus the buffer, targets in R. Counts against the trades-per-day budget (set it to 2 to fade after your own stop-out). Measured (with the failed-breakout exit and 2 trades per day): SPY 5m 0.69 → 0.59 (123 trades) · QQQ 5m 0.44 → 0.52 · gold 15m 0.88 → 1.15 (128 trades).", display = display.none)
gX = "Exit"
inStopMode = input.string("Opposite side of the range", "Stop", options = ["Opposite side of the range", "Range midpoint", "ATR from entry"], group = gX, tooltip = "Measured (midpoint): SPY 5m 0.69 → 0.64, drawdown −22 %. The classic opposite-side stop is the default.", display = display.none)
inStopAtr = input.float(1.5, " ATR stop distance (× ATR)", minval = 0.3, maxval = 10, step = 0.1, group = gX, display = display.none)
inStopBuf = input.float(0.1, " Stop buffer (× ATR beyond the level)", minval = 0, maxval = 2, step = 0.05, group = gX, display = display.none)
inTpUnit = input.string("R (risk multiples)", "Targets measured in", options = ["R (risk multiples)", "Range heights"], group = gX, tooltip = "Measured (range heights): SPY 5m 0.69 → 0.81 · QQQ 5m 0.44 → 0.56 · gold 15m 0.88 → 0.74. With the volume gate too: QQQ 1.01.", display = display.none)
inTp1 = input.float(1.0, "TP1 → stop to breakeven", minval = 0.2, maxval = 10, step = 0.1, group = gX, display = display.none)
inTp2 = input.float(2.0, "TP2 final", minval = 0.3, maxval = 20, step = 0.1, group = gX, display = display.none)
inBeAtTp1 = input.bool(true, "Move the stop to breakeven at TP1", group = gX, tooltip = "Measured (off): SPY 5m 0.69 → 0.68, win rate 34 → 42 % — more small wins, same result.", display = display.none)
inTrailOn = input.bool(false, "Trailing stop after TP1 (ATR)", group = gX, tooltip = "After TP1 the stop trails N ATR behind price instead of parking at breakeven — the classic 'let the winner run into the close'. Measured (profit factor, base → switch): SPY 5m 0.69 → 0.82 (win rate 34 → 51 %) · QQQ 5m 0.44 → 0.54 · gold 15m 0.88 → 0.89 — the only switch that helped on all three charts, still not enough on the ETFs.", display = display.none)
inTrailAtr = input.float(1.5, " Trail distance (× ATR)", minval = 0.3, maxval = 10, step = 0.1, group = gX, display = display.none)
inTimeStop = input.string("", "Time stop — HH:MM in the timezone (blank = off): exit if TP1 not reached", group = gX, tooltip = "Classic ORB discipline: if the breakout has not paid its first target by this time, it is not the day — close it and stop waiting for the bell. Measured (12:00, profit factor): SPY 5m 0.69 → 0.55 · QQQ 5m 0.44 → 0.34 · gold 15m 0.88 → 1.09 with the drawdown cut by two thirds; with the VWAP + EMA gates too, gold 1.55. Right for the New York gold open, wrong for the index ETFs.", display = display.none)
gF = "Filters (all off by default — each one measured, numbers in its tooltip)"
inVwapOn = input.bool(false, "VWAP side: longs above VWAP, shorts below", group = gF, tooltip = "The breakout bar must close on the VWAP's side of the trade. Measured (profit factor): SPY 5m and QQQ 5m — changed nothing (a close beyond the opening range at the open is on the VWAP's side anyway); gold 15m 0.88 → 1.11, and 1.36 together with the EMA filter. On a 24-hour market the VWAP has a whole session behind it — that is where this gate bites.", display = display.none)
inEmaOn = input.bool(false, "EMA trend: longs above the EMA, shorts below", group = gF, tooltip = "Measured (profit factor): SPY 5m 0.69 → 0.53 · QQQ 5m 0.44 → 0.41 · gold 15m 0.88 → 1.27 (1.36 with the VWAP gate too). Helps where the open trends with the day, hurts where the open reverses it.", display = display.none)
inEmaLen = input.int(200, " EMA length", minval = 5, maxval = 500, group = gF, display = display.none)
inEmaTf = input.timeframe("", " EMA timeframe (blank = chart)", group = gF, tooltip = "A higher timeframe is read from its last CLOSED bar — no lookahead.", display = display.none)
inVolOn = input.bool(false, "Breakout bar volume above its average", group = gF, tooltip = "Measured (profit factor): SPY 5m 0.69 → 0.78 · QQQ 5m 0.44 → 0.93 (1.01 with range-height targets) · gold 15m 0.88 → 0.90. The most useful gate on the index ETFs, still not enough to make them pay this summer.", display = display.none)
inVolMult = input.float(1.2, " Volume must be ≥ N × its average", minval = 0.5, maxval = 5, step = 0.1, group = gF, display = display.none)
inVolLen = input.int(20, " Volume average length", minval = 5, maxval = 200, group = gF, display = display.none)
inGapOn = input.bool(false, "Gap direction: longs only after a gap up, shorts only after a gap down", group = gF, tooltip = "The day's open (the first bar of the range window) against the previous daily close. 'Gap and go' — trade the breakout only when it continues the gap. Measured (profit factor): SPY 5m 0.69 → 0.91 with the drawdown more than halved · QQQ 5m 0.44 → 0.65 · gold 15m 0.88 → 0.79 (a 24-hour market barely gaps — the gate has nothing to read there).", display = display.none)
gP = "Position size (display only)"
inRiskUsd = input.float(0, "Risk per trade in account currency (0 = off)", minval = 0, step = 10, group = gP, tooltip = "Shows the size that risks this amount between entry and stop — on the ticket, the trade card and the Co-Pilot plan. Display only; the strategy twin trades one unit.", display = display.none)
gV = "Style"
inTheme = input.string("Aurora Neon", "Theme", options = ["Aurora Neon", "Royal Gold", "Colorblind-safe"], group = gV, display = display.none)
inShowOr = input.string("All days", "Opening-range boxes", options = ["All days", "Today only", "Off"], group = gV, display = display.none)
inShowLvls = input.bool(true, "Breakout levels (dotted) while the day is live", group = gV, display = display.none)
inZones = input.bool(false, "Gradient risk / reward zones", group = gV, display = display.none)
inTint = input.bool(true, "Tint candles while a trade is open", group = gV, display = display.none)
inDrawVwap = input.bool(false, "Draw VWAP (auto when its filter is on)", group = gV, display = display.none)
inDrawEma = input.bool(false, "Draw the EMA (auto when its filter is on)", group = gV, display = display.none)
inRangeTag = input.bool(true, "Range tag (height vs daily ATR) on each box", group = gV, display = display.none)
inZoneKeep = input.string("Last trade only", "Keep risk / reward zones for", options = ["Last trade only", "All trades"], group = gV, tooltip = "Zones of older trades are removed when a new trade opens, so the chart stays readable; the tickets and exit tags keep the history.", display = display.none)
inBoxResult = input.bool(true, "Colour finished days by their result (green · red · grey)", group = gV, display = display.none)
inMidline = input.bool(true, "Range midline (dotted)", group = gV, display = display.none)
inShowFake = input.bool(true, "Mark fake-outs (a wick beyond the edge that closed back inside)", group = gV, display = display.none)
gD = "Dashboard"
inDash = input.bool(false, "Show the dashboard", group = gD, display = display.none)
inDashPos = input.string("Top Right", "Position", options = ["Top Right", "Top Left", "Bottom Right", "Bottom Left", "Middle Right", "Middle Left"], group = gD, display = display.none)
inSize = input.string("Medium", "Size", options = ["Small", "Medium", "Large"], group = gD, display = display.none)
gN = "Co-Pilot (narrative)"
inNarr = input.string("Off", "Narrative depth", options = ["Off", "Brief", "Standard", "Detailed"], group = gN, tooltip = "Brief = one sentence (NOW). Standard adds WHY and PLAN. Detailed adds the track record and a glossary.", display = display.none)
inNarrPos = input.string("Bottom Left", "Position", options = ["Bottom Left", "Bottom Right", "Top Left", "Top Right", "Middle Left", "Middle Right"], group = gN, display = display.none)
inNarrSize = input.string("Auto", "Size", options = ["Auto", "Small", "Medium", "Large"], group = gN, display = display.none)
inNarrWrap = input.int(64, "Line width (characters)", minval = 30, maxval = 140, group = gN, display = display.none)
gL = "Alerts"
inAlertJson = input.bool(true, "alert() payload as JSON (webhooks)", group = gL, tooltip = "Choose 'Any alert() function call' when creating the alert.", display = display.none)
// ─────────────────────────────────────────────────────────────────── THEME
type Theme
color bg = #0B0E14
color frame = #B8923A
color accent = #D8B14A
color accentHi = #E8C25A
color up = #2BE7C7
color down = #FF3B5C
color flat = #6B7280
color txt = #D7DCE6
color txtHi = #ECEFF5
themeOf(string name) =>
Theme t = Theme.new()
if name == "Aurora Neon"
t.bg := #0E1118
t.frame := #2C3A5A
t.accent := #7C9CFF
t.accentHi := #A9B8FF
t.up := #2AF0C8
t.down := #FF4D6D
t.flat := #5B6472
t.txt := #E6E9EF
t.txtHi := #F2F4F9
else if name == "Colorblind-safe"
t.bg := #0E1014
t.frame := #3A4150
t.accent := #56B4E9
t.accentHi := #8ED0F5
t.up := #E69F00
t.down := #0072B2
t.flat := #8C92A0
t.txt := #E8EAED
t.txtHi := #FFFFFF
t
Theme th = themeOf(inTheme)
fmt(float x) => str.tostring(x, format.mintick)
// ─────────────────────────────────────────────────────────────────── ENGINE (all ta.* at global scope)
float atr = ta.atr(14)
float atrU = math.max(atr, syminfo.mintick)
float vwap = ta.vwap(hlc3)
float emaC = ta.ema(close, inEmaLen)
emaFn() => ta.ema(close, inEmaLen)
float emaH = request.security(syminfo.tickerid, inEmaTf == "" ? timeframe.period : inEmaTf, emaFn()[1], lookahead = barmerge.lookahead_on)
float emaF = inEmaTf == "" ? emaC : emaH
float volMa = ta.sma(nz(volume, 0.0), inVolLen)
dAtrFn() => ta.atr(14)
float dAtr = request.security(syminfo.tickerid, "D", dAtrFn()[1], lookahead = barmerge.lookahead_on)
float dAtrU = math.max(nz(dAtr, atrU * 10), syminfo.mintick)
float pClose = request.security(syminfo.tickerid, "D", close[1], lookahead = barmerge.lookahead_on)
// clock helpers (all in the input timezone)
int nowMin = hour(time, inTz) * 60 + minute(time, inTz)
string hhmm = str.tostring(hour(time, inTz), "00") + ":" + str.tostring(minute(time, inTz), "00")
sessEndMin(string sess) =>
string e = str.length(sess) >= 9 ? str.substring(sess, 5, 9) : "0000"
int hh = int(nz(str.tonumber(str.substring(e, 0, 2)), 0))
int mm = int(nz(str.tonumber(str.substring(e, 2, 4)), 0))
hh * 60 + mm
clockMin(string hm) =>
array<string> parts = str.split(hm, ":")
array.size(parts) == 2 ? int(nz(str.tonumber(array.get(parts, 0)), -1)) * 60 + int(nz(str.tonumber(array.get(parts, 1)), 0)) : -1
int winEndMin = sessEndMin(inTradeSess)
int minsLeft = winEndMin - nowMin
string leftTxt = minsLeft <= 0 ? "closing" : (minsLeft >= 60 ? str.tostring(math.floor(minsLeft / 60)) + " h " : "") + str.tostring(minsLeft % 60) + " m left"
int timeStopMin = clockMin(inTimeStop)
int lastEntryMin = clockMin(inLastEntry)
bool timeStopNow = timeStopMin >= 0 and nowMin >= timeStopMin
bool inRange = not na(time(timeframe.period, inRangeSess, inTz))
bool inTrade = not na(time(timeframe.period, inTradeSess, inTz))
bool rangeStart = inRange and not inRange[1]
bool rangeEnd = not inRange and inRange[1]
bool windowEnd = not inTrade and inTrade[1]
bool entryOpen = inTrade and (lastEntryMin < 0 or nowMin <= lastEntryMin)
bool volOk = not inVolOn or (not na(volume) and volume >= inVolMult * volMa)
bool vwapUp = not inVwapOn or close > vwap
bool vwapDn = not inVwapOn or close < vwap
bool emaUp = not inEmaOn or close > emaF
bool emaDn = not inEmaOn or close < emaF
// ─────────────────────────────────────────────────────────────────── STATE
type Day
float orH = na
float orL = na
bool set = false
int setBar = na
int startBar = na
int trades = 0
int brk = 0 // retest mode: side of the close-through we are waiting on
int brkBar = na
bool touched = false
string skip = "" // "" = tradable day; otherwise the reason
float todayR = 0.0
int dayNo = 0
bool done = false
float openPx = na
string log = ""
int cntUp = 0
int cntDn = 0
int lastBrk = 0
float brkExt = na
bool fakeUpLogged = false
bool fakeDnLogged = false
type Trade
int dir = 0
float entry = na
float sl = na
float sl0 = na
float r = na
float tp1 = na
float tp2 = na
int bar = na
bool tp1Hit = false
bool retest = false
bool fade = false
float lastR = na
string lastWhy = ""
type Stat
int n = 0
int wins = 0
int flat = 0
float sumR = 0.0
int nTp = 0
int nStop = 0
int nBe = 0
int nEod = 0
int days = 0
int daysTraded = 0
int daysSkipped = 0
int nTime = 0
int nFail = 0
int nFake = 0
type Ev
bool sigL = false
bool sigS = false
bool exit = false
bool tp1 = false
bool rangeSet = false
bool brkL = false
bool brkS = false
bool retestFail = false
bool fake = false
string why = ""
float r = na
int dir = 0
var Day dy = Day.new()
var Trade t = Trade.new()
var Stat st = Stat.new()
Ev ev = Ev.new()
bool gapUp = not na(dy.openPx) and dy.openPx > pClose
bool gapDn = not na(dy.openPx) and dy.openPx < pClose
bool gapOkL = not inGapOn or gapUp
bool gapOkS = not inGapOn or gapDn
logEv(string what) =>
dy.log := dy.log + (dy.log == "" ? "" : " · ") + hhmm + " " + what
closeTrade(float px, string why) =>
float rr = (px - t.entry) * t.dir / t.r
ev.exit := true
ev.why := why
ev.r := rr
ev.dir := t.dir
st.n := st.n + 1
st.wins := st.wins + (rr >= 0.05 ? 1 : 0)
st.flat := st.flat + (math.abs(rr) < 0.05 ? 1 : 0)
st.sumR := st.sumR + rr
if why == "TP2"
st.nTp := st.nTp + 1
else if why == "EOD"
st.nEod := st.nEod + 1
else if why == "BE"
st.nBe := st.nBe + 1
else if why == "TIME"
st.nTime := st.nTime + 1
else if why == "FAIL"
st.nFail := st.nFail + 1
else
st.nStop := st.nStop + 1
dy.todayR := dy.todayR + rr
logEv((why == "TP2" ? "TP2 " : why == "EOD" ? "flat " : why == "TIME" ? "time stop " : why == "BE" ? "breakeven " : why == "TRAIL" ? "trail " : why == "FAIL" ? "failed " : "stop ") + (rr >= 0 ? "+" : "") + str.tostring(rr, "0.0") + "R")
t.lastR := rr
t.lastWhy := why
t.dir := 0
openTrade(int d, bool viaRetest, float stopOverride) =>
float rangeH = dy.orH - dy.orL
float base = inStopMode == "Range midpoint" ? (dy.orH + dy.orL) / 2 : inStopMode == "ATR from entry" ? (d == 1 ? close - inStopAtr * atrU : close + inStopAtr * atrU) : (d == 1 ? dy.orL : dy.orH)
float anchor = na(stopOverride) ? base : stopOverride
float slPx = d == 1 ? math.min(anchor - inStopBuf * atrU, close - syminfo.mintick) : math.max(anchor + inStopBuf * atrU, close + syminfo.mintick)
float unit = inTpUnit == "Range heights" ? math.max(rangeH, syminfo.mintick) : math.abs(close - slPx)
t.dir := d
t.entry := close
t.sl := slPx
t.sl0 := slPx
t.r := math.abs(close - slPx)
t.tp1 := close + d * inTp1 * unit
t.tp2 := close + d * inTp2 * unit
t.bar := bar_index
t.tp1Hit := false
t.retest := viaRetest
t.fade := not na(stopOverride)
dy.trades := dy.trades + 1
logEv((d == 1 ? "▲ long " : "▼ short ") + fmt(close) + (viaRetest ? " (retest)" : t.fade ? " (fade)" : ""))
if dy.trades == 1
st.daysTraded := st.daysTraded + 1
if d == 1
ev.sigL := true
else
ev.sigS := true
if barstate.isconfirmed and not na(atr)
// ── the day
if rangeStart
dy.orH := high
dy.orL := low
dy.set := false
dy.setBar := na
dy.startBar := bar_index
dy.trades := 0
dy.brk := 0
dy.brkBar := na
dy.touched := false
dy.skip := ""
dy.todayR := 0.0
dy.dayNo := dy.dayNo + 1
dy.done := false
dy.openPx := open
dy.log := ""
dy.cntUp := 0
dy.cntDn := 0
dy.lastBrk := 0
dy.brkExt := na
dy.fakeUpLogged := false
dy.fakeDnLogged := false
st.days := st.days + 1
else if inRange
dy.orH := math.max(nz(dy.orH, high), high)
dy.orL := math.min(nz(dy.orL, low), low)
if rangeEnd and not dy.set and not na(dy.orH)
dy.set := true
dy.setBar := bar_index
ev.rangeSet := true
float rAtr = (dy.orH - dy.orL) / dAtrU
if inMinRange > 0 and rAtr < inMinRange
dy.skip := "range " + str.tostring(rAtr, "0.00") + " × daily ATR — too small (< " + str.tostring(inMinRange, "0.00") + ")"
else if inMaxRange > 0 and rAtr > inMaxRange
dy.skip := "range " + str.tostring(rAtr, "0.00") + " × daily ATR — too large (> " + str.tostring(inMaxRange, "0.00") + ")"
if dy.skip != ""
st.daysSkipped := st.daysSkipped + 1
logEv("range set" + (dy.skip != "" ? " · skipped" : ""))
// ── manage an open trade (stop first, then TP2, TP1, end of window)
if t.dir != 0
int d = t.dir
bool stopHit = d == 1 ? low <= t.sl : high >= t.sl
if stopHit
closeTrade(t.sl, t.tp1Hit and t.sl == t.entry ? "BE" : t.tp1Hit and inTrailOn and t.sl != t.sl0 ? "TRAIL" : "SL")
else if d == 1 ? high >= t.tp2 : low <= t.tp2
closeTrade(t.tp2, "TP2")
else
if not t.tp1Hit and (d == 1 ? high >= t.tp1 : low <= t.tp1)
t.tp1Hit := true
ev.tp1 := true
logEv("TP1")
if inBeAtTp1
t.sl := t.entry
if inFailExit and not t.fade and t.dir != 0 and (d == 1 ? close < dy.orH : close > dy.orL)
closeTrade(close, "FAIL")
if inTrailOn and t.tp1Hit and t.dir != 0
float trailPx = d == 1 ? close - inTrailAtr * atrU : close + inTrailAtr * atrU
t.sl := d == 1 ? math.max(t.sl, trailPx) : math.min(t.sl, trailPx)
if timeStopNow and not t.tp1Hit and t.dir != 0
closeTrade(close, "TIME")
if inEodFlat and windowEnd and t.dir != 0
closeTrade(close, "EOD")
// ── breakout bookkeeping (every confirmed bar once the range is set): consecutive closes beyond an edge,
// the extreme reached since the last break, fake-outs (a wick beyond the edge that closed back inside)
bool failUp = false
bool failDn = false
if dy.set
float upLvlB = dy.orH + inBreakBuf * atrU
float dnLvlB = dy.orL - inBreakBuf * atrU
bool upRaw = close > upLvlB
bool dnRaw = close < dnLvlB
dy.cntUp := upRaw ? dy.cntUp + 1 : 0
dy.cntDn := dnRaw ? dy.cntDn + 1 : 0
failUp := dy.lastBrk == 1 and not upRaw and close < dy.orH
failDn := dy.lastBrk == -1 and not dnRaw and close > dy.orL
if upRaw
dy.brkExt := dy.lastBrk == 1 ? math.max(nz(dy.brkExt, high), high) : high
dy.lastBrk := 1
else if dnRaw
dy.brkExt := dy.lastBrk == -1 ? math.min(nz(dy.brkExt, low), low) : low
dy.lastBrk := -1
if t.dir == 0 and not upRaw and not dnRaw and dy.lastBrk == 0 and (high > dy.orH or low < dy.orL)
ev.fake := true
st.nFake := st.nFake + 1
bool fakeAbv = high > dy.orH
if fakeAbv and not dy.fakeUpLogged
dy.fakeUpLogged := true
logEv("fake-out ▲")
else if not fakeAbv and not dy.fakeDnLogged
dy.fakeDnLogged := true
logEv("fake-out ▼")
// ── signals (only when flat, the range is set, inside the trade window, on a tradable day)
if t.dir == 0 and dy.set and entryOpen and dy.skip == "" and dy.trades < inMaxTrades
float upLvl = dy.orH + inBreakBuf * atrU
float dnLvl = dy.orL - inBreakBuf * atrU
float barRange = math.max(high - low, syminfo.mintick)
bool strongUp = not inStrongClose or (close - low) / barRange >= 1 - inStrongPct / 100.0
bool strongDn = not inStrongClose or (high - close) / barRange >= 1 - inStrongPct / 100.0
bool brkUp = close > upLvl and dy.cntUp >= inConfirmBars and strongUp
bool brkDn = close < dnLvl and dy.cntDn >= inConfirmBars and strongDn
bool longOk = vwapUp and emaUp and volOk and gapOkL
bool shortOk = vwapDn and emaDn and volOk and gapOkS
if inFadeFail and ((failUp and shortOk) or (failDn and longOk))
openTrade(failUp ? -1 : 1, false, dy.brkExt)
dy.lastBrk := 0
else if inEntryMode == "Close through"
if brkUp and longOk
openTrade(1, false, na)
else if brkDn and shortOk
openTrade(-1, false, na)
else
if dy.brk == 0
if brkUp and longOk
dy.brk := 1
dy.brkBar := bar_index
dy.touched := false
ev.brkL := true
logEv("broke out ▲")
else if brkDn and shortOk
dy.brk := -1
dy.brkBar := bar_index
dy.touched := false
ev.brkS := true
logEv("broke out ▼")
else
int bd = dy.brk
float edge = bd == 1 ? dy.orH : dy.orL
float tol = inRetestTol * atrU
if bar_index - dy.brkBar > inRetestWait
dy.brk := 0
ev.retestFail := true
ev.why := "retest expired"
logEv("no retest")
else if bd == 1 ? close < edge - tol : close > edge + tol
dy.brk := 0
ev.retestFail := true
ev.why := "closed back inside the range"
logEv("back inside")
else if bar_index > dy.brkBar
if bd == 1 ? low <= edge + tol : high >= edge - tol
dy.touched := true
if dy.touched and (bd == 1 ? close > edge : close < edge)
openTrade(bd, true, na)
dy.brk := 0
if failUp or failDn
dy.lastBrk := 0
if dy.set and t.dir == 0 and (dy.trades >= inMaxTrades or (not inTrade and dy.trades > 0))
dy.done := true
// ─────────────────────────────────────────────────────────────────── READINESS (display only)
bool flatNow = t.dir == 0
float rangeH = dy.set ? dy.orH - dy.orL : na
float rangeAtr = dy.set ? rangeH / dAtrU : na
float upLvlD = dy.set ? dy.orH + inBreakBuf * atrU : na
float dnLvlD = dy.set ? dy.orL - inBreakBuf * atrU : na
bool s1 = dy.set
bool s2 = entryOpen
bool s3 = dy.skip == ""
bool fVwapL = vwapUp
bool fVwapS = vwapDn
bool fEmaL = emaUp
bool fEmaS = emaDn
bool filtersOn = inVwapOn or inEmaOn or inVolOn or inGapOn
bool s4L = fVwapL and fEmaL and gapOkL
bool s4S = fVwapS and fEmaS and gapOkS
bool s5 = dy.trades < inMaxTrades
int side = filtersOn and (s4L != s4S) ? (s4L ? 1 : -1) : close >= (dy.set ? (dy.orH + dy.orL) / 2 : close) ? 1 : -1 // the side the filters allow, else the nearer edge
string sideWord = side == 1 ? "long" : "short"
string s1Txt = dy.set ? fmt(dy.orL) + " – " + fmt(dy.orH) + " · " + str.tostring(rangeAtr, "0.00") + " × daily ATR" : inRange ? "forming — " + (na(dy.orH) ? "first bar" : fmt(dy.orL) + " – " + fmt(dy.orH) + " so far") : "waiting for " + inRangeSess + " (" + inTz + ")"
string s2Txt = entryOpen ? "inside " + inTradeSess + (lastEntryMin >= 0 ? " · entries until " + inLastEntry : "") + " · " + leftTxt : inTrade ? "past the latest entry time " + inLastEntry + " · " + leftTxt : "outside " + inTradeSess + (dy.set ? " · no new entries" : "")
string s3Txt = dy.skip == "" ? ((inMinRange > 0 or inMaxRange > 0) ? "range size passes" : "no size gate") : dy.skip
string fTxt = (inVwapOn ? "VWAP " + (close > vwap ? "below price" : "above price") : "") + (inEmaOn ? (inVwapOn ? " · " : "") + "EMA " + (close > emaF ? "below price" : "above price") : "") + (inVolOn ? ((inVwapOn or inEmaOn) ? " · " : "") + "volume " + str.tostring(nz(volume) / math.max(volMa, 1e-9), "0.0") + "× avg" : "") + (inGapOn ? ((inVwapOn or inEmaOn or inVolOn) ? " · " : "") + "gap " + (gapUp ? "up" : gapDn ? "down" : "flat") : "")
string s4Txt = filtersOn ? fTxt + " → " + (s4L and s4S ? "either side" : s4L ? "longs only" : s4S ? "shorts only" : "no side passes") : "no filters on — either side"
string brkTxt = not dy.set ? "needs the range first" : dy.brk != 0 ? (dy.brk == 1 ? "broke out ▲ · retest of " + fmt(dy.orH) : "broke out ▼ · retest of " + fmt(dy.orL)) + (dy.touched ? " · touched" : "") + " · " + str.tostring(math.max(0, inRetestWait - (bar_index - nz(dy.brkBar, bar_index)))) + " bars left" : "long > " + fmt(upLvlD) + " · short < " + fmt(dnLvlD) + (close > dy.orH ? " · price above" + (inConfirmBars > 1 and not dy.done ? " · " + str.tostring(math.min(dy.cntUp, inConfirmBars)) + "/" + str.tostring(inConfirmBars) + " closes" : "") : close < dy.orL ? " · price below" + (inConfirmBars > 1 and not dy.done ? " · " + str.tostring(math.min(dy.cntDn, inConfirmBars)) + "/" + str.tostring(inConfirmBars) + " closes" : "") : " · price inside")
bool allPre = s1 and s2 and s3 and s5 and (s4L or s4S)
string missing = not flatNow ? "in a trade" : dy.done ? "done for today (" + str.tostring(dy.trades) + " of " + str.tostring(inMaxTrades) + " trades)" : not s1 ? "the range" : not s2 ? "the trade window" : not s3 ? "a tradable range size" : not (s4L or s4S) ? "a filter on either side" : inEntryMode == "Retest" and dy.brk != 0 ? "the retest" : "the breakout close"
string stateWord = t.dir == 1 ? "LONG OPEN" : t.dir == -1 ? "SHORT OPEN" : not dy.set ? (inRange ? "RANGE FORMING" : "WAITING FOR THE RANGE") : dy.skip != "" ? "DAY SKIPPED" : dy.done ? "DONE FOR TODAY" : not inTrade ? "OUTSIDE THE WINDOW" : not entryOpen ? "PAST THE ENTRY CUT-OFF" : dy.brk != 0 ? (dy.brk == 1 ? "BROKE OUT ▲ · RETEST?" : "BROKE OUT ▼ · RETEST?") : "RANGE SET · WATCHING"
color stateCol = t.dir == 1 ? th.up : t.dir == -1 ? th.down : dy.brk == 1 ? th.up : dy.brk == -1 ? th.down : dy.set and entryOpen and dy.skip == "" and not dy.done ? th.accentHi : th.flat
float openR = t.dir != 0 ? (close - t.entry) * t.dir / t.r : na
string sizeTxt = inRiskUsd > 0 and t.dir != 0 and t.r > 0 ? str.tostring(math.floor(inRiskUsd / t.r)) + (syminfo.type == "stock" or syminfo.type == "fund" ? " shares" : " units") + " for " + str.tostring(inRiskUsd, "#") + " risk" : ""
string chipTxt = t.dir != 0 ? (openR >= 0 ? "+" : "") + str.tostring(openR, "0.0") + "R now" + (t.tp1Hit ? " · stop at breakeven" : "") : dy.trades > 0 ? "today " + (dy.todayR >= 0 ? "+" : "") + str.tostring(dy.todayR, "0.0") + "R" : dy.set ? str.tostring(bar_index - nz(dy.setBar, bar_index)) + " bars since the range" : "day " + str.tostring(dy.dayNo)
// ─────────────────────────────────────────────────────────────────── CHART · range boxes, levels, trade levels
var array<box> orBoxes = array.new<box>()
var array<line> orLines = array.new<line>()
var array<label> orTags = array.new<label>()
var box curBox = na
var box curExt = na
var line curHi = na
var line curLo = na
var line curMid = na
var label curTag = na
color rangeCol = dy.skip != "" ? th.flat : th.accent
if barstate.isconfirmed
if rangeEnd and dy.set and inShowOr != "Off"
if inShowOr == "Today only"
for b in orBoxes
box.delete(b)
array.clear(orBoxes)
for l in orLines
line.delete(l)
array.clear(orLines)
for g in orTags
label.delete(g)
array.clear(orTags)
int x1 = math.max(nz(dy.startBar, bar_index - 1), bar_index - 250)
float midPx = (dy.orH + dy.orL) / 2
curBox := box.new(x1, dy.orH, bar_index, dy.orL, border_color = color.new(rangeCol, 100), border_width = 1, bgcolor = color.new(rangeCol, 82))
curExt := box.new(bar_index, dy.orH, bar_index, dy.orL, border_color = color.new(rangeCol, 100), border_width = 1, bgcolor = color.new(rangeCol, 93))
curHi := line.new(x1, dy.orH, bar_index, dy.orH, color = color.new(rangeCol, 15), width = 2)
curLo := line.new(x1, dy.orL, bar_index, dy.orL, color = color.new(rangeCol, 15), width = 2)
curMid := line.new(x1, midPx, bar_index, midPx, color = color.new(rangeCol, inMidline ? 60 : 100), width = 1, style = line.style_dotted)
array.push(orBoxes, curBox)
array.push(orBoxes, curExt)
array.push(orLines, curHi)
array.push(orLines, curLo)
array.push(orLines, curMid)
if inRangeTag
curTag := label.new(bar_index, dy.orH, "OR " + str.tostring(rangeAtr, "0.00") + "× dATR" + (dy.skip != "" ? " · skipped" : ""), style = label.style_label_lower_left, color = color.new(th.bg, 20), textcolor = dy.skip != "" ? th.flat : th.accent, size = size.tiny)
array.push(orTags, curTag)
while array.size(orBoxes) > 120
box.delete(array.shift(orBoxes))
while array.size(orLines) > 180
line.delete(array.shift(orLines))
while array.size(orTags) > 60
label.delete(array.shift(orTags))
else if dy.set and inTrade and not na(curExt)
box.set_right(curExt, bar_index)
line.set_x2(curHi, bar_index)
line.set_x2(curLo, bar_index)
line.set_x2(curMid, bar_index)
if inBoxResult and not na(curBox) and dy.set and t.dir == 0 and (dy.done or (not inTrade and inTrade[1]))
color resCol = dy.trades == 0 ? th.flat : dy.todayR > 0.05 ? th.up : dy.todayR < -0.05 ? th.down : th.flat
box.set_bgcolor(curBox, color.new(resCol, 86))
box.set_bgcolor(curExt, color.new(resCol, 94))
line.set_color(curHi, color.new(resCol, 45))
line.set_color(curLo, color.new(resCol, 45))
line.set_width(curHi, 1)
line.set_width(curLo, 1)
line.set_color(curMid, color.new(resCol, inMidline ? 75 : 100))
// breakout levels (dotted) while the day is live and no trade is open
bool lvlOn = inShowLvls and dy.set and inTrade and t.dir == 0 and dy.skip == "" and not dy.done
plot(lvlOn ? upLvlD : na, "Breakout level · long", color = color.new(th.up, 45), linewidth = 1, style = plot.style_circles, display = display.pane)
plot(lvlOn ? dnLvlD : na, "Breakout level · short", color = color.new(th.down, 45), linewidth = 1, style = plot.style_circles, display = display.pane)
// filters
plot(inVwapOn or inDrawVwap ? vwap : na, "VWAP", color = color.new(close > vwap ? th.up : th.down, 35), linewidth = 1, display = display.pane)
plot(inEmaOn or inDrawEma ? emaF : na, "EMA", color = color.new(th.accent, 40), linewidth = 1, display = display.pane)
// trade levels
bool inT = t.dir != 0
plot(inT ? t.entry : na, "Entry", color = color.new(th.accentHi, 20), linewidth = 1, style = plot.style_linebr, display = display.pane)
plot(inT ? t.sl : na, "Stop", color = color.new(th.down, 20), linewidth = 1, style = plot.style_linebr, display = display.pane)
plot(inT ? t.tp1 : na, "TP1", color = color.new(th.up, 55), linewidth = 1, style = plot.style_linebr, display = display.pane)
plot(inT ? t.tp2 : na, "TP2", color = color.new(th.up, 20), linewidth = 1, style = plot.style_linebr, display = display.pane)
barcolor(inTint and inT ? color.new(t.dir == 1 ? th.up : th.down, 15) : na, title = "Trade tint")
// gradient risk / reward zones: four bands each, densest at the entry, extended while the trade is open
var array<box> zones = array.new<box>()
var array<box> liveZ = array.new<box>()
gradBand(float top, float bot, color c, int x1, int x2, bool denseTop) =>
float h = (top - bot) / 4
for k = 0 to 3
float bTop = top - h * k
float bBot = bTop - h
int alpha = denseTop ? 88 + k * 2 : 96 - k * 2
box b = box.new(x1, bTop, x2, bBot, border_color = color.new(c, 100), bgcolor = color.new(c, alpha))
array.push(zones, b)
array.push(liveZ, b)
if barstate.isconfirmed
if (ev.sigL or ev.sigS) and inZones
if inZoneKeep == "Last trade only"
for b in zones
box.delete(b)
array.clear(zones)
array.clear(liveZ)
int d = t.dir
gradBand(math.max(t.entry, t.sl), math.min(t.entry, t.sl), th.down, bar_index, bar_index + 1, d == 1)
gradBand(math.max(t.entry, t.tp2), math.min(t.entry, t.tp2), th.up, bar_index, bar_index + 1, d == -1)
while array.size(zones) > 96
box.delete(array.shift(zones))
else if (t.dir != 0 or ev.exit) and array.size(liveZ) > 0
for b in liveZ
box.set_right(b, bar_index)
// event labels
if ev.sigL
label.new(bar_index, low, "ORB ▲ " + fmt(t.entry) + (t.retest ? " · retest" : t.fade ? " · fade" : "") + "\nstop " + fmt(t.sl) + " · TP2 " + fmt(t.tp2) + (sizeTxt != "" ? "\n" + sizeTxt : ""), style = label.style_label_up, color = color.new(th.up, 12), textcolor = th.bg, size = size.small,
tooltip = "Long: a bar closed above the opening range" + (t.retest ? " and price came back to the edge and closed above it again" : "") + ". Stop " + fmt(t.sl) + " · TP1 " + fmt(t.tp1) + " · TP2 " + fmt(t.tp2))
if ev.sigS
label.new(bar_index, high, "ORB ▼ " + fmt(t.entry) + (t.retest ? " · retest" : t.fade ? " · fade" : "") + "\nstop " + fmt(t.sl) + " · TP2 " + fmt(t.tp2) + (sizeTxt != "" ? "\n" + sizeTxt : ""), style = label.style_label_down, color = color.new(th.down, 12), textcolor = th.bg, size = size.small,
tooltip = "Short: a bar closed below the opening range" + (t.retest ? " and price came back to the edge and closed below it again" : "") + ". Stop " + fmt(t.sl) + " · TP1 " + fmt(t.tp1) + " · TP2 " + fmt(t.tp2))
if ev.brkL
label.new(bar_index, high, "broke out ▲ · waiting for the retest", style = label.style_label_down, color = color.new(th.bg, 15), textcolor = th.up, size = size.tiny)
if ev.brkS
label.new(bar_index, low, "broke out ▼ · waiting for the retest", style = label.style_label_up, color = color.new(th.bg, 15), textcolor = th.down, size = size.tiny)
if ev.retestFail
label.new(bar_index, high, "no retest · " + ev.why, style = label.style_label_down, color = color.new(th.bg, 15), textcolor = th.flat, size = size.tiny)
if ev.fake and inShowFake
bool fakeAbove = high > dy.orH
label.new(bar_index, fakeAbove ? high : low, "✕", style = fakeAbove ? label.style_label_down : label.style_label_up, color = color.new(th.bg, 100), textcolor = color.new(th.flat, 25), size = size.tiny, tooltip = "Fake-out: the bar poked beyond the range and closed back inside")
if ev.exit
string rTxt = (ev.r >= 0 ? "+" : "") + str.tostring(ev.r, "0.0") + "R"
string whyTxt = ev.why == "TP2" ? "target" : ev.why == "EOD" ? "flat at the close" : ev.why == "BE" ? "breakeven" : ev.why == "TIME" ? "time stop" : ev.why == "TRAIL" ? "trail" : ev.why == "FAIL" ? "failed breakout" : "stop"
label.new(bar_index, ev.dir == 1 ? high : low, (ev.dir == 1 ? "exit long · " : "exit short · ") + whyTxt + " " + rTxt, style = ev.dir == 1 ? label.style_label_down : label.style_label_up, color = color.new(th.bg, 15), textcolor = ev.r >= 0 ? th.up : th.down, size = size.tiny)
// ─────────────────────────────────────────────────────────────────── DASHBOARD
posOf(string p) =>
p == "Top Left" ? position.top_left : p == "Bottom Right" ? position.bottom_right : p == "Bottom Left" ? position.bottom_left : p == "Middle Right" ? position.middle_right : p == "Middle Left" ? position.middle_left : position.top_right
szLead() => inSize == "Small" ? size.small : inSize == "Large" ? size.large : size.normal
szBody() => inSize == "Small" ? size.tiny : inSize == "Large" ? size.normal : size.small
szSub() => inSize == "Small" ? size.tiny : inSize == "Large" ? size.small : size.tiny
row3(table tb, int r, string k, bool ok, string why, bool showMark) =>
color stripe = r % 2 == 1 ? color.new(th.accent, 94) : th.bg
table.cell(tb, 0, r, k, text_color = th.flat, text_size = szBody(), text_halign = text.align_left, bgcolor = stripe)
table.cell(tb, 1, r, showMark ? (ok ? "✓" : "✗") : "", text_color = ok ? th.up : th.down, text_size = szBody(), text_halign = text.align_center, bgcolor = stripe)
table.cell(tb, 2, r, why, text_color = ok ? th.txt : th.txtHi, text_size = szBody(), text_halign = text.align_left, text_font_family = font.family_monospace, bgcolor = stripe)
section(table tb, int r, string title) =>
table.cell(tb, 0, r, title, text_color = th.accent, text_size = szSub(), text_halign = text.align_left, bgcolor = color.new(th.accent, 90))
table.cell(tb, 1, r, "", bgcolor = color.new(th.accent, 90))
table.cell(tb, 2, r, "", bgcolor = color.new(th.accent, 90))
table.merge_cells(tb, 0, r, 2, r)
int readyN = (s1 ? 1 : 0) + (s2 ? 1 : 0) + (s3 ? 1 : 0) + ((s4L or s4S) ? 1 : 0) + (s5 ? 1 : 0)
var table dash = table.new(posOf(inDashPos), 3, 20, bgcolor = th.bg, frame_color = th.frame, frame_width = 1, border_width = 0, force_overlay = false)
if barstate.islast and inDash
table.clear(dash, 0, 0, 2, 19)
int r = 0
table.cell(dash, 0, r, "OPENING RANGE DESK", text_color = th.accentHi, text_size = szLead(), text_halign = text.align_left, bgcolor = th.bg, tooltip = "Opening range breakout: the range window, the trade window, the filters, the breakout — as a checklist, in order. Everything commits on the closed bar.")
table.cell(dash, 1, r, "", bgcolor = th.bg)
table.cell(dash, 2, r, syminfo.ticker + " · " + timeframe.period, text_color = th.accent, text_size = szSub(), text_halign = text.align_right, bgcolor = th.bg)
r += 1
table.cell(dash, 0, r, (t.dir == 1 ? "▲ " : t.dir == -1 ? "▼ " : "● ") + stateWord, text_color = stateCol, text_size = szLead(), text_halign = text.align_left, bgcolor = th.bg, tooltip = "Where today stands: range forming → range set → breakout (or retest) → trade → done.")
table.cell(dash, 1, r, "", bgcolor = th.bg)
color chipBg = t.dir != 0 ? color.new(openR >= 0 ? th.up : th.down, 75) : dy.trades > 0 ? color.new(dy.todayR >= 0 ? th.up : th.down, 75) : color.new(color.from_gradient(readyN, 0, 5, th.down, th.up), 75)
table.cell(dash, 2, r, chipTxt, text_color = th.txtHi, text_size = szBody(), text_halign = text.align_right, bgcolor = chipBg)
r += 1
if t.dir == 0
section(dash, r, "CHECKLIST · " + str.tostring(readyN) + " OF 5 STEPS")
r += 1
row3(dash, r, "1 · Range", s1, s1Txt, true)
r += 1
row3(dash, r, "2 · Window", s2, s2Txt, true)
r += 1
row3(dash, r, "3 · Range size", s3, s3Txt, true)
r += 1
row3(dash, r, "4 · Filters", s4L or s4S, s4Txt, true)
r += 1
row3(dash, r, "5 · Budget", s5, str.tostring(dy.trades) + " of " + str.tostring(inMaxTrades) + " trades today" + (dy.trades > 0 ? " · " + (dy.todayR >= 0 ? "+" : "") + str.tostring(dy.todayR, "0.0") + "R" : ""), true)
r += 1
row3(dash, r, "Breakout", dy.brk != 0 or (dy.set and (close > dy.orH or close < dy.orL)), brkTxt, false)
r += 1
row3(dash, r, "Missing", allPre and not dy.done, missing, false)
r += 1
if dy.log != ""
row3(dash, r, "Today", true, dy.log, false)
r += 1
else
section(dash, r, "TRADE · " + (t.dir == 1 ? "LONG" : "SHORT") + (t.retest ? " · RETEST" : " · CLOSE-THROUGH") + " · " + str.tostring(bar_index - t.bar) + " BARS")
r += 1
row3(dash, r, "Entry", true, fmt(t.entry) + " · 1R = " + fmt(t.r), false)
r += 1
row3(dash, r, "Stop", true, fmt(t.sl) + (t.tp1Hit and t.sl == t.entry ? " · breakeven" : inStopMode == "Range midpoint" ? " · range midpoint" : inStopMode == "ATR from entry" ? " · ATR stop" : " · other side of the range"), false)
r += 1
row3(dash, r, "Targets", t.tp1Hit, "TP1 " + fmt(t.tp1) + (t.tp1Hit ? " ✓" : "") + " · TP2 " + fmt(t.tp2) + (inTpUnit == "Range heights" ? " (range heights)" : ""), false)
r += 1
row3(dash, r, "Exit rule", true, "stop or TP2" + (inFailExit and not t.fade ? " · out if a bar closes back inside" : "") + (inTrailOn ? " · trail " + str.tostring(inTrailAtr, "0.0") + " ATR after TP1" : "") + (timeStopMin >= 0 ? " · time stop " + inTimeStop : "") + (inEodFlat ? " · flat after " + inTradeSess : " · may hold overnight"), false)
r += 1
if sizeTxt != ""
row3(dash, r, "Size", true, sizeTxt + " · " + fmt(t.r) + " per unit", false)
r += 1
if dy.log != ""
row3(dash, r, "Today", true, dy.log, false)
r += 1
row3(dash, r, "Range", true, fmt(dy.orL) + " – " + fmt(dy.orH) + " · " + str.tostring(rangeAtr, "0.00") + " × daily ATR", false)
r += 1
section(dash, r, "RECORD · PAPER, NO COSTS")
r += 1
row3(dash, r, "Days", true, str.tostring(st.days) + " seen · " + str.tostring(st.daysTraded) + " traded" + (st.daysSkipped > 0 ? " · " + str.tostring(st.daysSkipped) + " skipped" : "") + (st.nFake > 0 ? " · " + str.tostring(st.nFake) + " fake-outs" : ""), false)
r += 1
row3(dash, r, "Closed", st.n > 0 and st.sumR >= 0, st.n == 0 ? "no closed trades yet" : str.tostring(st.n) + " · " + str.tostring(math.round(st.wins / st.n * 100)) + "% win · " + str.tostring(st.flat) + " flat · avg " + (st.sumR / st.n >= 0 ? "+" : "") + str.tostring(st.sumR / st.n, "0.00") + "R", false)
r += 1
row3(dash, r, "How they ended", true, "TP2 " + str.tostring(st.nTp) + " · stop " + str.tostring(st.nStop) + " · BE " + str.tostring(st.nBe) + " · EOD " + str.tostring(st.nEod) + (st.nTime > 0 ? " · time " + str.tostring(st.nTime) : "") + (st.nFail > 0 ? " · failed " + str.tostring(st.nFail) : "") + (na(t.lastR) ? "" : " · last " + (t.lastR >= 0 ? "+" : "") + str.tostring(t.lastR, "0.0") + "R"), false)
r += 1
string fSum = (inVwapOn ? "VWAP · " : "") + (inEmaOn ? "EMA " + str.tostring(inEmaLen) + (inEmaTf != "" ? "@" + inEmaTf : "") + " · " : "") + (inVolOn ? "volume · " : "") + (inGapOn ? "gap · " : "") + (inMinRange > 0 or inMaxRange > 0 ? "range size · " : "") + (inTrailOn ? "trail · " : "") + (timeStopMin >= 0 ? "time " + inTimeStop + " · " : "") + (inConfirmBars > 1 ? str.tostring(inConfirmBars) + " closes · " : "") + (inStrongClose ? "strong close · " : "") + (inFailExit ? "fail-exit · " : "") + (inFadeFail ? "fade · " : "") + (lastEntryMin >= 0 ? "entries until " + inLastEntry + " · " : "")
table.cell(dash, 0, r, "OR " + inRangeSess + " · " + (inEntryMode == "Retest" ? "retest" : "close-through") + " · stop " + (inStopMode == "Range midpoint" ? "midpoint" : inStopMode == "ATR from entry" ? "ATR" : "far side") + " · TP " + str.tostring(inTp1, "0.0") + "/" + str.tostring(inTp2, "0.0") + (inTpUnit == "Range heights" ? " ranges" : "R") + (inEodFlat ? " · flat at close" : ""), text_color = th.flat, text_size = szSub(), text_halign = text.align_left, bgcolor = th.bg)
table.cell(dash, 1, r, "", bgcolor = th.bg)
table.cell(dash, 2, r, fSum == "" ? "no filters · classic rule" : "filters: " + str.substring(fSum, 0, str.length(fSum) - 3), text_color = th.flat, text_size = szSub(), text_halign = text.align_right, bgcolor = th.bg)
// ─────────────────────────────────────────────────────────────────── CO-PILOT
wrap(string s, int width) =>
array<string> words = str.split(s, " ")
string out = ""
string line = ""
for w in words
if str.length(line) + str.length(w) + 1 > width and line != ""
out := out + line + "\n"
line := w
else
line := line == "" ? w : line + " " + w
out + line
narrNow() =>
string o = ""
if t.dir != 0
o := "in a " + (t.dir == 1 ? "long" : "short") + " from " + fmt(t.entry) + (t.retest ? " (retest entry)" : " (close-through entry)") + " for " + str.tostring(bar_index - t.bar) + " bars, " + (openR >= 0 ? "+" : "") + str.tostring(openR, "0.0") + "R at this close — stop " + fmt(t.sl) + (t.tp1Hit ? " (breakeven after TP1)" : " at the " + (inStopMode == "Range midpoint" ? "range midpoint" : inStopMode == "ATR from entry" ? "ATR distance" : "other side of the range")) + ", TP2 " + fmt(t.tp2) + (inEodFlat ? ", flat after " + inTradeSess : "")
else if not dy.set
o := inRange ? "the opening range is forming (" + inRangeSess + " " + inTz + ") — " + (na(dy.orH) ? "first bar" : fmt(dy.orL) + " to " + fmt(dy.orH) + " so far") + "; nothing fires until the window closes" : "waiting for the opening-range window (" + inRangeSess + " " + inTz + ")"
else if dy.skip != ""
o := "today is skipped — " + dy.skip
else if dy.done
o := "done for today: " + str.tostring(dy.trades) + " trade" + (dy.trades == 1 ? "" : "s") + ", " + (dy.todayR >= 0 ? "+" : "") + str.tostring(dy.todayR, "0.0") + "R" + (t.lastWhy != "" ? ", the last one ended on the " + (t.lastWhy == "TP2" ? "target" : t.lastWhy == "EOD" ? "close" : t.lastWhy == "BE" ? "breakeven stop" : "stop") : "")
else if not entryOpen
o := "the range is " + fmt(dy.orL) + " to " + fmt(dy.orH) + " but " + (inTrade ? "the latest entry time " + inLastEntry + " has passed" : "the trade window " + inTradeSess + " is closed") + " — no new entries"
else
o := "the range is set: " + fmt(dy.orL) + " to " + fmt(dy.orH) + " (" + str.tostring(rangeAtr, "0.00") + " × the daily ATR); " + (dy.brk != 0 ? "price broke " + (dy.brk == 1 ? "out above and a retest of " + fmt(dy.orH) : "out below and a retest of " + fmt(dy.orL)) + " would be the entry (" + str.tostring(math.max(0, inRetestWait - (bar_index - nz(dy.brkBar, bar_index)))) + " bars left)" : "a long fires on a close above " + fmt(upLvlD) + ", a short on a close below " + fmt(dnLvlD)) + (filtersOn ? "; filters allow " + (s4L and s4S ? "either side" : s4L ? "longs only" : s4S ? "shorts only" : "neither side right now") : "")
o + (dy.log != "" ? "; today so far: " + dy.log : "")
narrWhy() =>
"Opening range breakout: the first " + inRangeSess + " of the session sets the day's first balance; a close beyond it says one side has lost, so the trade goes with the winner and the other side of the range is where the idea is wrong. " + (inEntryMode == "Retest" ? "The retest variant waits for price to come back to the edge and hold, trading the confirmation instead of the first push." : "The close-through variant trades the first confirmed push.")
narrPlan() =>
string o = ""
if t.dir != 0
o := "Stop first on every closed bar. TP1 " + fmt(t.tp1) + (inBeAtTp1 ? " moves the stop to breakeven" : " is noted") + "; TP2 " + fmt(t.tp2) + " is the final target" + (inEodFlat ? "; whatever is open closes on the first bar after " + inTradeSess : "") + ". The idea is wrong beyond " + fmt(t.sl0) + (inFailExit and not t.fade ? ", and a bar closing back inside the range ends it early" : "") + "." + (sizeTxt != "" ? " Size: " + sizeTxt + "." : "")
else
o := (dy.set ? "" : "Wait for the window " + inRangeSess + " to close; then ") + "a " + sideWord + " fires when a bar closes " + (side == 1 ? "above " + fmt(upLvlD) : "below " + fmt(dnLvlD)) + (inEntryMode == "Retest" ? ", price comes back to the edge within " + str.tostring(inRetestWait) + " bars and a bar closes back beyond it" : "") + (inConfirmBars > 1 ? " and holds for " + str.tostring(inConfirmBars) + " closes" : "") + (inStrongClose ? ", closing in the top part of its bar" : "") + (inFadeFail ? "; a break that closes back inside is traded the other way" : "") + (filtersOn ? ", with the filters on that side" : "") + ". Then: entry at that close, stop at the " + (inStopMode == "Range midpoint" ? "range midpoint" : inStopMode == "ATR from entry" ? str.tostring(inStopAtr, "0.0") + " ATR distance" : "other side of the range") + " plus a buffer, TP1 " + str.tostring(inTp1, "0.0") + " and TP2 " + str.tostring(inTp2, "0.0") + (inTpUnit == "Range heights" ? " range heights" : "R") + (inEodFlat ? ", flat after " + inTradeSess : "") + ". " + str.tostring(inMaxTrades) + " trade" + (inMaxTrades == 1 ? "" : "s") + " per day."
o
narrTrack() =>
st.n < 5 ? "Too few closed trades on this chart to say anything yet." : str.tostring(st.n) + " paper trades over " + str.tostring(st.daysTraded) + " traded days: " + str.tostring(math.round(st.wins / st.n * 100)) + "% won, avg " + (st.sumR / st.n >= 0 ? "+" : "") + str.tostring(st.sumR / st.n, "0.00") + "R; " + str.tostring(st.nTp) + " reached TP2, " + str.tostring(st.nStop) + " stopped, " + str.tostring(st.nBe) + " breakeven, " + str.tostring(st.nEod) + " closed at the end of the window. No costs, small sample — a description of the past on THIS chart, not an edge."
narrPosOf(string p) =>
p == "Top Left" ? position.top_left : p == "Top Right" ? position.top_right : p == "Bottom Right" ? position.bottom_right : p == "Middle Left" ? position.middle_left : p == "Middle Right" ? position.middle_right : position.bottom_left
narrBodySz() => inNarrSize == "Small" ? size.small : inNarrSize == "Medium" ? size.normal : inNarrSize == "Large" ? size.large : szBody()
narrHeadSz() => inNarrSize == "Small" ? size.normal : inNarrSize == "Medium" ? size.large : inNarrSize == "Large" ? size.large : szLead()
var table narrT = na
if barstate.islast
if not na(narrT)
table.delete(narrT)
narrT := na
if inNarr != "Off"
array<string> labs = array.new<string>()
array<string> vals = array.new<string>()
array.push(labs, "NOW")
array.push(vals, wrap(narrNow() + " (decision support, not a trade instruction)", inNarrWrap))
bool deep = inNarr == "Standard" or inNarr == "Detailed"
if deep
array.push(labs, "WHY")
array.push(vals, wrap(narrWhy(), inNarrWrap))
array.push(labs, "PLAN")
array.push(vals, wrap(narrPlan(), inNarrWrap))
if inNarr == "Detailed"
array.push(labs, "TRACK REC")
array.push(vals, wrap(narrTrack(), inNarrWrap))
array.push(labs, "GLOSSARY")
array.push(vals, wrap("Opening range = the high and low of the first minutes of the session. Close-through = a bar closing beyond the range edge. Retest = price returning to the broken edge and holding. Daily ATR = the previous day's 14-day average true range, the yardstick for the range size. Flat at the close = no overnight risk.", inNarrWrap))
int nr = array.size(labs)
bool atBottom = inNarrPos == "Bottom Left" or inNarrPos == "Bottom Right"
int rows = nr + 2 + (atBottom ? 1 : 0)
narrT := table.new(narrPosOf(inNarrPos), 2, rows, bgcolor = th.bg, frame_color = th.frame, frame_width = 1, border_color = color.new(th.frame, 70), border_width = 1)
table.cell(narrT, 0, 0, "◆ CO-PILOT", text_color = th.accentHi, bgcolor = color.new(th.accent, 88), text_size = narrHeadSz(), text_halign = text.align_left)
table.cell(narrT, 1, 0, stateWord + " · " + str.upper(inNarr), text_color = th.accent, bgcolor = color.new(th.accent, 88), text_size = narrBodySz(), text_halign = text.align_right)
for i = 0 to nr - 1
table.cell(narrT, 0, i + 1, array.get(labs, i), text_color = th.accent, bgcolor = th.bg, text_size = narrBodySz(), text_halign = text.align_left, text_valign = text.align_top)
table.cell(narrT, 1, i + 1, array.get(vals, i), text_color = th.txt, bgcolor = th.bg, text_size = narrBodySz(), text_halign = text.align_left, text_valign = text.align_top)
table.cell(narrT, 0, nr + 1, "", bgcolor = th.bg)
table.cell(narrT, 1, nr + 1, "Honest: levels are geometry, not promises. The record has no costs — measure before you trust it.", text_color = th.flat, bgcolor = th.bg, text_size = narrBodySz(), text_halign = text.align_left)
if atBottom
table.cell(narrT, 0, nr + 2, " ", bgcolor = th.bg)
table.cell(narrT, 1, nr + 2, " ", bgcolor = th.bg)
// ─────────────────────────────────────────────────────────────────── STRATEGY BRIDGE (real orders mirror the paper ledger; the engine above is byte-identical to the indicator)
// ORDER MATTERS: cancel_all() cancels every pending order placed this bar — a close_all() issued before it would be cancelled too.
if ev.exit
strategy.cancel_all()
if strategy.position_size != 0
strategy.close_all(comment = ev.why)
if ev.sigL
strategy.entry("L", strategy.long, comment = t.retest ? "ORB retest" : t.fade ? "ORB fade" : "ORB")
if ev.sigS
strategy.entry("S", strategy.short, comment = t.retest ? "ORB retest" : t.fade ? "ORB fade" : "ORB")
if t.dir != 0 and strategy.position_size != 0
strategy.exit("X", stop = t.sl, limit = t.tp2, comment_profit = "TP2", comment_loss = t.tp1Hit and t.sl == t.entry ? "BE" : "SL")
// SYNC guard: if the emulator holds a position the ledger does not know about, flatten at once and tag it.
if strategy.position_size != 0 and t.dir == 0 and not ev.exit and not ev.sigL and not ev.sigS
strategy.cancel_all()
strategy.close_all(comment = "SYNC")
// ─────────────────────────────────────────────────────────────────── ALERTS (confirmed bars only by construction)
payload(string evName, string why) =>
string js = '{"src":"OpeningRangeDesk","symbol":"' + syminfo.ticker + '","tf":"' + timeframe.period + '","event":"' + evName + '","side":"' + (t.dir == 1 or ev.dir == 1 ? "LONG" : t.dir == -1 or ev.dir == -1 ? "SHORT" : "NONE") + '","entry":' + fmt(nz(t.entry, close)) + ',"sl":' + fmt(nz(t.sl, 0)) + ',"tp1":' + fmt(nz(t.tp1, 0)) + ',"tp2":' + fmt(nz(t.tp2, 0)) + ',"orHigh":' + fmt(nz(dy.orH, 0)) + ',"orLow":' + fmt(nz(dy.orL, 0)) + ',"reason":"' + why + '"' + (na(ev.r) ? "" : ',"r":' + str.tostring(ev.r, "0.00")) + '}'
string tx = "Opening Range Desk · " + syminfo.ticker + " " + timeframe.period + " · " + evName + " · entry " + fmt(nz(t.entry, close)) + " · SL " + fmt(nz(t.sl, 0)) + " · TP " + fmt(nz(t.tp1, 0)) + " / " + fmt(nz(t.tp2, 0)) + " · OR " + fmt(nz(dy.orL, 0)) + "–" + fmt(nz(dy.orH, 0)) + (why != "" ? " · " + why : "")
inAlertJson ? js : tx
alertcondition(ev.rangeSet, "Range set", "Opening Range Desk: the opening range is final — watching for the breakout")
alertcondition(ev.sigL, "ORB long", "Opening Range Desk: long — a bar closed above the opening range")
alertcondition(ev.sigS, "ORB short", "Opening Range Desk: short — a bar closed below the opening range")
alertcondition(ev.brkL or ev.brkS, "Broke out — waiting for the retest", "Opening Range Desk: price closed beyond the range; the retest is the entry")
alertcondition(ev.tp1, "TP1 reached", "Opening Range Desk: TP1 reached — stop to breakeven")
alertcondition(ev.exit and ev.why == "TP2", "TP2 reached", "Opening Range Desk: final target reached")
alertcondition(ev.exit and (ev.why == "SL" or ev.why == "BE"), "Stopped", "Opening Range Desk: stop hit")
alertcondition(ev.exit and ev.why == "EOD", "Flat at the close", "Opening Range Desk: the trade window ended — position closed")
alertcondition(ev.exit and ev.why == "TIME", "Time stop", "Opening Range Desk: TP1 not reached by the time stop — position closed")
alertcondition(ev.exit and ev.why == "FAIL", "Failed breakout", "Opening Range Desk: a bar closed back inside the range — position closed")
alertcondition(ev.fake, "Fake-out", "Opening Range Desk: a wick beyond the range closed back inside")
alertcondition(ev.rangeSet and dy.skip != "", "Day skipped", "Opening Range Desk: the opening range failed the size gate — no trade today")
if ev.rangeSet
alert(payload("RANGE_SET", dy.skip), alert.freq_once_per_bar_close)
if ev.sigL
alert(payload("ORB_LONG", t.retest ? "retest" : t.fade ? "fade" : "close-through"), alert.freq_once_per_bar_close)
if ev.sigS
alert(payload("ORB_SHORT", t.retest ? "retest" : t.fade ? "fade" : "close-through"), alert.freq_once_per_bar_close)
if ev.tp1
alert(payload("TP1", ""), alert.freq_once_per_bar_close)
if ev.exit
alert(payload("EXIT", ev.why), alert.freq_once_per_bar_close)
No box yet?
Check that your chart includes the opening window in the selected timezone. Use candles that fit the window: five-minute candles fit the default fifteen-minute opening range. Larger candles cannot reconstruct the smaller window exactly.
Go deeper, one question at a time.
Open the chapter you need. All settings are here.
When is a breakout allowed?
The range must be locked, the trade window open, the range-size and side filters passed, and the daily allowance available with no trade open. An optional latest-entry cutoff can stop new entries sooner.
Default windows use America/New_York: opening range 09:30–09:45; entries 09:45–15:55. Decisions commit on closed candles. On a five-minute chart, the 09:45 candle locks the range at its 09:50 close and can count toward entry.
With flat-at-end enabled, the tool closes at the close of the first candle outside the trade window—not necessarily at exactly 15:55.
How does waiting for a retest work?
Close through: the required consecutive closes must finish strictly beyond the edge plus any breakout buffer. A close on or inside that boundary resets the count.
Retest: those closes first arm the setup. A later candle must visit the allowed area near the edge and close beyond the raw edge. The arming candle cannot enter.
Example: upper edge 106, ATR 8, default tolerance 0.25. A later low at or below 108 records a touch. A close above 106 enters; exactly 106 waits. A close below 104 cancels; exactly 104 survives. A recorded touch can carry forward.
The default deadline is twelve later candles; the thirteenth expires first. Filters and strong-close checks qualify the arming candle; they are not checked again at retest entry. Window, allowance and flat-position gates still apply.
What if price comes back?
A wick beyond an edge that closes back inside can receive a fake-out mark. Two consecutive closes are required by default, but they cannot prevent every reversal.
The optional failed-breakout exit closes a long when a later close falls below the raw upper edge; it can pass through the whole box. The short rule mirrors this. The optional failure fade trades the opposite way and uses the daily allowance.
What do stop, targets and R mean?
Stop: the plan’s loss-exit level. By default it sits beyond the opposite edge by 0.1 ATR. ATR measures recent price movement, including gaps.
1R: the price distance from entry to initial stop. The first target is that distance in the trade’s favor; the final target is twice it. Reaching TP1 moves the stop to entry by default. It does not take partial profit.
The indicator checks the existing stop before targets on each later candle. If both are touched, its paper record counts the stop first. A newly moved stop is not applied retroactively to that candle’s earlier wick.
Size display: divides the risk input by the entry-to-stop distance and rounds down. It does not convert currencies or account for contract point values. Check your instrument’s actual sizing separately. The strategy twin defaults to one unit; this risk display does not control its order size.
How do I read the panel and Co-Pilot?
State names the stage. Range shows the box; Window shows entry time; Range size and Filters show checks; Budget shows today’s allowance. Breakout lists the levels, and Missing names what remains.
Today is the session timeline. Record summarizes paper results without costs. Co-Pilot’s default Brief mode gives NOW. Standard adds WHY and PLAN; Detailed adds the record and glossary.

What does each setting change?
Defaults below refer to the indicator. Change one setting at a time.
Opening range
Choose which opening candles build the box and when entries are allowed. Size gates compare its height with the previous day’s fourteen-day ATR.
- Opening-range window
- 09:30–09:45
- Timezone
- America/New_York
- Trade window
- 09:45–15:55
- Flat at window end
- On
- Latest entry time
- Blank · whole window
- Minimum range × daily ATR
- 0 · off
- Maximum range × daily ATR
- 0 · off
Entry
Close through enters when the count completes. Retest waits for a later return. Buffers and tolerance use chart ATR: a measure of recent price movement, including gaps.
- Entry mode
- Close through
- Breakout buffer
- 0 × ATR
- Retest tolerance
- ATR × 0.25
- Retest deadline
- 12 candles
- Trades per day
- 1
False-breakout controls
Two-close confirmation is on. The other controls are optional: a close near its candle’s extreme, an early failure exit, or an entry against a failed breakout.
- Closes beyond the edge
- 2 in a row
- Strong close required
- Off
- Top / bottom part
- 30 percent
- Exit on a failed breakout
- Off
- Trade the failure
- Off
Exit
Stops can use the opposite edge, midpoint or ATR distance. Targets can use risk multiples or box heights. The time stop exits only if the first target has not been reached.
- Stop placement
- Opposite edge
- ATR stop distance
- 1.5 × ATR
- Stop buffer
- 0.1 × ATR
- Target units
- R · risk multiples
- First target · TP1
- 1R
- Final target · TP2
- 2R
- Move stop to entry at TP1
- On
- Trail after TP1
- Off
- Trail distance
- 1.5 × ATR
- Time stop
- Blank · off
Filters
VWAP is the volume-weighted average price; EMA is a moving average emphasizing recent prices. Filters can require the close to be on their side, enough volume, or a matching opening gap.
- VWAP side
- Off
- EMA side
- Off
- EMA length
- 200 candles
- EMA timeframe
- Blank · chart
- Volume gate
- Off
- Volume threshold
- 1.2 × average
- Volume average length
- 20 candles
- Gap direction
- Off
Position size
Display only. Read the sizing limitations in the stop-and-target chapter before using the number.
- Risk per trade in account currency
- 0 · off
Style
Control the drawings without changing the rules. Themes: Aurora Neon, Royal Gold or Colorblind-safe. Older tickets remain when older zones are removed.
- Theme
- Aurora Neon
- Opening-range boxes
- All days
- Breakout levels
- On
- Risk / reward zones
- On
- Tint open-trade candles
- On
- Draw VWAP
- Off · auto with filter
- Draw EMA
- Off · auto with filter
- Range-height tag
- On
- Keep zones for
- Last trade only
- Color finished days by result
- On
- Range midline
- On
- Mark wick fake-outs
- On
Dashboard
Show the checklist and move or resize it.
- Show dashboard
- On
- Position
- Top Right
- Size
- Medium
Co-Pilot
Brief gives NOW. Standard adds WHY and PLAN. Detailed adds the record and glossary. Off hides the narrative.
- Narrative depth
- Brief
- Position
- Bottom Left
- Size
- Auto
- Line width
- 64 characters
Alerts
Use structured JSON for a webhook, or switch to readable text. See the alert chapter for which events are included.
- JSON payload
- On
The twin starts with zones and dashboard off, and narrative Off. The trading inputs match.
Which alerts can I create?
Twelve named conditions cover: range set, day skipped, long, short, breakout armed, fake-out, first target, final target, stop, time stop, failed breakout and flat at the close. Choose Once Per Bar Close.
Any alert() function call sends only RANGE_SET, ORB_LONG, ORB_SHORT, TP1 and EXIT. It does not separately send armed or fake-out messages. JSON includes symbol, timeframe, event, side, plan prices, range edges and reason.
These messages report the indicator’s events; they are not broker fill confirmations.
How do I study it on my own chart?
- Keep the defaults and read the Missing row through a session.
- Load the strategy twin on a second chart.
- Set realistic costs in its properties, then compare one change at a time.
- Record losing examples as well as successful ones; check a separate period too.
The twin starts with capital 100,000, one-unit orders, fees of 0.02 percent per order and five ticks of slippage. Its order emulator can fill differently from the indicator’s stop-first paper record.

What did our measurements show?
We compared twenty-six variations on three charts with costs. Results differed by market; a retest did not improve every chart. None of the variations paid on the two index ETFs in that summer sample.
It was one summer, small samples and in-sample testing. The numbers belong beside that context in the lab notes. They do not establish how your next trade will end.
The lab notes are in review and will be linked here.
What can change the reading?
The tool reads chart candles in your chosen timezone. Feed, session, timeframe or input changes can recalculate history. Use an intraday chart that fits the opening window.
Decisions use confirmed candles. A higher-timeframe EMA uses its last closed candle. VWAP follows the chart’s session, which can differ from your opening-range window.
The indicator draws a rule-based plan, not a prediction. Its record omits costs. Measure your own chart and actual execution. This is research tooling, not financial advice.
Every change, dated
What shipped and what changed in Opening Range Desk, newest first. Ask for the next change below; what gets built lands here with a date, and on the lab queue on the front page.
Opening Range Desk v1.2.0 Shipped
Checks moves outside the session's first range.
Two words: ORB strategy Request Opening Range Desk
The ask it was built from: two words. We asked which range, entry and filters; no answer came, so every variation became a switch.
- Received
- Building
- Done
“Orb strategy”
Twenty-six variations, measured before shipping Lab note Opening Range Desk
Run with costs on across three charts. The index ETFs did not pay this summer; gold's open did with filters. Second close = default.
Something to change in Opening Range Desk?
A small friction or a big idea. Tell us what would make this tool work better for you.
Requests for a new tool go on the same queue: see what is asked for and what is being built.