当量化投资遇上Python,那些看似高深的金融模型突然变得触手可及。今天我们要做的,就是把诺贝尔奖级别的Fama-French三因子模型(FF3)从学术论文中解放出来,变成你Jupyter Notebook里可运行、可修改、可扩展的活代码。这不是一篇充满数学公式的理论文章,而是一份"所见即所得"的实战手册——我们会用中国A股市场真实数据,一步步演示如何用Python实现这个经典模型。
很多量化新手第一次接触FF3模型时,会被各种专业术语吓到:市场风险溢酬因子(Rmt)、市值因子(SMB)、账面市值比因子(HML)、阿尔法、贝塔...但实际上,这些听起来高大上的概念,本质上就是统计学中最基础的多重线性回归。
模型本质拆解:
用Python代码表示,模型其实就是:
python复制from statsmodels.formula.api import ols
model = ols("股票收益 ~ Rmt + SMB + HML", data=df).fit()
让我们用更"程序员友好"的方式理解这三个因子:
| 因子 | 金融定义 | Python类比 | 经济意义 |
|---|---|---|---|
| Rmt | 市场组合超额收益 | 大盘指数收益率 - 无风险利率 | 系统风险暴露 |
| SMB | 小市值股票组合收益 - 大市值组合收益 | df[small_cap].mean() - df[large_cap].mean() | 规模效应溢价 |
| HML | 高账面市值比组合收益 - 低比值组合收益 | df[high_bm].mean() - df[low_bm].mean() | 价值效应溢价 |
提示:在实际操作中,我们通常直接使用学术机构计算好的因子数据,避免自己构建投资组合的复杂过程。
国内常用的免费因子数据来源包括:
本文使用央财数据示范,其日频数据包含以下字段:
python复制import pandas as pd
factors = pd.read_csv('fivefactor_daily.csv',
index_col='trddy',
parse_dates=['trddy'])
print(factors.columns)
# 输出示例:
# Index(['mkt_rf', 'rf', 'smb', 'hml', 'rmw', 'cma'], dtype='object')
使用baostock获取个股数据并计算对数收益率:
python复制import baostock as bs
import numpy as np
# 登录baostock
lg = bs.login()
# 获取贵州茅台(600519)日线数据
rs = bs.query_history_k_data_plus(
"sh.600519",
fields="date,close",
start_date='2021-01-01',
end_date='2022-12-31',
frequency="d",
adjustflag="3")
# 转换为DataFrame
k_data = rs.get_data()
k_data = k_data.set_index('date')
k_data['close'] = k_data['close'].astype(float)
# 计算对数收益率
k_data['return'] = np.log(k_data['close'] / k_data['close'].shift(1))
k_data = k_data.dropna()
bs.logout()
将因子数据与个股数据按日期对齐:
python复制from datetime import datetime
# 确保日期范围一致
start_date = max(factors.index.min(), k_data.index.min())
end_date = min(factors.index.max(), k_data.index.max())
# 数据合并
merged_data = pd.merge(
factors.loc[start_date:end_date, ['mkt_rf', 'smb', 'hml']],
k_data['return'],
left_index=True,
right_index=True
)
# 重命名列
merged_data = merged_data.rename(columns={
'mkt_rf': 'Rmt',
'return': 'stock_return'
})
python复制import statsmodels.api as sm
# 添加常数项(阿尔法)
X = sm.add_constant(merged_data[['Rmt', 'smb', 'hml']])
y = merged_data['stock_return']
# 拟合模型
model = sm.OLS(y, X).fit()
# 输出结果摘要
print(model.summary())
典型回归结果输出示例:
code复制 OLS Regression Results
==============================================================================
Dep. Variable: stock_return R-squared: 0.487
Model: OLS Adj. R-squared: 0.481
Method: Least Squares F-statistic: 89.34
Date: Mon, 01 Jan 2023 Prob (F-statistic): 1.23e-38
Time: 12:00:00 Log-Likelihood: 678.91
No. Observations: 487 AIC: -1348.
Df Residuals: 483 BIC: -1331.
Df Model: 3
Covariance Type: nonrobust
==============================================================================
coef std err t P>|t| [0.025 0.975]
------------------------------------------------------------------------------
const 0.0002 0.000 0.785 0.433 -0.000 0.001
Rmt 0.8241 0.049 16.689 0.000 0.727 0.921
smb -0.3021 0.088 -3.419 0.001 -0.475 -0.129
hml 0.4583 0.079 5.774 0.000 0.302 0.614
==============================================================================
如何判断模型效果和因子显著性:
系数显著性:
模型解释力:
因子影响方向:
绘制各因子与收益率的散点矩阵图:
python复制import seaborn as sns
import matplotlib.pyplot as plt
sns.pairplot(merged_data,
vars=['stock_return', 'Rmt', 'smb', 'hml'],
kind='reg',
plot_kws={'line_kws':{'color':'red'}})
plt.show()
计算并可视化相关系数矩阵:
python复制corr_matrix = merged_data.corr()
sns.heatmap(corr_matrix,
annot=True,
cmap='coolwarm',
center=0)
plt.title('收益率与三因子相关系数矩阵')
plt.show()
封装分析流程为可重用函数:
python复制def analyze_ff3(stock_code, start_date, end_date):
"""三因子模型分析流水线"""
# 获取股票数据
stock_data = get_stock_data(stock_code, start_date, end_date)
# 获取因子数据
factors = get_factor_data(start_date, end_date)
# 数据合并与清洗
merged_data = merge_and_clean(stock_data, factors)
# 回归分析
model = run_ff3_regression(merged_data)
# 计算绩效指标
metrics = calculate_performance(stock_data)
return {
'coefficients': model.params.to_dict(),
'metrics': metrics,
'summary': model.summary2().tables[1]
}
实现常见的量化评价指标:
python复制def calculate_sharpe_ratio(returns, rf=0.000041):
"""计算年化夏普比率"""
excess_returns = returns - rf
return np.sqrt(252) * excess_returns.mean() / excess_returns.std()
def max_drawdown(prices):
"""计算最大回撤"""
peak = prices.expanding(min_periods=1).max()
drawdown = (peak - prices) / peak
return drawdown.max()
# 应用示例
sharpe = calculate_sharpe_ratio(merged_data['stock_return'])
mdd = max_drawdown(k_data['close'])
三因子模型可以进一步扩展为:
Carhart四因子模型:
收益 ~ Rmt + SMB + HML + UMDFama-French五因子模型:
收益 ~ Rmt + SMB + HML + RMW + CMA行业中性化处理:
python复制from sklearn.preprocessing import StandardScaler
# 对因子进行行业调整
def adjust_by_industry(factors, industry_dummies):
scaler = StandardScaler()
adjusted = factors.copy()
for col in factors.columns:
model = sm.OLS(factors[col], industry_dummies).fit()
adjusted[col] = scaler.fit_transform(model.resid.values.reshape(-1,1))
return adjusted
问题场景:
解决方案:
python复制# 前向填充停牌日数据
merged_data = merged_data.asfreq('D').ffill()
# 或者只保留共同交易日
merged_data = merged_data.dropna()
可能原因及对策:
因子数据质量问题:
股票特性不符:
时间区间选择:
检测方法:
python复制from sklearn.model_selection import TimeSeriesSplit
from sklearn.metrics import mean_squared_error
# 时间序列交叉验证
tscv = TimeSeriesSplit(n_splits=5)
mse_scores = []
for train_idx, test_idx in tscv.split(X):
X_train, X_test = X.iloc[train_idx], X.iloc[test_idx]
y_train, y_test = y.iloc[train_idx], y.iloc[test_idx]
model = sm.OLS(y_train, X_train).fit()
pred = model.predict(X_test)
mse_scores.append(mean_squared_error(y_test, pred))
print(f'交叉验证MSE: {np.mean(mse_scores):.6f}')
当模型表现良好准备实盘时,还需要考虑:
交易成本影响:
python复制# 考虑手续费后的净收益
def net_return(gross_return, turnover, fee=0.0003):
return gross_return - turnover * fee
因子衰减检验:
python复制# 滚动回归检验因子稳定性
rolling_betas = pd.DataFrame()
window_size = 60 # 3个月滚动窗口
for i in range(window_size, len(merged_data)):
roll_data = merged_data.iloc[i-window_size:i]
model = sm.OLS(roll_data['stock_return'],
sm.add_constant(roll_data[['Rmt','smb','hml']])).fit()
rolling_betas = rolling_betas.append(model.params, ignore_index=True)
rolling_betas.plot(title='滚动回归系数变化')
风险控制模块:
python复制# 简单的波动率控制
def volatility_adjusted_position(target_vol,
predicted_vol,
current_position):
leverage = target_vol / predicted_vol
return current_position * leverage
在实盘前,建议至少进行: