2. 市净率因子选股策略

任务目标

本案例使用市净率(PB)作为单因子,从一组股票池中筛选出低估值股票,并比较不同PB阈值下所构建投资组合的收益表现。核心目标是验证:PB阈值越低,组合是否越能跑赢基准。案例使用A股历史日线数据(2023年全年),以年化收益率和最大回撤作为组合表现的评价指标。

数据准备与依赖

本案例使用 pandas、numpy 进行数据处理,数据源为本地CSV文件(每股包含:date, code, close, pb 四列)。若使用真实行情数据,建议通过Tushare或AkShare获取,字段映射为本案例所需格式即可。以下代码假设当前目录下存在 stock_data.csv,包含10只股票2023年每个交易日的收盘价与市净率数据。

完整实现代码

import pandas as pd
import numpy as np

# ========== 1. 数据加载与预处理 ==========
df = pd.read_csv('stock_data.csv', parse_dates=['date'])
df = df.sort_values(['code', 'date']).reset_index(drop=True)

# 计算每只股票的日收益率
df['ret'] = df.groupby('code')['close'].pct_change()

# ========== 2. 构建等权每日调仓组合 ==========
def pb_portfolio(pb_threshold):
    """
    根据PB阈值筛选股票,构建等权每日调仓组合。
    返回组合的每日净值序列。
    """
    # 筛选PB低于阈值的股票
    filtered = df[df['pb'] < pb_threshold].copy()
    
    # 按日期分组,计算每日入选股票的等权收益率
    daily_ret = filtered.groupby('date')['ret'].mean().reset_index()
    daily_ret.columns = ['date', 'port_ret']
    
    # 与全市场基准(所有股票等权)对齐日期
    bench_ret = df.groupby('date')['ret'].mean().reset_index()
    bench_ret.columns = ['date', 'bench_ret']
    
    merged = pd.merge(bench_ret, daily_ret, on='date', how='left')
    merged['port_ret'] = merged['port_ret'].fillna(0)  # 无入选股票时收益为0
    
    # 计算净值序列
    merged['port_nav'] = (1 + merged['port_ret']).cumprod()
    merged['bench_nav'] = (1 + merged['bench_ret']).cumprod()
    return merged

# ========== 3. 评价指标计算 ==========
def evaluate(nav_series):
    """输入净值序列,返回年化收益率与最大回撤"""
    total_ret = nav_series.iloc[-1] / nav_series.iloc[0] - 1
    annual_ret = (1 + total_ret) ** (252 / len(nav_series)) - 1
    
    # 最大回撤
    cummax = nav_series.cummax()
    drawdown = nav_series / cummax - 1
    max_dd = drawdown.min()
    return annual_ret, max_dd

# ========== 4. 比较不同PB阈值 ==========
thresholds = [0.5, 0.8, 1.0, 1.5, 2.0]
results = []

for th in thresholds:
    result = pb_portfolio(th)
    ann_ret, max_dd = evaluate(result['port_nav'])
    bench_ann, bench_dd = evaluate(result['bench_nav'])
    results.append({
        'PB阈值': th,
        '组合年化收益': f'{ann_ret:.2%}',
        '组合最大回撤': f'{max_dd:.2%}',
        '基准年化收益': f'{bench_ann:.2%}',
        '基准最大回撤': f'{bench_dd:.2%}'
    })

# 输出结果表格
result_df = pd.DataFrame(results)
print(result_df.to_string(index=False))

# ========== 5. 最优阈值组合净值可视化数据导出 ==========
# 导出PB=0.8组合与基准的净值曲线数据(供SVG展示)
best = pb_portfolio(0.8)
best[['date', 'port_nav', 'bench_nav']].to_csv('pb_08_nav.csv', index=False)
print("\n净值数据已导出至 pb_08_nav.csv")

运行方式

  1. 准备数据文件 stock_data.csv,格式为 date, code, close, pb,其中 pb 为当日市净率(如2.5表示PB=2.5倍);
  2. 运行上述Python脚本(需安装 pandas、numpy);
  3. 脚本自动输出不同PB阈值下的组合与基准表现对比表。

验证方法与通过标准

  • 逻辑验证:检查 pb_portfolio 函数是否正确筛选出 pb < 阈值 的股票,且每日收益率计算无误;
  • 结果合理性:观察输出表格,PB阈值越低(如0.5),入选股票数量越少,组合年化收益与基准的差异应越明显;若所有阈值组合收益均与基准完全一致,说明数据中不存在PB低于最大阈值的股票,需检查数据范围;
  • 净值曲线验证:打开导出的 pb_08_nav.csv,确认净值序列从1.0开始单调变化,且组合净值与基准净值在2023年末的终值应与表格中年化收益计算结果一致(可通过 (1+年化收益)^(252/交易日数) 反推验证)。
PB=0.8组合 vs 基准 净值走势(2023年) 0.8 1.0 1.2 1.4 1月 3月 5月 7月 9月 12月 基准 PB<0.8组合 注:曲线为基于模拟数据的示意走势,实际结果以运行代码输出为准

结果解读要点

  • 若PB=0.5的组合年化收益显著高于PB=2.0的组合,说明低估值股票在样本期内表现更优,因子有效;
  • 若各阈值组合收益差异不大,可能原因包括:股票池本身估值集中、市场风格偏向成长股、或样本期过短;
  • 最大回撤指标用于衡量策略风险,低PB组合通常回撤更小,但需结合具体数据确认。