
用 Python 揭秘均值回归策略:你的收益从何而来?
2026年重磅升级已全面落地!欢迎加入专注财经数据与量化投研的【数据科学实战】知识星球!您将获取持续更新的《财经数据宝典》与《量化投研宝典》,双典协同提供系统化指引;星球内含 500 篇以上独有高质量文章,深度覆盖策略开发、因子分析、风险管理等核心领域,内容基本每日更新;同步推出的「量化因子专题教程」系列(含完整可运行代码与实战案例),系统详解因子构建、回测与优化全流程,并实现日更迭代。我们持续扩充独家内容资源,全方位赋能您的投研效率与专业成长。无论您是量化新手还是资深研究者,这里都是助您少走弯路、事半功倍的理想伙伴,携手共探数据驱动的投资未来!
很多刚学 Python 做量化的朋友,都会经历这样一个高光时刻:写完回测脚本,一运行,年化收益 50%,瞬间感觉自己离财富自由只差一个云服务器。
但真相往往很扎心——一个跑出超高收益的回测,通常不是发现了金矿,而是代码里埋了 Bug。
今天这篇文章,复盘的就是一个「跑不动」的周频动量模型的完整调试过程。作者本想做一个能给股票排序的模型,结果排序能力几乎等于抛硬币。在寻找原因的路上,他被自己的数字骗了整整四次,每一次「惊喜」最后都被证明是陷阱。最妙的是,真正的 Alpha(超额收益)其实从第一天起就存在,只是一直藏在市值阶梯的下一层。
对于学 Python 做数据分析、机器学习、量化的同学来说,这是一份关于「如何不被自己的代码欺骗」的绝佳教材。
判断一个排序模型好不好,关键指标叫 Rank IC(秩信息系数),它衡量的是「你预测的打分排名」和「实际发生的收益排名」之间的相关性。
而作者最初的模型只有 0.015,有些周甚至和 0 没区别。模型几乎分不清未来的赢家和输家。
接下来,就是四次「数字撒谎」的全过程。
早期版本的回测跑出了年化 50% 以上的收益。作者高兴了 30 秒,然后开始反胃——因为一个周频动量模型不可能在美股大盘股上做到 50%,连顶尖的量化机构都做不到。如果你的笔记本做到了,那一定是它在作弊。
Bug 出在选股票池的方式上。
作者的做法是:先筛选出「今天市值超过 100 亿美元」的公司,再把它们的历史价格拉回到 2021 年做回测。
听起来很合理,实则是剧毒操作。比如某只股票,今天是市值几十亿的大公司,所以进了名单;但在回测的那段历史窗口里,它其实只是一只从 0.11 美元涨到 1.54 美元的「仙股」。模型根本没预测到这个暴涨,它只是被人喂了一份「已经胜出」的名单。
这就是两个经典错误:
验证方法非常朴素:保持模型完全不变,只把那些在窗口期是仙股的约 9% 的名字剔除掉,看会发生什么。
# 同一个模型,唯一的区别只是股票池不同
# 剔除掉在回测窗口期内属于仙股的股票
clean = [s for s in universe if not was_penny_during_window(s)]
dirty_return = backtest(universe).annual_return # 污染版:+51%
clean_return = backtest(clean).annual_return # 干净版:+24%
# 仅仅去掉一小撮仙股,一半的「业绩」就走人了去掉一小撮被污染的票,收益直接腰斩,这就是铁证。真正的 Alpha 是分散在几百只股票上的,绝不会因为 9% 的名单消失就生死攸关。
正确做法是构建「时点正确(point-in-time)」的股票池:每一周只用当周已知的信息来决定哪些股票合格,并且把后来破产、退市的公司也包含进去。改完之后,虚高的 51% 收益坍缩到了 19%,连一段看起来惊心动魄的 27% 回撤也凭空消失了——因为它从来就不真实。
经验:一个好到离谱的回测,是一份 Bug 报告,不是一个发现。看到漂亮数字时,第一个要审查的不是模型,而是「你的数据是否让模型偷看了答案」。
从这里开始,作者不再看「裸收益」,转而看 Sharpe 比率(夏普比率),因为脱离风险谈收益毫无意义。靠扛住 40% 回撤换来的 19%,和平平稳稳赚来的 19%,完全是两回事。
Sharpe 的本质就是:收益 ÷ 波动率,即每单位风险换来多少回报。这里有一份「速查表」:
为什么要把及格线设在 0.5?因为 0.5 是「免费」的——买个指数基金然后去海边躺着就能拿到。一个策略要值得你搭数据管道、反复训练、承担隐藏 Bug 的风险,至少得跑赢「躺平」的收益。
记住「2.0 意味着 Bug」这条警告,后面还会再出现。
数据干净之后,Rank IC 诚实地弱到了 0.0014,基本就是噪声。作者差点在这里放弃。
然后他检查了一个一直「假设」却从未「验证」的东西:预测周期(horizon)。
他原本预测的是未来 5 天的收益,而 5 天恰恰是最糟糕的选择。这里涉及一个著名效应——短期反转(short-term reversal):在几天的尺度上,刚刚暴涨的股票往往会回吐一部分。而他想捕捉的「动量」,要到两周左右才真正发力。
于是他把预测周期从几天扫描到几周,发现信号在 5 天时几乎死透,在 12 ~ 16 天的区间里强了大约 10 倍。他一直把相机对准了一个主角根本不在的位置。换到 13 天周期后,Rank IC 爬升到了 0.015。
经验:「信号很弱」和「我测量错了信号」会产生一模一样的结果。在断定「没有 Alpha」之前,先确保你看的地方,是教科书说它应该出现的地方。
接下来这段很扎心。即使数据干净、周期也对了,这个策略在大盘股上依然变不了现。作者把它拆开找原因:
第一步,做多空美元中性(dollar-neutral):做多最强的、做空最弱的,两边等额,抵消掉大盘的整体上涨。原本多头策略的 Sharpe 看起来有 0.55,去掉市场贡献后,真正的选股 Sharpe 只剩 0.28。一半多的「能力」,其实只是「大盘在涨,而我恰好满仓做多」。
第二步,做行业中性(sector-neutral):只允许模型在同行业内下注(最好的科技股 vs 最差的科技股),不许跨行业押注。波动率从 17% 暴跌到 5.5%。听着很棒,但原因很残酷——账户约 90% 的波动来自「行业押注」,而不是选股。模型的「本事」主要是发现「科技股热、能源股冷」。剥掉这部分,纯粹的同行业内选股 Alpha 很小,扣掉真实交易成本后基本是盈亏平衡。
结论很难写进自己的复盘里:大盘股上的 Alpha 确实存在,但它只是 Beta 和行业暴露,披着一件「选股能力」的外衣。
有一个杠杆确实有用:EMA 平滑(指数移动平均)。与其按当天的原始打分排序,不如按过去 5 天打分的指数移动平均来排序。逻辑是:单日打分又跳又吵,平滑几天能让真信号显现,还能避免你因为一天的波动就疯狂调仓。
它真的有效。某一次运行中,平滑后的行业中性多空策略,净 Sharpe 达到了 0.57。在连续一周的盈亏平衡之后,作者承认自己很想相信这个数字。
而这恰恰是你最不该相信一个数字的时刻。
这里有个很多人不知道的真相:模型训练往往是非确定性的。像 LightGBM 这类梯度提升库,会为每棵树随机抽样行和特征,还会跨多个 CPU 线程按完成顺序累加梯度。而浮点数运算并不严格满足结合律,a + b + c 可能和 c + b + a 在最后一位小数上不同。对于这么微弱的信号,最后几位小数足以把结果推来推去。同样的代码跑两遍,会得到两个不同的 Sharpe。
所以在相信 0.57 之前,作者固定了随机性(设置随机种子、开启确定性模式),并跨多个种子重复实验,每个种子都是一个独立可复现的模型,来观察结果的分布。
# 这个结果是真的,还是我掷了个好骰子?
results = {col: [] for col in ["raw", "ema5", "ema8"]}
for seed in (0, 1, 2):
# 每个种子都可复现的滚动训练
oos = walk_forward(instruments, seed=seed)
# 对信号做 5 日 EMA 平滑
oos["ema5"] = ema(oos["pred"], span=5)
for col in results:
results[col].append(net_sharpe(oos, signal=col))
# 打印每种信号的均值、标准差和原始数据
for col, vals in results.items():
v = pd.Series(vals)
print(f"{col}: mean={v.mean():+.2f} std={v.std():.2f} ({vals})")输出结果:
raw mean=-0.25 std=0.14 (-0.29, -0.36, -0.10)
ema5 mean=+0.14 std=0.13 ( 0.05, 0.09, 0.29)
ema8 mean=+0.19 std=0.30 (-0.09, 0.15, 0.51)两个结论浮出水面:
这次实验还顺手揪出了一个生产环境的真 Bug:线上模型是非确定性重训的,意味着「是否发布信号」的开关,可能在完全相同的数据上无缘无故地翻转。固定随机种子修复了它。
经验:单次回测数字是一个样本,不是一个事实。如果你不能复现它、给它套上误差棒,那你拥有的不是结果,而是一则轶事。而且你越希望某个数字是真的,就越应该卖力地去弄死它。
当模型不给力时,人总想去找「魔法特征」。作者去了网上最火的地方:Smart Money Concepts(SMC,聪明钱概念)。
在短视频平台上,无数交易员对「订单块」「流动性扫单」「市场结构转变」深信不疑,说得像是击败市场的作弊码。
于是他做了测试:写了一个严格因果(无任何前视泄漏)的 SMC 检测库,识别波段高低点、标记订单块、测量到流动性池的距离。然后做了一次干净的 A/B 测试——同样的 5 折交叉验证,一次用基础特征,一次额外加上 SMC 特征。
结果是一次干净利落的统计学拒绝:
加上 SMC 特征后,核心排序指标 Rank IC 下降了超过 30%,信息比率也被拖低。它给了模型更多过拟合的空间,而不是更多信号。
经验:在让任何流行指标接近你的实盘组合之前,先用对抗的方式去检验它。如果数据说它更差,就删掉,不管有多少「大师」告诉你相反的话。社交媒体上的百万播放量,不会给一个指标赋予数学上的优势。
到这一步,作者已经用诚实的测量,排除了在大盘股上变现的每一种方法。瓶颈不在策略构建,而在原始信号强度——大盘股就是没多少 Alpha。它们是地球上被分析得最透彻、定价最有效的股票。动量在那里几乎失效再正常不过:所有人都已经跑过你这个模型了。
于是他换了个问题:在更小、更少人关注的股票里,Alpha 是不是更强?他不需要新数据,直接把已有的预测按市值分桶,测量每个桶内的 Rank IC。
# Alpha 是否依赖市值?
# 为每一行附上其时点正确的市值数据
merged = pd.merge_asof(
oos.sort_values("datetime"), caps.sort_values("date"),
left_on="datetime", right_on="date",
by="instrument", direction="backward")
# 按不同市值区间分桶统计
for name, lo, hi in [("small $0.5-2B", 5e8, 2e9),
("mid $2-10B", 2e9, 1e10),
("large $10-50B", 1e10, 5e10),
("mega >$50B", 5e10, 1e18)]:
b = merged[(merged.market_cap >= lo) & (merged.market_cap < hi)]
ts = tear_sheet(b)
print(f"{name}: rank_IC={ts['rank_ic_mean']:+.4f}")输出结果:
small $0.5-2B : rank_IC=+0.0573 decile_spread=+0.0406
mid $2-10B : rank_IC=+0.0203 decile_spread=+0.0157
large $10-50B : rank_IC=+0.0157 decile_spread=+0.0047
mega >$50B : rank_IC=+0.0241 decile_spread=+0.0003梯度完美符合有效市场假说:公司越小,模型预测的赢家和输家之间的差距就越大。
最小的那一桶看起来无比惊艳。但作者已经吃过太多次「惊艳」的亏——那些微盘股,又一次是「后来长成大盘股」的幸存者,幸存者偏差正在虚高它们。没有一份幸存者干净的小盘股数据集,这个数字不可信。
但中盘股那一桶,既有潜力又站得住脚。于是他做了真正的检验:单独导入一个 2 ~ 10 亿美元的中盘股票池,专门训练一个中盘模型(而不是让一个全局模型摊薄在所有市值上),并跑完整套机器——行业中性、EMA 平滑、多种子误差棒、以及更真实的中盘交易成本(因为流动性更差)。
中盘专用模型,行业中性多空,EMA-5,真实 30bps 成本,5 个种子:
净超额 Sharpe = +0.81 +/- 0.23 (每个种子都为正,最差也有 +0.61)Sharpe 在 0.8 量级,市场中性,每个种子都为正。然后是真正说服他的那一步:他只在「今天仍然是中盘股」的票上重跑,扔掉所有「长成了大盘股」的赢家。如果这是幸存者偏差,它会崩塌。结果它稳稳守在 +0.77。这东西是真的。
当然还有最后一道诚实的门槛:他仍然缺少那些退市或归零的中盘股,补上这个洞需要更好的数据源。但这是整个项目里,第一次出现一个「经受住了他所有破坏尝试」的结果。
这整个故事,给学 Python 做数据与量化的我们留下了几条价值千金的纪律:
那个动量 Alpha 从第一天起就存在,作者只是不断地测错它,又不断地被自己的测错结果欺骗。四次,屏幕上的数字撒了谎,每次谎言都是同一个形状:它看起来比现实更好,而它看起来越好,你就越想相信它。
最后他唯一信任的数字,是那个在他反复、刻意、不怀好意地想让它消失之后,依然为正的数字:+0.77。
在这个游戏里,「真 Alpha」和「自我幻觉」之间的差别,往往就是一个你忘了跑五遍的数字。
2026年全面升级已落地!【数据科学实战】知识星球核心权益如下:
星球已沉淀丰富内容生态——涵盖量化文章专题教程库、因子日更系列、高频数据集、PyBroker实战课程、专家深度分享与实时答疑服务。无论您是初探量化的学习者,还是深耕领域的从业者,这里都是助您少走弯路、高效成长的理想平台。诚邀加入,共探数据驱动的投资未来!
好文推荐
1. 用 Python 打造股票预测系统:Transformer 模型教程(一)
2. 用 Python 打造股票预测系统:Transformer 模型教程(二)
3. 用 Python 打造股票预测系统:Transformer 模型教程(三)
4. 用 Python 打造股票预测系统:Transformer 模型教程(完结)
6. YOLO 也能预测股市涨跌?计算机视觉在股票市场预测中的应用
9. Python 量化投资利器:Ridge、Lasso 和 Elastic Net 回归详解
好书推荐