Strasmore Research
Learn Matt ConnorBy Matt Connor · data as of September 17, 2026 · refreshed weekly

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.

QuerySanta Claus rally window return by year, SPY, with the full calendar December alongside
23 rows (showing 20)
yearmeasured_frommeasured_tosanta_window_pctcalendar_december_pct
2003Dec 23Jan 52.474.54
2004Dec 23Jan 4-1.612.53
2005Dec 22Jan 40.48-0.72
2006Dec 21Jan 40.040.78
2007Dec 21Jan 3-2.21-1.65
2008Dec 23Jan 57.760.17
2009Dec 23Jan 51.51.36
2010Dec 23Jan 41.16.12
2011Dec 22Jan 41.940.41
2012Dec 21Jan 32.060.18
2013Dec 23Jan 30.192.04
2014Dec 23Jan 5-2.9-0.8
2015Dec 23Jan 5-2.26-2.31
2016Dec 22Jan 40.531.43
2017Dec 21Jan 31.080.7
2018Dec 21Jan 31.46-9.33
2019Dec 23Jan 30.372.4
2020Dec 23Jan 51.023.26
2021Dec 23Jan 41.484.26
2022Dec 22Jan 40.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 year
Run this yourself

The 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.

QuerySanta Claus window vs. the rest of December vs. the whole month, SPY, all years on record
labelyears_measuredup_yearshit_rate_pctmean_pctmedian_pctworst_pctworst_year
Santa Claus window231669.60.610.53-2.92014
December before the window231773.90.210.85-12.682018
Calendar December231565.20.450.7-9.332018
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 DESC
Run this yourself

The 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.

QueryThe five weakest Santa Claus windows on record, with the calendar year that followed each
labelmeasured_frommeasured_tosanta_window_pctfollowing_year_pct
Dec 2014 to Jan 2015Dec 23Jan 5-2.9-0.81
Dec 2015 to Jan 2016Dec 23Jan 5-2.269.64
Dec 2007 to Jan 2008Dec 21Jan 3-2.21-38.28
Dec 2004 to Jan 2005Dec 23Jan 4-1.613.01
Dec 2023 to Jan 2024Dec 21Jan 3-0.8323.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 5
Run this yourself

The 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.

QueryFailed vs. successful Santa Claus windows and the calendar year that followed
labelwindows_countedfollowing_year_downfollowing_year_down_pctfollowing_year_mean_pctfollowing_year_median_pct
Window fell6233.32.26.33
Window rose16318.812.8813.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 ASC
Run this yourself

Of 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.

QuerySPY volume in each Santa Claus window session as a share of its year's average daily volume
bucketavg_volume_vs_year_pctmedian_volume_vs_year_pctyears_measured
Dec -552.339.223
Dec -457.852.323
Dec -372.165.323
Dec -266.356.823
Dec -190.488.123
Jan +1110.292.223
Jan +294.990.323
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_order
Run this yourself

All 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.
#seasonality#santa claus rally#calendar effects#sp500#december#spy