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")

运行方式

  1. 将代码中的YOUR_替换为你的Tushare Pro
  2. 保存为pe_strategy.py,在终端执行:
python pe_strategy.py

验证方法与通过标准

运行后观察控制台输出,满足以下条件即判定策略实现成功:

  • 程序无报错,正常输出回测区间、累计收益、年化收益和最大回撤等指标
  • 生成backtest_result.csv文件,包含策略净值和基准净值两列数据
  • 策略累计收益与沪深300累计收益均为合理数值(通常在-30%至+50%区间内,具体取决于市场行情)
  • 策略净值曲线在回测区间内连续,无缺失值

策略逻辑说明

本策略的核心筛选逻辑为:在每个调仓日,计算全市场股票的PE(TTM),剔除PE为负(亏损)或PE大于100(极端高估)的股票,然后计算各行业内剩余股票的平均PE,选出PE低于行业平均的股票,按PE升序取前10只构成等权组合。每20个交易日调仓一次,持有期内不做个股调整。

低估值选股策略流程图 数据获取 股票列表 + 日线行情 + PE数据 初步过滤 ① PE(TTM) > 0(盈利为正) ② PE(TTM) < 100(剔除极端值) 行业对比 计算行业平均PE 筛选PE < 行业平均 排序选股 按PE升序取前10只 构建等权组合 每20个交易日调仓 回测计算 每日计算组合收益 结果输出 累计收益 / 年化 / 回撤 基准对比 沪深300指数同期表现 调仓周期:20个交易日 | 持仓数量:10只 | 权重方式:等权

常见问题

  • 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接口自行映射