🚀 AI 一键生成 joinquant 策略代码

JoinQuant 财务选股:多维度对比 PE、PB 与 PS 估值指标

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

问题描述

如何同时获取股票的 PE、PB 和 PS 值进行选股?

解决方案

在聚宽(JoinQuant)平台中,要同时获取股票的 PE(市盈率)、PB(市净率)和 PS(市销率)进行选股,主要使用 get_fundamentals 函数配合 valuation 表来实现。

核心逻辑说明

  1. 数据表:使用 valuation(市值数据表)。
  2. 字段
    • valuation.pe_ratio:市盈率(PE, TTM)。
    • valuation.pb_ratio:市净率(PB)。
    • valuation.ps_ratio:市销率(PS, TTM)。
  3. 查询方法:构建 query 对象,在 filter 中同时设置这三个指标的筛选条件(例如 PE < 30 且 PB < 3 且 PS < 5),并根据其中某一项(如 PE)进行排序。

下面是一个完整的策略示例。该策略每天开盘时运行,选取满足 PE、PB、PS 阈值条件的股票,并按 PE 从小到大排序,买入排名前 10 的股票。

策略代码

# -*- coding: utf-8 -*-
from jqdata import *

def initialize(context):
    """
    初始化函数,设定基准、手续费、滑点等
    """
    # 设定沪深300作为基准
    set_benchmark('000300.XSHG')
    # 开启动态复权模式(真实价格)
    set_option('use_real_price', True)
    
    # 设定股票类每笔交易时的手续费
    set_order_cost(OrderCost(close_tax=0.001, open_commission=0.0003, close_commission=0.0003, min_commission=5), type='stock')
    
    # 设定全局变量,用于控制持仓数量
    g.stock_num = 10 
    
    # 每天开盘时运行选股和调仓
    run_daily(rebalance, time='09:30')

def rebalance(context):
    """
    调仓函数
    """
    # 1. 获取满足 PE/PB/PS 条件的股票列表
    target_list = get_stock_list(context)
    
    # 2. 卖出不在目标列表中的股票
    for stock in context.portfolio.positions:
        if stock not in target_list:
            order_target_value(stock, 0)
            
    # 3. 买入目标列表中的股票
    if len(target_list) > 0:
        # 将资金平分给目标股票
        position_per_stk = context.portfolio.total_value / len(target_list)
        for stock in target_list:
            order_target_value(stock, position_per_stk)

def get_stock_list(context):
    """
    选股逻辑函数:同时根据 PE, PB, PS 进行筛选
    """
    # 获取当前时间
    date = context.current_dt.strftime("%Y-%m-%d")
    
    # 构建查询语句
    # 查询 valuation 表中的 code, pe_ratio, pb_ratio, ps_ratio
    q = query(
        valuation.code,
        valuation.pe_ratio,
        valuation.pb_ratio,
        valuation.ps_ratio
    ).filter(
        # 过滤条件:
        # 1. PE (TTM) 大于0且小于30
        valuation.pe_ratio > 0,
        valuation.pe_ratio < 30,
        
        # 2. PB 小于 3
        valuation.pb_ratio < 3,
        
        # 3. PS (TTM) 小于 5
        valuation.ps_ratio < 5
    ).order_by(
        # 按 PE 从小到大排序
        valuation.pe_ratio.asc()
    )
    
    # 执行查询,获取数据
    # 这里的 limit 设为 g.stock_num * 3 是为了预留空间过滤停牌和ST股票
    df = get_fundamentals(q, date=date)
    
    if len(df) == 0:
        return []
    
    # 初步筛选出的股票代码列表
    initial_list = list(df['code'])
    
    # 进一步过滤:去除 ST 股和停牌股
    final_list = filter_paused_and_st_stocks(initial_list)
    
    # 截取目标持仓数量
    return final_list[:g.stock_num]

def filter_paused_and_st_stocks(stock_list):
    """
    过滤停牌和ST股票
    """
    current_data = get_current_data()
    filtered_list = []
    for stock in stock_list:
        # 如果未停牌 且 不是ST
        if not current_data[stock].paused and not current_data[stock].is_st:
            # 还可以过滤掉刚上市不久的次新股(可选),这里简单判断是否上市超过1天
            if 'start_date' in get_security_info(stock).__dict__: # 确保有上市日期属性
                 filtered_list.append(stock)
            else:
                 filtered_list.append(stock)
                 
    return filtered_list

代码解析

  1. query(valuation.code, ...): 这是构建查询的核心。我们选择了 valuation 表中的代码、PE、PB 和 PS 字段。
  2. .filter(...): 这里实现了“同时获取”并筛选的逻辑。
    • valuation.pe_ratio < 30: 筛选市盈率小于30的股票。
    • valuation.pb_ratio < 3: 筛选市净率小于3的股票。
    • valuation.ps_ratio < 5: 筛选市销率小于5的股票。
    • valuation.pe_ratio > 0: 通常我们会排除亏损股(PE为负)。
  3. .order_by(...): 选股通常需要一个优先级,这里示例使用了按 PE 从低到高排序。你可以根据需求改为按 PB (valuation.pb_ratio.asc()) 或 PS 排序。
  4. get_fundamentals(q): 发送查询请求,返回一个 Pandas DataFrame。
  5. 过滤 ST 和停牌: get_fundamentals 返回的是财务数据符合条件的股票,但这些股票可能处于停牌或 ST 状态,无法交易。因此,使用 get_current_data() 进行二次过滤是实盘和回测中必须的步骤。

Q&A

Q1: valuation.pe_ratio 是静态 PE 还是动态 PE?
A1: 在聚宽的 valuation 表中,pe_ratio 默认指的是 PE (TTM),即滚动市盈率(过去12个月的净利润),这是量化投资中最常用的指标。如果需要静态 PE(基于上年度财报),可以使用 pe_ratio_lyr 字段。

Q2: 如何获取全市场的 PE、PB、PS 数据而不进行筛选?
A2: 如果你想获取全市场数据进行分析,只需去掉 .filter() 中的阈值条件(保留 valuation.code),或者只保留 valuation.pe_ratio > 0 这种基础清洗条件。但请注意,get_fundamentals 单次查询有返回行数限制(通常为 5000 行),全市场股票数量接近该限制,建议分批查询或只查询特定指数成分股。

Q3: 可以在回测中每天都调用这个函数吗?
A3: 可以。get_fundamentals 在回测中非常高效。但要注意,财务数据(如 PB、PS 的分母部分)是按天更新的,而分子(净利润、营收)是按季度更新的。因此,虽然价格每天变动导致估值每天变动,但基本面因子的剧烈变化通常发生在财报发布期。