集合竞价那 10 分钟,你的行情接口在「装档」吗?

集合竞价那 10 分钟,你的行情接口在「装档」吗?

作者:量化专家 | 迪雅数据量化学院 适合读者:用行情接口做监控、告警或日内策略的个人开发者

上一篇《回测十年跑得准,先过数据源这关》聊的是历史数据。这一篇说一个只在盘中 10 分钟内出现、但足够让你的监控脚本误报一整年的坑。


一、一个真实的场景

你在 9:25 盯着自己写的监控脚本,它每分钟拉一次自选股的实时行情。9:25 那一刻,屏幕跳出一行告警:

【异动】XX股份 现价 0.00,跌幅 -100.00%

你手忙脚乱打开交易软件——股票明明好好的,昨天收盘 18.60,现在集合竞价挂的是 18.75。

问题不在股票上。问题在这个数据源在集合竞价时段给你返回了 0。

这不是个例。A 股每个交易日 9:15–9:25 是集合竞价窗口,9:20 之前还能撤单、9:20 之后不能撤,9:25 撮合出开盘价。这段时间里,"最新价"这个字段在不同的数据源、甚至同一个数据源的不同接口上,含义完全不同


二、"装档"是指什么

这是行情圈的一个说法,指数据源在非连续交易时段,用一个占位值填进本该有真实价格的字段。

常见的占位手法有四种,危害程度完全不同:

装档方式你看到的值危害
填 00.00极高。任何按百分比算涨跌幅的代码都会算出 -100%,直接触发告警
填昨收18.60(昨收)最高。看起来"正常",但涨跌幅永远是 0%,你的异动监控彻底失效且不报错
填昨结/前收但字段含义被改数值正常高。你以为拿到的是竞价撮合价,其实是昨收,逻辑判断全歪
返回空 + 明确标注时段null / 无此条干净。你的代码能判断出"这是空档期",可正确处理

注意第二行那格——它比填 0 更危险。填 0 至少会炸出告警让你发现,填昨收则是静默失效:你的监控在 9:15–9:25 这 10 分钟里变成了一个"永远显示涨跌幅 0%"的摆设,而且它不报错、日志干净、看起来一切正常。

一个可以立刻验证的真实样本

随手拉一份实时行情快照,你会看到这种形态:

[
  {
    "代码": "sz000001",
    "名称": "平安银行",
    "最新价": "10.93",
    "涨跌幅": "-0.09",
    "成交量": "0",
    "成交额": "0",
    "今开": "10.94",
    "昨收": "10.94",
    "最高": "10.93",
    "最低": "10.93"
  }
]

价格字段全有值,但成交量和成交额是 0。这不是错误——它反映的是"这个时刻还没有成交"。问题是:

  • 如果你的告警逻辑里有"成交量放大 N 倍"这一条,0 倍 会不会误判?
  • 如果你的策略用 成交额 / 成交量 去算均价,这个除法会不会炸?

同一份数据,"有值"和"有意义"是两件事。 数据源没错,是你的代码需要知道这个边界。


三、为什么会装档

三个层面的原因,都是真实的工程约束:

1. 集合竞价本身没有"连续成交价" 9:15–9:25 沪深两市是撮合阶段,只公布虚拟开盘参考价。严格说,这 10 分钟里不存在"最新成交价"这个量。数据源要么给你虚拟参考价,要么给你一个占位符——它可以选,但很多源的实现是"如果字段空就填 0"。

2. 上游推送的字段不齐 部分行情源在竞价期间只推成交量、不推最新价。中间层做字段拼装时,缺失字段用默认值兜底,默认值恰好是 0.0(因为很多语言的数值类型默认就是 0)。

3. 缓存穿透后的兜底值 如果你的请求打到了缓存未命中的路径,有些实现会返回一个结构完整但内容为空的骨架,价格字段是 0。

这三个原因都不是 bug,是设计选择。 但对你的代码来说,结果是一样的:拿到了一个假数据,而且不知道它是假的。


四、怎么验证你的数据源

不用等第二天开盘。下面这套检查你现在就能跑,全部只看字段行为,不依赖你选哪家源

检查 1:非交易时段调用,看返回什么

选一个周末或交易日 15:30 之后,调一次实时行情接口。看三个点:

  • HTTP 状态码:干净的做法是 200 + 空数据,或者明确的状态标识。返回 500 或超时的是粗糙做法,意味着你必须在客户端加"非交易时段判断",否则半夜监控必炸。
  • 价格字段的值:是 null、不返回该字段、返回 0、还是返回昨收?
  • 有没有时段标识:理想的响应里会带一个字段告诉你"这是收盘后快照"或"非交易时段",让你的代码有依据可判断。

检查 2:看字段的"语义声明"

看文档里 last_price(或对应字段)的定义。如果文档只写"最新价"三个字,没有说明非交易时段的行为,这本身就是个信号——说明实现方没有把这个边界当回事。

检查 3:写一个时段的探针(关键)

这才是最该做的一步。写个脚本,在交易日 9:149:31 之间,每 30 秒调一次,把时间戳和价格字段一起打到日志里:

import time, requests, json
from datetime import datetime

API = "https://api.cxdy.vip/api/hq"
TOKEN = "你的Token"
SYMBOL = "sh600519"

log = []
while True:
    now = datetime.now()
    if now.hour == 9 and 14 <= now.minute <= 31:
        try:
            r = requests.get(API, params={
                "apiToken": TOKEN,
                "symbol": SYMBOL,
            }, timeout=10).json()
            log.append({
                "t": now.strftime("%H:%M:%S"),
                "raw": r,
            })
        except Exception as e:
            log.append({"t": now.strftime("%H:%M:%S"), "error": str(e)})
    elif now.hour == 9 and now.minute > 31:
        break
    time.sleep(30)

with open("auction_probe.json", "w", encoding="utf-8") as f:
    json.dump(log, f, ensure_ascii=False, indent=2)

print(f"采集 {len(log)} 条,看 9:25 前后价格字段怎么变")

跑完之后,重点看这三行日志:

观察点干净的表现危险的表现
9:15–9:20价格有值(虚拟参考价)或明确为空标记返回 0
9:20–9:25价格稳定、不跳变(不可撤单期)字段闪烁、值忽有忽无
9:25 瞬间价格从参考价切到开盘价,且能看出这是切换点无法区分,或切换时出现异常值

这张表的价值在于:它不评判哪家源好,它只告诉你"你的代码需要为哪种情况写防御"。


五、防御写法:三层保护

知道了坑在哪,代码里加三层就够。

第一层:时段时间窗判断

from datetime import time as t

def is_auction_period(now):
    """A股集合竞价:9:15-9:25;连续竞价:9:30-11:30, 13:00-15:00"""
    hm = now.time()
    return t(9, 15) <= hm < t(9, 25)

def is_continuous_trading(now):
    hm = now.time()
    return (t(9, 30) <= hm < t(11, 30)) or (t(13, 0) <= hm < t(15, 0))

第二层:值合法性校验(这一层最关键)

不要相信任何单个价格字段。用下面的规则过滤:

def sanitize_quote(q, pre_close):
    """过滤装档值。返回 (是否可信, 价格)"""
    if q is None:
        return False, None
    # 规则1:0 或负数一律不可信
    if q <= 0:
        return False, None
    # 规则2:偏离昨收超过 ±20% 的,A股除新股外基本不可能,判定为异常
    if pre_close and pre_close > 0:
        dev = abs(q - pre_close) / pre_close
        if dev > 0.20:
            return False, None
        # 规则3:完全不偏离(正好等于昨收)在竞价期高度可疑,标记为存疑
        if dev == 0:
            return "suspect", q
    return True, q

第三层:告警去抖

监控脚本不要在单次异常值上直接告警,要求"连续 2 次采样都异常"才推。这一层能挡掉绝大部分由装档、缓存穿透、网络抖动引起的误报。

class Debouncer:
    def __init__(self, need=2):
        self.need = need
        self.hits = {}
    def check(self, key, is_bad):
        n = self.hits.get(key, 0)
        n = n + 1 if is_bad else 0
        self.hits[key] = n
        return n >= self.need   # 连续 N 次才真告警

六、另一种思路:把竞价数据单独拿出来

上面讲的是"防御"——用一个通用实时接口,然后自己过滤掉不可信的值。

还有另一种设计思路:既然竞价这 10 分钟的数据结构和连续竞价完全不同,就给它一个独立的接口。

这样做的直接好处是:字段名里可以带上单位,从源头消除歧义。看一个真实的竞价数据返回:

[
  {
    "股票代码": "000001.SZ",
    "交易日期": "20260526",
    "集合竞价成交量(股)": "867700",
    "集合竞价成交均价(元)": 10.71,
    "集合竞价成交金额(元)": 9293067,
    "昨收价(元)": 10.68,
    "集合竞价换手率(%)": 0,
    "集合竞价量比": 1.02,
    "流通股本(万股)": 1940560
  }
]

注意三个细节:

1. 单位写在字段名里成交量(股)成交均价(元)流通股本(万股))。你不用去翻文档确认"这个 volume 是手还是股"——歧义在设计阶段就被消掉了。这比事后写注释可靠得多。

2. 没有"最新价"字段,只有"成交均价"。这个命名是诚实的:集合竞价输出的是一个撮合均价,不是某个瞬时成交价。字段名如实描述了它的语义,你也就不会误用。

3. 换手率 是 0,但成交量不是 0。867700 股对应的换手率太小,被抹成了 0。注意——这是小数精度问题,不是装档。区分这两者很重要:

现象性质你要做什么
成交量 867700,换手率显示 0精度抹零用成交量自己算,别依赖这个字段
成交量 0,价格有值(收盘后)非交易时段加时段判断
价格 0,成交量有值真装档加值合法性校验

同一个"0",可能是三种完全不同的东西。 这就是为什么"看到 0 就当异常处理"是偷懒做法——它会让你在正常场景下也误报。

另一种是历史竞价数据,字段设计又不一样:

[
  {
    "股票代码": "sh688595",
    "股票名称": "芯海科技",
    "交易日期": "2026-09-09",
    "集合竞价价格": 29.4001,
    "集合竞价成交量": 4864,
    "竞价涨幅": 1.17,
    "开盘价": 29.4001,
    "竞价换手率": 0.34,
    "竞价量比": 0.23,
    "竞价情绪": "平开"
  }
]

这里有个有意思的字段——竞价情绪(值 平开)。它把"竞价价格相对昨收的偏离程度"直接算成了一个可读标签。分类这件事在服务端做完,你不用在自己的代码里再维护一套阈值表。

但请注意两个接口的 symbol 格式不一样zpjj 返回 000001.SZ(后缀式),lsjj 返回 sh688595(前缀式)。同一个站内的两个接口都不统一

这不是迪雅独有的问题——几乎每一家数据源都存在接口间格式不一致,因为不同接口往往由不同的数据管线维护。这引出了下一篇的主题:字段校准。你的代码必须能吸收这种差异,而不是假设"既然是同一家,格式肯定一样"。


七、把这件事写进你的选型清单

上一篇的结论是"数据源要口径稳定"。这一篇补上一条更具体的:

在选行情接口时,除了看"能不能拿到实时数据",还要专门确认两件事:

  1. 非交易时段 / 集合竞价时段,价格字段返回什么? 文档里有没有明确写?
  2. 有没有办法判断"这个值是不是有效值"? 是靠你猜,还是响应里有字段可以判断?

这两条都能明确回答的源,说明它的实现方认真想过边界情况。回答不了的,你就得在客户端自己补防御层——补得上,但那是你替它承担的维护成本

第三条判断标准,是看它有没有为特殊时段做专门的数据结构。做这件事的成本不低(要单独维护一条数据管线),愿意做的源,通常意味着它对数据质量有明确要求。


八、写在最后

集合竞价那 10 分钟,占一个交易日的 1/24。但因为它是每天都会准时发生的边界情况,一旦没处理,你的监控脚本就会在每一个交易日都经历一次数据失真。

比"拿到 0"更麻烦的是"拿到昨收"——它不报错,让你以为一切正常。同样麻烦的是把"精度抹零"当成"装档"来防——它会让你在正常场景下也误报。

判断方法很简单:找个周末,或者今天收盘后,调一次你的接口。 看它返回什么。这五分钟的测试,能省掉你未来无数个被误报告警吵醒的早上。

本文为个人量化研究经验分享,数据接口信息以各平台官网最新公布为准。市场有风险,回测与监控结果不代表实盘收益,不构成任何投资建议。


关于本文用到的数据

文中的竞价数据结构示例,取自迪雅数据(diyadata.cn)的集合竞价接口。它提供两个互补的接口:

接口用途数据范围
早盘竞价 zpjj当日竞价数据,字段带单位后缀当日
历史集合竞价 lsjj历史竞价回溯,含「竞价情绪」分类字段历史区间

两者的 symbol 格式不同(000001.SZsh688595),建议按本文的归一化思路统一处理。迪雅数据目前提供 50+ 个 A 股数据接口,具体字段与调用方式见官网接口文档。

下一篇预告:《同一只股票,两个数据源给出的"成交量"差 100 倍——字段校准 6 个校准点》

评分:
在线客服
工作日 9:00 - 18:00 在线回复

产品使用、订单、售后问题
点击下方按钮在线沟通

点击开始 QQ 咨询

客服QQ:64935542

官方微信自助客服

扫一扫添加微信客服
获取专属技术支持