1. 基于市盈率的低估值选股策略
任务说明
本案例将使用A股历史行情数据,实现一个基于市盈率(PE)的低估值选股策略:筛选出市盈率低于所属行业平均且盈利稳定的股票,构建等权投资组合,并回测该组合在指定区间内的收益表现。回测结果将与同期沪深300指数进行对比,以评估策略有效性。
环境与依赖
需要安装以下Python库:
pip install pandas numpy tushare
本案例使用Tushare Pro接口获取数据,需要注册并获取(https://tushare.pro),在代码中替换YOUR_。
完整实现代码
import pandas as pd
import numpy as np
import tushare as ts
from datetime import datetime, timedelta
# ========== 配置 ==========
ts.set_('YOUR_')
pro = ts.pro_api()
# 回测区间与调仓参数
START_DATE = '20230101'
END_DATE = '20231231'
REBALANCE_DAYS = 20 # 每20个交易日调仓一次
TOP_N = 10 # 每次选10只股票
# ========== 数据获取 ==========
def get_stock_basic():
"""获取全部A股基础信息(股票代码、名称、行业)"""
df = pro.stock_basic(exchange='', list_status='L',
fields='ts_code,name,industry')
return df.dropna(subset=['industry'])
def get_daily_data(ts_code, start, end):
"""获取单只股票的日线行情(前复权)"""
df = pro.daily(ts_code=ts_code, start_date=start, end_date=end)
return df
def get_pe_data(trade_date):
"""获取指定交易日全部股票的PE、PB等估值指标"""
df = pro.daily_basic(trade_date=trade_date,
fields='ts_code,pe,pe_ttm,pb')
return df
# ========== 核心策略 ==========
def select_stocks(trade_date, stock_basic, lookback_days=250):
"""
在trade_date当日筛选股票:
1. PE(TTM) > 0(盈利为正)
2. PE(TTM) 低于所属行业当日平均PE
3. 过去一年净利润为正(用PE>0近似,因日频数据限制)
返回筛选后的股票代码列表
"""
# 获取当日全部估值数据
pe_df = get_pe_data(trade_date)
if pe_df is None or pe_df.empty:
return []
# 合并行业信息
merged = pe_df.merge(stock_basic[['ts_code', 'industry']], on='ts_code')
# 过滤条件1:PE(TTM) > 0(盈利稳定,排除亏损股)
merged = merged[(merged['pe_ttm'] > 0) & (merged['pe_ttm'] < 100)]
# 计算行业平均PE(剔除PE为负或异常大的值)
industry_avg = merged.groupby('industry')['pe_ttm'].transform(
lambda x: x[(x > 0) & (x < 100)].mean()
)
# 过滤条件2:PE低于行业平均
merged = merged[merged['pe_ttm'] < industry_avg]
# 按PE升序排列,取前TOP_N只
merged = merged.sort_values('pe_ttm')
selected = merged.head(TOP_N)['ts_code'].tolist()
return selected
def backtest():
"""执行回测:定期调仓,计算组合净值"""
# 获取交易日历
cal = pro.trade_cal(start_date=START_DATE, end_date=END_DATE)
trade_days = cal[cal['is_open'] == 1]['cal_date'].tolist()
# 获取股票基础信息
stock_basic = get_stock_basic()
# 初始化组合与净值
portfolio = [] # 当前持仓股票列表
cash = 1.0 # 初始资金归一化为1
nav_curve = [] # 净值曲线
# 获取沪深300指数作为基准
hs300 = pro.index_daily(ts_code='000300.SH',
start_date=START_DATE, end_date=END_DATE)
hs300 = hs300.set_index('trade_date')['close']
for i, day in enumerate(trade_days):
# 调仓日:重新选股
if i % REBALANCE_DAYS == 0:
new_portfolio = select_stocks(day, stock_basic)
if new_portfolio:
portfolio = new_portfolio
# 计算当日组合收益
if portfolio:
daily_rets = []
for ts_code in portfolio:
# 获取当日收盘价(用前复权价近似)
daily = get_daily_data(ts_code, day, day)
if daily is not None and not daily.empty:
daily_rets.append(daily['pct_chg'].iloc[0] / 100)
if daily_rets:
# 等权组合当日收益
port_ret = np.mean(daily_rets)
cash *= (1 + port_ret)
# 记录净值
nav_curve.append({'date': day, 'nav': cash})
# 构建净值序列
nav_df = pd.DataFrame(nav_curve).set_index('date')
nav_df['nav'] = nav_df['nav'].fillna(method='ffill')
# 计算基准净值
hs300_nav = hs300 / hs300.iloc[0]
# 对齐日期
result = pd.DataFrame({
'strategy': nav_df['nav'],
'benchmark': hs300_nav
}).dropna()
return result
# ========== 运行与验证 ==========
if __name__ == '__main__':
result = backtest()
# 计算策略与基准的累计收益
strategy_return = result['strategy'].iloc[-1] - 1
benchmark_return = result['benchmark'].iloc[-1] - 1
# 计算年化收益(假设252个交易日)
days = len(result)
annual_return = (result['strategy'].iloc[-1]) ** (252 / days) - 1
# 计算最大回撤
cummax = result['strategy'].cummax()
drawdown = (result['strategy'] - cummax) / cummax
max_drawdown = drawdown.min()
print(f"回测区间:{START_DATE} 至 {END_DATE}")
print(f"策略累计收益:{strategy_return:.2%}")
print(f"沪深300累计收益:{benchmark_return:.2%}")
print(f"策略年化收益:{annual_return:.2%}")
print(f"策略最大回撤:{max_drawdown:.2%}")
print(f"调仓次数:{len(result) // REBALANCE_DAYS}")
# 保存净值数据
result.to_csv('backtest_result.csv')
print("净值数据已保存至 backtest_result.csv")
运行方式
- 将代码中的
YOUR_替换为你的Tushare Pro - 保存为
pe_strategy.py,在终端执行:
python pe_strategy.py
验证方法与通过标准
运行后观察控制台输出,满足以下条件即判定策略实现成功:
- 程序无报错,正常输出回测区间、累计收益、年化收益和最大回撤等指标
- 生成
backtest_result.csv文件,包含策略净值和基准净值两列数据 - 策略累计收益与沪深300累计收益均为合理数值(通常在-30%至+50%区间内,具体取决于市场行情)
- 策略净值曲线在回测区间内连续,无缺失值
策略逻辑说明
本策略的核心筛选逻辑为:在每个调仓日,计算全市场股票的PE(TTM),剔除PE为负(亏损)或PE大于100(极端高估)的股票,然后计算各行业内剩余股票的平均PE,选出PE低于行业平均的股票,按PE升序取前10只构成等权组合。每20个交易日调仓一次,持有期内不做个股调整。
常见问题
- API调用频率限制:Tushare Pro对积分较低的账号有调用频率限制,若运行中报错,可在
get_daily_data和get_pe_data函数中增加time.sleep(0.5)暂停 - 数据缺失:部分股票在调仓日可能停牌或无PE数据,代码已通过
if daily is not None and not daily.empty跳过缺失数据 - 行业分类:本案例使用Tushare的行业分类(证监会行业),如需使用申万行业可调用
pro.index_classify接口自行映射