🚀 AI 一键生成 joinquant 策略代码

如何利用 finance.run_query 查询高管持股变动构建跟随策略?

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

问题描述

我想写一个策略,当公司高管增持股票时就买入,怎么实现?

解决方案

这是一个非常经典的事件驱动策略。在聚宽(JoinQuant)平台上,我们可以利用 finance.run_query 接口查询 高管增持(STK_MANAGEMENT_HOLD_CHANGE) 数据来实现。

策略设计思路

  1. 数据来源:使用 finance.STK_MANAGEMENT_HOLD_CHANGE 表。
  2. 选股逻辑
    • 在每天开盘前,查询 前一个交易日 公告的增持信息。
    • 筛选条件:change_number > 0(变动股数大于0,即增持)。
  3. 交易逻辑
    • 买入:如果有股票出现高管增持信号,且当前没有持仓或持仓时间已到,则买入。
    • 卖出:由于高管增持通常被视为中长期利好,我们设定一个 持仓周期(例如 20 个交易日)。买入后持有满 20 天,如果期间没有新的增持信号,则卖出。
  4. 资金管理:将资金等分,或者根据信号数量动态分配。

策略代码实现

以下是完整的策略代码,您可以直接复制到聚宽的回测环境中运行。

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

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.hold_period = 20        # 设定持仓天数,高管增持通常为中长线逻辑
    g.max_stock_count = 10    # 最大持仓股票数量,防止资金过于分散
    
    # 用于记录每只股票的持仓天数:{stock_code: days_held}
    g.hold_days_counter = {} 

def before_market_open(context):
    """
    开盘前运行:查询高管增持数据
    """
    # 获取前一个交易日
    prev_date = context.previous_date
    
    # 查询 finance.STK_MANAGEMENT_HOLD_CHANGE 表
    # 筛选条件:
    # 1. pub_date (公告日期) 为前一交易日
    # 2. change_number > 0 (增持)
    q = query(
        finance.STK_MANAGEMENT_HOLD_CHANGE.code,
        finance.STK_MANAGEMENT_HOLD_CHANGE.change_number,
        finance.STK_MANAGEMENT_HOLD_CHANGE.pub_date
    ).filter(
        finance.STK_MANAGEMENT_HOLD_CHANGE.pub_date == prev_date,
        finance.STK_MANAGEMENT_HOLD_CHANGE.change_number > 0
    )
    
    df = finance.run_query(q)
    
    # 今日计划买入的股票列表
    g.buy_list = []
    
    if not df.empty:
        # 去重,因为同一天可能有多个高管增持同一只股票
        g.buy_list = list(set(df['code'].tolist()))
        log.info(f"检测到高管增持信号的股票: {g.buy_list}")
    else:
        log.info("昨日无高管增持公告")

def market_open(context):
    """
    盘中运行:执行买卖操作
    """
    # 1. 卖出逻辑:检查持仓天数
    # 获取当前所有持仓
    current_positions = list(context.portfolio.positions.keys())
    
    for stock in current_positions:
        # 如果股票在今天的买入名单里(说明又有高管增持了),重置持仓天数,继续持有
        if stock in g.buy_list:
            g.hold_days_counter[stock] = 0
            log.info(f"{stock} 再次出现增持信号,重置持仓时间")
            continue
        
        # 更新持仓天数
        if stock in g.hold_days_counter:
            g.hold_days_counter[stock] += 1
        else:
            g.hold_days_counter[stock] = 1
            
        # 如果持仓超过设定周期,卖出
        if g.hold_days_counter[stock] >= g.hold_period:
            order_target_value(stock, 0)
            log.info(f"{stock} 持仓满 {g.hold_period} 天,执行止盈/止损卖出")
            del g.hold_days_counter[stock]

    # 2. 买入逻辑
    # 过滤掉停牌、退市或ST的股票(这里简单过滤停牌)
    current_data = get_current_data()
    
    # 计算当前可用资金分配
    # 假设我们要维持 g.max_stock_count 只股票,计算每只股票应该分配的资金
    # 注意:这里采用简单的资金管理,有空位才买
    
    current_hold_count = len(context.portfolio.positions)
    if current_hold_count < g.max_stock_count:
        # 剩余可买的股票数量
        slots_available = g.max_stock_count - current_hold_count
        # 待买入列表(剔除已经在持仓中的)
        targets = [s for s in g.buy_list if s not in context.portfolio.positions]
        
        # 如果待买入的比空位多,截取前面几个
        targets = targets[:slots_available]
        
        if len(targets) > 0:
            # 将剩余可用资金平均分配给要买的股票
            cash_per_stock = context.portfolio.available_cash / len(targets)
            
            for stock in targets:
                if current_data[stock].paused:
                    log.info(f"{stock} 停牌,跳过")
                    continue
                
                # 买入
                order_value(stock, cash_per_stock)
                # 初始化持仓计数器
                g.hold_days_counter[stock] = 0
                log.info(f"买入高管增持股票: {stock}")

def after_market_close(context):
    pass

代码关键点解析

  1. 数据查询 (finance.run_query):

    • 我们查询的是 finance.STK_MANAGEMENT_HOLD_CHANGE 表。
    • pub_date == context.previous_date: 这一点非常重要。为了避免“未来函数”,我们只能在今天开盘前,获取昨天晚上或者昨天白天公告的数据。
    • change_number > 0: 确保是增持(减持为负数)。
  2. 持仓控制 (g.hold_days_counter):

    • 高管增持不是短线打板策略,通常需要一定的时间发酵。因此策略设定了 g.hold_period = 20(20个交易日,约一个月)。
    • 使用一个字典来记录每只股票持有了多少天。
    • 逻辑增强:如果一只股票在持有期间,再次发布了增持公告,我们将它的持仓计数器重置为0,重新开始计算持有期(相当于利好加强,继续持有)。
  3. 资金管理:

    • 设置了 g.max_stock_count 来限制最大持仓只数,避免资金过于分散导致收益微薄,或者资金不足导致无法买入。

常见问题 Q&A

Q: 为什么回测时有时候买入的股票很少?
A: 高管增持并不是每天都会发生的事件。在市场低迷期,增持公告会变多;在市场高位,增持相对较少。这是正常的事件驱动特征。

Q: 如何优化这个策略?
A:

  1. 增加过滤条件:目前只要增持就买。可以优化为:增持金额超过 100 万,或者增持比例超过总股本的 0.1%,以过滤掉象征性的增持。
  2. 结合估值:只买入增持且 PE/PB 处于历史低位的股票。
  3. 结合技术面:增持且股价站上 20 日均线时再买入。

Q: 为什么使用 pub_date 而不是 change_date
A: change_date 是高管实际买入股票的日期,但普通投资者只有在上市公司发布公告(pub_date)后才知道。如果在回测中使用 change_date,就属于使用了“未来数据”,实盘中是无法实现的。必须使用 pub_date