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")
运行方式
- 准备数据文件
stock_data.csv,格式为date, code, close, pb,其中pb为当日市净率(如2.5表示PB=2.5倍); - 运行上述Python脚本(需安装
pandas、numpy); - 脚本自动输出不同PB阈值下的组合与基准表现对比表。
验证方法与通过标准
- 逻辑验证:检查
pb_portfolio函数是否正确筛选出pb < 阈值的股票,且每日收益率计算无误; - 结果合理性:观察输出表格,PB阈值越低(如0.5),入选股票数量越少,组合年化收益与基准的差异应越明显;若所有阈值组合收益均与基准完全一致,说明数据中不存在PB低于最大阈值的股票,需检查数据范围;
- 净值曲线验证:打开导出的
pb_08_nav.csv,确认净值序列从1.0开始单调变化,且组合净值与基准净值在2023年末的终值应与表格中年化收益计算结果一致(可通过(1+年化收益)^(252/交易日数)反推验证)。
结果解读要点
- 若PB=0.5的组合年化收益显著高于PB=2.0的组合,说明低估值股票在样本期内表现更优,因子有效;
- 若各阈值组合收益差异不大,可能原因包括:股票池本身估值集中、市场风格偏向成长股、或样本期过短;
- 最大回撤指标用于衡量策略风险,低PB组合通常回撤更小,但需结合具体数据确认。