Bottom line: using yesterday’s RTH range as a ruler, measured up from yesterday’s high and down from yesterday’s low, today travels a full 1× on at least one side only 22.8% of the time, and 0.5× (half a range) 48.4% of the time.
Where today opens is the best clue: open above yesterday’s high, and 67.4% of days travel 0.5× up; open below yesterday’s low, and 74.0% travel 0.5× down and 48.9% a full 1×.
How I use it: a level one range beyond yesterday’s range rarely gets reached the same day, only about 11–12% of the time on a given side. The exception is a day that opens below yesterday’s low: nearly half of those travel a full 1× down.
This study came out of two things I was curious about.
The first is stops. Al Brooks often puts his stop one range-height away from some range, then picks the actual stop level as the chart develops; and that range is sometimes yesterday’s trading range. So within a single day, does price actually get one full range beyond yesterday’s range?
The second is chasing breakouts. When I traded breakouts and saw a string of strong bull bars at the open, I often couldn’t resist getting in, only to end up trapped at the high of the day. What’s going on there? How far can today really go, and where is it likely to run out?
So I treat yesterday’s RTH range (high − low) as a ruler: measure up from yesterday’s high and down from yesterday’s low, and see how many multiples today reaches at most. The prior-day breakout post already showed that 90% of days break at least one side; this post asks the next question: after the break, how far does price go? And do yesterday’s close and today’s open change that?
2010–2026 · All days · today travels at least 1× yesterday's range on one side
22.8%
Sample 3,816 days · reaches 0.5×: 48.4% · breaks yesterday's high or low: 89.6%
How many of yesterday's ranges today reachesX axis: multiples of yesterday's range. 0× means breaking yesterday's high / low, 0.5× is half a range, 1× is a full range (the two dashed lines). Y axis: share of days that got there.
Yesterday's range = yesterday's RTH high − low. Reaching k× above = today's high ≥ yesterday's high + k × range; below = today's low ≤ yesterday's low − k × range. 0× must be a strict break (at least 1 tick). Where yesterday closed = the close's position within yesterday's range, split into thirds. Hover (or tap) the chart to see the numbers.
The farther out, the rarer
| Reached yesterday’s range × | Above | Below | Either side |
|---|---|---|---|
| 0× (breaks yesterday’s high or low) | 55.7% | 43.7% | 89.6% |
| 0.5× | 26.7% | 23.1% | 48.4% |
| 1× | 11.1% | 11.8% | 22.8% |
| 2× | 1.7% | 3.0% | 4.6% |
- The first row is the same as in the prior-day breakout post (same sample, same rules): 90% of days break at least one side.
- But how far price goes after the break drops off quickly: half of days reach half a range, and only a little over 20% reach a full range.
- Past about 0.85×, the downside wins: for a simple break of yesterday’s high or low, upside is more common (55.7% vs 43.7%); at 0.8× the two are about even (15.4% vs 15.3%); beyond that, below stays ahead of above, and at 2× below (3.0%) is almost twice above (1.7%). Another way to see it: on days that break below, 27.1% go on to a full 1×; on days that break above, only 19.9%.
So days that travel a whole range beyond yesterday’s are uncommon, only a little over 20%; days that travel half a range are about half.
Yesterday’s bar: it sets the direction
In the two tables below, ↑0.5 means reaching 0.5× above, ↓1 means a full 1× below, and so on.
| Condition | ↑0.5 | ↓0.5 | ↑1 | ↓1 |
|---|---|---|---|---|
| All (3,816 days) | 26.7% | 23.1% | 11.1% | 11.8% |
| Yesterday up (2,080 days) | 34.6% | 16.2% | 14.2% | 7.8% |
| Yesterday down (1,709 days) | 16.8% | 31.4% | 7.3% | 16.7% |
| Closed top third (1,685 days) | 37.0% | 13.6% | 14.9% | 6.2% |
| Closed middle (1,082 days) | 24.9% | 26.2% | 11.4% | 14.3% |
| Closed bottom third (1,049 days) | 11.9% | 35.2% | 4.6% | 18.3% |
- Direction follows yesterday: when yesterday closed up (or in its top third), today reaches 0.5× above more than twice as often as 0.5× below; when yesterday closed down, it’s the reverse.
- But it doesn’t go farther: however yesterday closed, “reaches 0.5× on at least one side” stays between 46% and 50%. Yesterday’s bar helps you pick a direction, but it doesn’t tell you whether today will be a big day.
- Where yesterday closed (top / middle / bottom third) works about as well as whether it was an up or down bar, and closes near either end show it a little more clearly.
Where today opens: the biggest effect
| Today opened | ↑0.5 | ↓0.5 | ↑1 | ↓1 |
|---|---|---|---|---|
| Above yesterday’s high (947 days) | 67.4% | 5.1% | 32.8% | 1.7% |
| Top third of the range (885 days) | 25.9% | 10.5% | 7.1% | 4.2% |
| Middle third (771 days) | 11.9% | 14.7% | 3.6% | 6.1% |
| Bottom third (647 days) | 5.6% | 32.1% | 1.9% | 11.6% |
| Below yesterday’s low (566 days) | 3.9% | 74.0% | 1.4% | 48.9% |
- Lower opens travel farther than higher opens: both open outside yesterday’s range, but on days that open below the low, 74.0% travel 0.5× down and 48.9% a full 1×; on days that open above the high, only 67.4% and 32.8% go up. Days that fall out of yesterday’s range tend to fall faster and farther.
- Only opens outside yesterday’s range have a real chance at 1×: open above the high, and a third of days travel a full 1× up; open below the low, and nearly half travel a full 1× down.
- Open in the middle, and today is probably a small day: on either side, only 26.1% reach 0.5× and 9.7% a full 1×; even breaking yesterday’s high or low happens only 73.3% of the time, the lowest of any opening location.
Definition and how to reproduce it
How many of yesterday's ranges today travels
- Data: cleaned 1-minute ES bars, 2010-06 to 2026-09. See how I cleaned it.
- RTH: 09:30–16:15 US Eastern, no overnight session. Both today and yesterday use RTH only.
- Which day is yesterday: the previous stock market trading day; if yesterday was removed in cleaning, or the two days are on different contracts, today is skipped. Exactly the same as the prior-day breakout post, 3,816 days in total.
- Yesterday’s range: R = yesterday’s high − yesterday’s low.
- Reaching k×: above means today’s high ≥ yesterday’s high + k × R; below means today’s low ≤ yesterday’s low − k × R. 0× must be a strict break (at least 1 tick), consistent with the breakout post.
- Where yesterday closed: (close − low) ÷ R; ≥ 2/3 is the top third, ≤ 1/3 the bottom third. This ratio is also called IBS (Internal Bar Strength).
- Where today opened: (today’s open − yesterday’s low) ÷ R; above 1 means above yesterday’s high, below 0 means below yesterday’s low, and the range in between is split into thirds.
I have a CSV of cleaned 1-minute ES futures bars (Databento's ES.c.0 continuous contract, with columns ts_event, instrument_id, open, high, low, close; timestamps in UTC). Please write Python code that measures how many multiples of yesterday's RTH range today's RTH session travels, measured up from yesterday's high and down from yesterday's low. Rules: 1. Convert timestamps to US Eastern time (US/Eastern), handling daylight saving time automatically. Use RTH only, 09:30–16:15, meaning 1-minute bars from 09:30 to 16:14; no overnight session. Each day's open = the open of the 09:30 bar, close = the close of the 16:14 bar, high / low = the highest high / lowest low of the RTH bars. 2. "Yesterday" is the most recent stock market trading day before today, based on the NYSE calendar: pandas' built-in holiday rules plus the unscheduled closures on 2012-10-29, 2012-10-30, 2018-12-05 and 2025-01-09. If that day isn't in my data, or the two days have different instrument_id values, skip today. 3. Yesterday's range R = yesterday's high − yesterday's low. Multiple traveled above = (today's high − yesterday's high) ÷ R; below = (yesterday's low − today's low) ÷ R. Convert prices to whole ticks (× 4, rounded) before comparing, to avoid floating-point errors. 4. Reaching k×: for k = 0 it must be a strict break (at least 1 tick beyond); for k > 0 it must travel ≥ k×. Print the sample size and, for k = 0, 0.5, 0.8, 1 and 2, the share reaching it above, below and on either side. Then check every 0.05× to find the multiple from which "reached below" beats "reached above", and print the share reaching 1× among days that broke above yesterday's high and among days that broke below yesterday's low. 5. By group, print the share reaching 0.5× and 1× above and below, plus the share reaching 0×, 0.5× and 1× on either side: - Yesterday up (close > open) / yesterday down; - Where yesterday closed in its range, (close − low) ÷ R: top third (≥ 2/3), middle, bottom third (≤ 1/3); - Where today opened relative to yesterday's range, (open − yesterday's low) ÷ R: above yesterday's high (> 1), top third, middle, bottom third, below yesterday's low (< 0). Please add comments to every step explaining why it's done that way.
# How many of yesterday's ranges today travels: use yesterday's RTH range as a ruler, measuring up from yesterday's high and down from yesterday's 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", "open", "high", "low", "close"])
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(o=("open", "first"), h=("high", "max"), l=("low", "min"), c=("close", "last"),
contract=("instrument_id", "first"))
# Yesterday = the previous stock market trading day; skip today if yesterday was removed in cleaning or the two days are on different contracts (same as the prior-day breakout post)
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)]
yday = day.loc[prev[today.index]].set_index(today.index)
same = today.contract == yday.contract
today, yday = today[same], yday[same]
# Convert to whole ticks (0.25 points) before comparing, to avoid floating-point errors
tick = lambda x: np.round(x * 4).astype(int)
R = tick(yday.h - yday.l) # yesterday's range
up = tick(today.h - yday.h) # how far today's high went above yesterday's high
dn = tick(yday.l - today.l) # how far today's low went below yesterday's low
def hit(x, k):
"""Reached k × yesterday's range: 0× must be a strict break (at least 1 tick); for k > 0, x ≥ k × R"""
return x > 0 if k == 0 else x * 20 >= round(k * 20) * R
print(f"Sample {len(R)} days")
for k in [0, 0.5, 0.8, 1, 2]:
u, d = hit(up, k), hit(dn, k)
print(f"{k}×: above {100 * u.mean():.1f}% below {100 * d.mean():.1f}% either side {100 * (u | d).mean():.1f}%")
# From which multiple does "reached below" overtake "reached above" (checked every 0.05×)
cross = next(k / 20 for k in range(1, 41) if hit(dn, k / 20).mean() > hit(up, k / 20).mean())
print(f"From {cross}× on, below beats above")
# Share reaching 1× after a break: only days that broke yesterday's high (low)
print(f"Days that broke above: {100 * hit(up, 1)[hit(up, 0)].mean():.1f}% reached 1×; days that broke below: {100 * hit(dn, 1)[hit(dn, 0)].mean():.1f}%")
# Groups: yesterday up / down; which third of the range yesterday closed in; where today opened relative to yesterday's range
ibs = (yday.c - yday.l) / (yday.h - yday.l) # where yesterday closed in its range, 0 = low, 1 = high
loc = (today.o - yday.l) / (yday.h - yday.l) # where today opened in yesterday's range, > 1 = above yesterday's high
groups = {
"Yesterday up": yday.c > yday.o, "Yesterday down": yday.c < yday.o,
"Closed top third": ibs >= 2 / 3, "Closed middle third": (ibs > 1 / 3) & (ibs < 2 / 3), "Closed bottom third": ibs <= 1 / 3,
"Opened above yesterday's high": loc > 1, "Opened top third": (loc >= 2 / 3) & (loc <= 1), "Opened middle third": (loc > 1 / 3) & (loc < 2 / 3),
"Opened bottom third": (loc >= 0) & (loc <= 1 / 3), "Opened below yesterday's low": loc < 0,
}
flags = pd.DataFrame({"0.5x up": hit(up, 0.5), "0.5x down": hit(dn, 0.5), "1x up": hit(up, 1), "1x down": hit(dn, 1),
"0x either": hit(up, 0) | hit(dn, 0), "0.5x either": hit(up, 0.5) | hit(dn, 0.5), "1x either": hit(up, 1) | hit(dn, 1)})
rows = {}
for name, m in groups.items():
rows[name] = {"days": m.sum(), **flags[m].mean()}
print(pd.DataFrame(rows).T.round(3).to_string())Limitations
- It only checks whether price got there, not which side came first: on days that reach 0.5× on both sides, which way price went first, and whether it hit a stop along the way, isn’t measured.
- Timing isn’t measured: whether a multiple is reached in the morning or right before the close matters a lot for day trading; this only looks at the whole day.
- Days that open outside the range have already traveled part of the way at the open: when today opens above yesterday’s high, the open is already some distance past the high, so “reaching 0.5×” requires less movement than it looks.