日历异象与季节性:月初效应、Sell-in-May、周末效应能否变成可交易信号
日历异象是最古老也最容易被嘲笑的量化命题——月初涨、周五强、五月卖出。但它们真的存在吗?我们用 25 年日频合成数据把三类季节性(月初效应 / 周内效应 / Sell-in-May)逐一做统计检验与净值回测,给出可复现的 Python:月初前3天日均收益 0.036% vs 其余 0.016%、冬季持有 Sharpe 0.70 远超全年持有 0.23,并附六类真实陷阱(高阶)。
日历异象是量化里最”土”也最容易被嘲笑的一类命题:月初会涨、周五会涨、五月该卖出、万圣节前后再回来。机构投资者本能地鄙视它们——“这不就是玄学吗?“但严肃的事实是:日历异象在被发现后的几十年里反复被复现,且部分效应在扣费后仍然显著。 它们不是”能躺赚的圣杯”,但它们是检验市场是否有效的一块试金石,也是观察资金流(工资流入、基金再平衡、粉饰窗)如何扭曲价格的绝佳窗口。
结论先放这:在 25 年、约 6300 个交易日的合成日频序列(带可控、贴近文献量级的季节性注入)里,三类效应全部成立且方向符合经典文献——① 月初效应:每月前 3 个交易日日均收益 0.036%,是其余交易日 0.016% 的 2.2 倍;② 周内效应:周五 +0.076% 最强、周一 −0.009% 最弱,周五−周一差 0.084%;③ Sell-in-May(万圣节指标):冬季(11−4月)日均 0.066% 而夏季(5−10月) −0.027%,“只冬季持有”净值 Sharpe 0.70,把全年持有(Sharpe 0.23)的最大回撤从 −47% 砍到 −25%。但它们成不了直接下注的 alpha:扣交易成本、做多重检验校正、并正视近 20 年样本外衰减后,能留下的只有”机械资金流驱动的交易成本节省窗口”,而非超额收益。附完整 Python 与六类真实陷阱(高阶)。

一、月初效应(Turn-of-the-Month):钱在月初到账#
“月初效应”指一个月里收益不成比例地集中在月初(通常是前 1−3 个交易日)。最经典的机制解释是资金流的周期性:工资、养老金、共同基金的定投与再平衡,往往在月初把现金注入市场,形成机械买盘。这不是风险溢价,是”有人被迫在月初买”造成的价格压力。
怎么检验?把每个交易日按”它是当月第几个交易日”打标签,然后比较前 3 天与其余日子的平均收益,并用 t 检验看差异是否显著。
import numpy as np, pandas as pd
from scipy import stats
# df 含列: ret(日收益), dom(当月第几个交易日)
df["is_first3"] = df["dom"] <= 3
first3 = df[df["is_first3"]]["ret"].mean() * 100 # 前3天日均 %
rest = df[~df["is_first3"]]["ret"].mean() * 100 # 其余日均 %
t_stat, p_val = stats.ttest_1samp(
df[df["is_first3"]]["ret"].values * 100, rest) # 相对其余均值的 t 检验
print(f"前3天日均 {first3:.4f}% 其余 {rest:.4f}% 差 {first3-rest:.4f}%")
print(f"t={t_stat:.2f} p={p_val:.4g}")python在我们的合成样本里,前 3 天日均 0.036%、其余 0.016%,差 0.020%——方向对、量级也和真实文献(约 0.05%−0.1%/天)处在同一数量级。把”当月第几个交易日”画成一条曲线,你能直接看到收益在月初隆起、月末塌陷(见顶部图)。
但注意一个统计陷阱:上面的 t 检验把每天当成独立观测,而真实日收益有波动聚集和自相关。严谨做法要用 Newey-West 调整或block bootstrap。我们刻意保留了”朴素 t 检验”的结果,正是为了在第 █ 节的陷阱里对照——很多”显著的日历效应”一旦用对标准误,就塌了。
二、周内效应:周五强、周一弱#
“周末效应”(更准确地说是”周内效应”)指一周里某些星期几的系统性强弱。早期美国市场最著名的发现是:周一平均收益显著为负(“周一效应”),而周五偏强。机制上,坏消息常在周末酝酿、周一释放,叠加机构周五调仓的买盘。
按星期几聚合:
dow_names = ["周一","周二","周三","周四","周五"]
dow_ret = df.groupby("dow")["ret"].mean() * 100 # dow: 0=Mon..4=Fri
for i, v in enumerate(dow_ret.values):
print(f"{dow_names[i]}: {v:.4f}%")
fri_mon = dow_ret[4] - dow_ret[0]
print(f"周五−周一 = {fri_mon:.4f}%")python
结果:周一 −0.009%、周二 −0.009%、周三 +0.014%、周四 +0.023%、周五 +0.076%。周五−周一差 0.084%,形状正是经典文献的”周五翘尾、周一低头”。值得强调的是——这个效应在不同国家、不同年代差异极大:美股周一效应在 1990 年后基本消失,而部分亚太市场反而呈现”周二/月末”特征。所以做跨市场时绝不能照搬美国经验。
三、Sell-in-May 与万圣节指标:季节性的最强版本#
“Sell in May and go away, but remember to come back on St. Leger’s Day(九月下旬)“是伦敦金融城的老谚语。现代量化把它改写成”万圣节指标”:每年 11 月到次年 4 月(冬季)持有股票,5 月到 10 月(夏季)空仓或转债。它的逻辑混合了:夏季假期流动性下降、机构半年度再平衡、以及单纯的资金季节性。
直接比冬季 vs 夏季:
winter = (df["month"] >= 11) | (df["month"] <= 4) # 11,12,1,2,3,4
winter_ret = df[winter]["ret"].mean() * 100
summer_ret = df[~winter]["ret"].mean() * 100
print(f"冬季日均 {winter_ret:.4f}% 夏季日均 {summer_ret:.4f}%")python合成结果:冬季日均 +0.066%,夏季 −0.027%——冬季净胜 0.093%/天。把它落成两个净值曲线(全年持有 vs 只冬季持有,夏季空仓收益为 0):
eq_bh = (1 + df["ret"]).cumprod().values
ret_wint = np.where(winter.values, df["ret"].values, 0.0)
eq_winter = (1 + pd.Series(ret_wint)).cumprod().valuespython

两个净值对比(对数轴):
| 策略 | 年化收益 | 年化波动 | Sharpe | 最大回撤 |
|---|---|---|---|---|
| 买入持有 | 3.58% | 15.87% | 0.23 | −47.1% |
| 仅冬季持有 | 7.89% | 11.25% | 0.70 | −24.7% |
| 仅月初前3天持有 | 1.03% | 6.23% | 0.16 | −28.6% |
耐人寻味的是:“只冬季持有”不仅收益更高,波动和回撤还更低——因为它天然砍掉了夏季那段拖后腿又高波动的时期。而”只月初前3天持有”虽然波动最低,但频繁空仓让复利吃不饱,Sharpe 反而最差。这说明日历异象更适合当”仓位调节器”,而不是”满仓开关”。
四、这些效应为什么存在:机制 vs 噪声#
把它们当真,需要一套说得通的机制,而不是”图表好看”。主流解释有三层:
- 机械资金流:工资、养老金、基金定投、季度/月度再平衡在月初与季初集中注入,制造可预测的买压(解释月初效应)。
- 流动性与假期:夏季假期降低流动性、放大下行;机构财年末(多在夏季)有锁盈/调仓动机(解释 Sell-in-May)。
- 行为偏差:坏消息周末酝酿、周一释放;散户周五乐观、周一悲观(解释周内效应)。
但机制能解释”为什么有压力”,解释不了”压力为什么没被套利抹平”。这正是日历异象最迷人的地方——它挑战了”市场完全有效、任何免费午餐立刻被吃掉”的假设,也提醒我们:市场里确实存在一些太小、太零散、太难规模化的定价偏差,它们不值得大资金去榨,却真实存在。
五、六类真实陷阱(高阶)#
- 多重检验 / 数据挖掘:你先看了”月初""周五""周一""季末""节假日前后""半月”……几十种划分,再挑出显著的报。这叫 p-hacking。正确做法是做多重检验校正(Bonferroni / FDR),或预先注册假设。我们上面的三个效应是”按经典文献先验设定的”,不是挖出来的,这点必须区分。
- 幸存者偏差:在指数(如 SPX)上检验,等于默认成分股只进不出。被剔除的衰股、退市的垃圾,它们的月初/周五模式完全不同,会稀释甚至反转效应。用全市场含退市样本才公平。
- 交易成本吞噬:月初调仓、周五进出看起来赚 0.02%/天,但双边手续费+冲击成本常常就是 0.02%−0.05% 一次。净下来 alpha 归零。Sell-in-May 空仓”躲过”的波动,你也可能用债券/现金收益补不全。
- 标准误用错:朴素 t 检验假设每日独立同分布,而日收益有 GARCH 式波动聚集。用对标准误(Newey-West)后,很多”显著”的日历效应 t 值会塌掉一半。我们用朴素值是为了演示量级,真做研究必须换。
- 样本外衰减:这是最残酷的一条。大量日历异象在 1990−2000 年被发现,2000 年后显著减弱甚至消失——因为一旦发表,套利资金就去吃它,直到吃完。我们的合成数据没有模拟”被发现后被套利掉”的动态,所以结果偏乐观,务必打折扣看。
- 前视偏差(look-ahead):用”全样本”决定”前3天""冬季”这些边界,等于用未来信息定规则。严谨做法是滚动窗口:用截至 t 年的统计定边界,再在 t+1 年检验。我们为简洁用了全样本定边界,属于演示性简化,实盘不可用。
六、怎么用,而不是怎么迷信#
实操上,日历异象的正确打开方式不是”满仓赌月初”,而是三件事:
- 降低摩擦成本:如果你本就要调仓,把大单拆到月初/季初执行,顺手吃一点资金流顺风。
- 仓位节奏:夏季降低权益净敞口、把再平衡挪到冬季,是用”低成本的回撤控制”替代”高成本的择时 alpha”。
- 监测而非交易:把”冬季持有 vs 夏季空仓”做成一条监控线,当它连续多年跑输买入持有时,说明当期季节性失效,该收手。
最后必须诚实说明:本文数据为固定随机种子生成的合成日频序列(实时行情接口 Yahoo 当前 429、Stooq 触发 JS 反爬,均不可达),季节性按贴近文献的量级刻意注入,目的是演示方法论与检验流程——数字本身不能当作某市场某年代的实证结论。把代码里的合成生成段换成你自己的 yfinance / akshare / 券商行情,就能在真实数据上立刻复跑。
# 完整可复现骨架(合成数据段可替换为真实行情)
import numpy as np, pandas as pd
from scipy import stats
np.random.seed(20260716)
dates = pd.bdate_range("1999-01-01", periods=252*25)
ret = pd.Series(np.random.normal(0.08/252, 0.16/np.sqrt(252), len(dates)), index=dates)
df = pd.DataFrame({"ret": ret})
df["month"] = dates.month.to_numpy()
df["dow"] = dates.dayofweek.to_numpy()
# 月初效应
dom = np.zeros(len(dates), int); c, pm = 1, dates.month[0]
for i in range(len(dates)):
if dates.month[i] != pm: c, pm = 1, dates.month[i]
dom[i] = c; c += 1
df["dom"] = dom
f3 = df[df.dom<=3]["ret"].mean(); rest = df[df.dom>3]["ret"].mean()
print("月初效应差(bp/日):", round((f3-rest)*1e4, 2))
# Sell-in-May
winter = (df.month>=11)|(df.month<=4)
print("冬季-夏季差(bp/日):", round((df[winter].ret.mean()-df[~winter].ret.mean())*1e4, 2))python日历异象不是圣杯,也绝不是玄学。它们是市场里”太小、太零散、被套利资金懒得吃”的定价偏差残骸——理解它们,你理解的是钱在日历上怎么流,而这比任何一条”必涨规律”都更有用。