🚀 AI 一键生成 joinquant 策略代码

JoinQuant 实战:如何构建基于 PE Band 的指数 ETF 投资策略?

joinquant | 发布时间: 2025/12/5
以下内容由 EasyQuant 生成。

问题描述

我想写一个根据指数 PE 估值波段来买卖 ETF 的策略

解决方案

这是一个基于 沪深300指数(000300.XSHG)PE(市盈率)估值波段 交易策略。

策略逻辑

  1. 核心思想:价值投资,低买高卖。当指数整体估值(PE)处于历史低位时买入,处于历史高位时卖出。
  2. 标的:300ETF(510300.XSHG)。
  3. 指数PE计算方法
    • 获取沪深300所有成分股。
    • 获取成分股的总市值(Market Cap)和市盈率(PE)。
    • 指数PE = 成分股总市值之和 / 成分股总净利润之和
    • 注:这里采用“总市值/总净利润”法(即加权PE),比简单的“PE求平均”更准确反映指数整体估值。
  4. 交易信号
    • 买入信号:当 指数PE < 低估阈值(例如 11.5倍),视为低估,全仓买入。
    • 卖出信号:当 指数PE > 高估阈值(例如 16.0倍),视为高估,清仓卖出。
    • 持仓:在两个阈值之间时,保持原有仓位不动。

策略代码

# -*- coding: utf-8 -*-
# 导入函数库
from jqdata import *
import pandas as pd

def initialize(context):
    # 1. 设定基准
    set_benchmark('000300.XSHG')
    # 2. 开启动态复权模式(真实价格)
    set_option('use_real_price', True)
    # 3. 设置手续费(股票类,买入万三,卖出万三+千一印花税)
    set_order_cost(OrderCost(close_tax=0.001, open_commission=0.0003, close_commission=0.0003, min_commission=5), type='stock')
    
    # --- 策略参数设置 ---
    # 目标指数:沪深300
    g.index_code = '000300.XSHG'
    # 交易标的:300ETF
    g.etf_code = '510300.XSHG'
    
    # 估值波段阈值 (基于沪深300历史数据经验值)
    # 历史低位通常在 10-12倍左右,高位在 15-17倍左右
    g.pe_buy_threshold = 11.5  # 买入阈值
    g.pe_sell_threshold = 16.0 # 卖出阈值
    
    # 每天开盘时运行
    run_daily(market_open, time='09:30')

def market_open(context):
    # 1. 计算当前指数的加权PE
    current_pe = get_index_pe(context, g.index_code)
    
    if current_pe is None:
        return

    # 记录PE值到图表,方便回测查看
    record(Index_PE=current_pe)
    
    # 获取当前现金和持仓
    cash = context.portfolio.available_cash
    position = context.portfolio.positions[g.etf_code].total_amount
    
    # 2. 交易逻辑
    # 信号:低估 -> 买入
    if current_pe < g.pe_buy_threshold:
        if cash > 0:
            log.info(f"当前PE: {current_pe:.2f} < 买入阈值 {g.pe_buy_threshold},执行买入")
            order_value(g.etf_code, cash)
            
    # 信号:高估 -> 卖出
    elif current_pe > g.pe_sell_threshold:
        if position > 0:
            log.info(f"当前PE: {current_pe:.2f} > 卖出阈值 {g.pe_sell_threshold},执行卖出")
            order_target(g.etf_code, 0)
            
    # 处于中间波段 -> 持仓不动
    else:
        pass
        # log.info(f"当前PE: {current_pe:.2f},处于持有/观望区间")

def get_index_pe(context, index_code):
    """
    计算指数的加权PE (总市值 / 总净利润)
    """
    # 获取指数成分股
    stocks = get_index_stocks(index_code)
    
    if not stocks:
        log.error("获取指数成分股失败")
        return None
        
    # 查询成分股的市值和PE(TTM)
    # valuation.market_cap 单位: 亿元
    # valuation.pe_ratio: 市盈率(TTM)
    q = query(
        valuation.code,
        valuation.market_cap,
        valuation.pe_ratio
    ).filter(
        valuation.code.in_(stocks),
        valuation.pe_ratio > 0  # 过滤掉亏损股,避免计算异常,也可以选择不过滤
    )
    
    df = get_fundamentals(q)
    
    if df is None or len(df) == 0:
        return None
        
    # 计算原理:
    # 单个股票净利润 = 市值 / PE
    # 指数PE = 总市值 / 总净利润
    
    # 1. 计算每只股票的净利润 (单位:亿元)
    df['net_profit'] = df['market_cap'] / df['pe_ratio']
    
    # 2. 计算总市值和总净利润
    total_market_cap = df['market_cap'].sum()
    total_net_profit = df['net_profit'].sum()
    
    # 3. 计算指数加权PE
    if total_net_profit > 0:
        index_pe = total_market_cap / total_net_profit
        return index_pe
    else:
        return None

常见问题与优化建议

1. 为什么使用固定阈值(11.5 和 16.0)?

  • 原因:在回测中,实时计算“过去10年的PE百分位”非常耗时,容易导致回测超时。
  • 经验值:对于沪深300,历史上PE通常在 10倍到 18倍之间波动。11.5以下通常是极具性价比的底部区域,16以上风险开始积聚。
  • 如何优化:你可以根据当前的市场环境调整这两个参数。例如在牛市预期强时,可以适当调高卖出阈值。

2. 如何改成“动态百分位”策略?

如果你希望策略自动计算百分位(例如:PE处于过去5年 20% 分位点买入),你需要维护一个历史PE列表。由于聚宽 initialize 中不能大量查询历史财务数据,通常的做法是:

  • 使用固定阈值(如上代码)。
  • 或者在本地计算好历史PE数据,作为自定义数据上传到聚宽的研究环境读取。

3. 为什么过滤掉 PE < 0 的股票?

  • 代码中 valuation.pe_ratio > 0 过滤了亏损股。
  • 理由:亏损股的PE是负数,直接参与加权计算会抵消盈利股的利润,导致算出来的指数PE异常偏高或偏低(甚至为负)。在指数估值计算中,通常的做法是剔除亏损股,或者将亏损股的PE视为无穷大处理。

4. 适合的标的

  • 该策略最适合 宽基指数(如沪深300、上证50),因为它们的盈利能力相对稳定,PE估值逻辑有效。
  • 对于 成长型指数(如创业板指、科创50),PE波动范围极大(可能在30-100之间波动),需要重新设定阈值,或者改用 PEG 等指标。

Q&A

Q: 这个策略可以直接用于实盘吗?
A: 这是一个基础的择时策略模板。实盘前建议:1. 结合当前宏观环境调整阈值;2. 考虑分批建仓(例如PE每跌0.5买入20%)而不是一次性全仓,以平滑风险。

Q: 如何查看回测中的PE变化曲线?
A: 代码中包含 record(Index_PE=current_pe),回测结束后,在结果页面的图表下方,勾选 "Index_PE" 即可看到指数估值随时间变化的曲线,方便你验证买卖点是否合理。

Q: 可以换成中证500指数吗?
A: 可以。将 g.index_code 改为 '000905.XSHG',将 g.etf_code 改为 '510500.XSHG'注意:中证500的估值中枢比沪深300高,你需要将买入阈值调整为 20-25 左右,卖出阈值调整为 35-40 左右(具体需参考中证500历史估值)。