
用 Python 揭秘均值回归策略:你的收益从何而来?
2026年重磅升级已全面落地!欢迎加入专注财经数据与量化投研的【数据科学实战】知识星球!您将获取持续更新的《财经数据宝典》与《量化投研宝典》,双典协同提供系统化指引;星球内含 500 篇以上独有高质量文章,深度覆盖策略开发、因子分析、风险管理等核心领域,内容基本每日更新;同步推出的「量化因子专题教程」系列(含完整可运行代码与实战案例),系统详解因子构建、回测与优化全流程,并实现日更迭代。我们持续扩充独家内容资源,全方位赋能您的投研效率与专业成长。无论您是量化新手还是资深研究者,这里都是助您少走弯路、事半功倍的理想伙伴,携手共探数据驱动的投资未来!
如果你正在学习 Python 量化投资,那么「因子投资」一定是绕不开的话题。从 1992 年 Fama 和 French 提出三因子模型,到 2020 年 Gu、Kelly、Xiu 用机器学习横扫资产定价领域,再到神经网络自动构造因子,这个领域已经发生了翻天覆地的变化。
最近读到一篇非常硬核的实战梳理文章,作者把 60 年的因子投资学术文献串起来,从 CAPM 一路讲到神经网络,还附带了 Python 实现思路。今天我把核心内容整理成中文版,并配上 Python 代码示例,希望能帮你快速理清这条技术主线。
CAPM(资本资产定价模型)告诉我们:股票收益只由市场组合驱动,其它都是噪声。但 1981 年 Banz 发现小盘股有超额收益,CAPM 解释不了,这种现象被称为「异象」(anomaly)。
Ross 在 1976 年提出 APT(套利定价理论),把资产收益写成多个因子的线性组合:
后续最有名的实现就是 Fama-French 系列:
数据可以直接从 Kenneth French Data Library 下载,Python 里一行代码就能搞定:
# 使用 pandas_datareader 读取 Fama-French 五因子数据
import pandas_datareader.data as web
import pandas as pd
# 拉取 1963 年至今的月度五因子数据
ff5 = web.DataReader(
"F-F_Research_Data_5_Factors_2x3", # 五因子数据集名称
"famafrench", # 数据源
start="1963-07-01" # 起始日期
)[0]
# 单位是百分比,需要除以 100 转成小数
ff5 = ff5 / 100
print(ff5.head()) # 查看前几行:MKT-RF、SMB、HML、RMW、CMA、RF学术界发现因子主要有两种方法。
按某个特征把股票分成 5 组或 10 组,跟踪每组未来收益,看头尾两组差异是否显著。
# 一个简化的投资组合排序示例
import numpy as np
import pandas as pd
def sort_portfolios(df, characteristic, n_groups=10):
"""
按某个特征对股票分组并计算各组平均收益
df: 包含 stock_id、date、return、characteristic 的 DataFrame
characteristic: 排序使用的特征列名
n_groups: 分组数,默认 10 组(十分位数)
"""
# 每个时间点根据特征分组
df["group"] = df.groupby("date")[characteristic].transform(
lambda x: pd.qcut(x, n_groups, labels=False) # 等频分组
)
# 计算各组每期平均收益
group_returns = df.groupby(["date", "group"])["return"].mean().unstack()
# 多空组合:买入第一组,卖空最后一组
long_short = group_returns[0] - group_returns[n_groups - 1]
return long_short
# 多空组合的 t 统计量决定因子是否「显著」这是 1973 年的经典方法,分两步:第一步用时间序列回归估计每只股票的因子载荷 beta;第二步在每个时点做横截面回归,估计因子风险溢价 gamma。
import statsmodels.api as sm
import numpy as np
def fama_macbeth(returns, factors):
"""
Fama-MacBeth 两步回归
returns: 各资产收益矩阵,形状为 (T, N)
factors: 因子收益矩阵,形状为 (T, K)
"""
T, N = returns.shape
K = factors.shape[1]
# 第一步:时间序列回归,估计每只股票的 beta
betas = np.zeros((N, K))
X = sm.add_constant(factors) # 加截距项
for i in range(N):
model = sm.OLS(returns[:, i], X).fit()
betas[i, :] = model.params[1:] # 取因子系数,不要截距
# 第二步:每期横截面回归,估计因子溢价 gamma
gammas = np.zeros((T, K))
Xb = sm.add_constant(betas)
for t in range(T):
model = sm.OLS(returns[t, :], Xb).fit()
gammas[t, :] = model.params[1:]
# 风险溢价是 gamma 的时间均值
risk_premia = gammas.mean(axis=0)
# t 统计量用于显著性检验
t_stats = risk_premia / (gammas.std(axis=0) / np.sqrt(T))
return risk_premia, t_statsSheppard(2023)用这套方法在 25 个 Fama-French 组合上跑出来:市场溢价约 6.66%、SMB 约 2.87%、HML 约 2.81%(年化)。但 J 检验统计量高达 95.29,说明三因子模型作为完整描述其实是被拒绝的。
这是很多 Python 量化新手最容易栽的坑。p 值是 P(D|H),即「假设原假设成立时观察到当前数据的概率」,但我们想知道的是 P(H|D),即「在数据下原假设成立的概率」。
Harvey(2017)提出了贝叶斯化 p 值(Bayesianised p-value):
import numpy as np
def bayesianised_p_value(t_stat, prior_odds):
"""
Harvey (2017) 提出的贝叶斯化 p 值
t_stat: 回归得到的 t 统计量
prior_odds: 你对原假设为真的先验赔率,p/(1-p)
比如先验认为 86% 概率原假设成立,则 prior_odds = 0.86/0.14 ≈ 6
返回值:原假设为真的后验概率
"""
# 公式:Bpv = exp(-t²/2) * prior / (1 + exp(-t²/2) * prior)
likelihood_ratio = np.exp(-t_stat ** 2 / 2)
bpv = likelihood_ratio * prior_odds / (1 + likelihood_ratio * prior_odds)
return bpv
# 案例:t = 2(约 5% 的传统 p 值),先验赔率 6
bpv = bayesianised_p_value(t_stat=2.0, prior_odds=6)
print(f"贝叶斯化 p 值:{bpv:.3f}") # 约 0.448
# 也就是说原假设为真的概率仍有 44.8%,远不是「显著」Harvey、Liu、Zhu(2016)整理了文献中超过 300 个因子,其中很多在样本外都无法复现。Chen 和 Zimmermann(2020)估计已发表收益的「出版偏差」约 12%,也就是说论文里报的 8% 收益,真实可能只有 7%。
💡 教训:看到一个新因子不要急着上车,至少把 t 值门槛提到 3 以上再说。
Khandani 和 Lo(2007)记录了一段惨痛历史:2007 年 8 月,一家量化基金因次贷损失被迫平仓。由于所有 quant 基金都「在同一个池塘里钓鱼」(持仓高度相似),强制抛售引发了多米诺骨牌:
这告诉我们一个残酷事实:因子投资在正常时期是分散化的,在危机时期是高度相关的。杠杆 + 流动性蒸发是因子策略最大的杀手。
Gu、Kelly、Xiu(2020)做了迄今最全面的机器学习对比实验:
关键结论:
# 一个 Gu et al. 风格的浅层神经网络示例
import torch
import torch.nn as nn
class StockReturnNet(nn.Module):
"""预测股票月度收益的浅层神经网络"""
def __init__(self, n_features, hidden_dims=[32, 16, 8]):
super().__init__()
layers = []
in_dim = n_features
# 三层隐藏层就够了,再深效果反而变差
for h in hidden_dims:
layers.append(nn.Linear(in_dim, h)) # 全连接层
layers.append(nn.ReLU()) # 激活函数
layers.append(nn.BatchNorm1d(h)) # 批归一化稳定训练
in_dim = h
layers.append(nn.Linear(in_dim, 1)) # 输出层:预测收益
self.net = nn.Sequential(*layers)
def forward(self, x):
return self.net(x)
# 注意:金融数据信噪比极低,不要照搬 CV 领域那种 50 层网络
model = StockReturnNet(n_features=94, hidden_dims=[32, 16, 8])💡 重要发现:线性模型与非线性模型的差距,主要来自变量之间的交互作用,而不是单个变量的非线性变换。
Fang 等(2020)提出了 NNAFC 框架,思路是用神经网络从原始 OHLCV 数据自动构造因子。
核心创新点:
import torch
def differentiable_rank_kernel(x, p=1.83):
"""
可微的排序近似函数
x: 输入张量
p: 控制锐度的超参数,p=1.83 时 95% 数据落在 ±2 std 内
"""
mean = x.mean()
std = x.std()
# 用 sigmoid 函数平滑替代不可微的 rank()
return 1.0 / (1.0 + torch.exp(-p * (x - mean) / (2 * std)))
def rank_ic_loss(factor_values, returns):
"""
Rank IC 损失函数:用于神经网络因子构造
factor_values: 网络输出的因子值
returns: 实际收益
"""
rank_f = differentiable_rank_kernel(factor_values)
rank_r = differentiable_rank_kernel(returns)
# 计算两个 rank 序列的相关系数
rank_f_centered = rank_f - rank_f.mean()
rank_r_centered = rank_r - rank_r.mean()
cov = (rank_f_centered * rank_r_centered).mean()
std_f = rank_f_centered.std()
std_r = rank_r_centered.std()
# 因为是损失函数所以取负号(要最大化相关性)
return -cov / (std_f * std_r + 1e-8)实验结果(中国 A 股数据):
把 50 个 NNAFC 因子和 50 个专家因子组合,LSTM 版本年化收益 29.9%、最大回撤 15.0%、夏普比率 3.289,全方位碾压纯专家因子。
简单一句话:目前还说不清。
主要原因是不同 ESG 数据商对同一家公司的评分差距巨大(Berg、Koelbel、Rigobon,2020)。在数据标准化之前,相关研究的结论可信度都要打折扣。
读完这些材料,我有几点核心收获分享给学 Python 的朋友:
如果你正在用 Python 做量化研究,建议从 Kenneth French 数据库的因子开始,先把 Fama-MacBeth 回归手撸一遍,再尝试 sklearn 里的 ElasticNet 和 PCA,最后用 PyTorch 实现简单的浅层神经网络做因子预测。这条路径走完,你对量化研究会有完全不同的理解。
2026年全面升级已落地!【数据科学实战】知识星球核心权益如下:
星球已沉淀丰富内容生态——涵盖量化文章专题教程库、因子日更系列、高频数据集、PyBroker实战课程、专家深度分享与实时答疑服务。无论您是初探量化的学习者,还是深耕领域的从业者,这里都是助您少走弯路、高效成长的理想平台。诚邀加入,共探数据驱动的投资未来!
好文推荐
1. 用 Python 打造股票预测系统:Transformer 模型教程(一)
2. 用 Python 打造股票预测系统:Transformer 模型教程(二)
3. 用 Python 打造股票预测系统:Transformer 模型教程(三)
4. 用 Python 打造股票预测系统:Transformer 模型教程(完结)
6. YOLO 也能预测股市涨跌?计算机视觉在股票市场预测中的应用
9. Python 量化投资利器:Ridge、Lasso 和 Elastic Net 回归详解
好书推荐