五月賣出、離場觀望有效嗎?S&P 500實測
以2005年至2026年的S&P 500數據檢驗「五月賣出、離場觀望」法則,解析夏季與冬季報酬差距,以及季節切換在實際投資中的成本。
「Sell in May and go away」是股市最知名的日曆效應法則:在4月底賣出,整個夏季持有現金,10月底再買回。以 S&P 500 ETF 追蹤標的的價格計算,從2005年4月最後一個收市日到2026年4月最後一個收市日,11月至翌年4月的半年平均報酬率確實高於5月至10月的半年。兩者存在差距,但差距小於這句諺語所暗示的程度。夏季半年平均也錄得上漲,這正是該法則無法作為投資計畫的原因。
規則內容,以及如何進行測試
這句話源自倫敦一項古老習慣:春季賽馬會結束後離開城市,秋季再返回。以現代且可測試的形式來看,這是一項包含兩個日期的切換規則。投資人自10月最後一個交易日至4月最後一個交易日持有廣泛股票指數,其餘六個月則不持有任何資產。
測試這項規則需要兩項格言本身未提供的資料:價格序列與明確的計算期間。以下所有數字均使用 SPY,也就是最早成立的 S&P 500 ETF;價格取自 global_markets.delayed_stocks_minute_aggs 表格中的正常交易時段分鐘線資料,期間從2005年4月最後一個交易日收市至2026年4月最後一個交易日收市。本文所稱的單月報酬,是當月最後一個正常交易時段價格相對於前一個月價格的變化,僅計算價格。股息將在下文另設章節說明,而月報酬如何計算則解釋這項區分為何會改變結果。
五月賣出、離場觀望是否有效?2005年至2026年的紀錄
每個數據背後的精確 SQL 語法
WITH monthly AS (
SELECT toStartOfMonth(toDate(toTimeZone(window_start, 'America/New_York'))) AS month_start,
argMax(close, window_start) AS month_close
FROM global_markets.delayed_stocks_minute_aggs
WHERE ticker = 'SPY'
AND toDate(toTimeZone(window_start, 'America/New_York')) >= toDate('2005-04-01')
AND toDate(toTimeZone(window_start, 'America/New_York')) <= toDate('2026-04-30')
AND (toHour(toTimeZone(window_start, 'America/New_York')) * 60
+ toMinute(toTimeZone(window_start, 'America/New_York'))) BETWEEN 570 AND 959
GROUP BY month_start
)
SELECT toYear(apr_m.month_start) AS year,
round(100 * (toFloat64(oct_m.month_close) / toFloat64(apr_m.month_close) - 1), 2) AS may_oct_pct,
round(100 * (toFloat64(apr_next.month_close) / toFloat64(oct_m.month_close) - 1), 2) AS nov_apr_pct
FROM monthly AS apr_m
INNER JOIN monthly AS oct_m ON oct_m.month_start = addMonths(apr_m.month_start, 6)
INNER JOIN monthly AS apr_next ON apr_next.month_start = addMonths(apr_m.month_start, 12)
WHERE toMonth(apr_m.month_start) = 4
ORDER BY year每個點都將一個夏季與其後的冬季配對。圖表包含 21 組完整配對,從 2005 年夏季開始,至截至4月的冬季 2026 結束。
在多數單一年份中,兩條線走勢不一致;但在表現最差的一年,兩者反而一致。4月底至10月底期間,2008,夏季階段的表現為 -29.81%,是整段期間最深的跌幅。其後的冬季表現為 -10.03%。若投資人在4月底賣出,確實避開了大幅下跌,卻又在最後一段跌勢中重新進場。這套日曆策略只在整段跌勢的一半期間與市場損失方向一致。
2015年以來,夏季缺口是否已經消退?
這句格言真正主張的是平均值,而這些平均值所依據的期間同樣重要。以2015年為分界,將整段期間拆成長度相近的兩半,並以相同方法計算。
每個數據背後的精確 SQL 語法
WITH monthly AS (
SELECT toStartOfMonth(toDate(toTimeZone(window_start, 'America/New_York'))) AS month_start,
argMax(close, window_start) AS month_close
FROM global_markets.delayed_stocks_minute_aggs
WHERE ticker = 'SPY'
AND toDate(toTimeZone(window_start, 'America/New_York')) >= toDate('2005-04-01')
AND toDate(toTimeZone(window_start, 'America/New_York')) <= toDate('2026-04-30')
AND (toHour(toTimeZone(window_start, 'America/New_York')) * 60
+ toMinute(toTimeZone(window_start, 'America/New_York'))) BETWEEN 570 AND 959
GROUP BY month_start
),
pairs AS (
SELECT toYear(apr_m.month_start) AS season_year,
toFloat64(oct_m.month_close) / toFloat64(apr_m.month_close) - 1 AS may_oct,
toFloat64(apr_next.month_close) / toFloat64(oct_m.month_close) - 1 AS nov_apr
FROM monthly AS apr_m
INNER JOIN monthly AS oct_m ON oct_m.month_start = addMonths(apr_m.month_start, 6)
INNER JOIN monthly AS apr_next ON apr_next.month_start = addMonths(apr_m.month_start, 12)
WHERE toMonth(apr_m.month_start) = 4
),
tagged AS (
SELECT may_oct,
nov_apr,
arrayJoin(['All seasons 2005-2025',
if(season_year <= 2014, 'First half 2005-2014', 'Second half 2015-2025')]) AS era
FROM pairs
)
SELECT era,
count() AS season_count,
round(100 * avg(may_oct), 2) AS avg_may_oct_pct,
round(100 * avg(nov_apr), 2) AS avg_nov_apr_pct,
round(100 * (avg(nov_apr) - avg(may_oct)), 2) AS winter_minus_summer_pct,
countIf(may_oct > 0) AS summers_positive
FROM tagged
GROUP BY era
ORDER BY era在全部 21 個季節期間,5月至10月的平均報酬為 3.88%,11月至翌年4月的平均報酬為 6.06%,相差 2.18 個百分點。這項差距就是該規則的全部依據。再看夏季欄位:其中 16 個夏季的期末高於期初,因此平均而言,離場反而錯過了上漲,而不是避開下跌。
兩個期間的結果有所不同。較早的區段相差 4.84 個百分點,較晚的區段則相差 -0.24 個百分點。常見的說法往往只引用這項差距,卻不說明背後涵蓋哪些年份;但年份本身就是論據。任何季節性統計,都是對某段歷史期間的主張,而這段期間由引用該統計的人自行選定。
真的是8月與9月效應嗎?
六個月是一種粗略的衡量方式。將日曆拆分成各月份,可以檢視夏季期間是否全面偏弱,或是其中某一段時間主導了整體結果。
每個數據背後的精確 SQL 語法
WITH monthly AS (
SELECT toStartOfMonth(toDate(toTimeZone(window_start, 'America/New_York'))) AS month_start,
argMax(close, window_start) AS month_close
FROM global_markets.delayed_stocks_minute_aggs
WHERE ticker = 'SPY'
AND toDate(toTimeZone(window_start, 'America/New_York')) >= toDate('2005-04-01')
AND toDate(toTimeZone(window_start, 'America/New_York')) <= toDate('2026-04-30')
AND (toHour(toTimeZone(window_start, 'America/New_York')) * 60
+ toMinute(toTimeZone(window_start, 'America/New_York'))) BETWEEN 570 AND 959
GROUP BY month_start
),
month_returns AS (
SELECT m1.month_start AS month_start,
100 * (toFloat64(m1.month_close) / toFloat64(m0.month_close) - 1) AS ret_pct
FROM monthly AS m1
INNER JOIN monthly AS m0 ON m0.month_start = addMonths(m1.month_start, -1)
)
SELECT formatDateTime(month_start, '%b') AS label,
count() AS readings,
round(avg(ret_pct), 2) AS avg_return_pct,
round(quantileDeterministic(0.5)(ret_pct, cityHash64(month_start)), 2) AS median_return_pct,
countIf(ret_pct > 0) AS positive_readings
FROM month_returns
GROUP BY label
ORDER BY min(toMonth(month_start))弱勢集中在9月。9月平均為-0.75%,在21年中有12年收高,中位數為0.38%。8月平均為0.28%。7月位於賣方跳過的期間中段,平均為2.63%;10月則是該規則重新買回前的最後一個月,平均為0.97%。
這段期間的效應範圍相當狹窄:夏季末段出現一段偏弱期,而前後月份的表現與全年其他月份大致相近。若在4月底退出市場以避開9月,也會一併錯過5月、6月、7月與8月。
What the switch actually costs
The seasonal gap is a gross number. A rule has to survive its own frictions.
每個數據背後的精確 SQL 語法
WITH monthly AS (
SELECT toStartOfMonth(toDate(toTimeZone(window_start, 'America/New_York'))) AS month_start,
argMax(close, window_start) AS month_close
FROM global_markets.delayed_stocks_minute_aggs
WHERE ticker = 'SPY'
AND toDate(toTimeZone(window_start, 'America/New_York')) >= toDate('2005-04-01')
AND toDate(toTimeZone(window_start, 'America/New_York')) <= toDate('2026-04-30')
AND (toHour(toTimeZone(window_start, 'America/New_York')) * 60
+ toMinute(toTimeZone(window_start, 'America/New_York'))) BETWEEN 570 AND 959
GROUP BY month_start
),
pairs AS (
SELECT toYear(apr_m.month_start) + 1 AS year,
toFloat64(oct_m.month_close) / toFloat64(apr_m.month_close) AS summer_ratio,
toFloat64(apr_next.month_close) / toFloat64(oct_m.month_close) AS winter_ratio
FROM monthly AS apr_m
INNER JOIN monthly AS oct_m ON oct_m.month_start = addMonths(apr_m.month_start, 6)
INNER JOIN monthly AS apr_next ON apr_next.month_start = addMonths(apr_m.month_start, 12)
WHERE toMonth(apr_m.month_start) = 4
)
SELECT year,
round(exp(sum(log(summer_ratio * winter_ratio)) OVER (ORDER BY year)), 2) AS buy_and_hold_growth,
round(exp(sum(log(winter_ratio)) OVER (ORDER BY year)), 2) AS winter_only_growth
FROM pairs
ORDER BY yearA dollar held through every season from the end of April 2005 grew to $6.21 by the end of April 2026. The same dollar in the market only from November through April, idle the rest of the time, grew to $3.14. Both lines are price only with nothing deducted, which flatters the switching rule. Here is what the chart leaves out.
- Two trades every year, in every one of the 21 seasons. The ticket is free at most brokers now. The bid ask spread paid on each round trip is not.
- Tax. In a taxable account the April sale realizes whatever gain built up since November. A November to April holding period runs under a year by construction, which places the gain at short-term rates for a US taxpayer.
- Idle cash. The winter-only line credits the sidelined money with zero. A real holder would park it, and where to park idle cash covers the instruments people use for that.
- Dividends, which the next panel prices out.
每個數據背後的精確 SQL 語法
SELECT toYear(ex_dividend_date) AS year,
round(sumIf(cash_amount, toMonth(ex_dividend_date) >= 5 AND toMonth(ex_dividend_date) <= 10), 3) AS may_oct_dividends_usd,
round(sumIf(cash_amount, toMonth(ex_dividend_date) < 5 OR toMonth(ex_dividend_date) > 10), 3) AS nov_apr_dividends_usd
FROM global_markets.stocks_dividends
WHERE ticker = 'SPY'
AND cash_amount > 0
AND ex_dividend_date >= toDate('2005-01-01')
AND ex_dividend_date <= toDate('2025-12-31')
GROUP BY year
ORDER BY yearSPY's ex-dividend dates land in March, June, September and December, so two of the four fall inside the six months a seller sits out. In 2025, $3.592 per share of distributions carried a May to October ex-date, against $3.689 in the other six months. The two columns track each other across the whole chart. Sitting out half the calendar forgoes roughly half the fund's income every year, on top of whatever the price does in those months. The ex-dividend date is the cutoff that decides who gets paid.
One more cost carries no line item. Index returns arrive in clusters, and missing the best days puts numbers on what a handful of absent sessions does to a long-run balance. Six months out of every year is a lot of absent sessions. That is the same arithmetic behind dollar cost averaging as a discipline about time in the market rather than timing.
為何著名的日曆效應規則值得懷疑
12個月可形成4,095種非空子集選擇方式。若以一段價格歷史資料搜尋這個空間,總會有某些結果看起來十分突出。這是搜尋本身的特性。
Sell in May 相較於上週才挖掘出的規則,有一項合理優勢:早在本次樣本開始前,它就已廣為人知。因此,本頁涵蓋的年份可作為樣本外測試,而非經過擬合的結果。測試顯示的差距幅度有限,且集中在單一月份;採用這項切換規則的表現,仍落後於同期的買進並持有策略。前視偏誤說明回測如何在不知不覺中預先學會答案,而日曆效應規則正是典型案例:只有在已知報酬後,才挑選表現較佳的月份。
FAQ
Does sell in May and go away still work?
On SPY prices from April 2005 through April 2026, November to April averaged 6.06% against 3.88% for May to October, so the seasonal gap survives in the full sample. The summer half still averaged a gain, with 16 of the 21 summers finishing higher than they started.
Which months are actually the weak ones?
In this window the weakness concentrates in September, which averaged -0.75% and finished higher in 12 of 21 years. July, another month the rule sits out, averaged 2.63% over the same span.
Does selling in May beat buying and holding?
Not over this sample. A dollar held continuously from the end of April 2005 grew to $6.21 by April 2026, against $3.14 for the winter-only switch, and that comparison counts no trading costs, no tax, and no dividends.
What happens to dividends if you sell in May?
A fund's ex-dividend dates do not pause for the summer. Two of SPY's four quarterly ex-dates fall inside the May to October window, and a holder who is out of the fund on an ex-date does not receive that distribution.
Is a seasonal pattern the same as a prediction?
No. A historical average across 21 seasons describes what happened over a stated window. Individual years scatter widely around that average, as the first chart shows, and the window itself is a choice.
Every panel here is a stored query with its SQL attached. Change the ticker, move the window, and check whether the seasonal gap holds on the Strasmore terminal.