
刚从CAPM跑到Fama-French三因子模型我花了整整两周才把整个逻辑链路跑通。这篇不是教科书复述是我用Python从零实现两个经典资产定价模型的全过程记录。里面既有回归代码和因子构建方法也有我踩过的数据坑、显著性不通过时的排查思路希望能帮你少走点弯路。先说清楚这篇文章适合谁看你如果刚接触量化金融对CAPM和Fama French Three Factor model这两个名字耳熟但没真正手写过代码或者你已经会调包跑回归但不清楚结果怎么解读、因子怎么构建——那这篇内容正好对你的胃口。我会先快速过一遍两个模型的核心逻辑然后重点讲怎么用Python拿到数据、构建因子、跑回归以及怎么判断模型到底行不行。我默认你已经有Python基础环境Anaconda或者官方Python都行pandas、numpy、statsmodels这几个库装好了。如果你的环境还没准备好这部分我后面也会给出一条龙安装说明。但咱们先说模型本身。1. 模型思路拆解CAPM和Fama-French三因子到底在干什么1.1 CAPM一个因子解释一切CAPM的核心逻辑一句话就能说清楚——资产的预期收益率只跟它对市场组合的敏感程度有关。这个敏感程度就是beta而市场组合的收益率减去无风险利率就是市场风险溢价。公式长这样E(Ri) Rf βi × (E(Rm) - Rf)拆开看Ri是资产i的收益率Rf是无风险利率Rm是市场组合收益率βi是资产i相对市场组合的敏感度。数学上我们很少直接算期望而是用历史数据做时间序列回归Ri - Rf α β × (Rm - Rf) ε如果你拿真实数据去跑这条回归会发现很多股票和市场指数回归后的alpha截距项显著不为零。这说明市场组合这一个因子解释不了所有收益率差异。这就是CAPM最常被诟病的地方。我最早拿到几只用万得数据算出来的股票回归结果发现不少股票的超额收益和市场超额收益之间的关系很弱R方甚至不到0.1当时就意识到光靠CAPM想解释A股是远远不够的。1.2 Fama-French三因子补上规模和估值两个维度Fama和French在1992年的论文里做的事其实就是在CAPM的基础上加了两个因子——规模因子SMB和价值因子HML。他们发现市值小的公司收益率普遍高于市值大的公司账面市值比高的公司收益率普遍高于账面市值比低的公司而且这两个规律在CAPM框架下无法被beta解释。于是三因子模型的回归方程变成Ri - Rf α β1 × (Rm - Rf) β2 × SMB β3 × HML ε这里SMBSmall Minus Big做多小市值股票、做空大市值股票的组合收益率衡量规模效应HMLHigh Minus Low做多高账面市值比股票、做空低账面市值比股票的组合收益率衡量价值效应加入这两个因子后模型的R方通常会有明显提升。如果你用A股市场的数据来做比如拿中证500指数或者创业板指跑一下三因子模型的R方一般会在0.8以上而单用CAPM很可能只有0.5到0.7。原因是A股市场小市值效应和价值效应长期存在尤其在早年壳价值高企的年代SMB因子的解释力非常强。1.3 为什么学这两个模型比学复杂模型更值我见过不少新手一上来就学机器学习选股、LSTM预测股价但连CAPM都没跑通过。这是个误区——深度学习模型可以记住历史规律却不会告诉你一个beta为1.2的股票在牛市里会放大多少涨幅、在熊市里又会放大多少跌幅。CAPM和三因子模型是整个资产定价的基础语法。你学会了这套方法后面再看Carhart四因子加入动量因子UMD、Fama-French五因子加入盈利因子RMW和投资因子CMA就是同一套逻辑框架里的加变量问题。回归流程、显著性判断、因子构建方法全都是一样的套路。而且这两个模型是金融学术界被验证次数最多的模型数据来源公开、计算标准明确不像机器学习模型那样有很多调参黑盒。你跑出来的beta、SMB系数、t值可以直接和学术论文里的结果对比方便检验自己哪里做错了。2. Python环境准备一次性配好省得来回折腾2.1 Python环境安装如果你的电脑上还没有Python环境我建议直接用Anaconda。它自带pandas、numpy、matplotlib、statsmodels这些库省去一个个装的麻烦。去官网下载对应你操作系统的安装包一路点Next就行。装完后打开Anaconda Prompt或者命令行输入python --version看到Python 3.8以上的版本号就说明装成功了。这里提醒一点我之前在Windows上遇到过一个大坑官网下载的Python装完后命令行输入python却提示“Python was not found; run without arguments to install from the Microsoft Store”。这是因为Windows自带的应用程序执行别名把命令拦截了。解决办法是到 设置 → 应用 → 应用执行别名把“python.exe”和“python3.exe”两个选项都关掉然后再试一次。Linux环境下安装更简单sudo apt-get update sudo apt-get install python3 python3-pipmacOS建议直接装Anaconda避免手动管理编译器工具链的麻烦。2.2 必要的库安装我建议用国内镜像源安装速度快到飞起。直接执行pip install pandas numpy statsmodels matplotlib requests -i https://pypi.tuna.tsinghua.edu.cn/simple每个库的作用pandas处理表格型数据是金融数据分析的核心numpy科学计算底层库statsmodels正统的统计回归模型库OLS、t检验、R方都有现成接口matplotlib数据可视化画收益率曲线和残差图requests发送HTTP请求从数据源拉取数据如果你的环境是Anaconda前三个可能已经预装了可以用pip list | grep pandas确认一下。2.3 写好一个最小的验证脚本很多时候环境装完不一定真的能用我习惯先跑一个最小脚本验证整个链路import pandas as pd import numpy as np import statsmodels.api as sm # 生成模拟数据验证回归流程 np.random.seed(42) n 500 market np.random.randn(n) * 0.02 smb np.random.randn(n) * 0.01 hml np.random.randn(n) * 0.01 stock 1.2 * market 0.5 * smb 0.3 * hml np.random.randn(n) * 0.01 df pd.DataFrame({ MKT: market, SMB: smb, HML: hml, STOCK: stock }) X df[[MKT, SMB, HML]] X sm.add_constant(X) y df[STOCK] model sm.OLS(y, X).fit() print(model.summary())如果这段代码能跑通并且打印出回归结果表格你的环境就基本就绪了。3. 数据获取与处理金融分析的最脏最累部分3.1 数据源选择搞金融数据分析数据是第一关。学术研究一般用CRSP和Compustat数据库但收费昂贵。个人学习我推荐用akshare这个免费开源库它封装了多个公开数据源的接口可以拿到A股行情、财务报表、指数行情等数据。安装pip install akshare -i https://pypi.tuna.tsinghua.edu.cn/simpleakshare不需要API密钥直接调用函数接口。不过接口有时会因上游网站改版而变化用之前最好去GitHub上的文档确认一下最新用法。3.2 算收益率一个容易被忽略的坑拿到股票价格数据后必须先把价格转成收益率。很多人一上来就用df[close].pct_change()直接算这在研究里一般没有什么问题但如果考虑分红送股这个简单计算就会出误差。更稳妥的做法是用后复权价格akshare里可以取到复权因子这样算出来的收益率才真实反映投资回报。收益率计算还有一个隐藏细节用什么频率的数据。CAPM和FF三因子模型的标准做法是用月度收益率数据这样能减少非同步交易带来的噪声也和Fama-French原始论文的设定一致。但A股历史只有30多年月度数据点不过三四百个。为了增加样本量很多国内研究会用周度甚至日度数据。日度数据的噪声很大特别是A股有涨跌停板限制某些股票连续涨停时收益率严重偏离真实值回归结果可能失真。我的建议是做练习用周度数据兼顾样本量和数据质量。3.3 用akshare拉取股票和指数数据import akshare as ak import pandas as pd import numpy as np import datetime # 拉取股票日线数据以平安银行为例 start_date 20180101 end_date 20231231 stock_df ak.stock_zh_a_hist( symbol000001, perioddaily, start_datestart_date, end_dateend_date, adjustqfq ) # 拉取沪深300指数作为市场组合代理 index_df ak.stock_zh_index_daily( symbolsh000300 )这里有个需要注意的地方——如果用的是日线数据后续要重采样成周度。pandas有一个重采样方法# 将日期列转为datetime类型 stock_df[日期] pd.to_datetime(stock_df[日期]) stock_df stock_df.set_index(日期) # 每周最后一个交易日取收盘价再算周收益率 weekly_close stock_df[收盘].resample(W).last() weekly_ret weekly_close.pct_change().dropna()利率方面无风险利率可以用一年期国债收益率也可以简化处理用shibor。akshare里有ak.bond_zh_us_rate()接口能直接取中国国债收益率。在数据分析里无风险利率的精度影响其实不大因为月度无风险收益率就千分之几和股票波动的量级不在一个层级上。3.4 构建SMB和HML因子教科书公式落地构建Fama-French三因子是最容易出错的环节。学术标准做法是每年按市值分成大小两组按账面市值比分成高、中、低三组然后六组组合交叉再算两个因子组合的加权收益率。我没法直接用akshare一键算出SMB和HML因为需要市值和财务数据。这里给出一个适用于A股的简化构建方法每月末按股票总市值排序取前50%作为大市值组B后50%作为小市值组S每月末按市净率PB倒数作为账面市值比的近似取前30%作为高价值组H后30%作为低价值组L中间40%作为中价值组M六组组合按市值加权月收益率分别计算SMB 1/3×(S/H S/M S/L) - 1/3×(B/H B/M B/L)HML 1/2×(S/H B/H) - 1/2×(S/L B/L)这一段公式看起来很枯燥但理解了它的逻辑就通了SMB是控制住价值分组之后小市值组合和大市值组合的收益率差HML是控制住市值分组之后高账面市值比组合和低账面市值比组合的收益率差。构造逻辑本质上是把单个股票层面的特征转成了组合层面的收益序列用来代表某一种风格溢价。代码实现的核心结构# 假设每月都有市值和PB数据 def calc_smb_hml(monthly_data): monthly_data: DataFrame包含股票代码、月度收益率、市值、PB 返回当月的SMB和HML值 # 按市值排序分组 median_size monthly_data[market_cap].median() monthly_data[size_group] np.where(monthly_data[market_cap] median_size, B, S) # 按PB倒数排序分组 monthly_data[bm] 1 / monthly_data[pb] low_30 monthly_data[bm].quantile(0.3) high_70 monthly_data[bm].quantile(0.7) monthly_data[value_group] np.where( monthly_data[bm] high_70, H, np.where(monthly_data[bm] low_30, L, M) ) # 六组市值加权收益率 group_ret monthly_data.groupby([size_group, value_group]).apply( lambda x: np.average(x[return], weightsx[market_cap]) ) smb (group_ret[(S,H)] group_ret[(S,M)] group_ret[(S,L)]) / 3 - \ (group_ret[(B,H)] group_ret[(B,M)] group_ret[(B,L)]) / 3 hml (group_ret[(S,H)] group_ret[(B,H)]) / 2 - \ (group_ret[(S,L)] group_ret[(B,L)]) / 2 return smb, hml实际操作时数据量大建议用循环或groupby逐月处理。因子构建和学术论文保持一致口径很重要因为Fama-French因子本身就是针对特定市场构建的你换一个市场就要重新构建。国内很多券商研究所对A股SMB和HML因子的构建方法和Fama原文略有差异主要体现在分组比例和加权方式上这会导致因子值不完全一样但分析结论通常大同小异。4. 回归实现从计算beta到完整三因子模型4.1 单因子回归手动算出CAPM的beta环境就绪、因子数据也构建好之后就可以进入正题了。我用沪深300指数的周度收益率作为市场组合代理以贵州茅台作为标的股票先跑一个最简单的CAPM回归。为什么选茅台因为它是A股权重股、流动性好日内非同步交易问题小回归结果比较干净。import statsmodels.api as sm # 假设准备好以下Seriesindex为时间 # ret_stock : 股票周收益率 # ret_market : 市场指数周收益率 # rf : 无风险利率按周折算 # 计算超额收益率 exret_stock ret_stock - rf exret_market ret_market - rf # CAPM回归解释变量只有市场超额收益 X sm.add_constant(exret_market) model_capm sm.OLS(exret_stock, X).fit() print(model_capm.summary()) beta model_capm.params[MKT] alpha model_capm.params[const] print(fbeta {beta:.4f}, alpha {alpha:.4f})跑完你会发现beta大概在0.8到0.9之间。茅台的波动天生比大盘小一档这是消费白马股的共性。alpha如果为负说明这段时间持有茅台承担的市场风险没有换来足够的超额补偿——这有可能是因为同期白酒板块整体处于估值下杀阶段。这里的关键是sm.add_constant()这一步很多人忘了加截距项结果回归出来的斜率是从原点硬拟合的完全错了。statsmodels的OLS不会自动帮你加常数项这一步必须显式操作。4.2 因子标准化处理量纲对齐避免误导在跑多因子回归之前有一步重要预处理容易被忽视——因子标准化。SMB和HML因子的原始数值很小周度收益率通常不到1%和MKT因子的量级差不多所以这里不标准化问题也不大。但如果后续扩展到五因子模型盈利因子和投资因子的构建方式不同数值量级差异可能很大不标准化会导致回归系数难以横向比较。标准化的方法可以用z-score即对每个时间序列减去均值再除以标准差def zscore(series): return (series - series.mean()) / series.std() df[MKT_std] zscore(df[MKT]) df[SMB_std] zscore(df[SMB]) df[HML_std] zscore(df[HML])标准化之后的回归系数反映的是“因子变动一个标准差时资产超额收益率的变动量”这在对比不同因子的经济含义时很直观。不过要注意如果因子值本身就是收益率形式SMB和HML本质上是组合收益率学术论文一般不做标准化因为系数可以直接读作因子暴露度。做不做标准化取决于你的研究对象——如果你只关心一个资产在某个因子上的载荷也就是beta直接用原始数据回归即可如果你拿这个模型做横截面选股标准化会让因子之间更可比。4.3 三因子回归一次回归看清所有代码结构和单因子几乎一样只是解释变量从1个变成3个# 准备三因子回归的数据 X3 sm.add_constant(df[[MKT, SMB, HML]]) model_ff3 sm.OLS(exret_stock, X3).fit() print(model_ff3.summary())看回归结果时按重要程度排序R方三因子模型对个股收益率的解释程度。个股层面R方通常在0.6到0.9之间指数层面通常更高三个因子系数的t值看p值是否小于0.05衡量因子暴露度是否显著截距alpha三因子模型下alpha的t值如果显著说明资产有定价异象模型没能解释它举例说明如果茅台跑三因子回归后SMB系数是个负的大数说明它是典型的大盘股HML系数可能不显著或者为负反映出茅台到底算成长股还是价值股是有争议的——它的账面市值比不高市场更愿意按品牌溢价给它估值。4.4 稳健性检验样本期敏感性检查单次回归跑完别急着下结论。金融模型最怕的是结论只对特定样本期成立。我之前拿2018年到2020年的数据跑某个消费股三因子模型的alpha显著为正换成2021年到2023年数据再跑alpha变成了负的。同一只股票不同时间段逻辑完全变了。稳健性检验的标准做法是分两个子样本期重新回归比如以2020年为界对比两段回归的系数符号和显著性如果某个因子系数只在特定时期显著结论就要打折扣在报告结果时我习惯把分样本回归结果附在后面说明这个因子暴露度是不是时间稳定的。5. 结果解读与常见问题排查5.1 怎样判断模型拟合得好不好三因子模型的拟合优度可以从几个维度判断。R方是最直观的指标但个股层面我不追求过高的R方——单只股票的独特波动太大市场因子、规模因子、价值因子能解释60%已经相当不错。如果你拿的是行业指数或者宽基指数R方通常在0.9以上。还有一个重要指标是回归残差的自相关。金融时间序列数据通常有自相关OLS回归的系数估计仍然是一致的但标准误可能偏小导致t值虚高。解决办法是使用Newey-West调整标准误from statsmodels.stats.sandwich_covariance import cov_hac model_ff3 sm.OLS(exret_stock, X3).fit(cov_typeHAC, cov_kwds{maxlags: 5}) print(model_ff3.summary())用HAC标准误后会发现部分t值略降但核心结论一般不变。5.2 数据频率要统一不然全是垃圾最常见的错误是无风险利率用年化的4%市场收益用日收益率然后直接相减。日度无风险收益率大约是4%/252≈0.016%这个量级影响很小但如果你把无风险利率当作常数一年4%直接减那日度超额收益率就全错了。正确做法是把无风险利率换算成与数据频率一致的周期——日度除以252周度除以52月度除以12。另一个数据坑是时间对齐。用akshare拉日线数据如果股票停牌某些日期就会缺失。使用resample(W)重采样后不同股票的对齐日期可能不一致建议统一用市场的交易日历做左连接all_dates ak.tool_trade_date_hist_sina() trade_dates pd.to_datetime(all_dates[trade_date]) df df.reindex(trade_dates).dropna()5.3 因子t值不显著怎么办先把数据检查一遍遇到因子的t值不显著先按顺序排查第一检查因子值是不是构建错了。SMB和HML的值如果小到10的负几次方级别大概率是分组逻辑出了问题。打印几行因子值看看量级是否符合常识。第二检查股票代码是否对应正确的行业和板块。比如拿科创板股票和沪深300指数做对比风格不匹配导致因子载荷奇怪很正常。第三检查时间窗口。A股在特定年份风格非常极端比如2020年是核心资产大年大市值因子暴露高的股票表现特别好2021年到2023年小盘风格占优SMB因子强度完全不同。5.4 回归结果的格式整理一页纸说清三行代码的结论整理结果时我习惯用一个简洁的表格输出指标CAPMFF三因子市场因子系数0.85***0.82***SMB系数-0.35**HML系数--0.28*截距alpha0.00120.0008R方0.420.57星号代表显著性水平**表示p0.01**表示p0.05*表示p0.1。在学术报告或代码注释里这是一个专业、简洁的展示方式。6. 我的几点实操体会代码跑通不算完真正有价值的是理解结果背后的经济含义。第一次跑通CAPM回归时看到beta等于1.5的股票在熊市里跌得像自由落体我才真正理解了风险溢价这个概念不是教材上一个干巴巴的公式而是真金白银的代价。对于想继续深挖的朋友我给三个方向建议一是把样本从单一股票扩展到某几个行业指数对比不同行业的因子暴露差异。你会发现消费行业的HML系数普遍为负而银行地产的HML系数显著为正这比单看一只股票能学到更多。二是尝试加入第四个动量因子构建Carhart四因子模型。动量因子用过去12个月剔除最近1个月的累计收益率排序构建逻辑和三因子构建方式完全类似代码改动量很小。三是把三因子模型用到投资组合上。比如你构建了一个股票池的组合每周算组合的市值加权收益率再去跑三因子回归看组合的alpha是不是显著为正。如果显著为正说明组合相对因子模型产生了超额收益——这就是量化投资评估的底层逻辑。我最后想说的是这些模型在学术圈已经被讨论了几十年但自己动手跑一遍才能真正理解它们的能力边界。CAPM的beta不是上帝给的常数而是特定时期、特定市场下的统计产物SMB和HML因子也不是万能钥匙它们自身的构建方式就决定了它们只能捕捉特定的风险溢价维。你拿Python跑出来的每一个数字都应该问一句——这个数字背后对应的经济和金融逻辑是什么。带着这个问题去学习会比盲目堆模型参数有效得多。