🚀 AI 一键生成 ptrade 策略代码

PTrade 财务选股:构建高净资产收益率(ROE)优选策略

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

问题描述

怎样筛选净资产收益率(ROE)连续三年大于15%的股票?

解决方案

这是一个非常经典的价值投资选股策略。在PTrade中实现筛选“连续三年ROE大于15%”的股票,主要涉及以下几个关键点:

  1. 获取财务数据:使用 get_fundamentals 函数。
  2. 指定数据表和字段:使用 profit_ability(盈利能力表)中的 roe(净资产收益率)字段。
  3. 指定报表类型:必须使用 report_types='4',代表筛选年报数据。
  4. 动态计算年份:为了避免“未来函数”(即在财报公布前就用到了财报数据),需要根据当前回测日期动态判断应该读取哪几年的年报。通常年报在次年的4月30日前公布。

以下是完整的策略代码实现:

策略代码

def initialize(context):
    """
    初始化函数
    """
    # 设置股票池为空,稍后在 before_trading_start 中筛选
    g.security = []
    set_universe(g.security)
    
    # 设置手续费(可选)
    set_commission(commission_ratio=0.0003, min_commission=5.0, type="STOCK")
    
    # 设定一个全局变量,用于记录上一次筛选的年份,避免每天重复查询大量数据
    g.last_check_year = 0

def before_trading_start(context, data):
    """
    盘前处理:每天开盘前运行,用于筛选股票
    """
    current_dt = context.blotter.current_dt
    current_year = current_dt.year
    current_month = current_dt.month
    
    # ----------------------------------------------------------------
    # 1. 确定要查询的年份范围
    # ----------------------------------------------------------------
    # 逻辑:年报通常在次年4月30日前披露完毕。
    # 如果当前月份 >= 5月,说明去年的年报已经出齐,可以查 [去年, 前年, 大前年]
    # 如果当前月份 < 5月,去年的年报可能还没出,只能查 [前年, 大前年, 大大前年]
    if current_month >= 5:
        end_report_year = current_year - 1
    else:
        end_report_year = current_year - 2
        
    start_report_year = end_report_year - 2  # 往前推2年,共3年
    
    # ----------------------------------------------------------------
    # 2. 避免每天重复计算,仅在年份变化或关键月份时重新筛选
    #    这里为了演示逻辑,简化为:如果计算出的截止年份变了,就重新筛选
    # ----------------------------------------------------------------
    if end_report_year == g.last_check_year:
        return
        
    log.info("开始筛选 %s 至 %s 年连续三年ROE > 15%% 的股票" % (start_report_year, end_report_year))
    
    # 获取全市场A股代码
    all_stocks = get_Ashares()
    
    # ----------------------------------------------------------------
    # 3. 查询财务数据
    # ----------------------------------------------------------------
    # 查询盈利能力表(profit_ability)中的ROE字段
    # report_types='4' 代表年报
    # 注意:一次性查询全市场几千只股票的3年数据可能会比较慢或超时,
    # 实际实盘中建议分批查询或缩小范围。这里为演示直接查询。
    fundamentals = get_fundamentals(
        security=all_stocks,
        table='profit_ability',
        fields='roe',
        start_year=str(start_report_year),
        end_year=str(end_report_year),
        report_types='4'
    )
    
    selected_stocks = []
    
    # ----------------------------------------------------------------
    # 4. 处理数据并筛选
    # ----------------------------------------------------------------
    if fundamentals is not None:
        # PTrade Python3.5环境下,按年份范围查询返回的是 Panel 对象
        # Items轴是股票代码,Major轴是时间(年份),Minor轴是字段(roe)
        # 如果是 Python3.11 环境,返回的可能是 MultiIndex DataFrame,需根据实际环境调整
        
        # 这里按标准 Panel 结构处理
        if hasattr(fundamentals, 'items'):
            for stock in fundamentals.items:
                # 获取该股票的 DataFrame (Index=日期, Column=roe)
                df = fundamentals[stock]
                
                # 过滤掉数据不全的股票(必须有3年的数据)
                if len(df) < 3:
                    continue
                
                # 核心筛选逻辑:判断这3年的ROE是否都大于15
                # PTrade中 roe 字段通常是百分比数值(如 15.5 代表 15.5%)
                # 使用 min() 函数判断最小的一年是否也大于 15
                roe_values = df['roe'].values
                if min(roe_values) > 15:
                    selected_stocks.append(stock)
    
    log.info("筛选完成,共选出 %d 只股票" % len(selected_stocks))
    
    # 更新全局股票池和 PTrade 股票池
    g.security = selected_stocks
    set_universe(g.security)
    
    # 更新检查年份标记
    g.last_check_year = end_report_year

def handle_data(context, data):
    """
    盘中处理:简单的买入持有逻辑
    """
    if not g.security:
        return

    # 简单的等权重买入逻辑
    # 1. 卖出不在股票池中的股票
    for stock in context.portfolio.positions:
        if stock not in g.security:
            order_target(stock, 0)
    
    # 2. 买入股票池中的股票(等权重)
    if len(g.security) > 0:
        # 计算每只股票应该分配的资金
        target_value = context.portfolio.portfolio_value / len(g.security)
        
        for stock in g.security:
            # 过滤停牌、跌停等无法买入的情况
            if data[stock].is_open == 0:
                continue
            
            # 调仓到目标市值
            order_target_value(stock, target_value)

代码逻辑详解

  1. 时间窗口的选择 (before_trading_start)

    • 策略的核心难点在于避免未来函数
    • 代码中判断 current_month >= 5。因为根据A股规定,上市公司必须在4月30日前披露上一年度的年报。
    • 如果在5月1日之后,我们可以安全地使用去年的年报数据。
    • 如果在5月1日之前,去年的数据可能不全,为了保证连续性,我们退而求其次,使用前年、大前年、大大前年的数据。
  2. 数据获取 (get_fundamentals)

    • table='profit_ability':ROE数据位于盈利能力表中。
    • report_types='4':非常重要,这指定了我们只看年报。如果不加这个参数,可能会混入季报数据,导致逻辑错误(季报的ROE通常不能直接和年报ROE比较,除非做年化处理)。
    • start_yearend_year:用于指定获取数据的年份区间。
  3. 数据筛选

    • get_fundamentals 在查询多只股票、多个年份时,返回的数据结构通常是 pandas.Panel(在PTrade Python 3.5环境中)。
    • 我们遍历每只股票,检查其返回的数据长度是否为3(确保三年都有数据)。
    • 使用 min(roe_values) > 15 来确保每一年的ROE都超过15%。
  4. 交易执行 (handle_data)

    • 这是一个基本的等权重调仓逻辑。如果股票被剔除出名单,就卖出;如果在名单内,就调整持仓至平均市值。

常见问题提示

  • 数据量问题get_fundamentals 一次性查询全市场4000+只股票的3年数据可能会比较耗时或触发接口限制。如果遇到超时问题,可以将 all_stocks 切片分批查询。
  • ROE单位:PTrade中的 roe 字段单位通常是百分比数值(例如返回 18.5 代表 18.5%),因此代码中直接与 15 比较是正确的。
  • Python版本:上述代码基于PTrade标准的Python 3.5环境编写(使用Panel结构)。如果您的券商环境已升级至Python 3.11,get_fundamentals 返回的将是 DataFrame(多级索引),此时遍历逻辑需要微调(通常使用 df.groupby(level=0) 进行遍历)。

Q&A

Q: 为什么不使用 get_history 来获取ROE?
A: get_history 主要用于获取行情数据(如开盘价、收盘价、成交量)。ROE属于财务数据,必须通过 get_fundamentals 获取。

Q: 如何筛选“扣非ROE”?
A: 在 get_fundamentals 中,可以将字段 roe 改为 roe_cut(净资产收益率%扣除摊薄),这通常更能反映企业主营业务的盈利能力。

Q: 策略回测时为什么5月之前买入的是前年的优绩股?
A: 这是为了严谨性。在现实世界中,直到4月底年报披露完毕前,我们无法确切知道所有公司去年的完整ROE。为了防止回测利用“未公开信息”作弊,必须等待年报披露期结束后再使用当年的数据。