ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Fama-French三因子模型实战:用Python构造因子与回归检验

Fama-French三因子模型实战:用Python构造因子与回归检验 做量化这两年我最大的感悟是策略跑不出来多数时候不是代码的问题而是你对“超额收益到底从哪里来”的假设太简陋。CAPM用市场收益解释一切但在A股你会发现小市值风格强的时候随便一篮子小盘股都能跑出漂亮的净值beta却弱得可怜风格一切换回撤又全回来了。直到我把Fama-French三因子模型真正用Python在本地数据上完整跑了一遍才看清收益结构长什么样。这篇文章不绕弯子直接从因子构造讲到回归实现再把回测方法和实战中踩过的坑一起倒给你。无论你是刚开始学量化还是已经在跑多因子组合这份代码和思路都可以直接拿来当基础框架。1. 从CAPM到三因子市场贝塔解释不了的那部分收益1.1 CAPM的底层假设与软肋资本资产定价模型CAPM的核心就一句话股票的预期超额收益只跟它对市场组合的敏感程度有关也就是[ E(R_i) - R_f \beta_i \times (E(R_m) - R_f) ]它假设所有投资者同质预期、可以无风险借贷、市场没有摩擦并且所有超额收益都能用一个beta解释。但这些假设在真实市场里几乎都不成立。最明显的问题是单用beta解释横截面收益时残差很大。你拿A股的数据去做回归经常发现beta不显著alpha却非常显著——这不是说选股能力厉害而是CAPM漏掉了风格因子。我最早跑日频回归时拿某只长期强势的股票做检验日收益对大盘指数收益回归beta只有0.7但alpha高达0.001且检验显著。当时还以为找到了“穿越牛熊”的神器后来发现它就是一只高账面市值比的价值型小盘股纯粹是风格收益。没有因子模型做基准很容易把风格暴露当成能力。1.2 规模效应和价值效应三因子模型的经验基础Fama和French在1992年、1993年的论文里做了大量横截面检验发现两个规律非常稳定市值较小的公司长期平均收益普遍高于市值较大的公司。账面市值比Book-to-MarketBM较高的公司也就是传统意义上的价值股长期平均收益普遍高于BM较低的成长股。于是他们把这两个效应量化成两个因子SMBSmall Minus Big小市值减大市值组合的收益差和HMLHigh Minus Low高BM减低BM组合的收益差。加上市场因子就构成了三因子模型。为什么会有规模溢价和价值溢价学界一直有争论。风险补偿派认为小公司盈利波动大、融资约束强价值股面临财务困境风险所以高收益是承担风险的补偿行为金融派则认为投资者过度外推成长股的业绩导致成长股被高估、价值股被低估。对做量化的人来说经济解释可以慢慢体会更重要的是把它当成一种可复现、可回测的风险暴露。1.3 三因子模型公式与回归方程三因子模型的完整表达式是[ E(R_i) - R_f \alpha_i \beta_{mkt} \times (R_m - R_f) \beta_{smb} \times SMB \beta_{hml} \times HML \varepsilon_i ]注意这里的alpha是扣掉市场、规模、价值三个风格后的剩余收益。如果原来的组合收益能被SMB和HML解释掉alpha就会变得不显著说明策略真正赚的是风格钱而不是模型无法解释的alpha钱。在实战里我们一般把再平衡周期设定为每月或每季度一次。但做研究时为了充分利用日频数据也可以用日频因子序列做回归。下面第二部分就讲因子序列怎么构造这是整个流程里最容易出错也是最核心的一环。2. 手把手构造SMB和HML分组方法、数据口径与Python实现2.1 数据准备行情、市值和账面市值比从哪里来要构造因子最少需要四个字段日期、股票代码、当日收益率、当日总市值。另外还需要一个用于计算账面市值比的股东权益数据。A股便利的做法是用akshare或tushare这类开源库拉行情和财务数据也可以直接用本地Wind或聚宽的导出数据。我的建议是无论用什么数据源先把最底层的数据处理成统一格式字段含义说明date交易日期统一成datetime类型stock_id股票代码例如600000.SHret日收益率必须是用复权价格计算的收益率mktcap总市值单位为元bm账面市值比股东权益 / 总市值股票列表记得剔除上市不足一年、长期停牌、以及退市股票的缺失区间。这里最容易忽略的是退市股票——如果只保留当前正常上市的公司后面的回测一定会带幸存者偏差历史收益被系统性高估。2.2 月度调仓与2x3分组逻辑Fama-French原文的构造方法可以简化总结为“2x3分组”。先按市值大小分成两组小市值S和大市值B分界点用中位数。再按账面市值比分成三组低BML成长股、中BMM、高BMH价值股分界点用30%和70%分位数。这样就得到六个组合SL、SM、SH、BL、BM、BH前一位是规模后一位是价值分组。所有组合收益都采用市值加权也就是组合内每只股票按市值占比加权平均。然后按如下公式计算SMB和HML[ SMB \frac{SL SM SH}{3} - \frac{BL BM BH}{3} ][ HML \frac{SH BH}{2} - \frac{SL BL}{2} ]SMB的含义是在三个BM组内部小市值组合相对大市值组合的收益差然后取平均剥离掉价值风格的影响。HML的含义同理在大小市值两个组内部高BM组合相对低BM组合的收益差取平均剥离掉规模风格的影响。2.3 计算市场因子、SMB和HML的Python代码下面这段代码是核心构造逻辑。假设你已经把数据清洗成前面表格里的格式放在DataFramedf里import pandas as pd import numpy as np def compute_factors(df, rf_daily0.02 / 252): df df.dropna(subset[ret, mktcap, bm]).copy() # 1. 用日期分组计算市值排名和BM排名 df[size_rank] df.groupby(date)[mktcap].rank(pctTrue) df[bm_rank] df.groupby(date)[bm].rank(pctTrue) # 2. 市值分组50% 为小市值 S50% 为大市值 B df[size_group] np.where(df[size_rank] 0.5, S, B) # 3. BM分组30% 为 L30%-70% 为 M70% 为 H df[bm_group] np.where( df[bm_rank] 0.3, L, np.where(df[bm_rank] 0.7, M, H) ) # 4. 组合标签 df[port] df[size_group] df[bm_group] # 5. 市值加权组合收益 def vw_return(g): w g[mktcap] / g[mktcap].sum() return float(np.dot(g[ret], w)) port_ret df.groupby([date, port]).apply(vw_return).unstack() # 6. 市场收益全市场市值加权实际研究建议用t-1市值做权重 df[mkt_w] df[mktcap] / df.groupby(date)[mktcap].transform(sum) mkt_ret df.groupby(date).apply( lambda g: float(np.dot(g[ret], g[mkt_w])) ) mkt_ret pd.Series(mkt_ret, indexport_ret.index, nameMKT) # 7. 构造因子 factors pd.DataFrame(indexport_ret.index) factors[MKT] mkt_ret - rf_daily factors[SMB] ( port_ret[[SL, SM, SH]].mean(axis1) - port_ret[[BL, BM, BH]].mean(axis1) ) factors[HML] ( port_ret[[SH, BH]].mean(axis1) - port_ret[[SL, BL]].mean(axis1) ) return factors, port_ret factors, port_ret compute_factors(df)有几个点要强调。第一vw_return用的是当日市值做权重严格来说会有未来函数因为当日收益率是用当日收盘价算的而当日市值也是收盘市值。更严谨的做法是把上一期市值作为权重。对日频数据我一般会先按股票和日期排序用groupby(stock_id)[mktcap].shift(1)生成前一日市值再算加权收益。第二如果某个组合当天没有股票port_ret会出现NaN最好用全市场等权收益补上否则因子序列会断。2.4 几个必须处理的口径细节第一个是财务数据的时点匹配。账面市值比里的股东权益必须使用已经公开披露的财报数不能用未来数据。A股上市公司年报一般在次年前四个月发布所以每年5月1日之后再使用上年年报数据是相对保守且安全的做法。如果非要提高时效性可以用一季报或三季报但一定要在财报发布日期之后才允许进入样本。第二个是复权。股票价格在除权除息后会有跳空如果直接用不复权价算收益率日收益率会出现巨大负值。研究用后复权价计算日收益比较稳妥因为后复权价能保证历史价格连续。前复权价虽然看盘方便但新股上市时可能出现负价格不太适合批量计算。第三个是ST、停牌和极端值。ST股票经常连续涨跌停流动性差且交易规则受限一般建议剔除。停牌股当天没有成交收益率应该填0而不是NaN或者直接删掉当天记录。极端收益率通常用缩尾winsorize处理比如把每天收益率在1%和99%分位数之外的样本拉回到边界值。3. 回归建模与结果解读为什么我的alpha突然不显著了3.1 选好被解释对象因子构造好之后接下来要回答的核心问题是某个策略组合的收益到底有多少来自三因子暴露多少来自真正的alpha。被解释对象可以是单个股票也可以是某个组合。我更建议先用组合而不是个股做回归因为个股的噪声太大回归R方经常不到0.1看不出规律。比如你有一个十只股票的小组合每天计算组合的等权或市值加权收益率然后统一减去无风险利率得到excess_ret再和前面构造的因子序列对齐。对齐时最常出现的问题是日期错位。因子序列用的是交易日组合收益可能有几天因为停牌或新加入股票导致缺失。用pd.concat([excess_ret, factors], axis1).dropna()是最简单的做法但要小心别把连续几天的数据都删光尤其是加密货币那种非交易日和数据源不一致的场景。3.2 用statsmodels跑OLS回归并做Newey-West调整金融时间序列的回归残差通常有自相关和异方差直接使用普通最小二乘的标准误会低估真实波动。statsmodels提供了HAC异方差自相关一致标准误选项实操中我用Newey-West调整滞后阶数通常取5或10。import statsmodels.api as sm # factors 是前面构造的三因子日频序列 # excess_ret 是要检验的组合超额收益 data pd.concat([excess_ret, factors], axis1).dropna() y data[excess_ret] X sm.add_constant(data[[MKT, SMB, HML]]) model sm.OLS(y, X).fit( cov_typeHAC, cov_kwds{maxlags: 5} ) print(model.summary())看到输出后第一件事看alpha的t值。t值的绝对值小于2通常说明alpha在统计上不显著t值显著且系数为正才说明策略有模型无法解释的正超额收益。第二件事看系数符号。beta_mkt一般应在0.8到1.2之间如果偏离太多说明组合本身风格很极端。beta_smb为正表示组合偏向小市值beta_hml为正表示组合偏向价值股。第三件事看整体R方。如果R方在0.8以上说明组合收益绝大部分能被三因子解释这时候还想追求超额alpha难度就很大。3.3 一张回归结果表怎么读我拿一段A股数据做过一个虚拟组合的回归结果大概是下面这样变量系数标准误t值P值const0.00040.00031.330.185MKT0.950.0247.500.000SMB0.680.0513.600.000HML0.210.045.250.000R方0.84F统计量显著。这个结果的意思是组合基准收益对市场很敏感同时大幅暴露在小市值上中等程度暴露在价值风格上。alpha不显著说明组合所谓的超额收益很可能来自小市值和价值暴露而不是选股能力。真正做策略评审时我会同时输出CAPM单因子回归和三因子回归的对比这样能很直观地看到加入SMB和HML之后alpha从显著变成不显著的过程。3.4 因子共线性问题VIF与正交化三因子内部也可能存在相关性尤其是市场因子和SMB。A股历史上小市值和整体行情经常同涨同跌导致MKT和SMB的相关性忽高忽低。回归前先看一下相关矩阵然后算方差膨胀因子from statsmodels.stats.outliers_influence import variance_inflation_factor factor_cols [MKT, SMB, HML] Xv data[factor_cols].copy() Xv[const] 1 vif pd.Series( [variance_inflation_factor(Xv.values, i) for i in range(len(factor_cols))], indexfactor_cols ) print(vif)VIF一般小于5都在可接受范围内。如果某个因子VIF超过10就要考虑是不是数据里有极端值或者样本期太短。更彻底的解法是对因子做正交化处理用SMB和HML分别对市场因子回归取残差但我个人不推荐一上来就正交化因为正交化会改变因子的经济含义而且不同顺序的正交化结果完全不同。4. 滚动窗口与多空组合回测三因子在A股的稳定性检验4.1 为什么单次回归不够金融市场不是稳态。一个组合的因子暴露和因子收益都会随着宏观环境、投资者结构、监管制度变化。单次拿全样本回归得到的平均暴露很可能会掩盖2017年之前小市值持续占优、2017年之后核心资产占优的风格切换。所以我会把样本切成滚动窗口比如每次用过去252个交易日约一年做回归然后往前滚动21天得到一条alpha和因子系数的时序曲线。这个做法的目的不是为了拟合更好看而是为了观察三点因子系数的符号有没有发生过翻转。alpha在哪个时间段突然变显著背后对应什么市场状态。组合的“打法”是否始终如一。4.2 滚动窗口回归的Python代码代码逻辑不复杂核心就是循环切片rolling_results [] for end in range(252, len(data) 1, 21): window data.iloc[end - 252:end] y window[excess_ret] X sm.add_constant(window[[MKT, SMB, HML]]) model sm.OLS(y, X).fit() rolling_results.append({ end_date: window.index[-1], const: model.params[const], mkt: model.params[MKT], smb: model.params[SMB], hml: model.params[HML], r2: model.rsquared, }) df_rolling pd.DataFrame(rolling_results).set_index(end_date)跑完以后我会先画三条系数曲线再画alpha系数的t值曲线。如果t值长时间超过2可能是市场结构变化也可能是被解释组合自身漂移了。比如一个持仓中小盘成长的策略在行情转向价值风格后hml系数会从负转正同时alpha可能一落千丈。4.3 用SMB/HML构造多空组合三因子里的SMB和HML本身就是多空组合的收益序列可以直接作为策略回测。比如SMB就是做多小市值组合、做空大市值组合的每日收益。做多空组合的累计收益曲线能直观看出某种风格在历史上的赚钱效应。cum_smb (1 factors[SMB]).cumprod() cum_hml (1 factors[HML]).cumprod() # 你可以继续计算年化、夏普、最大回撤 annual_return cum_smb.iloc[-1] ** (252 / len(cum_smb)) - 1 sharpe factors[SMB].mean() / factors[SMB].std() * np.sqrt(252) max_drawdown (cum_smb / cum_smb.cummax() - 1).min()回测结果出现大幅波动时不要急着下结论“因子失效”。先拆成时间段看尤其在A股小市值因子历史上长期有效但在2017年和2021年之后出现过明显的回撤期。用三因子模型做基准和做风格轮动是两回事前者只需要识别暴露后者还要预测风格切换难度完全不在一个量级。4.4 从回测结果反推模型问题如果回测里SMB多空收益年化很高但只在特定年份集中贡献大概率是因为A股散户占比高、壳价值长期存在小市值因子里有大量“垃圾股溢价”。这类因子在注册制推进、退市常态化之后有效性会下降。更合理的做法是检查多空组合每个月的胜率和相对大市值组合的超额收益分布而不是只看累计净值。5. 最容易翻车的五个细节财务滞后、复权、共线性和幸存者偏差5.1 财务数据时间滞后——前视偏差这是新手最容易踩的坑。账面市值比里的“账面价值”取哪个季度的财报必须严格滞后。我见过有人直接把最新财报的股东权益和当日股价合并结果模型历史回测极好实盘却崩了原因就是用了未来数据。正确做法有两种一是每年5月1日之后统一使用上年年报数据中间不更新二是用merge_asof按财报发布日期做向后匹配保证当前交易日在财报公布之后才使用新数据。5.2 复权问题对因子收益的影响如果你用不复权价算日收益率遇到除权除息日会出现虚假的-10%或-30%收益市值加权组合可能会因此产生巨大回撤。用前复权价算历史收益率则存在未来复权因子的问题——每次除权后整体历史价格都会重新调整导致同一段历史收益在不同时间算出来不一样。所以我更推荐用后复权价计算收益率序列因为它不改变历史价格相对关系而且每次除权只是向后延续研究一致性更好。5.3 极端值和NaN怎么处理市值、BM和收益率都会有极端值。市值和BM最好先取对数再按MAD中位数绝对偏差缩尾避免几家超大盘公司或财务指标异常的公司主导分组。收益率极端值通常是因为数据源出现了除权价格未修正、停牌后复牌补跌等情况缩尾阈值为1%到99%分位数已经够用。处理NaN时组合日收益最好别用0填充而是用全市场等权收益临时顶替否则会低估真实波动。5.4 幸存者偏差隐形收益放大器做多因子研究时如果用“当前还在交易的股票”回溯到五年前那么已经退市的股票就完全消失了。而退市的往往是亏损、财务造假或经营恶化的股票它们的负收益没被统计进去历史策略收益自然会被高估。解决方法是在每一个历史时点使用当时还存续的全部股票包括后来退市的股票直到它的最后一个交易日。很多公开数据源不方便拿退市股历史数据但量化研究里这一步不能省。5.5 因子收益和策略收益不能循环论证最后说一个逻辑问题。如果你用三因子模型去检验一个“小市值价值策略”而这个小市值价值策略本身就是用构成SMB和HML的同一批股票选出来的那回归结果当然显显著。这不是模型有问题而是你的检验对象和因子之间有天然重叠。更严谨的做法是用样本外股票或剔除了重合股票后的组合做检验或者先设定好规则再跑因子不要反复调参直到回归显著。我在实际项目中已经把三因子回归作为所有股票多头策略的标配基准。每次调完仓位先输出三因子回归摘要再决定是否继续优化alpha。如果你也想复现建议先用一个小样本跑通流程比如取50只股票、一年日频数据构造因子加回归然后再扩大到全市场。分组代码、回归代码都可以直接复用最容易出现问题的永远不是算法而是数据口径。祝顺利。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进