ORB article

Backtesting ORB: Build a Session-Based Trading Sample

How to backtest Opening Range Breakouts with session windows, filters, and R-based reporting.

Opening range high Opening range low Breakout close Range window
Backtesting ORB: Build a Session-Based Trading Sample

Encode the exact rules, not your memory of them

A backtest is only worth reading if it runs the same code the live engine runs. The spec is short. Encode all of it, verbatim, before you collect a single trade.

SessionOpen (GMT)Close (GMT)Opening range
Sydney22:0007:00First 15 min (M1)
Asia00:0009:00First 15 min (M1)
London07:0015:00First 15 min (M1)
New York13:3020:00First 15 min (M1)

The range is the high and low of the first 15 one-minute bars after the session open. It is set by the clock and never redrawn. Entry logic, exactly as the EA evaluates it, once per closed M1 bar:

  • Long: M1 bar closes above the OR high. Wick touches do not count.
  • Short: M1 bar closes below the OR low.
  • Volume filter on the same bar: tick volume ≥ 1.5× the average of the 20 bars immediately before it. Both conditions or no signal.
  • Latches: at most one long and one short per session per day. After a side fires, it stays fired.
  • Stop = opposite side of the range. 1R = |entry close − stop|. Target 1 = 1R, Target 2 = 2R.

Instruments: XAUUSD, XAGUSD, US100/NQ. If your backtest triggers on a high/low touch, or fires twice on the same side, it is testing a different strategy.

Data: what the test actually runs on

The rules are only half the spec. The other half is the feed, and three properties of M1 data decide whether your numbers mean anything:

  • Tick volume is broker-specific. The 1.5× filter compares tick counts, and two brokers on the same instrument produce different tick counts for the same minute. A signal that passes on one feed can fail on another. Backtest on the same broker feed you will trade against live, or accept that pass rates will not transfer.
  • Missing bars shift the volume window. The average uses the 20 bars immediately before the signal bar. In thin Sydney hours, M1 bars are sometimes skipped entirely. Decide up front how your loop handles a gap — skip the signal or look back 20 existing bars — and do the same thing every time. Silent gap handling is an untracked parameter.
  • Know whether prices are bid, ask, or mid. Most MT5 M1 history is bid. That is fine, but it means the spread adjustment in the next section is mandatory, not optional — a long fills at the ask your data never shows.

Lookahead: the four bugs that fake an edge

Every one of these makes the equity curve better than reality. Check for all four.

  • Wick entries. Filling at the OR high the moment price touches it uses information (the close) that does not exist yet, and fills you at a price the confirmed-close rule never trades. The engine acts only on closed bars. Your loop must too: process bar t using data through bar t's close, decide, then advance.
  • Volume average includes the signal bar. The 1.5× comparison uses the 20 bars before the breakout bar (bars 2–21 back), never the breakout bar itself. Including it drags the average toward the spike and inflates pass rates.
  • Range peeking. The OR is not tradeable until minute 15 closes. If your code computes the day's OR from the full session and then scans from minute 1, bars 1–15 can "break" a range that was not finished. Freeze the box when bar 15 closes, then start evaluating at bar 16.
  • Optimistic intrabar resolution. An M1 bar can span both the stop and Target 1. OHLC data does not tell you which traded first. Resolve it pessimistically: if a bar touches both, book the stop. If that single rule flips your result from positive to negative expectancy, the edge was never there.

Spread and slippage: price them in R, per trade

With bid-only data, a long enters at the ask and a short exits at the ask, so every round trip pays the spread plus slippage. The cost only matters relative to 1R — and 1R is the range width plus breakout distance, which changes daily.

InstrumentTypical spread + slip (round trip)Small OR day (1R)Cost as % of 1RWide OR day (1R)Cost as % of 1R
US1003.0 pts18 pts16.7%60 pts5.0%
XAUUSD0.453.015.0%10.04.5%
XAGUSD0.0350.2017.5%0.705.0%

Worked example, US100 New York session: OR high 20,120, OR low 20,080. Confirming close 20,126. Stop 20,080, so 1R = 46 pts; Target 1 = 20,172, Target 2 = 20,218. A 2-pt spread plus 1 pt of slippage costs 3 pts, or 0.065R, per trade. A "+0.10R per trade" gross result is nearly gone after costs. Rules for the test:

  • Deduct a fixed cost in points from every trade, sized from your broker's real fills, not the advertised minimum spread.
  • Use session-specific spreads. Sydney spreads on US100 and metals run several times wider than New York's; a flat all-day spread flatters the thin sessions most.
  • Report expectancy gross and net. If the edge survives only gross, it does not exist.

Session time: get GMT right or every trade is wrong

The sessions are defined in GMT, full stop. The live engine does all session math in GMT and converts to broker server time internally, DST-safe. Your backtest has to replicate that, and this is where most home-built tests silently break:

  • Broker server time moves. A typical MT5 server runs GMT+2 in winter and GMT+3 in summer. Hardcode "NY open = 16:30 server" and for half the year your opening range starts a full hour late — 15 "opening range" bars taken from the middle of someone else's session. Convert every bar timestamp to GMT first, then apply the fixed windows.
  • Sydney crosses midnight. The 22:00–07:00 window opens on day D and closes on day D+1. A naive "group trades by calendar date" splits the session in two and double-counts ranges. Key each session by its GMT open timestamp, not by date.
  • Verify with a spot check. Pick five random dates across both DST regimes. Confirm the OR bars are exactly 22:00–22:14, 00:00–00:14, 07:00–07:14, 13:30–13:44 GMT on all five. If even one drifts by an hour, throw the run away.

Metrics that matter

Win rate alone tells you nothing about a 1R/2R system. Report in R, per session, per instrument.

MetricDefinitionIllustrative sample (200 NY trades)
Expectancy(win% × avg win) − (loss% × avg loss), in R0.40 × 1.6R − 0.60 × 1.0R = +0.04R gross
Net expectancyExpectancy after spread + slippage+0.04R − 0.07R = −0.03R → reject
Max consecutive lossesLongest losing streak in the sampleAt a 40% win rate, streaks of 10–12 losses are statistically normal over 200 trades
Max drawdown (R)Peak-to-trough equity in RSize positions so this is survivable, not so it looks small
Sample sizeTrades per session/instrument cellUnder ~100 trades per cell, differences are mostly noise

The illustrative numbers above are arithmetic, not a performance claim. Two points on them. First, the gross/net line is the whole game: a small positive gross expectancy is routinely a negative net one. Second, the streak math is why max consecutive losses belongs in every report — a trader sized to tolerate 5 straight losses will quit a system that is behaving exactly as its own backtest predicted. Break everything out per session: Sydney is thin with more fakeouts, New York has the best follow-through. Same rules, different distributions. A blended number hides which session is paying and which is bleeding.

Overfitting the OR duration

The method uses one number: 15 minutes, every session, every day. The temptation is to scan 5, 10, 15, 20, 30, 45, 60 minutes and keep the best. Do the math on what that scan actually is: 7 durations × 4 sessions × 3 instruments = 84 variants. The best of 84 random samples looks good by construction; its out-of-sample result regresses toward the pack. Guard rails:

  • One parameter set, decided before the test. 15-minute range, close confirmation, 1.5× volume on 20 bars, 1R/2R targets. Run it. Read the result. Do not iterate on the same data.
  • If you must compare durations, demand neighborhood stability. A real effect at 15 minutes also shows at 10 and 20. A spike at exactly 25 minutes with losses at 20 and 30 is noise wearing a suit.
  • Hold out data. Split by time, never randomly: tune on nothing, confirm on the most recent 12 months you never touched. One look at the holdout. If you go back and adjust, it is no longer a holdout.
  • Count your experiments. Every variant you tried and discarded is a draw from the same deck. Ten tweaks means your best run must clear a much higher bar than a single pre-registered test.

The same discipline applies to the volume threshold. 1.5× over 20 bars is the rule as taught. If 1.4× or 1.7× changes your conclusion, you do not have an edge in the filter — you have a curve fit to one dataset.

Pre-flight checklist

  • Sessions fixed in GMT: 22:00, 00:00, 07:00, 13:30 opens. Verified on dates in both DST regimes.
  • OR frozen at bar 15; evaluation starts at bar 16; closed-bar closes only.
  • Volume average = 20 bars before the signal bar; signal bar excluded. Same broker feed as live; gap handling fixed and documented.
  • One long + one short latch per session per day.
  • Stop/target ambiguity on a single bar resolved as a loss.
  • Session-specific spread + slippage deducted from every trade; results reported gross and net, in R.
  • Expectancy, max consecutive losses, and max drawdown reported per session and per instrument.
  • No parameter chosen after seeing the data it is scored on.

A backtest built this way will look worse than the ones on social media. That is the point. It is also how the Lab treats its own live record: every signal, losers included, is logged openly. No hype, no guarantees, just data.

Test it, then watch it live

Compare your backtest against the live feed: the ORB Levels page publishes each session's range as it completes, and every signal — winners and losers — is logged openly in the Discord results channel. Premium adds ORB Flow Engine context and institutional option flow for target placement. Education, not financial advice.

Join the Discord