Santa Claus Rally: Hit Rate and Returns by Year
The Santa Claus rally, measured year by year from SPY: the seven-session window, its hit rate, mean and median return, the worst windows, and what followed.
The Santa Claus rally is the stock market's habit of rising over one specific seven-session window: the last five trading days of December plus the first two trading days of January. Measured on SPY, the S&P 500 index fund, that window closed higher in 16 of the 23 years on record here, a hit rate (the share of windows that finished up) of 69.6%, with a mean return of 0.61% and a median of 0.53%. Every window below is dated and measured from the same daily closes, then set beside plain December returns and tested against the old claim that a missing Santa preceded a bad year.
What is the Santa Claus rally window, exactly?
Yale Hirsch, creator of the Stock Trader's Almanac, named the pattern in 1972 and fixed its boundaries. The window is the final five trading sessions of December and the first two trading sessions of January, seven sessions in all. Its return is measured close to close: from the close of the session just before the window opens (the sixth-to-last December session) to the close of the second January session.
Two details matter in practice. First, the window counts trading sessions rather than calendar dates. Weekends and the Christmas and New Year's Day closures drop out, so the window opens anywhere from December 22 to December 27 depending on where Christmas lands, and it ends between January 3 and January 5. Second, a half-day counts as a full session. When December 24 is a trading day the market closes at 1:00 p.m. ET, and that shortened session is one of the five.
Hirsch also attached a couplet to the pattern, which is where the folklore about failed windows comes from:
If Santa Claus should fail to call, bears may come to Broad and Wall. (Yale Hirsch, Stock Trader's Almanac, 1972)
Broad and Wall is the corner where the New York Stock Exchange stands. The rhyme makes a testable claim, and it is tested below.
Santa Claus rally returns year by year
The panel computes every complete window in SPY's daily history. Each row names the session the window is measured from (the last close before it opens), the session it ends on, the window's return, and, for contrast, the return of that whole calendar December, measured from the last November close to the last December close.
| year | measured_from | measured_to | santa_window_pct | calendar_december_pct |
|---|---|---|---|---|
| 2003 | Dec 23 | Jan 5 | 2.47 | 4.54 |
| 2004 | Dec 23 | Jan 4 | -1.61 | 2.53 |
| 2005 | Dec 22 | Jan 4 | 0.48 | -0.72 |
| 2006 | Dec 21 | Jan 4 | 0.04 | 0.78 |
| 2007 | Dec 21 | Jan 3 | -2.21 | -1.65 |
| 2008 | Dec 23 | Jan 5 | 7.76 | 0.17 |
| 2009 | Dec 23 | Jan 5 | 1.5 | 1.36 |
| 2010 | Dec 23 | Jan 4 | 1.1 | 6.12 |
| 2011 | Dec 22 | Jan 4 | 1.94 | 0.41 |
| 2012 | Dec 21 | Jan 3 | 2.06 | 0.18 |
| 2013 | Dec 23 | Jan 3 | 0.19 | 2.04 |
| 2014 | Dec 23 | Jan 5 | -2.9 | -0.8 |
| 2015 | Dec 23 | Jan 5 | -2.26 | -2.31 |
| 2016 | Dec 22 | Jan 4 | 0.53 | 1.43 |
| 2017 | Dec 21 | Jan 3 | 1.08 | 0.7 |
| 2018 | Dec 21 | Jan 3 | 1.46 | -9.33 |
| 2019 | Dec 23 | Jan 3 | 0.37 | 2.4 |
| 2020 | Dec 23 | Jan 5 | 1.02 | 3.26 |
| 2021 | Dec 23 | Jan 4 | 1.48 | 4.26 |
| 2022 | Dec 22 | Jan 4 | 0.8 | -6.19 |
The exact SQL behind every number
WITH
spy_days AS
(
SELECT
date,
toYear(date) AS y,
toMonth(date) AS m,
argMax(toFloat64(close), _ingest_time) AS close
FROM global_markets.stocks_daily_aggs
WHERE ticker = 'SPY'
AND toMonth(date) IN (11, 12, 1)
GROUP BY date
),
ranked AS
(
SELECT
date,
y,
m,
close,
row_number() OVER (PARTITION BY y, m ORDER BY date DESC) AS sessions_from_end,
row_number() OVER (PARTITION BY y, m ORDER BY date ASC) AS sessions_from_start
FROM spy_days
)
SELECT
toUInt16(if(m = 1, y - 1, y)) AS year,
concat(formatDateTime(anyIf(date, m = 12 AND sessions_from_end = 6), '%b '),
toString(toDayOfMonth(anyIf(date, m = 12 AND sessions_from_end = 6)))) AS measured_from,
concat(formatDateTime(anyIf(date, m = 1 AND sessions_from_start = 2), '%b '),
toString(toDayOfMonth(anyIf(date, m = 1 AND sessions_from_start = 2)))) AS measured_to,
round((anyIf(close, m = 1 AND sessions_from_start = 2)
/ anyIf(close, m = 12 AND sessions_from_end = 6) - 1) * 100, 2) AS santa_window_pct,
round((anyIf(close, m = 12 AND sessions_from_end = 1)
/ anyIf(close, m = 11 AND sessions_from_end = 1) - 1) * 100, 2) AS calendar_december_pct
FROM ranked
GROUP BY year
HAVING countIf(m = 11 AND sessions_from_end = 1) = 1
AND countIf(m = 12 AND sessions_from_end = 6) = 1
AND countIf(m = 1 AND sessions_from_start = 2) = 1
ORDER BY yearThe first complete window on record here is December 2003, and the most recent is December 2025, measured from the Dec 23 close to the Jan 5 close. That latest window returned -0.03%, inside a calendar December that returned -0.22%. Read the two return columns side by side: the window and the month it sits inside are separate measurements, and the next panel sums them up.
Is the Santa Claus rally the same as a strong December?
No. "December is a good month" and "the last five sessions of December plus two of January are good" are different claims, and the data keeps them apart. The panel scores three stretches over the same years: the Santa Claus window, the December sessions before it (the last November close through the window's base close), and the full calendar month. The mean is the simple average return. The median is the middle value when every year is lined up in order, which a single extreme year cannot drag around.
| label | years_measured | up_years | hit_rate_pct | mean_pct | median_pct | worst_pct | worst_year |
|---|---|---|---|---|---|---|---|
| Santa Claus window | 23 | 16 | 69.6 | 0.61 | 0.53 | -2.9 | 2014 |
| December before the window | 23 | 17 | 73.9 | 0.21 | 0.85 | -12.68 | 2018 |
| Calendar December | 23 | 15 | 65.2 | 0.45 | 0.7 | -9.33 | 2018 |
The exact SQL behind every number
WITH
spy_days AS
(
SELECT
date,
toYear(date) AS y,
toMonth(date) AS m,
argMax(toFloat64(close), _ingest_time) AS close
FROM global_markets.stocks_daily_aggs
WHERE ticker = 'SPY'
AND toMonth(date) IN (11, 12, 1)
GROUP BY date
),
ranked AS
(
SELECT
date,
y,
m,
close,
row_number() OVER (PARTITION BY y, m ORDER BY date DESC) AS sessions_from_end,
row_number() OVER (PARTITION BY y, m ORDER BY date ASC) AS sessions_from_start
FROM spy_days
),
windows AS
(
SELECT
toUInt16(if(m = 1, y - 1, y)) AS year,
anyIf(close, m = 11 AND sessions_from_end = 1) AS november_close,
anyIf(close, m = 12 AND sessions_from_end = 6) AS base_close,
anyIf(close, m = 12 AND sessions_from_end = 1) AS december_close,
anyIf(close, m = 1 AND sessions_from_start = 2) AS end_close
FROM ranked
GROUP BY year
HAVING countIf(m = 11 AND sessions_from_end = 1) = 1
AND countIf(m = 12 AND sessions_from_end = 6) = 1
AND countIf(m = 1 AND sessions_from_start = 2) = 1
),
unpivoted AS
(
SELECT
year,
arrayJoin([
('Santa Claus window', (end_close / base_close - 1) * 100),
('December before the window', (base_close / november_close - 1) * 100),
('Calendar December', (december_close / november_close - 1) * 100)
]) AS pair,
tupleElement(pair, 1) AS label,
tupleElement(pair, 2) AS return_pct
FROM windows
)
SELECT
label,
count() AS years_measured,
countIf(return_pct > 0) AS up_years,
round(countIf(return_pct > 0) * 100 / count(), 1) AS hit_rate_pct,
round(avg(return_pct), 2) AS mean_pct,
round(quantileDeterministic(0.5)(return_pct, toUInt32(year)), 2) AS median_pct,
round(min(return_pct), 2) AS worst_pct,
toString(argMin(year, return_pct)) AS worst_year
FROM unpivoted
GROUP BY label
ORDER BY label DESCThe Santa Claus window posted a hit rate of 69.6% and a mean of 0.61% across seven sessions. The December sessions before it, roughly three weeks of trading, returned a mean of 0.21% with a hit rate of 73.9%. The full calendar month came in at a mean of 0.45% and a hit rate of 65.2%. The first two rows are the two pieces of the season, the sessions before the window and the seven inside it, and each carries its own hit rate; the month's figure blends them. The worst single window was -2.9%, in the December 2014 window; the worst calendar December was -9.33%, in 2018.
Seasonal claims share this trap. The Sell in May pattern covers a six-month stretch, and the January effect is a small-cap tilt in the opening weeks of the year. Neither is this seven-session, large-cap window, and a headline that blends them together measures nothing.
The worst Santa Claus rally windows, dated
The panel ranks the five weakest windows among those whose following calendar year has already finished, and puts that following year's return in the last column.
| label | measured_from | measured_to | santa_window_pct | following_year_pct |
|---|---|---|---|---|
| Dec 2014 to Jan 2015 | Dec 23 | Jan 5 | -2.9 | -0.81 |
| Dec 2015 to Jan 2016 | Dec 23 | Jan 5 | -2.26 | 9.64 |
| Dec 2007 to Jan 2008 | Dec 21 | Jan 3 | -2.21 | -38.28 |
| Dec 2004 to Jan 2005 | Dec 23 | Jan 4 | -1.61 | 3.01 |
| Dec 2023 to Jan 2024 | Dec 21 | Jan 3 | -0.83 | 23.3 |
The exact SQL behind every number
WITH
spy_days AS
(
SELECT
date,
toYear(date) AS y,
toMonth(date) AS m,
argMax(toFloat64(close), _ingest_time) AS close
FROM global_markets.stocks_daily_aggs
WHERE ticker = 'SPY'
GROUP BY date
),
ranked AS
(
SELECT
date,
y,
m,
close,
row_number() OVER (PARTITION BY y, m ORDER BY date DESC) AS sessions_from_end,
row_number() OVER (PARTITION BY y, m ORDER BY date ASC) AS sessions_from_start
FROM spy_days
WHERE m IN (12, 1)
),
windows AS
(
SELECT
toUInt16(if(m = 1, y - 1, y)) AS year,
toUInt16(year + 1) AS next_year,
anyIf(date, m = 12 AND sessions_from_end = 6) AS base_date,
anyIf(date, m = 1 AND sessions_from_start = 2) AS end_date,
anyIf(close, m = 12 AND sessions_from_end = 6) AS base_close,
anyIf(close, m = 12 AND sessions_from_end = 1) AS december_close,
anyIf(close, m = 1 AND sessions_from_start = 2) AS end_close
FROM ranked
GROUP BY year
HAVING countIf(m = 12 AND sessions_from_end = 6) = 1
AND countIf(m = 1 AND sessions_from_start = 2) = 1
),
year_ends AS
(
SELECT
y,
argMax(close, date) AS year_end_close
FROM spy_days
WHERE m = 12
AND date < toStartOfYear(today())
GROUP BY y
)
SELECT
concat('Dec ', toString(w.year), ' to Jan ', toString(w.next_year)) AS label,
concat(formatDateTime(w.base_date, '%b '), toString(toDayOfMonth(w.base_date))) AS measured_from,
concat(formatDateTime(w.end_date, '%b '), toString(toDayOfMonth(w.end_date))) AS measured_to,
round((w.end_close / w.base_close - 1) * 100, 2) AS santa_window_pct,
round((n.year_end_close / w.december_close - 1) * 100, 2) AS following_year_pct
FROM windows AS w
INNER JOIN year_ends AS n ON n.y = w.next_year
ORDER BY santa_window_pct ASC
LIMIT 5The weakest window with a finished following year was Dec 2014 to Jan 2015, measured from Dec 23 to Jan 5, at -2.9%. The calendar year that followed it returned -0.81%. The second-weakest, Dec 2015 to Jan 2016, printed -2.26%, and its following year returned 9.64%. Read the last column top to bottom before drawing a lesson from it. Five dated rows is the entire sample of "worst".
Does a failed Santa Claus rally predict a down year?
The couplet says a missing Santa precedes a bear market. The test is simple: split every window by whether it rose or fell, then count how many in each group were followed by a down calendar year, meaning the next December's final close sat below this December's.
| label | windows_counted | following_year_down | following_year_down_pct | following_year_mean_pct | following_year_median_pct |
|---|---|---|---|---|---|
| Window fell | 6 | 2 | 33.3 | 2.2 | 6.33 |
| Window rose | 16 | 3 | 18.8 | 12.88 | 13.61 |
The exact SQL behind every number
WITH
spy_days AS
(
SELECT
date,
toYear(date) AS y,
toMonth(date) AS m,
argMax(toFloat64(close), _ingest_time) AS close
FROM global_markets.stocks_daily_aggs
WHERE ticker = 'SPY'
GROUP BY date
),
ranked AS
(
SELECT
date,
y,
m,
close,
row_number() OVER (PARTITION BY y, m ORDER BY date DESC) AS sessions_from_end,
row_number() OVER (PARTITION BY y, m ORDER BY date ASC) AS sessions_from_start
FROM spy_days
WHERE m IN (12, 1)
),
windows AS
(
SELECT
toUInt16(if(m = 1, y - 1, y)) AS year,
toUInt16(year + 1) AS next_year,
anyIf(close, m = 12 AND sessions_from_end = 6) AS base_close,
anyIf(close, m = 12 AND sessions_from_end = 1) AS december_close,
anyIf(close, m = 1 AND sessions_from_start = 2) AS end_close
FROM ranked
GROUP BY year
HAVING countIf(m = 12 AND sessions_from_end = 6) = 1
AND countIf(m = 1 AND sessions_from_start = 2) = 1
),
year_ends AS
(
SELECT
y,
argMax(close, date) AS year_end_close
FROM spy_days
WHERE m = 12
AND date < toStartOfYear(today())
GROUP BY y
)
SELECT
if(w.end_close < w.base_close, 'Window fell', 'Window rose') AS label,
count() AS windows_counted,
countIf(n.year_end_close < w.december_close) AS following_year_down,
round(countIf(n.year_end_close < w.december_close) * 100 / count(), 1) AS following_year_down_pct,
round(avg((n.year_end_close / w.december_close - 1) * 100), 2) AS following_year_mean_pct,
round(quantileDeterministic(0.5)((n.year_end_close / w.december_close - 1) * 100,
toUInt32(w.year)), 2) AS following_year_median_pct
FROM windows AS w
INNER JOIN year_ends AS n ON n.y = w.next_year
GROUP BY label
ORDER BY label ASCOf the 6 windows that fell, 2 preceded a down calendar year, a rate of 33.3%. Of the 16 windows that rose, 3 preceded a down year, a rate of 18.8%. The mean following-year return was 2.2% after a failed window and 12.88% after a rising one, with medians of 6.33% and 13.61%.
Two cautions belong beside those figures. The failed-window group is small, and a handful of years can make almost any rule look real, or look broken. And a full year's return tends to hinge on a few sessions scattered across it, the arithmetic behind missing the best days of the market, so seven sessions in late December hold no special claim over the other 245. The table reports which windows preceded which years. Nothing in it explains why, and it makes no forecast for the December ahead.
Why the window is thin: half-days and holiday volume
Every Santa Claus window contains sessions that sit next to a market closure, and often a 1:00 p.m. half-day; our guide to stock market holidays and early closes lists each one. Those sessions trade on far less volume than an ordinary day, which is one structural reason to hold the window's statistics loosely: a price move on thin volume takes fewer dollars to produce. The panel compares each of the seven sessions with the average daily SPY volume of its own calendar year, then averages that ratio across every year on record. Dec -1 is the last session of December, Dec -5 the fifth-last, and Jan +1 the first session of January.
| bucket | avg_volume_vs_year_pct | median_volume_vs_year_pct | years_measured |
|---|---|---|---|
| Dec -5 | 52.3 | 39.2 | 23 |
| Dec -4 | 57.8 | 52.3 | 23 |
| Dec -3 | 72.1 | 65.3 | 23 |
| Dec -2 | 66.3 | 56.8 | 23 |
| Dec -1 | 90.4 | 88.1 | 23 |
| Jan +1 | 110.2 | 92.2 | 23 |
| Jan +2 | 94.9 | 90.3 | 23 |
The exact SQL behind every number
WITH
spy_days AS
(
SELECT
date,
toYear(date) AS y,
toMonth(date) AS m,
argMax(toFloat64(volume), _ingest_time) AS volume
FROM global_markets.stocks_daily_aggs
WHERE ticker = 'SPY'
GROUP BY date
),
yearly AS
(
SELECT
y,
avg(volume) AS avg_volume
FROM spy_days
GROUP BY y
),
ranked AS
(
SELECT
date,
y,
m,
volume,
row_number() OVER (PARTITION BY y, m ORDER BY date DESC) AS sessions_from_end,
row_number() OVER (PARTITION BY y, m ORDER BY date ASC) AS sessions_from_start
FROM spy_days
WHERE m IN (12, 1)
),
slots AS
(
SELECT
if(r.m = 12, concat('Dec -', toString(r.sessions_from_end)),
concat('Jan +', toString(r.sessions_from_start))) AS bucket,
if(r.m = 12, toInt32(0) - toInt32(r.sessions_from_end),
toInt32(r.sessions_from_start)) AS slot_order,
r.y AS y,
r.volume / yr.avg_volume * 100 AS volume_vs_year_pct
FROM ranked AS r
INNER JOIN yearly AS yr ON yr.y = r.y
WHERE (r.m = 12 AND r.sessions_from_end <= 5)
OR (r.m = 1 AND r.sessions_from_start <= 2)
)
SELECT
bucket,
round(avg(volume_vs_year_pct), 1) AS avg_volume_vs_year_pct,
round(quantileDeterministic(0.5)(volume_vs_year_pct, toUInt32(y)), 1) AS median_volume_vs_year_pct,
count() AS years_measured
FROM slots
GROUP BY bucket, slot_order
ORDER BY slot_orderAll five December sessions in the window traded below their year's average volume: 52.3%, 57.8%, 72.1%, 66.3% and 90.4% of it, in window order. The two January sessions came in at 110.2% and 94.9%. The chart draws where the trough falls; in a typical year that is the half-day or the session after Christmas. A return earned on that little trading is real, but it is set on a thinner market than the one that prices stocks the rest of the year.
FAQ
When does the Santa Claus rally start and end?
It covers the last five trading sessions of December and the first two of January, seven sessions in total. The start shifts with the calendar, from December 22 to December 27, and the window ends on the second trading day of January.
What is the average Santa Claus rally return?
On SPY, across 23 windows, the mean return was 0.61% and the median 0.53%, with the window closing higher 69.6% of the time. Those are seven-session figures and are not annualized.
Does a failed Santa Claus rally mean a bad year ahead?
In this record, 2 of the 6 windows that fell were followed by a down calendar year. That is a small sample describing past sequences only, and it is not a forecast.
Is the Santa Claus rally the same as the January effect?
No. The January effect describes small-cap stocks outperforming large caps in early January. The Santa Claus rally is a seven-session index window that ends on the second trading day of January. They overlap by two sessions and measure different things.
Is December usually a good month for stocks?
On SPY, calendar December closed higher in 65.2% of the years measured, with a mean return of 0.45%. The Santa Claus window is a separate, shorter measurement inside that month.
Every panel above carries its SQL underneath; expand any one to see how a window was cut. To rerun the windows on another ticker, or watch the current December take shape session by session, ask on the Strasmore terminal.
Data notes
- Returns are SPY price returns, close to close, with dividends excluded. SPY's December distribution goes ex-dividend around the third Friday of the month, ahead of the window.
- A window is counted only when the last November session, the sixth-to-last December session and the second January session all exist in the history, so a partial first or last year drops out rather than printing a broken number.
- The following-year test uses calendar years that have already finished; the most recent window stays out of it until its year completes.
- Volume ratios compare each session with the average daily volume of its own calendar year; for the current year that average covers the sessions recorded so far.