用Python实战拆解Fama-French模型:从数据获取到策略回测的全流程指南

当我在第一次接触Fama-French三因子模型时,面对那些晦涩的学术论文和复杂的数学公式,最大的困惑是:这些理论到底如何转化为可执行的量化策略?本文将通过Python代码,带你一步步实现从数据获取、因子构建到策略回测的完整流程,让你真正掌握因子投资的核心逻辑。

1. 环境准备与数据获取

在开始因子建模前,我们需要搭建Python分析环境。推荐使用Anaconda创建独立环境:

conda create -n factor_investing python=3.8
conda activate factor_investing
pip install pandas numpy statsmodels matplotlib tushare akshare

数据源选择是因子投资的第一步。对于A股市场,Tushare和AKShare是两个常用的免费数据接口。以下是使用Tushare获取股票基础数据的示例:

import tushare as ts

pro = ts.pro_api('你的token')
# 获取全A股列表
stock_list = pro.stock_basic(exchange='', list_status='L')
# 获取日线行情
df_daily = pro.daily(ts_code='600519.SH', start_date='20200101', end_date='20221231')

提示:在实际应用中,建议将获取的数据存储到本地数据库(如SQLite)中,避免重复请求接口。

完整的因子分析需要以下几类数据:

  • 价格数据:日/周/月收益率
  • 财务数据:市值、账面市值比(BM)、盈利指标等
  • 宏观经济数据:无风险利率、市场收益率

我们可以用Pandas构建统一的数据处理管道:

def clean_finance_data(raw_df):
    """财务数据清洗函数"""
    df = raw_df.copy()
    # 处理缺失值
    df = df[df['total_mv'].notna()]
    # 计算BM比率
    df['bm_ratio'] = df['total_share'] * df['navps'] / df['total_mv']
    return df

2. 因子构建与组合排序

Fama-French模型的核心在于三个因子:市场因子(MKT)、规模因子(SMB)和价值因子(HML)。让我们看看如何用Python构建这些因子。

2.1 市值因子(SMB)构建

市值因子通过做多小市值股票、做空大市值股票构建。具体步骤如下:

  1. 每月末根据流通市值将股票分为三组:小(S)、中(M)、大(B)
  2. 计算S组和B组的等权平均收益率
  3. SMB = (S组收益率 - B组收益率)
def build_smb_factor(data):
    """构建SMB因子"""
    # 按市值三分位分组
    data['size_group'] = data.groupby('trade_date')['total_mv'].transform(
        lambda x: pd.qcut(x, 3, labels=['S', 'M', 'B']))
    
    # 计算各组收益率
    group_ret = data.groupby(['trade_date', 'size_group'])['ret'].mean().unstack()
    smb = group_ret['S'] - group_ret['B']
    return smb

2.2 价值因子(HML)构建

价值因子通过做高BM股票、做空低BM股票构建:

  1. 每月末根据BM比率将股票分为三组:高(H)、中(M)、低(L)
  2. 计算H组和L组的等权平均收益率
  3. HML = (H组收益率 - L组收益率)
def build_hml_factor(data):
    """构建HML因子"""
    # 排除BM为负的股票
    valid_data = data[data['bm_ratio'] > 0].copy()
    
    # 按BM三分位分组
    valid_data['bm_group'] = valid_data.groupby('trade_date')['bm_ratio'].transform(
        lambda x: pd.qcut(x, 3, labels=['L', 'M', 'H']))
    
    # 计算各组收益率
    group_ret = valid_data.groupby(['trade_date', 'bm_group'])['ret'].mean().unstack()
    hml = group_ret['H'] - group_ret['L']
    return hml

2.3 双重排序与组合构建

为了提高因子纯度,实践中常采用双重排序法。以下代码展示如何同时按市值和BM进行分组:

def double_sort_portfolio(data, var1, var2, n_groups=3):
    """双重排序构建投资组合"""
    # 第一次排序
    data['group1'] = data.groupby('trade_date')[var1].transform(
        lambda x: pd.qcut(x, n_groups, labels=False))
    
    # 第二次排序
    data['group2'] = data.groupby(['trade_date', 'group1'])[var2].transform(
        lambda x: pd.qcut(x, n_groups, labels=False))
    
    # 计算组合收益率
    port_ret = data.groupby(['trade_date', 'group1', 'group2'])['ret'].mean()
    return port_ret.unstack().unstack()

3. 模型检验与统计分析

构建完因子后,我们需要检验这些因子是否真的能解释股票收益。Fama-MacBeth回归是常用的检验方法。

3.1 时间序列回归

首先对每只股票进行时间序列回归,估计因子暴露:

import statsmodels.api as sm

def time_series_regression(stock_ret, factors):
    """单只股票的时间序列回归"""
    X = sm.add_constant(factors)
    model = sm.OLS(stock_ret, X)
    results = model.fit()
    return results.params

3.2 Fama-MacBeth回归

接着进行两步Fama-MacBeth回归:

def fama_macbeth_regression(stock_rets, factors):
    """Fama-MacBeth回归"""
    # 第一步:时间序列回归获取因子暴露
    betas = stock_rets.apply(lambda x: time_series_regression(x, factors))
    
    # 第二步:截面回归
    lambda_vals = []
    for date in stock_rets.index:
        X = betas.T
        y = stock_rets.loc[date]
        model = sm.OLS(y, X)
        results = model.fit()
        lambda_vals.append(results.params)
    
    # 计算因子溢价
    lambda_df = pd.DataFrame(lambda_vals, index=stock_rets.index)
    return lambda_df.mean(), lambda_df.std() / np.sqrt(len(lambda_df))

3.3 Newey-West调整

由于金融数据常存在自相关性,我们需要使用Newey-West调整标准误:

def newey_west_adjustment(residuals, max_lags=3):
    """Newey-West标准误调整"""
    n = len(residuals)
    X = np.column_stack([np.ones(n), residuals])
    
    # 计算白噪声方差
    sigma = np.dot(residuals, residuals) / n
    
    # 计算自相关项
    for lag in range(1, max_lags+1):
        gamma = np.dot(residuals[lag:], residuals[:-lag]) / n
        sigma += 2 * (1 - lag/(max_lags+1)) * gamma
    
    # 计算调整后的协方差矩阵
    cov = np.linalg.inv(X.T @ X) * sigma
    return np.sqrt(np.diag(cov))

4. 策略回测与绩效评估

最后,我们需要评估基于这些因子的投资策略表现。

4.1 多因子组合构建

结合多个因子信号构建投资组合:

def build_multifactor_portfolio(data, factors):
    """构建多因子组合"""
    # 标准化因子值
    z_scores = data.groupby('trade_date')[factors].transform(
        lambda x: (x - x.mean()) / x.std())
    
    # 计算综合得分
    data['composite_score'] = z_scores.mean(axis=1)
    
    # 按得分分组
    data['portfolio'] = data.groupby('trade_date')['composite_score'].transform(
        lambda x: pd.qcut(x, 5, labels=False))
    
    # 计算组合收益率
    port_ret = data.groupby(['trade_date', 'portfolio'])['ret'].mean().unstack()
    return port_ret

4.2 绩效评估指标

计算常见的绩效指标:

def performance_metrics(returns, rf=0.02/12):
    """计算策略绩效指标"""
    excess_ret = returns - rf
    stats = {
        '年化收益率': returns.mean() * 12,
        '年化波动率': returns.std() * np.sqrt(12),
        '夏普比率': excess_ret.mean() / excess_ret.std() * np.sqrt(12),
        '最大回撤': (returns.cumsum() - returns.cumsum().cummax()).min()
    }
    return pd.Series(stats)

4.3 可视化分析

使用Matplotlib进行结果可视化:

import matplotlib.pyplot as plt

def plot_cumulative_returns(returns, title):
    """绘制累计收益率曲线"""
    cum_ret = (1 + returns).cumprod()
    plt.figure(figsize=(10, 6))
    cum_ret.plot()
    plt.title(title)
    plt.ylabel('Cumulative Return')
    plt.grid(True)
    plt.show()

5. 实战中的关键问题与解决方案

在实际应用中,我发现有几个关键问题需要特别注意:

因子衰减问题:许多传统因子(如市值因子)在A股市场的有效性正在下降。解决方案是结合更多元化的因子,如质量因子、动量因子等。

数据频率选择:虽然日频数据包含更多信息,但月频数据通常能获得更稳健的结果。我的经验是从月频开始,验证因子有效性后再考虑更高频率。

参数过拟合:避免在因子构建和组合优化中使用过多参数。一个实用的方法是保留20%的数据作为样本外测试集。

交易成本影响:高频调仓会导致交易成本侵蚀收益。可以通过以下方式降低影响:

  • 延长调仓周期(如季度调仓)
  • 设置调仓阈值(如权重变化超过5%才调整)
  • 使用换手率约束优化
def portfolio_optimization_with_tcost(expected_ret, cov_matrix, current_weights, tcost=0.001):
    """考虑交易成本的组合优化"""
    from cvxpy import Variable, Minimize, quad_form, sum as cvx_sum
    
    n = len(expected_ret)
    w = Variable(n)
    # 目标函数:风险调整后收益 - 交易成本
    ret = expected_ret.T @ w
    risk = quad_form(w, cov_matrix)
    turnover = cvx_sum(abs(w - current_weights)) * tcost
    objective = Maximize(ret - 0.5 * risk - turnover)
    
    # 约束条件
    constraints = [cvx_sum(w) == 1, w >= 0]
    prob = Problem(objective, constraints)
    prob.solve()
    return np.array(w.value).flatten()

通过完整的Python实现流程,我们不仅理解了Fama-French模型的数学原理,更重要的是掌握了将其转化为实际投资策略的方法论。记住,因子投资不是简单的套用公式,而是需要不断验证、迭代和创新的过程。

Logo

码道开发者社区,聚焦华为云码道 CodeArts 代码智能体,沉淀 Agent、Skill、鸿蒙开发实战内容,供开发者查阅资料、交流技术、分享工程实践

更多推荐