🚀 AI 一键生成 qmt 策略代码

QMT API 教程:如何获取十大股东数据并构建事件驱动策略?

qmt | 发布时间: 2025/11/28
以下内容由 EasyQuant 生成。

问题描述

如何获取上市公司的十大股东持仓数据,并根据股东变化来构建交易策略?

解决方案

在 QMT (Quantitative Trading Platform) 中,获取上市公司十大股东数据主要使用 ContextInfo.get_top10_share_holder 接口。

基于股东变化构建策略通常遵循**“筹码集中度”逻辑:如果前十大股东持股比例总和增加,通常意味着筹码趋于集中,主力资金可能在收集筹码,视为看多信号;反之,如果持股比例下降,意味着筹码分散,主力可能在派发,视为看空**信号。

以下是详细的 API 说明及完整的策略代码实现。

1. 核心 API 说明

函数名: ContextInfo.get_top10_share_holder(stock_list, data_name, start_time, end_time)

  • stock_list: (list) 股票代码列表,如 ['600519.SH']
  • data_name: (string) 数据类型。
    • 'holder': 十大股东(包含流通和非流通)。
    • 'flow_holder': 十大流通股东。
  • start_time: (string) 开始时间,如 '20220101'
  • end_time: (string) 结束时间,如 '20230101'

返回值:
返回 pandas.DataFrame(单股多季度或多股单季度)或 pandas.Panel(多股多季度)。
主要字段包括:holdRatio (持股比例), holdNum (持股数量), changReason (变动原因) 等。


2. 策略逻辑设计

本策略是一个低频趋势策略,逻辑如下:

  1. 标的:选取一只代表性股票(如贵州茅台 600519.SH)。
  2. 周期:日线级别回测(因为财报数据发布频率较低,无需分钟级)。
  3. 信号生成
    • 获取过去一年的十大流通股东数据。
    • 按发布日期分组,计算每一期十大流通股东的持股比例总和
    • 对比最新一期上一期的持股比例总和。
    • 买入信号:如果最新一期总和 > 上一期总和(筹码集中),且当前无持仓,则全仓买入。
    • 卖出信号:如果最新一期总和 < 上一期总和(筹码分散),且当前有持仓,则清仓卖出。
  4. 风控:由于财报数据有滞后性(公告日晚于报告期),策略在实盘中依赖于数据的及时更新。

3. 策略代码实现

# -*- coding: gbk -*-
import pandas as pd
import time

def init(ContextInfo):
    # 1. 设置策略基本参数
    ContextInfo.stock = '600519.SH'  # 标的股票:贵州茅台
    ContextInfo.account_id = '600000248' # 请替换为您的真实资金账号
    ContextInfo.set_account(ContextInfo.account_id)
    
    # 设置股票池
    ContextInfo.set_universe([ContextInfo.stock])
    
    # 2. 策略控制变量
    ContextInfo.check_interval = 5 # 每隔5天检查一次数据,减少运算量
    ContextInfo.days_counter = 0

def handlebar(ContextInfo):
    # 跳过非交易日或未完成的K线
    if not ContextInfo.is_last_bar():
        return

    # 限制检查频率
    ContextInfo.days_counter += 1
    if ContextInfo.days_counter < ContextInfo.check_interval:
        return
    ContextInfo.days_counter = 0

    # 1. 获取当前时间
    current_time = ContextInfo.get_bar_timetag(ContextInfo.barpos)
    # 将时间戳转换为字符串格式 YYYYMMDD
    end_date = timetag_to_datetime(current_time, '%Y%m%d')
    # 设置开始时间为一年前,确保能取到至少两期财报数据
    start_date = timetag_to_datetime(current_time - 365 * 24 * 3600 * 1000, '%Y%m%d')

    # 2. 获取十大流通股东数据
    # data_name 使用 'flow_holder' 获取流通股东,也可以用 'holder' 获取全部股东
    try:
        df = ContextInfo.get_top10_share_holder(
            [ContextInfo.stock], 
            'flow_holder', 
            start_date, 
            end_date
        )
    except Exception as e:
        print(f"获取数据异常: {e}")
        return

    if df is None or df.empty:
        return

    # 3. 数据处理
    # 返回的 df index 通常是日期,columns 是字段名
    # 我们需要按日期分组,计算持股比例(holdRatio)的总和
    
    # 确保索引是日期格式以便排序
    df.index = pd.to_datetime(df.index)
    
    # 按日期分组求和,计算每一期十大股东的持股比例总和
    # holdRatio 单位通常是百分比
    concentration_series = df.groupby(df.index)['holdRatio'].sum().sort_index()

    # 确保至少有两期数据进行比较
    if len(concentration_series) < 2:
        print("历史财报数据不足,无法比较")
        return

    # 获取最近两期的数据
    last_ratio = concentration_series.iloc[-1]      # 最新一期
    prev_ratio = concentration_series.iloc[-2]      # 上一期
    report_date = concentration_series.index[-1].strftime('%Y-%m-%d')

    # 获取当前持仓
    positions = get_trade_detail_data(ContextInfo.account_id, 'stock', 'position')
    current_holding = 0
    for pos in positions:
        if pos.m_strInstrumentID == ContextInfo.stock:
            current_holding = pos.m_nVolume
            break

    # 4. 交易逻辑
    # 信号:筹码集中(最新比例 > 上期比例) -> 买入
    if last_ratio > prev_ratio:
        if current_holding == 0:
            print(f"[{end_date}] 筹码集中信号: 最新({last_ratio:.2f}%) > 上期({prev_ratio:.2f}%)。报告期:{report_date}。执行买入。")
            # 全仓买入 (使用 order_target_percent 需要在回测设置中设置初始资金)
            order_target_percent(ContextInfo.stock, 0.9, ContextInfo, ContextInfo.account_id)
        else:
            # 已经是持仓状态,继续持有
            pass
            
    # 信号:筹码分散(最新比例 < 上期比例) -> 卖出
    elif last_ratio < prev_ratio:
        if current_holding > 0:
            print(f"[{end_date}] 筹码分散信号: 最新({last_ratio:.2f}%) < 上期({prev_ratio:.2f}%)。报告期:{report_date}。执行卖出。")
            # 清仓
            order_target_percent(ContextInfo.stock, 0, ContextInfo, ContextInfo.account_id)
    
    else:
        print(f"[{end_date}] 筹码无变化。")

4. 代码关键点解析

  1. 数据获取范围get_top10_share_holder 需要足够长的时间窗口(代码中设置为过去365天),因为财报是季度发布的。如果时间窗口太短,可能只获取到一期数据,无法计算变化。
  2. 数据聚合:API 返回的数据包含前10名股东的详细信息(即每个日期有10行数据)。代码中使用 df.groupby(df.index)['holdRatio'].sum() 将同一日期的10位股东持股比例加总,得到当期的“集中度”。
  3. 交易执行:使用了 order_target_percent 函数。
    • 0.9 代表调整仓位至总资产的 90%(买入)。
    • 0 代表调整仓位至 0%(清仓卖出)。
  4. 时间处理:使用了 timetag_to_datetime 将 QMT 的毫秒时间戳转换为 API 所需的 'YYYYMMDD' 字符串格式。

5. 注意事项

  • 数据滞后性:这是此类策略最大的风险点。例如,一季报(截止3月31日的数据)可能在4月20日才公告。在回测中,QMT 的数据通常是按照公告日生效的,这避免了“未来函数”问题,但也意味着信号会有显著延迟。
  • 数据清洗:有时上市公司会发布修正公告,导致同一报告期有两条记录,或者数据缺失。实际生产环境中需要增加更健壮的数据清洗逻辑。
  • 回测模式:请确保在“回测模式”下运行此代码,并下载好对应的财务数据和行情数据。

Q&A

Q: 如何区分“十大股东”和“十大流通股东”?
A: 在 get_top10_share_holder 函数的第二个参数中设置。传入 'holder' 获取前十大股东(包含限售股),传入 'flow_holder' 获取前十大流通股东。通常二级市场交易更关注流通股东的变化。

Q: 这个策略适合高频交易吗?
A: 不适合。股东数据每季度才更新一次(年报、一季报、半年报、三季报),信号频率极低,属于中长线基本面/资金面策略。

Q: 如果我想跟踪特定机构(如“证金公司”)的持仓怎么办?
A: API 返回的 DataFrame 中包含 holdName(股东名称)字段。你可以在代码中遍历数据,筛选 holdName 包含特定关键字的行,然后单独计算其持股变化。