问题描述
解决方案
在聚宽(JoinQuant)平台中,要同时获取股票的 PE(市盈率)、PB(市净率)和 PS(市销率)进行选股,主要使用 get_fundamentals 函数配合 valuation 表来实现。
核心逻辑说明
- 数据表:使用
valuation(市值数据表)。 - 字段:
valuation.pe_ratio:市盈率(PE, TTM)。valuation.pb_ratio:市净率(PB)。valuation.ps_ratio:市销率(PS, TTM)。
- 查询方法:构建
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
代码解析
query(valuation.code, ...): 这是构建查询的核心。我们选择了valuation表中的代码、PE、PB 和 PS 字段。.filter(...): 这里实现了“同时获取”并筛选的逻辑。valuation.pe_ratio < 30: 筛选市盈率小于30的股票。valuation.pb_ratio < 3: 筛选市净率小于3的股票。valuation.ps_ratio < 5: 筛选市销率小于5的股票。valuation.pe_ratio > 0: 通常我们会排除亏损股(PE为负)。
.order_by(...): 选股通常需要一个优先级,这里示例使用了按 PE 从低到高排序。你可以根据需求改为按 PB (valuation.pb_ratio.asc()) 或 PS 排序。get_fundamentals(q): 发送查询请求,返回一个 Pandas DataFrame。- 过滤 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 的分母部分)是按天更新的,而分子(净利润、营收)是按季度更新的。因此,虽然价格每天变动导致估值每天变动,但基本面因子的剧烈变化通常发生在财报发布期。