🚀 AI 一键生成 joinquant 策略代码

JoinQuant 策略开发:基于市销率(PS)的低估值选股模型

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

问题描述

怎么编写一个基于市销率 PS 的选股策略?

解决方案

编写基于市销率(Price-to-Sales Ratio, PS)的选股策略,核心逻辑是寻找营收能力强但市场估值相对较低的股票。PS = 总市值 / 主营业务收入。这种策略常用于评估尚未盈利但有营收增长的成长型公司,或者周期性行业。

以下是一个完整的聚宽(JoinQuant)策略代码。

策略逻辑说明

  1. 股票池:沪深300指数成分股(确保流动性)。
  2. 选股因子:市销率(PS_TTM)。
  3. 过滤条件
    • 剔除停牌股票。
    • 剔除ST、*ST股票。
    • 剔除涨跌停股票(可选,本策略暂不剔除以保持简洁)。
    • 剔除 PS 为负或无效值的股票。
  4. 排序逻辑:按 PS 值从小到大排序(选取低估值)。
  5. 持仓数量:持有 PS 最低的 10 只股票。
  6. 调仓频率:按月调仓(每月第一个交易日)。
  7. 资金分配:等权重买入。

策略代码

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

def initialize(context):
    """
    初始化函数,设定基准、手续费、滑点等
    """
    # 设定沪深300作为基准
    set_benchmark('000300.XSHG')
    # 开开启真实价格模式(动态复权)
    set_option('use_real_price', True)
    # 过滤掉order系列API产生的比error级别低的log
    log.set_level('order', 'error')
    
    # 设定股票交易手续费:买入万分之三,卖出万分之三加千分之一印花税,最低5元
    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_monthly(weekly_selection, monthday=1, time='09:30')

def weekly_selection(context):
    """
    选股与调仓主逻辑
    """
    # 1. 获取股票池:沪深300
    target_index = '000300.XSHG'
    initial_list = get_index_stocks(target_index)
    
    # 2. 过滤掉 ST 和 停牌 的股票
    current_data = get_current_data()
    valid_list = []
    for code in initial_list:
        # 剔除停牌 和 ST
        if not current_data[code].paused and not current_data[code].is_st:
            valid_list.append(code)
            
    # 3. 查询财务数据:市销率 (PS)
    # valuation.ps_ratio: 市销率(TTM)
    q = query(
        valuation.code,
        valuation.ps_ratio
    ).filter(
        valuation.code.in_(valid_list),
        valuation.ps_ratio > 0  # 剔除无效或负值
    ).order_by(
        valuation.ps_ratio.asc() # 按 PS 从小到大排序
    ).limit(g.stock_num) # 取前 N 只
    
    df = get_fundamentals(q)
    
    # 获取最终的目标股票列表
    target_list = list(df['code'])
    
    # 4. 执行调仓
    rebalance(context, target_list)

def rebalance(context, target_list):
    """
    调仓执行函数
    """
    # 获取当前持仓
    current_holdings = list(context.portfolio.positions.keys())
    
    # 1. 卖出不在目标列表中的股票
    for stock in current_holdings:
        if stock not in target_list:
            order_target_value(stock, 0)
            
    # 2. 买入目标列表中的股票
    if len(target_list) > 0:
        # 等权重分配资金
        # 注意:这里简单处理,用总资产除以目标数量。实际交易中可能需要预留少量现金防止手续费不够
        value_per_stock = context.portfolio.total_value / len(target_list)
        
        for stock in target_list:
            order_target_value(stock, value_per_stock)
            
    log.info("本月调仓完成,持有股票数量: %d" % len(target_list))
    log.info("持有列表: %s" % target_list)

代码关键点解析

  1. get_fundamentalsquery:

    • 这是聚宽获取财务数据的核心函数。
    • valuation.ps_ratio:代表市销率(TTM,滚动12个月)。
    • .filter(valuation.ps_ratio > 0):排除掉数据异常或营收为负的情况。
    • .order_by(valuation.ps_ratio.asc()):核心策略逻辑,即选择 PS 值最小的股票。
  2. get_current_data:

    • 用于获取当天的实时状态,主要用于过滤停牌(paused)和 ST 股票(is_st),防止买入无法交易或风险过大的标的。
  3. order_target_value:

    • 这是最常用的下单函数之一,它会自动计算需要买入或卖出的股数,使该股票的持仓市值达到指定金额。如果目标金额为 0,则表示清仓。
  4. run_monthly:

    • 定时运行函数,这里设置为每月第一个交易日进行选股和调仓。低频调仓有助于减少手续费对收益的侵蚀。

常见问题 (Q&A)

Q1: 为什么要用市销率(PS)而不是市盈率(PE)?
A: 市盈率依赖于净利润,而很多高成长公司(如科技股)或者周期性低谷的公司可能暂时亏损(PE为负或极高),但营收依然在增长。市销率相对于净利润更稳定,不易被会计手段操纵,适合评估这类公司。

Q2: 代码中的 valuation.ps_ratio 是静态的还是动态的?
A: 在聚宽的 valuation 表中,ps_ratio 默认通常是 TTM(Trailing Twelve Months,滚动12个月),即使用最近4个季度的财务数据计算,这样能更及时地反映公司当前的估值状态。

Q3: 如何修改持仓数量?
A: 修改 initialize 函数中的 g.stock_num = 10,将 10 改为您想要的数量即可。

Q4: 如何更换股票池?
A: 修改 weekly_selection 函数中的 target_index。例如,改为 '000905.XSHG' 即可基于中证500指数进行选股。