🚀 AI 一键生成 ptrade 策略代码

PTrade 价值投资策略:如何用 get_fundamentals 实现格雷厄姆“捡烟蒂”选股?

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

问题描述

如何编写一个基于格雷厄姆价值投资理念的选股策略?

解决方案

这是一个基于**本杰明·格雷厄姆(Benjamin Graham)**价值投资理念的PTrade量化选股策略。

策略核心理念

格雷厄姆被称为“价值投资之父”,其核心思想是寻找市场价格低于其内在价值的股票(即寻找“安全边际”)。

本策略选取了格雷厄姆最经典的几个量化指标进行筛选(防御型投资者标准):

  1. 低市盈率 (PE):PE < 15(或适度放宽至20),代表回本周期短。
  2. 低市净率 (PB):PB < 1.5,代表资产折价。
  3. 格雷厄姆指数 (PE * PB):PE * PB < 22.5。这是格雷厄姆的一个著名推论。
  4. 流动性 (流动比率):流动资产 / 流动负债 > 1.5,确保公司短期偿债能力强。
  5. 市值过滤:剔除极小市值的股票,保证流动性。

策略实现逻辑

  1. 股票池:选用中证800(000906.XBHS)作为基础池,覆盖大盘和中盘蓝筹。
  2. 调仓频率:按月调仓(每20个交易日)。
  3. 资金管理:等权重买入筛选出的前N只股票。

PTrade 策略代码

import pandas as pd
import numpy as np

def initialize(context):
    """
    初始化函数,设置策略参数
    """
    # 设置回测基准:沪深300
    set_benchmark('000300.SS')
    # 设置佣金:万三
    set_commission(commission_ratio=0.0003, min_commission=5.0, type="STOCK")
    # 设置滑点:0.2%
    set_slippage(slippage=0.002)
    
    # 策略参数设置
    g.stock_pool = '000906.XBHS'  # 基础股票池:中证800
    g.max_hold_stocks = 10        # 最大持仓数量
    g.rebalance_days = 20         # 调仓周期(天)
    g.days_counter = 0            # 计数器
    
    # 格雷厄姆筛选阈值
    g.pe_limit = 20.0             # 市盈率上限
    g.pb_limit = 1.5              # 市净率上限
    g.graham_number_limit = 22.5  # PE * PB 上限
    g.current_ratio_limit = 1.5   # 流动比率下限 (流动资产/流动负债)

def before_trading_start(context, data):
    """
    盘前处理
    """
    pass

def handle_data(context, data):
    """
    盘中处理,每日运行
    """
    # 检查是否是调仓日
    if g.days_counter % g.rebalance_days == 0:
        rebalance(context)
    
    g.days_counter += 1

def rebalance(context):
    """
    调仓核心逻辑
    """
    log.info("开始进行格雷厄姆价值选股调仓...")
    
    # 1. 获取基础股票池
    check_stocks = get_index_stocks(g.stock_pool)
    if not check_stocks:
        log.warning("获取股票池失败")
        return

    # 2. 获取财务数据
    # 我们需要:市盈率(PE), 市净率(PB), 流动资产, 流动负债
    # 注意:PTrade对单次查询量有限制,如果股票池很大,建议分批查询或只查核心指标
    
    # 查询估值表 (valuation)
    df_val = get_fundamentals(
        check_stocks, 
        'valuation', 
        fields=['pe_ttm', 'pb', 'market_cap']
    )
    
    # 查询资产负债表 (balance_statement) 用于计算流动比率
    # 注意:balance_statement 数据量较大,这里只取需要的字段
    df_bal = get_fundamentals(
        check_stocks, 
        'balance_statement', 
        fields=['total_current_assets', 'total_current_liability']
    )
    
    # 3. 数据清洗与合并
    if df_val is None or df_bal is None:
        log.warning("财务数据获取失败")
        return
        
    # 将索引转为列,方便合并 (PTrade返回的df索引通常是股票代码)
    df_val['code'] = df_val.index
    df_bal['code'] = df_bal.index
    
    # 合并数据
    df = pd.merge(df_val, df_bal, on='code', how='inner')
    
    # 剔除停牌、ST等(简单剔除PE/PB为0或负值的异常数据)
    df = df[df['pe_ttm'] > 0]
    df = df[df['pb'] > 0]
    
    # 4. 计算格雷厄姆指标
    # 计算流动比率 = 流动资产 / 流动负债
    df['current_ratio'] = df['total_current_assets'] / df['total_current_liability']
    
    # 计算格雷厄姆乘积 = PE * PB
    df['graham_multiplier'] = df['pe_ttm'] * df['pb']
    
    # 5. 执行筛选条件
    # 条件1: PE < 20
    # 条件2: PB < 1.5
    # 条件3: PE * PB < 22.5
    # 条件4: 流动比率 > 1.5
    
    condition = (
        (df['pe_ttm'] < g.pe_limit) &
        (df['pb'] < g.pb_limit) &
        (df['graham_multiplier'] < g.graham_number_limit) &
        (df['current_ratio'] > g.current_ratio_limit)
    )
    
    selected_df = df[condition]
    
    # 6. 排序与截取
    # 按照格雷厄姆乘积从小到大排序,越小越安全
    selected_df = selected_df.sort_values(by='graham_multiplier', ascending=True)
    
    # 取前N只
    target_stocks = selected_df['code'].head(g.max_hold_stocks).tolist()
    
    log.info("选出股票列表: %s" % target_stocks)
    
    # 7. 执行交易
    adjust_position(context, target_stocks)

def adjust_position(context, target_stocks):
    """
    执行具体的买卖操作
    """
    # 获取当前持仓
    current_positions = list(context.portfolio.positions.keys())
    
    # 卖出不在目标列表中的股票
    for stock in current_positions:
        if stock not in target_stocks:
            # 检查是否停牌等状态,PTrade的order_target会自动处理,但最好加个日志
            order_target_value(stock, 0)
            log.info("卖出: %s" % stock)
            
    # 如果没有选出股票,则空仓
    if len(target_stocks) == 0:
        return
        
    # 买入目标股票
    # 资金分配:等权重
    position_count = len(target_stocks)
    total_value = context.portfolio.portfolio_value
    if position_count > 0:
        value_per_stock = total_value / position_count
        
        for stock in target_stocks:
            order_target_value(stock, value_per_stock)
            log.info("买入/调仓: %s, 目标金额: %s" % (stock, value_per_stock))

代码详解

  1. initialize:

    • 设置了交易的基础环境(手续费、滑点)。
    • 定义了格雷厄姆策略的关键阈值:PE < 20, PB < 1.5, PE*PB < 22.5
    • 设置了调仓周期为20个交易日(约一个月)。
  2. rebalance (核心逻辑):

    • 数据获取: 使用 get_fundamentals 获取估值表(valuation)和资产负债表(balance_statement)。
    • 指标计算:
      • 流动比率 (Current Ratio): 这是一个非常重要的格雷厄姆指标,用于衡量企业的偿债能力和安全性。计算公式为 流动资产 / 流动负债
      • 格雷厄姆乘积: PE * PB,这是格雷厄姆用来平衡成长性和资产价值的综合指标。
    • 筛选: 同时满足低估值(PE, PB)和高安全边际(流动比率)的股票。
    • 排序: 按照 PE * PB 从低到高排序,优先买入“最便宜”的股票。
  3. adjust_position:

    • 执行标准的等权重调仓。先卖出非目标持仓,再买入目标持仓。

注意事项

  • 数据缺失处理: 代码中简单的使用了 inner join>0 过滤,实际运行时如果遇到财报空窗期,可能会导致选出的股票较少。
  • Python版本: 代码完全兼容 Python 3.5 语法(不使用 f-string)。
  • API调用: 严格遵循 PTrade 文档,使用 get_index_stocksget_fundamentals
  • 生存偏差: 该策略使用的是 pe_ttm(滚动市盈率),在回测中表现较好,但在实际交易中需注意财报发布的滞后性(PTrade的 get_fundamentals 默认处理了未来函数问题,取的是当时已发布的数据)。

Q&A

Q: 为什么使用中证800而不是全A股?
A: 格雷厄姆策略偏好业绩稳定的大中型公司。全A股包含大量微盘股和壳资源,虽然数据上可能符合低PE/PB,但基本面风险极大(如财务造假)。中证800覆盖了主要的大中盘股,数据质量相对更好,且流动性更有保障。

Q: 如何让策略更激进?
A: 可以放宽筛选条件,例如将 g.pe_limit 提高到 25,或者移除 g.current_ratio_limit 的限制。也可以将排序规则改为按 pe_ttm 排序。

Q: 为什么不使用 get_snapshot 获取价格?
A: 在选股阶段,我们主要依赖财务数据。get_fundamentals 中的 valuation 表已经包含了基于最新收盘价计算的 PE 和 PB,这比手动获取快照再计算要高效且准确。