Bottom line: on 89.6% of days, today’s RTH breaks at least one side of yesterday’s RTH high or low. Only 10.4% of days break neither.
One side at a time: yesterday’s high gets taken out 55.7% of the time, yesterday’s low 43.7%.
How I use it: once you’ve decided which way you’re trading today, yesterday’s high or low on that side is a fairly reliable target.
I’ve been going through Al Brooks’ old live sessions, and he keeps bringing up the “90% rule” for yesterday’s high and low. So I tested it on cleaned ES data from 2010 to 2026, looking at one thing only: does today break yesterday’s high or low?
2010–2026 · at least one side broken (Prior day H/L)
89.6%
3,816 trading days
Up only
46.0%
1,755 days
Down only
34.0%
1,297 days
Both sides
9.7%
369 days
Neither
10.4%
395 days
By yearRight column: share of days that broke at least one side. Click a year to see only that year.
Prior day vs overnight2010–2026
It’s basically the 90% rule
Out of 3,816 trading days, 89.6% broke at least one side of yesterday’s range. That’s almost exactly the 90% Brooks talks about.
And it holds up year after year: every year falls between 80.0% and 93.2%. Even the quietest year, 2017, didn’t drop below 80%.
In other words, yesterday’s high and low are the two most reliable magnets on the chart. Mark them before the open. Odds are, price will test at least one of them today.
The rest: about one in ten each
- Neither side broken (10.4%): price stays inside yesterday’s range all day. An inside day.
- Both sides broken (9.7%): it breaks one side, then reverses and breaks the other. Usually a big, volatile day or a two-sided chop. An outside day.
Both are rare, and both are days when breakout trades tend to fail.
Direction bias: more upside breaks than downside
Up only is 46.0%, down only just 34.0%, a 12-point gap. In 16 years, only 2022 had more downside breaks than upside.
Is that just because most years in the sample went up? I checked against the data: it’s related, but not one-to-one:
| Year | ES full-year change | Up only | Down only |
|---|---|---|---|
| 2011 | −0.8% | 42.9% | 39.9% |
| 2018 | −6.7% | 43.3% | 33.5% |
| 2022 | −19.0% | 39.9% | 42.9% |
Full-year change: from the RTH open on the year’s first trading day to the RTH close on its last. The continuous contract isn’t back-adjusted, so roll gaps are included and this is only approximate.
2011 and 2018 both finished down for the year, and yesterday’s range still broke to the upside more often. Only a bear market like 2022, down 19% for the year, flipped the direction. So more upside breaks is the norm; you only need to switch in a sustained bear market.
By year: the hit rate tells you the market’s mood
2017 had the lowest hit rate, just 80.0%: volatility was tiny that year, and one day in five stayed stuck inside yesterday’s range. Strongly trending or volatile years like 2022, 2013 and 2020 were all above 92%.
So when yesterday’s high and low keep getting broken, the market is in an active phase. When inside days start piling up, it’s consolidating, and that’s when breakout trades should back off.
Compared with the overnight high and low
Switch to “Overnight H/L” and you’ll see the overnight range gets broken more often (94.4%), and both sides break more than twice as often as with the prior day (21.2% vs 9.7%).
The reason is the width of the two ranges. Yesterday’s RTH range is about as wide as today’s RTH range, with a median width ratio of 1.01. The overnight range is narrower, with a median of only 0.77 of the same day’s RTH range. The narrower the range, the higher the odds of a one-sided or two-sided break. See Breaking the Overnight High or Low.
Definition and how to reproduce it
Breaking the prior day's high or low
- Data: cleaned 1-minute ES bars, 2010-06 to 2026-09. See how I cleaned it.
- RTH: 09:30–16:15 US Eastern, i.e. 1-minute bars from 09:30 to 16:14, or 81 five-minute bars. The overnight session is excluded.
- Prior day’s high / low: the highest high / lowest low of all of yesterday’s RTH bars. Today also uses RTH only.
- Which day is “yesterday”: the most recent stock market trading day before today. After a market holiday, it’s the last trading day before the holiday. For example, the Tuesday after Martin Luther King Jr. Day uses the previous Friday.
- Skipped days: if yesterday was removed during cleaning (a half day, the Monday of a roll, etc.), or the two days are different contracts, today doesn’t count, and I don’t search further back. 108 days were skipped, leaving 3,816.
- Upside breakout: today’s RTH high is strictly above yesterday’s high, i.e. at least 1 tick (0.25 points) higher. Exactly equal doesn’t count. Gapping open above yesterday’s high counts as a breakout.
- Downside breakout: today’s RTH low is strictly below yesterday’s low.
- Four outcomes: up only, down only, both sides, neither.
I have a CSV of cleaned 1-minute ES futures bars (Databento's ES.c.0 continuous contract, with columns ts_event, instrument_id, high, low; timestamps in UTC). Please write Python code that measures whether today's RTH breaks yesterday's RTH high or low. Rules: 1. Convert timestamps to US Eastern time (US/Eastern), handling daylight saving time automatically. RTH = 09:30–16:15, meaning 1-minute bars from 09:30 to 16:14 (405 bars). Exclude the overnight session. 2. Yesterday's high / low = the highest high / lowest low of all of yesterday's RTH bars. Today's high / low also uses today's RTH only. 3. "Yesterday" is the most recent stock market trading day before today, based on the NYSE calendar: skip market holidays (for example, the Tuesday after Martin Luther King Jr. Day uses the previous Friday). Build the NYSE holidays with pandas' built-in holiday rules, and add the unscheduled closures on 2012-10-29, 2012-10-30, 2018-12-05 and 2025-01-09. 4. If that "yesterday" isn't in my data (it was removed during cleaning), skip today; don't search further back. If the two days have different instrument_id values (different contracts), skip as well. 5. Upside breakout: today's RTH high is strictly greater than yesterday's high (equal doesn't count, i.e. at least 1 tick of 0.25 above). Downside breakout: today's RTH low is strictly less than yesterday's low. 6. Put each day into one of four outcomes: up only, down only, both sides, neither. Print the sample size, the share of each outcome, the share of days that broke at least one side, and a table by year. Please add comments to every step explaining why it's done that way.
# Prior-day breakout: did today's RTH break yesterday's RTH high / low?
# Input: cleaned 1-minute ES bars (Databento ES.c.0, timestamps in UTC, with an instrument_id column)
import numpy as np
import pandas as pd
from pandas.tseries.holiday import (
AbstractHolidayCalendar, Holiday, GoodFriday, USMartinLutherKingJr, USPresidentsDay,
USMemorialDay, USLaborDay, USThanksgivingDay, nearest_workday, sunday_to_monday,
)
class NYSE(AbstractHolidayCalendar): # NYSE full-day closures
rules = [
Holiday("New Year's Day", month=1, day=1, observance=sunday_to_monday),
USMartinLutherKingJr, USPresidentsDay, GoodFriday, USMemorialDay,
Holiday("Juneteenth", month=6, day=19, start_date="2022-01-01", observance=nearest_workday),
Holiday("Independence Day", month=7, day=4, observance=nearest_workday),
USLaborDay, USThanksgivingDay,
Holiday("Christmas", month=12, day=25, observance=nearest_workday),
]
CLOSED = pd.to_datetime(["2012-10-29", "2012-10-30", "2018-12-05", "2025-01-09"]) # unscheduled closures
df = pd.read_csv("ES-1min_clean.csv", usecols=["ts_event", "instrument_id", "high", "low"])
t = pd.to_datetime(df.ts_event, utc=True).dt.tz_convert("US/Eastern").dt.tz_localize(None)
hm = t.dt.hour * 100 + t.dt.minute
rth = df[(hm >= 930) & (hm < 1615)].assign(date=t.dt.normalize()) # RTH 09:30–16:15
day = rth.groupby("date").agg(hi=("high", "max"), lo=("low", "min"), contract=("instrument_id", "first"))
# Yesterday = the previous stock market trading day (after a holiday, the last day before it)
cal = pd.bdate_range(day.index.min() - pd.Timedelta(days=10), day.index.max())
cal = cal[~cal.isin(NYSE().holidays(cal.min(), cal.max()).union(CLOSED))]
prev = pd.Series(cal[:-1], index=cal[1:]).reindex(day.index)
today = day[prev.isin(day.index)] # yesterday must also be in the cleaned data, else skip
yday = day.loc[prev[today.index]].set_index(today.index)
same = today.contract == yday.contract # both days must be the same contract
today, yday = today[same], yday[same]
up = today.hi > yday.hi # strictly above; equal doesn't count
down = today.lo < yday.lo
cls = pd.Series(np.select([up & down, up, down], ["both sides", "up only", "down only"], "neither"), index=today.index)
print("sample days", len(cls))
print((cls.value_counts(normalize=True) * 100).round(1).to_string())
print("at least one side %.1f%%" % (100 * (cls != "neither").mean()))Limitations
- It only checks whether price broke, not how far: one tick beyond is a breakout. So this number tells you yesterday’s high or low will get tested, not how far a breakout will run or whether it holds.
- More upside breaks is the norm, but not a law: in a sustained bear market like 2022, the direction flips.
- Holidays and days around rolls are skipped: those days aren’t normal anyway, but these stats don’t describe them.
(First published on Xiaohongshu. Updated in October 2026: switched to the cleaned data, standardized RTH as 09:30–16:15 and updated the numbers; added the interactive stats and the reproduction card.)