🚀 AI 一键生成 joinquant 策略代码

JoinQuant 事件驱动策略:如何利用财报预告数据进行交易?

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

问题描述

如何编写一个根据财报预告超预期来买入股票的事件驱动策略?

解决方案

编写一个基于“财报预告超预期”的事件驱动策略,核心在于获取上市公司的业绩预告数据,并定义什么是“超预期”。

在量化回测中,通常有两种方式定义“超预期”:

  1. 基于分析师一致预期:比较预告净利润与分析师预测均值(需要额外的高级数据源)。
  2. 基于同比高增长:比较预告净利润下限与去年同期相比的增长率(数据获取简单,本策略采用此方法)。

以下是一个完整的策略代码。该策略的逻辑是:每日开盘前扫描昨日发布的业绩预告,买入预告净利润同比增长超过一定阈值(如50%)的股票,并持有固定天数(如5天)后卖出。

策略代码

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

def initialize(context):
    """
    初始化函数
    """
    # 设定沪深300作为基准
    set_benchmark('000300.XSHG')
    # 开启动态复权模式(真实价格)
    set_option('use_real_price', True)
    # 过滤掉order系列API产生的比error级别低的log
    log.set_level('order', 'error')
    
    # --- 策略参数设置 ---
    # 业绩预告增长阈值(%),例如50代表同比增长50%
    g.growth_threshold = 50  
    # 持仓天数
    g.hold_days = 5
    # 最大持仓数量
    g.max_stocks = 10
    
    # 记录持仓股票的买入日期 {security: buy_date}
    g.holdings = {} 
    
    # 股票类每笔交易时的手续费
    set_order_cost(OrderCost(close_tax=0.001, open_commission=0.0003, close_commission=0.0003, min_commission=5), type='stock')
    
    # 定时运行:每天开盘前运行选股
    run_daily(before_market_open, time='09:00')
    # 定时运行:每天开盘时交易
    run_daily(market_open, time='09:30')

def before_market_open(context):
    """
    开盘前运行:获取业绩预告数据,生成买入列表
    """
    # 获取当前回测日期的前一个交易日
    prev_date = context.previous_date
    
    # 查询业绩预告表 finance.STK_FIN_FORCAST
    # 筛选条件:
    # 1. 公告日期为前一个交易日(确保信息已公开且未利用未来数据)
    # 2. 净利润变动幅度下限 > 阈值
    q = query(
        finance.STK_FIN_FORCAST.code,
        finance.STK_FIN_FORCAST.profit_inc_min
    ).filter(
        finance.STK_FIN_FORCAST.pub_date == prev_date,
        finance.STK_FIN_FORCAST.profit_inc_min > g.growth_threshold
    )
    
    df = finance.run_query(q)
    
    # 初步筛选出的股票列表
    candidates = list(df['code'])
    
    # 进一步过滤股票(去停牌、去ST、去涨停)
    g.buy_list = filter_stocks(context, candidates)
    
    if len(g.buy_list) > 0:
        log.info("今日选出业绩超预期股票: %s" % g.buy_list)

def market_open(context):
    """
    开盘交易逻辑
    """
    # 1. 卖出逻辑:检查持仓时间
    sell_list = []
    current_date = context.current_dt.date()
    
    # 遍历当前持仓
    for security in list(context.portfolio.positions.keys()):
        # 如果该股票在我们的记录中
        if security in g.holdings:
            buy_date = g.holdings[security]
            # 计算持有天数(自然日或交易日均可,这里简单用交易日逻辑近似)
            # 获取所有交易日列表来计算准确的持有交易日天数会更精确,这里简化处理
            # 如果当前日期 > 买入日期 + 持有天数 (简单逻辑)
            # 为了精确控制,我们在买入时记录,每日检查
            pass 
        else:
            # 如果不在记录中(可能是手动买入或初始化前的),将其加入卖出列表或补录
            # 这里选择直接卖出
            sell_list.append(security)
    
    # 检查 g.holdings 中的股票是否到期
    for security, buy_date in list(g.holdings.items()):
        # 获取从买入日期到现在的交易日天数
        days_held = get_trade_days(start_date=buy_date, end_date=current_date)
        # 如果持有天数超过设定值(注意:买入当天算第1天,所以长度要大于 g.hold_days)
        if len(days_held) > g.hold_days:
            sell_list.append(security)
    
    # 执行卖出
    for security in sell_list:
        order_target_value(security, 0)
        if security in g.holdings:
            del g.holdings[security]
        log.info("持有到期,卖出: %s" % security)

    # 2. 买入逻辑
    # 计算当前还可以买入多少只股票
    current_pos_count = len(context.portfolio.positions)
    available_slots = g.max_stocks - current_pos_count
    
    if available_slots > 0 and len(g.buy_list) > 0:
        # 平均分配资金
        cash_per_stock = context.portfolio.available_cash / available_slots
        
        for security in g.buy_list[:available_slots]:
            # 再次检查是否停牌或涨停(防止开盘瞬间状态变化)
            current_data = get_current_data()[security]
            if current_data.paused or current_data.last_price >= current_data.high_limit:
                continue
                
            # 买入
            order_value(security, cash_per_stock)
            # 记录买入日期
            g.holdings[security] = current_date
            log.info("业绩预告利好,买入: %s" % security)

def filter_stocks(context, stock_list):
    """
    过滤股票:去停牌、去ST、去科创板(可选)、去涨停(无法买入)
    """
    if not stock_list:
        return []
    
    curr_data = get_current_data()
    filtered_list = []
    
    for stock in stock_list:
        # 过滤停牌
        if curr_data[stock].paused:
            continue
        # 过滤ST
        if curr_data[stock].is_st:
            continue
        # 过滤涨停(昨日收盘价即为今日涨停价,或者开盘即涨停,这里用昨日收盘价简单判断,实际交易中order函数会处理)
        # 这里主要过滤掉已经退市的或者数据异常的
        if 'XSHE' not in stock and 'XSHG' not in stock:
            continue
            
        filtered_list.append(stock)
        
    return filtered_list

策略实现详解

  1. 数据源 (finance.STK_FIN_FORCAST)

    • 这是聚宽提供的上市公司业绩预告数据库。
    • 关键字段 pub_date (公告日期):我们筛选 pub_date == context.previous_date,即在回测当天的前一个交易日晚上发布的公告,确保在回测当天开盘时已知晓该信息,避免未来函数。
    • 关键字段 profit_inc_min (净利润变动幅度下限):这是判断是否“超预期”的核心指标。代码中设置 g.growth_threshold = 50,意味着只关注预告净利润同比增长下限超过 50% 的股票。
  2. 事件驱动逻辑

    • 信号触发:当上市公司发布高增长预告时,视为利好事件。
    • 执行:在事件发生后的第一个交易日开盘买入。
    • 退出:事件驱动策略通常捕捉的是短期情绪或估值修复,因此设置了固定持有期 g.hold_days = 5。持有满5个交易日后卖出。
  3. 风险控制

    • 过滤 ST 股:避免风险过大的标的。
    • 过滤停牌股:无法交易的股票剔除。
    • 分散持仓:设置 g.max_stocks 限制最大持仓数量,防止单只股票仓位过重。
  4. 注意事项

    • 业绩预告的发布时间:A股业绩预告通常集中在1月底(年报预告)、4月、7月、10月。因此,该策略在非财报披露期可能长时间空仓,属于正常的事件驱动特征。
    • 超预期的定义:本策略简化了“超预期”的定义为“高增长”。在实际更复杂的量化研究中,通常会引入分析师一致预期数据(finance.STK_ANALYST_PREDICT),计算 (预告净利润 - 分析师一致预期) / 分析师一致预期,这需要更复杂的数据处理。

Q&A

Q: 为什么使用 pub_date == context.previous_date 而不是 current_dt?
A: 在回测引擎中,before_market_open 运行在开盘前(如09:00)。此时我们能获取到的确定的公开信息是昨天收盘后到今天开盘前发布的信息。在聚宽数据库中,通常将晚间发布的公告归为当天的 pub_date。为了稳健起见,我们查询前一个交易日发布的预告,并在今日开盘买入。

Q: 如何优化“超预期”的定义?
A: 可以结合 finance.STK_FIN_FORCAST 中的 type 字段。例如,只选择 type 为 "预增" (Pre-increase) 或 "扭亏" (Turnaround) 的股票。此外,可以结合量价因子,例如要求发布预告后股价没有大幅高开(防止利好兑现),或者结合市值因子只做中小盘股。

Q: 策略长时间不交易怎么办?
A: 这是财报事件策略的特性。可以通过组合其他策略(如多因子选股)来填补空窗期,或者放宽筛选条件(如降低增长阈值)。