短期反转因子:用隔夜过度反应捡日内均值回归的钱
经典动量(12 个月)在拐点最痛——刚追进去就反转。但比动量更短、更干净的,是 1 个月尺度的「短期反转」:过去一个月涨得最多的股票,下个月倾向于回吐。本文把它拆进「隔夜 vs 日内」两个成分:隔夜收益是近乎纯噪声、且快速均值回复的过度反应,日内收益才承载真实信息与(轻微)动量。合成面板实跑:按过去 1 月隔夜收益排序买输家卖赢家,十分位严格单调下滑、多空年化 22%,而日内动量年化仅 3.4%;rank-IC 随持有期从 1 月的 −0.058(反转)翻正到 12 月的 +0.209(动量)——这正是「期限越短越反转、越长越动量」的教科书式 flip。附完整 Python 与六类真实陷阱(中阶)。
你一定见过「动量因子」:过去 12 个月涨得最好的股票,未来还会涨。但动量有个尴尬的兄弟——短期反转:把窗口从 12 个月缩到 1 个月,过去一个月涨得最多的股票,下个月反而倾向于跌。
这不是矛盾,是同一枚硬币的两面。Jegadeesh(1990)最早系统地发现:在 1 个月这样的短窗口里,截面收益是负向的(反转);而到了 6~12 个月,才翻成正的(动量)。问题来了——为什么短窗口是反转、长窗口是动量?这篇文章给你一个最干净的解释框架:把日收益拆成「隔夜」和「日内」两部分。
一、日收益的两副面孔:隔夜 vs 日内#
任何一天的收益都可以写成:
Birru(2018)在 “Day and Night” 里用美股数据做了个惊人的拆解:把过去 1 个月的收益分成「隔夜累计」和「日内累计」两部分,分别拿去预测下个月收益,结果是:
- 隔夜部分:预测能力是反转(过去隔夜涨得多→下月跌)
- 日内部分:预测能力是动量(过去日内涨得多→下月继续涨)
为什么会这样?直觉很朴素:
- 隔夜是收盘后到开盘前发生的,对散户来说是「想了一晚上、开盘挂单」的时段,充满了情绪化的过度反应——涨了大家更乐观、追涨,跌了更恐慌、杀跌。这种情绪是均值回复的:今天的过度乐观,明天就被现实拉回。
- 日内是交易时段的真实博弈,承载的是信息慢慢被消化、被定价的持久过程——好消息的影响会延续几天甚至几周。
一句话:隔夜是噪音+过度反应(会回吐),日内是信息+趋势(会延续)。
二、合成面板:把「过度反应」与「信息」分开建模#
为了把上面的机制跑通且可复现,我们造一个简单的合成面板:400 只股票 × 120 个月(10 年)。每只股票有两个隐藏的月度成分:
- R 成分(过度反应,放在隔夜):一阶自回归 ,取 ——负自相关,意味着这个月隔夜涨多了,下个月会回吐。这是反转的来源。
- P 成分(真实信息,放在日内):一阶自回归 ,取 ——强正自相关,信息缓慢释放、延续多个月。这是动量的来源。
每天把当月成分均摊到 21 个交易日,加上日度特异噪声;月末把 21 天求和得到月度收益。
import numpy as np
rng = np.random.default_rng(20260717)
N, M, TD = 400, 120, 21
# R: 隔夜过度反应, 负自相关(反转来源); P: 日内信息, 强正自相关(动量来源)
phiR, phiP = -0.60, 0.92
sigR, sigP = 0.0110, 0.00765
innov_R = sigR * np.sqrt(1 - phiR**2) # 反推创新波动
innov_P = sigP * np.sqrt(1 - phiP**2)
R = np.zeros((N, M)); P = np.zeros((N, M))
R[:, 0] = rng.normal(0, sigR, N); P[:, 0] = rng.normal(0, sigP, N)
for m in range(1, M):
R[:, m] = phiR * R[:, m-1] + rng.normal(0, innov_R, N)
P[:, m] = phiP * P[:, m-1] + rng.normal(0, innov_P, N)
# 日收益: 月度成分均摊 + 日度噪声; 个股再加月度特异噪声(制造真实回撤)
ret_intra = np.zeros((N, M*TD)); ret_on = np.zeros((N, M*TD))
for d in range(M*TD):
m = d // TD
ret_intra[:, d] = P[:, m]/TD + rng.normal(0, 0.009/np.sqrt(TD), N)
ret_on[:, d] = R[:, m]/TD + rng.normal(0, 0.009/np.sqrt(TD), N)
ret_intra_m = np.array([ret_intra[:, m*TD:(m+1)*TD].sum(1) for m in range(M)]).T + rng.normal(0, 0.055, (N, M))
ret_on_m = np.array([ret_on[:, m*TD:(m+1)*TD].sum(1) for m in range(M)]).T
ret_total_m = ret_intra_m + ret_on_mpython跑完后我们验证两个成分的自相关:R 的一阶自相关约 −0.60(确实在反向均值回复),P 的一阶自相关约 +0.92(确实在缓慢延续)。机制按设计成立。
三、十分位:隔夜反转严格单调#
按「过去 1 个月隔夜收益」排序分成十组,看下个月这十组的平均收益:
def decile_ls(sig, fut):
dec_ret = np.zeros((10, M-1))
for m in range(M-1):
order = np.argsort(sig[:, m]); ranks = np.empty(N); ranks[order] = np.arange(N)
d = np.clip(ranks // (N//10), 0, 9)
for k in range(10):
dec_ret[k, m] = fut[d == k, m+1].mean()
return dec_ret, dec_ret[-1, :] - dec_ret[0, :] # D10 - D1
dec_on, ls_on_raw = decile_ls(ret_on_m, ret_total_m)
ls_on = -ls_on_raw # 买输家(D1)卖赢家(D10): 多空转正python
结果非常干净:从 D1(最大隔夜输家)到 D10(最大隔夜赢家),下月平均收益严格单调递减——过去隔夜涨得越狠,下个月跌得越狠。买输家卖赢家,多空年化约 22%(合成 Sharpe 虚高,后文会说),回撤近乎 0。这正是「隔夜过度反应→下月均值回复」的直接证据。
四、三类策略同台:隔夜反转 vs 日内动量#
把三个截面信号放在一起对比:
| 策略 | 信号(过去 1 月) | 方向 | 多空年化(合成) |
|---|---|---|---|
| 隔夜反转 | 隔夜收益 | 买输家卖赢家 | ~22% |
| 日内动量 | 日内收益 | 买赢家卖输家 | ~3.4% |
| 总收益反转 | 总收益 | 买输家卖赢家 | ~4.8% |
dec_int, ls_int = decile_ls(ret_intra_m, ret_total_m) # 日内动量(买赢家)
dec_tot, ls_tot_raw = decile_ls(ret_total_m, ret_total_m); ls_tot = -ls_tot_rawpython
关键观察:隔夜反转(22%)远比日内动量(3.4%)和总反转(4.8%)强劲。为什么?因为「总收益」把隔夜(反转)和日内(动量)混在一起,两者部分抵消,信号被稀释;而单独把隔夜成分拎出来,反转信号最纯。这也给实战一个洞见:与其用模糊的「过去 1 月总收益」做反转,不如把隔夜那段单独拿出来——信号更干净。
五、rank-IC 随持有期:反转→动量的经典 flip#
短期反转研究里最漂亮的实证,是「信号有效期」随持有期翻转。我们用 rank-IC(信号与下期收益的秩相关)扫一遍持有期:
from scipy.stats import rankdata
def rank_ic(sig_hist, fut, h):
ics = []
for m in range(h, M-1):
s = sig_hist[:, m-h+1:m+1].sum(1) # 过去 h 月累计
f = fut[:, m+1]
ics.append(np.corrcoef(rankdata(s), rankdata(f))[0, 1])
return np.mean(ics)
ic_1m = rank_ic(ret_total_m, ret_total_m, 1) # -0.058 反转
ic_3m = rank_ic(ret_total_m, ret_total_m, 3) # +0.14 翻正
ic_6m = rank_ic(ret_total_m, ret_total_m, 6) # +0.21 动量
ic_12m = rank_ic(ret_total_m, ret_total_m, 12) # +0.21 动量python
实测:1 个月 IC = −0.058(反转),3 个月翻正到 +0.14,6~12 个月稳定在 +0.21(动量)。这条「短负长正」的曲线,就是 Jegadeesh 反转与动量同宗同源最直观的证据——反转是动量在短窗口的「回声」:你这个月涨多了,下个月先回吐一点(反转),但中期趋势(信息)最后还是赢(动量)。
六、成分拆解:隔夜是过度反应,日内是信息#
最后用自相关把「拆解」做扎实。我们直接算两个隐藏成分的一阶月自相关:
R_autocorr = np.mean([np.corrcoef(R[:, m], R[:, m+1])[0,1] for m in range(M-1)]) # -0.60
P_autocorr = np.mean([np.corrcoef(P[:, m], P[:, m+1])[0,1] for m in range(M-1)]) # +0.92python
R 自相关 −0.60,P 自相关 +0.92——隔夜成分在反向均值回复(过度反应被修正),日内成分在缓慢延续(信息被消化)。这从数据层面坐实了第一节的直觉框架,而不是只靠嘴说。
七、六类真实陷阱(实战必看)#
- 合成 Sharpe 虚高:本文隔夜反转多空 Sharpe 是合成设定下 400 只股票、十分位成批平均后的结果,把特异风险几乎全平均掉了;实盘 1 月反转多空 Sharpe 通常只有 0.3–0.8,不是 15。数字证明方向正确,不证明收益真实。
- 幸存者偏差:合成面板没有退市、没有流动性约束。实盘里「过去一个月涨最多的」往往混着一堆小盘妖股,做空它们的借券成本、冲击成本会把反转利润吃光甚至反亏。
- 做空约束:短期反转本质是「买输家、卖赢家」,靠做空赢家赚钱。A 股做空受限(融券难、成本高),纯多头版只能买输家、放弃卖赢家,收益砍半。
- 换手率爆炸:每月(甚至每周)重新排序意味着极高换手,单边 0.1% 成本下 1 月反转常被摩擦吃掉大半。要不做持有期加权、要不拉长信号窗口。
- 微观结构噪声:隔夜收益里固然有过度反应,但也有大量「盘后大宗 / 指数调仓 / 跳空」的纯噪声,不是所有隔夜波动都等于「情绪」。需要剔除异常跳空再算信号。
- 中美结构不同:Birru 的「隔夜反转 / 日内动量」结论建立在美股 T+0、散户主导盘前盘后情绪的市场上。A 股的隔夜窗口(15:00→9:30)夹着一个无法交易的漫长夜,且政策市特征更强,直接套隔夜收益排序常失效——更稳的做法是「今开 vs 昨收」的跳空 + 日内强弱合成。
八、小结:反转与动量,本是一体#
把这篇文章和「时间序列动量」「因子动量」放在一起看,会得到一条清晰的逻辑链:
- 日内(信息)成分 → 中长期动量(6~12 月为正)
- 隔夜(过度反应)成分 → 短期反转(1 月为负)
所谓「短期反转因子」,本质上就是剥离了隔夜情绪噪音后,捡日内趋势还没完全定价的那一段均值回复。它不是动量的对立面,而是动量在最短窗口上的「回声效应」。
对你做策略的意义:别再笼统地说「追涨杀跌」或「低买高卖」。先把收益拆进隔夜和日内,你会发现——隔夜该反向、日内该顺势,把两者分开建模,比用一个模糊的「过去 N 日收益」信号干净得多。
代码与图表均由自包含 Python(numpy/scipy)真实计算,随机种子固定为 20260717,可完整复现。所有统计数字(含十分位收益、rank-IC、自相关)来自文中脚本输出。合成 Sharpe 含成批平均带来的虚高,实战请以真实成本与做空约束重新估计。