🚀 AI 一键生成 joinquant 策略代码

如何用 Python 在 JoinQuant 平台实现格雷厄姆数字(Graham Number)价值投资策略?

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

问题描述

请帮我编写一个基于格雷厄姆数字的价值选股策略。

解决方案

这是一个基于**本杰明·格雷厄姆(Benjamin Graham)**经典价值投资理论设计的选股策略。

策略核心逻辑:格雷厄姆数字 (Graham Number)

格雷厄姆数字是衡量股票每股内在价值的指标。根据格雷厄姆的理论,防御型投资者应寻找市盈率(PE)低于15且市净率(PB)低于1.5的股票。

公式推导如下:
$$15 \times 1.5 = 22.5$$
$$PE \times PB < 22.5$$

由此得出格雷厄姆数字公式:
$$ \text{Graham Number} = \sqrt{22.5 \times \text{每股收益(EPS)} \times \text{每股净资产(BVPS)}} $$

策略逻辑:

  1. 选股范围:全A股(剔除ST、停牌、次新股)。
  2. 核心过滤
    • 市盈率 (PE) > 0 (保证公司盈利)。
    • PE $\times$ PB < 22.5 (满足格雷厄姆的安全边际)。
  3. 排序因子:按照 $PE \times PB$ 从小到大排序(估值越低越好),或者按照 (格雷厄姆数字 / 当前股价) 从大到小排序。
  4. 调仓频率:按月调仓。
  5. 资金分配:等权重买入排名前 N 的股票。

策略代码实现

# -*- coding: utf-8 -*-
from jqdata import *
import pandas as pd
import numpy as np

def initialize(context):
    """
    初始化函数
    """
    # 设定基准为沪深300
    set_benchmark('000300.XSHG')
    # 开启动态复权模式(真实价格)
    set_option('use_real_price', True)
    # 过滤掉order系列API产生的比error级别低的log
    log.set_level('order', 'error')
    
    # --- 策略参数设置 ---
    g.stock_num = 10          # 持仓股票数量
    g.graham_limit = 22.5     # 格雷厄姆乘数上限 (PE * PB < 22.5)
    
    # 设置交易成本(股票佣金万三,印花税千一)
    set_order_cost(OrderCost(close_tax=0.001, open_commission=0.0003, close_commission=0.0003, min_commission=5), type='stock')
    
    # 按月运行,每月第一个交易日进行调仓
    run_monthly(trade, monthday=1, time='09:30')

def trade(context):
    """
    交易主函数
    """
    # 1. 获取初始股票池
    # 选取全市场股票
    all_stocks = list(get_all_securities(['stock']).index)
    
    # 2. 过滤股票
    # 剔除停牌、ST、退市、上市不满60天的次新股
    check_out_list = filter_basic_stock(all_stocks, context)
    
    # 3. 获取财务数据并计算格雷厄姆指标
    # 查询 PE(市盈率) 和 PB(市净率)
    # 这里使用 valuation.pe_ratio (PE-TTM) 和 valuation.pb_ratio
    q = query(
        valuation.code,
        valuation.pe_ratio,
        valuation.pb_ratio
    ).filter(
        valuation.code.in_(check_out_list),
        valuation.pe_ratio > 0,  # 剔除亏损股
        valuation.pb_ratio > 0
    )
    
    df = get_fundamentals(q, date=context.previous_date)
    
    if df is None or len(df) == 0:
        log.warn("未获取到财务数据,跳过本次调仓")
        return

    # 4. 计算格雷厄姆因子 (Graham Factor)
    # 逻辑:PE * PB < 22.5 等价于 股价 < 格雷厄姆数字
    # 我们计算 PE * PB 值,值越小代表安全边际越高
    df['graham_score'] = df['pe_ratio'] * df['pb_ratio']
    
    # 筛选满足格雷厄姆条件的股票 (PE * PB < 22.5)
    df = df[df['graham_score'] < g.graham_limit]
    
    # 按照得分从小到大排序 (越小越便宜)
    df = df.sort_values(by='graham_score', ascending=True)
    
    # 取前 N 只股票作为目标持仓
    target_list = list(df['code'][:g.stock_num])
    
    log.info(f"本月选股数量: {len(target_list)}")
    log.info(f"选股列表: {target_list}")
    
    # 5. 执行交易
    do_rebalance(context, target_list)

def do_rebalance(context, target_list):
    """
    调仓执行函数
    """
    # 获取当前持仓
    current_holdings = list(context.portfolio.positions.keys())
    
    # 卖出不在目标列表中的股票
    for stock in current_holdings:
        if stock not in target_list:
            order_target_value(stock, 0)
    
    # 买入目标列表中的股票
    if len(target_list) > 0:
        # 等权重分配资金
        # 注意:这里使用 total_value 可能会包含停牌股票的市值,更严谨的做法是计算可用资金
        # 简单起见,这里使用总资产/目标数量
        position_value = context.portfolio.total_value / len(target_list)
        
        for stock in target_list:
            order_target_value(stock, position_value)

def filter_basic_stock(stock_list, context):
    """
    基础过滤器:剔除停牌、ST、次新股
    """
    current_data = get_current_data()
    return_list = []
    
    for stock in stock_list:
        # 剔除停牌
        if current_data[stock].paused:
            continue
        # 剔除ST
        if current_data[stock].is_st:
            continue
        # 剔除上市不满60天的次新股
        # 获取股票上市时间
        start_date = get_security_info(stock).start_date
        days_listed = (context.current_dt.date() - start_date).days
        if days_listed < 60:
            continue
        # 剔除涨跌停(无法买入或卖出,虽然回测引擎可能撮合,但实盘需剔除)
        if current_data[stock].last_price >= current_data[stock].high_limit:
            continue
        if current_data[stock].last_price <= current_data[stock].low_limit:
            continue
            
        return_list.append(stock)
        
    return return_list

策略说明与 Q&A

1. 为什么使用 PE * PB 而不是直接计算格雷厄姆数字?

数学上是等价的。

  • 格雷厄姆数字公式:$V = \sqrt{22.5 \times EPS \times BVPS}$
  • 买入条件:$Price < V$
  • 两边平方:$Price^2 < 22.5 \times EPS \times BVPS$
  • 移项:$\frac{Price}{EPS} \times \frac{Price}{BVPS} < 22.5$
  • 即:$PE \times PB < 22.5$
    直接使用聚宽提供的 PE 和 PB 数据计算更加方便且数据源统一。

2. 策略的风险点在哪里?

  • 价值陷阱:低估值的股票可能基本面正在恶化(如夕阳行业),导致股价持续下跌,虽然 PE/PB 很低,但未来盈利可能消失。
  • 市场风格:在成长股牛市中,深度价值策略往往跑输大盘。
  • 持仓分散度:代码中设置了持仓10只,如果资金量大建议增加到20-30只以分散个股暴雷风险。

3. 如何优化这个策略?

  • 增加盈利能力过滤:例如要求 ROE > 8% 或 10%,避免买入资产质量太差的公司。
  • 增加现金流过滤:要求经营性现金流为正,避免纸面富贵。
  • 行业中性化:银行和地产行业通常 PE/PB 极低,容易导致策略全仓银行地产。可以限制单一行业的持仓比例,或者在各行业内分别选股。

4. 回测建议

建议回测时间拉长(例如5年以上),因为价值回归通常需要较长的周期。

常见问题 (Q&A)

Q: 为什么 get_fundamentals 只能获取昨天的数据?
A: 为了避免未来函数。在回测中,当天的财务数据(如市值、PE)通常在收盘后才能确定,因此在盘中交易时只能使用前一个交易日的数据。

Q: 代码中的 set_option('use_real_price', True) 是什么意思?
A: 这开启了动态复权模式。这意味着回测引擎会使用真实的股票历史价格进行撮合,并在分红送股时自动调整持仓数量。这是最接近实盘的模拟方式。

Q: 如果选出的股票少于10只怎么办?
A: 代码逻辑是 df[df['graham_score'] < g.graham_limit]。如果市场整体高估,满足条件的股票可能很少。在这种情况下,策略会自动只买入满足条件的股票,剩余资金会保留为现金。这是符合格雷厄姆“宁缺毋滥”的投资原则的。