A rigorous, honesty-first backtester for the Opening Range Breakout (ORB) day-trading strategy.
This project does two things:
- Implements ORB variants — from a naive fixed-target version to the strategy documented in academic research.
- Tests them with out-of-sample discipline so the results mean something, instead of curve-fitting until a number turns green.
The headline finding is deliberately unglamorous: a naive ORB loses money, the research-grade version has a real but thin and currently-decaying edge, and aggressive parameter-filtering makes things worse, not better. The point of this repo is to show that honestly.
| Strategy | avg R / trade | Profit Factor | Verdict |
|---|---|---|---|
| Naive ORB — 15-min range, fixed 2R target | −0.083 | 0.11–0.48 | Loses even before costs |
| Research ORB — 5-min candle direction, 1R stop, 10R + EOD exit | +0.224 | 1.31 | Real edge (full sample) |
| Research ORB — out-of-sample (recent 12 months) | +0.065 | 1.09 | Marginally profitable |
| Research ORB — last 3 months | −0.316 | 0.60 | Currently in drawdown |
All results are reported in R-multiples (profit measured in units of the trade's own risk), which is independent of position sizing and commission scaling — the number that actually tells you whether an edge exists.
The equity curve climbs steadily in-sample, then flattens and rolls over in the most recent months — the strategy is currently in the deepest drawdown of the entire 2-year sample.
A textbook ORB (mark the first 15 minutes, enter on a breakout close, stop at the opposite side, target a fixed 2R) was tested on 372 trades of SPY over 18 months. It lost, and the trade log showed why: the 2R target was hit only 14% of the time — you need >33% to break even at 2R. ORB has an asymmetric, fat-tailed payoff, and a fixed 2R target amputates exactly the right tail that pays for everything.
Following Zarattini, Barbon & Aziz ("Can Day Trading Really Be Profitable?", 2023):
- Opening range = the first 5-minute candle.
- Direction = the sign of that candle (closed up → long, down → short).
- Entry = open of the 2nd 5-minute bar.
- Stop = the candle's opposite extreme (= 1R).
- Target = 10R, i.e. effectively "let it run"; exit at the market close if untouched.
This single structural change flipped QQQ from −0.083R to +0.224R per trade. Win rate is only 27%, but the average winner (+3.49R) dwarfs the average loser (−0.99R) — exactly the mechanism the paper describes.
The paper's supporting filters (relative-volume "stock-in-play", VWAP bias, minimum range) were tested with strict in-sample / out-of-sample separation. Every filter that looked best in-sample lost money out-of-sample:
| Variant | In-sample avg R | Out-of-sample avg R |
|---|---|---|
| base (no filters) | +0.383 | +0.065 |
| + RVOL ≥ 1.25 | +0.774 | −0.140 |
| + RVOL ≥ 1.0 | +0.564 | −0.074 |
| + min-range ≥ 1.0 | +0.140 | −0.137 |
The more the strategy was filtered, the worse it did on unseen data. The robust choice is the plain, unfiltered base strategy. This table is the whole lesson: optimizing a backtest until it looks great is how you build something that bleeds live.
| File | Purpose |
|---|---|
orb_backtest.py |
Naive ORB backtester (configurable range, R:R, longs/shorts, costs) |
orb_zarattini.py |
Research-grade ORB (5-min direction, 1R stop, 10R+EOD) with in/out-of-sample split |
compare_filters.py |
Honest filter comparison — reports IS vs OOS side by side |
fetch_data.py |
Pulls extended intraday history from Polygon.io into a CSV |
trades_zarattini.csv |
Trade log from the research strategy |
equity_zarattini.png |
Cumulative-R equity curve with IS/OOS boundary |
requirements.txt |
Python dependencies |
# 1. Environment
python3 -m venv venv
source venv/bin/activate
pip install -r requirements.txt
# 2. Get data (free Polygon.io API key: https://polygon.io/dashboard/signup)
export POLYGON_API_KEY=your_key_here
python fetch_data.py --symbol QQQ --interval 5 --months 24 --out qqq_5min.csv
# 3. Backtest the research strategy with an out-of-sample split
python orb_zarattini.py --csv qqq_5min.csv --oos-split
# 4. See why filters overfit
python compare_filters.py
# (optional) the naive baseline
python orb_backtest.py --csv qqq_5min.csv --or-min 15 --rr 2.0 --point-value 1Raw price CSVs are git-ignored (large and reproducible); regenerate them with fetch_data.py.
- The out-of-sample edge is marginal (PF ~1.1) and, in the most recent months, negative. This is not a money printer.
- Results assume ~1 cent/side slippage and no commissions (typical for a modern equity broker). Real fills, gaps, and missed entries will erode a thin edge further.
- A positive backtest is necessary but nowhere near sufficient. The only real test of whether this edge is alive is forward paper-trading.
- Data: Polygon.io aggregates (free tier).
- Zarattini, Barbon & Aziz, A Profitable Day Trading Strategy for the U.S. Equity Market / Can Day Trading Really Be Profitable? (2023), SSRN.
