Bottom line: across the five weekdays, direction barely differs. 54.6% of all days close up in RTH; only Friday is a bit higher (58.1%), and that’s true in both 2010–2017 and 2018–2026.
The range differences are clearer and more stable: Monday’s median intraday range is 0.83%, the smallest; Thursday’s is 0.96%, the largest. Both halves of the data show the same order.
Al Brooks’ idea that money tends to buy at the end of the week and the end of the month, so prices tend to rise: Friday fits; but the last trading day of the month is actually more likely to fall, closing up in RTH only 47.6% of the time.
How I use it: I don’t use the weekday to call direction. But it helps set expectations: on Mondays I don’t put targets too far away, and on Thursdays I leave my stops a bit wider.
Trading folklore has plenty of weekday sayings: “Mondays like to reverse”, “longs take profits before the Friday close”, “Thursdays tend to sell off”. I split 16 years of ES trading days by weekday and looked at three things: the share of up days, the mean and median change, and the intraday range.
2010–2026 · sample 3,924 days · all days up 54.6% · median intraday range 0.90%
Share of up daysRTH close above the RTH open. Dot: the rate; line: 95% interval; dashed line: all days (54.6%).
How to read it: the vertical line is the 95% interval. Given this many days, the true rate most likely sits inside it, and the fewer the days, the longer the line. Only when the whole line sits on one side of the dashed line can you say that weekday differs from normal; if the line crosses the dashed line, the gap may just be luck.
Mean and median changeA few big up or down days pull the mean around; the median is the typical day.
Intraday range(RTH high − RTH low) ÷ open. Bar: median; dot: mean.
Weekdays follow the calendar: in a week with a Monday holiday, Tuesday is still Tuesday. Close to close requires yesterday to be the previous stock-market trading day on the same contract. Hover (or tap) the charts to see every number for that weekday.
Direction: only Friday stands out a little
| Weekday | Days | RTH up | Up close to close |
|---|---|---|---|
| All days | 3,924 | 54.6% | 55.2% |
| Monday | 698 | 55.7% | 57.9% |
| Tuesday | 838 | 53.1% | 52.7% |
| Wednesday | 833 | 52.2% | 56.1% |
| Thursday | 818 | 54.4% | 54.3% |
| Friday | 737 | 58.1% | 55.7% |
RTH up: the RTH close is above the RTH open. Up close to close: today’s close is above yesterday’s close, including the overnight move.
- Friday closes up in RTH most often: 58.1%, with a 95% interval of 54.5%–61.6%, whose lower end just touches the 54.6% for all days. It’s 57.5% in 2010–2017 and 58.6% in 2018–2026, first in both halves. That’s the only directional difference that holds up in both periods.
- Tuesday has weakened in recent years: in 2018–2026, Tuesday closed up close to close only 49.3% of the time, the only weekday below half; but in 2010–2017 it was 56.6%, the highest of the five. The two periods disagree, so it looks like a feature of recent years rather than a rule.
- Monday has the highest close-to-close up share (57.9%), but that includes the weekend gap; in Monday’s RTH session alone, 55.7% close up, about normal.
- “Thursdays sell off” doesn’t show up in ES: Thursday closes up close to close 54.3% of the time, about the same as 55.2% for all days.
Al Brooks on week-end and month-end buying
In a March 2024 live session, Al Brooks talked about quarter-end “window dressing”: institutions have to file quarterly reports and want those reports to show they own the stocks that did well, so they buy into the end of the quarter, and the market tends to rally into month end. The idea is often extended to every Friday and every month end: fund managers want the accounts they manage to look good at each reporting point, so everyone leans toward buying; the more people buy, the more prices tend to rise, and rising prices draw in more buyers. So weeks, months and quarters should close up a little more often.
Checking that against ES data:
| Days | RTH up | Up close to close | |
|---|---|---|---|
| All days | 3,924 | 54.6% | 55.2% |
| Friday | 737 | 58.1% | 55.7% |
| Month end | 187 | 47.6% | 45.4% |
| Quarter end | 64 | 51.6% | 54.7% |
Month end: the last stock market trading day of each month; quarter end: the month end in March, June, September and December. Months where that day was removed in cleaning don’t count.
- Friday fits: 58.1% of Fridays close up in RTH, the highest of the week, in both halves of the data.
- The last day of the month is actually more likely to fall: only 47.6% close up in RTH, and 45.4% close to close. The close-to-close 95% interval is 38.4%–52.6%, entirely below the usual 55.2%, so it doesn’t look like luck.
- Quarter end shows no difference: only 64 days, about the same as normal, with a wide interval.
So in ES, “end of the week leans bullish” holds up, but “end of the month leans bullish” doesn’t, at least not on the last trading day. One possible reason (not tested): Al was describing the general tendency in the last week or two before quarter end, not the very last day; and on the last day of the month, big money rebalances, selling some stock after a run-up, which could offset the buying or even reverse it. In the same live session, Al himself said calendar patterns are never very strong and you won’t make much money trading them.
Mean and median aren’t the same
The second chart shows both the mean and the median change, and on some weekdays they differ quite a bit. For example, close to close, Monday’s mean is only +0.04%, but its median is +0.08%, higher than Tuesday’s or Thursday’s.
The reason is that a few big down days pull the mean lower: of the 5 biggest close-to-close drops in 16 years, 3 were Mondays (2020-03-16 −10.45%, 2020-03-09 −7.25%, 2011-08-08 −7.16%), all after big weekend news that hit all at once on Monday.
So for a “typical day”, look at the median; the mean is easily skewed by a few extreme days.
Range: smallest on Monday, largest on Thursday
| Weekday | All | 2010–2017 | 2018–2026 |
|---|---|---|---|
| Monday | 0.83% | 0.75% | 0.88% |
| Tuesday | 0.89% | 0.84% | 0.94% |
| Wednesday | 0.93% | 0.85% | 0.98% |
| Thursday | 0.96% | 0.89% | 1.04% |
| Friday | 0.89% | 0.79% | 1.00% |
The numbers are the median intraday range: (RTH high − RTH low) ÷ open.
- Both halves have Monday smallest and Thursday largest. Thursday’s range is about 16% wider than Monday’s: at 6,000 points, a typical Monday RTH session moves about 50 points and a typical Thursday about 58.
- Possible reasons (not tested): much of Monday’s news has already been absorbed in the Sunday-evening overnight session, so RTH has less new information; every Thursday has initial jobless claims at 8:30 a.m., and many major data releases and central bank meetings also tend to fall on Wednesdays and Thursdays.
Definition and how to reproduce it
Day-of-week effect
- Data: cleaned 1-minute ES bars, 2010-06 to 2026-09, 3,924 trading days. See how I cleaned it.
- RTH: 09:30–16:15 US Eastern. The open is the open of the 09:30 1-minute bar, and the close is the close of the 16:14 bar.
- RTH up day: RTH close > RTH open.
- Close to close: (today’s close − yesterday’s close) ÷ yesterday’s close. Yesterday = the previous stock market trading day, with the same rules as the prior-day breakout post; if yesterday was removed in cleaning, or the two days are on different contracts, the day doesn’t count.
- Intraday range: (RTH high − RTH low) ÷ open, the same as in the intraday range post.
- Weekday: by calendar. In a week with a Monday holiday, Tuesday is still Tuesday.
- Month end / quarter end: month end is the last stock market trading day of each month (by the NYSE calendar), and months where that day was removed don’t count; quarter end is the month end in March, June, September and December.
- 95% interval: calculated with the Wilson method; it means “given this many days, the true rate most likely falls in this range”.
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 checks whether ES's RTH moves and ranges differ from Monday to Friday. 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. 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. Compute three numbers for each day: - RTH change % = (close − open) ÷ open × 100; above 0 is an up day; - Close-to-close change % = (today's close − yesterday's close) ÷ yesterday's close × 100. Yesterday = 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 the data, or the two days have different instrument_id values, skip the close-to-close number for that day; - Intraday range % = (RTH high − RTH low) ÷ open × 100. 3. Weekdays follow the calendar (in a week with a Monday holiday, Tuesday is still Tuesday). 4. Group by weekday and print, for each group: the number of days, the RTH up-day share and its 95% interval (Wilson method), the close-to-close up share, the mean and median close-to-close change, and the median range. 5. Split the data into 2010–2017 and 2018–2026 and print the same table for each half, to see whether the results hold in both. 6. Then look at Fridays, month ends and quarter ends: month end = the last stock market trading day of each month (by the NYSE calendar; months where that day isn't in the data don't count), quarter end = the month end in March, June, September and December. For each, print the number of days, the RTH up-day share and the close-to-close up share with their 95% intervals, and compare with all days. Finally, list the 5 biggest close-to-close drops. Please add comments to every step explaining why it's done that way.
# Day-of-week effect: from Monday to Friday, do ES's RTH up-day share, daily change and intraday range differ?
# 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
DAYS = ["Mon", "Tue", "Wed", "Thu", "Fri"]
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"))
# Close to close: yesterday = the previous stock market trading day; skip the day if yesterday was removed in cleaning or the contracts differ
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)
linked = (prev.values[1:] == day.index.values[:-1]) & (day.contract.values[1:] == day.contract.values[:-1])
day["rth"] = (day.c - day.o) / day.o * 100 # RTH change, %
day["c2c"] = np.r_[np.nan, np.where(linked, (day.c.values[1:] / day.c.values[:-1] - 1) * 100, np.nan)]
day["range"] = (day.h - day.l) / day.o * 100 # intraday range, %
day["weekday"] = day.index.dayofweek # 0 = Monday … 4 = Friday
def wilson(k, n, z=1.96):
"""95% interval for the up-day share (Wilson method)"""
p = k / n
c = (p + z * z / (2 * n)) / (1 + z * z / n)
h = z * np.sqrt(p * (1 - p) / n + z * z / (4 * n * n)) / (1 + z * z / n)
return f"{100 * (c - h):.1f}–{100 * (c + h):.1f}"
def table(d):
g = d.groupby("weekday")
tab = pd.DataFrame({
"days": g.size(),
"RTH up %": g["rth"].apply(lambda s: 100 * (s > 0).mean()),
"95% interval": g["rth"].apply(lambda s: wilson((s > 0).sum(), len(s))),
"c2c up %": g["c2c"].apply(lambda s: 100 * (s.dropna() > 0).mean()),
"c2c mean %": g["c2c"].mean(),
"c2c median %": g["c2c"].median(),
"median range %": g["range"].median(),
}).round(3)
tab.index = DAYS
return tab
print(f"Sample {len(day)} days; RTH up {100 * (day['rth'] > 0).mean():.1f}%, "
f"up close to close {100 * (day['c2c'].dropna() > 0).mean():.1f}%, median range {day['range'].median():.2f}%")
print(table(day).to_string())
for a, b in [(2010, 2017), (2018, 2026)]: # check each half separately to see if it holds up
print(f"\n{a}–{b}")
print(table(day[(day.index.year >= a) & (day.index.year <= b)]).to_string())
# Week, month and quarter ends: month end = the last stock market trading day of each month (months where that day was removed don't count); quarter end = month end in March, June, September, December
last_day = pd.Series(cal, index=cal).groupby([cal.year, cal.month]).max()
day["month_end"] = day.index.isin(last_day.values)
day["quarter_end"] = day["month_end"] & day.index.month.isin([3, 6, 9, 12])
print()
for name, m in [("All days", day.index == day.index), ("Friday", day["weekday"] == 4), ("Month end", day["month_end"]), ("Quarter end", day["quarter_end"])]:
s = day[m]
cc = s["c2c"].dropna()
print(f"{name}: {len(s)} days, RTH up {100 * (s['rth'] > 0).mean():.1f}% (95% interval {wilson((s['rth'] > 0).sum(), len(s))}), "
f"up close to close {100 * (cc > 0).mean():.1f}% (95% interval {wilson((cc > 0).sum(), len(cc))})")
print("\nThe 5 biggest close-to-close drops:")
print(day["c2c"].nsmallest(5).round(2).to_string())Limitations
- The directional differences are all small: apart from Friday, every weekday is within the margin of error of the average, and 16 years of data can’t say more.
- Mondays and Fridays have smaller samples: Martin Luther King Jr. Day, Presidents’ Day, Memorial Day and Labor Day fall on Mondays, Good Friday is a Friday, and the half-day Friday after Thanksgiving was removed; at each quarterly contract roll, cleaning also removed the expiration Friday and the following Monday. So there are only 698 Mondays and 737 Fridays, versus more than 810 each for Tuesday through Thursday.
- News days aren’t separated: FOMC, CPI and jobs-report days are mixed in with ordinary days; the range differences may come mostly from which weekdays those events fall on.
- RTH only: part of Monday’s difference happens in the Sunday-evening overnight session, which isn’t looked at separately here.