休市提醒日股将于 2026-11-03 休市(文化日),距今天还有 5 天。全部公告
文章

有值但已经错了:行情数据静默失效的六类检测与代价

链路中断会报错,错误的值不会。一次价格尖峰、一段时间戳回退、一组交叉盘口,都能安静穿过采集与落库,直到在下游的假告警或回测结果里显形。本文按 TIQEX 已文档化字段给出六类静默失效的判据,并附一个无第三方依赖、可直接运行的 Python 校验器与阈值标定脚本。

行情数据质量校验的难点不在“数据有没有到”,而在“到了的数据对不对”。链路中断会报错,错误的值不会:一次价格尖峰、一段时间戳回退、一组交叉盘口,都能安静穿过采集、落库与消费三层,直到在下游的假告警或回测结果里显形。本文按 TIQEX 已文档化的字段口径给出六类静默失效的判据,并附一个可直接运行的 Python 校验器。

一个不会报警的故障

设想一条已在生产运行的管道:WebSocket 连上了,type=subscribed 回执正常,tick 帧以稳定速率到达,连接数、帧率、HTTP 状态码全绿。但日志显示,某个标的的 tick.at 已停在十二分钟前——上游推送早已停止,客户端收到的只是同一个旧值的重复帧。

这类故障的共性是传输层健康、内容层已错,而多数团队只监控前者。更隐蔽的是,即使状态码为 2xx,业务层仍可能失败:TIQEX 的错误文档明确指出,响应信封的 succ 字段在 2xx 下仍可能为 false,因此两者必须同时检查。只看 HTTP 200 就放行,等于把内容层的判据全部让给下游。

结论:生产级管道需要两条独立的健康度信号——传输健康度与内容健康度(值域、时间、自洽性)。本文只讨论后者。

静默失效为什么比中断更贵

中断是显式失败:触发重试与告警,代价可见且有界——几秒到几分钟的缺口,加一次人工确认。静默失效不触发任何机制,代价沿链路放大:

  1. 假信号成本。一条尖峰进入告警系统,触发一次人工核查。被侵蚀的是告警通道的信噪比——假告警占比上升时,真实信号被忽略的概率同步上升。
  2. 重算成本。错误值一旦进入回测数据集或报表,修复不是改一行记录,而是重跑整条管道并重新分发结论。
  3. 信任成本。最贵的一段。团队开始对全部数据打折扣,决策退回人工判断。

作为量级参照,假设一次假告警核查为十五分钟、一次回测重跑为四小时——这是示例参数,不是实测数字。真正值得注意的是成本的不对称:检测本身几乎免费。六条断言都在本地对已到达的数据执行,不发出 API 调用,不消耗配额。用零边际成本换取下游可信度,正是把校验前移到采集端的理由。

六类静默失效与最小检测判据

以下六类覆盖了“值不为空但已经错了”的绝大多数情况,共同点是都能用一条廉价断言检出,无需统计模型或人工标注。

#失效类型典型表现最小检测判据主要误报来源
1陈旧值 stale帧持续到达,at 不前进now − at > k × 典型间隔休市时段
2时间戳回退at 小于上一条按标的维护水位线乱序到达
3价格尖峰last 偏离中位数一个数量级|last − median| > k × 1.4826 × MAD真实跳空
4K 线不自洽high 小于 max(open, close)落库前对每根 bar 断言复权口径混用
5有价无量伪更新volume 未变而 last 变化对 (last, volume) 联合判断集合竞价
6盘口交叉bid 大于 ask 或报价非正中间价计算前断言瞬时单边

三点补充。第 1 类必须结合交易时段:加密市场 7×24 运行(见加密市场页),股票与期货有休市窗口,同一阈值在非交易时段会大量误报。第 3 类用中位数绝对偏差(MAD)而非标准差,因为标准差会被尖峰污染——一个极端值抬高阈值,让下一次尖峰逃逸。第 4 类依赖 OHLCV 字段口径,复权口径混用是最常见的误报来源。

一个可以直接运行的校验器

下面的实现把六条规则收在一个类里,只依赖标准库。输入是 TIQEX WebSocket 的 tick 帧(at 为 RFC3339 时间),输出为违规原因列表,空列表代表通过。订阅报文与帧格式见WebSocket 实时行情接入指南。

Python
# quality.py — 行情数据质量校验:六条廉价断言,仅依赖标准库
from collections import deque
from datetime import datetime, timezone


class QualityGate:
    """median_window: 尖峰检测窗口长度;spike_k: MAD 倍数阈值;stale_after: 陈旧阈值(秒)。"""

    def __init__(self, median_window=200, spike_k=8.0, stale_after=60.0):
        self.prices = deque(maxlen=median_window)
        self.spike_k = spike_k
        self.stale_after = stale_after
        self.last_at = {}
        self.last_vol = {}

    def check(self, tick, now=None):
        now = now or datetime.now(timezone.utc)
        sym = tick["symbol"]
        out = []
        at = datetime.fromisoformat(tick["at"].replace("Z", "+00:00"))

        # 1 陈旧值:帧仍在到达,时间戳不再前进
        lag = (now - at).total_seconds()
        if lag > self.stale_after:
            out.append("stale %s lag=%.0fs" % (sym, lag))

        # 2 时间戳回退:同一标的后一条早于前一条
        prev = self.last_at.get(sym)
        if prev is not None and at < prev:
            out.append("time_regression %s" % sym)
        self.last_at[sym] = at if prev is None else max(at, prev)

        last = tick.get("last")

        # 3 价格尖峰:相对滚动中位数的 MAD 偏离
        if isinstance(last, (int, float)):
            if len(self.prices) >= 30:
                med = sorted(self.prices)[len(self.prices) // 2]
                mad = sorted(abs(x - med) for x in self.prices)[len(self.prices) // 2]
                if mad and abs(last - med) > self.spike_k * 1.4826 * mad:
                    out.append("spike %s %s" % (sym, last))
            self.prices.append(last)

        # 4 有价无量的伪更新:volume 未变而 last 变化
        vol = tick.get("volume")
        if isinstance(vol, (int, float)) and isinstance(last, (int, float)):
            pv = self.last_vol.get(sym)
            if pv is not None and pv[0] == vol and pv[1] != last:
                out.append("phantom_update %s" % sym)
            self.last_vol[sym] = (vol, last)

        # 5 盘口交叉 / 非正报价:不得进入中间价计算
        bid, ask = tick.get("bid"), tick.get("ask")
        if isinstance(bid, (int, float)) and isinstance(ask, (int, float)):
            if bid <= 0 or ask <= 0 or bid > ask:
                out.append("crossed_book %s bid=%s ask=%s" % (sym, bid, ask))

        return out


def check_bar(bar):
    """第 6 类:K 线内部自洽性,落库前逐根断言。"""
    o, h, l, c = (bar[k] for k in ("open", "high", "low", "close"))
    if h < l or h < max(o, c) or l > min(o, c):
        return ["inconsistent_bar %s" % bar.get("openTime")]
    return []

用法是把 check() 接在收帧回调的第一行:空列表就落库,非空则连同违规原因一并写入,而不是丢弃。

阈值不该写死

把 stale_after=60 当作全局常量是下一个常见错误:同一套阈值用在 1 分钟 K 线和日线上,误报率会差一个数量级。可行做法是从标的自身的历史分布反推,并按标的缓存:

Python
# thresholds.py — 从历史分布反推阈值,不要写死全局常量
import statistics as st


def suggest_thresholds(bars):
    """bars 取自 /api/v1/market/equity/kline 的 bars[] 数组。"""
    gaps = [bars[i]["closeTime"] - bars[i - 1]["closeTime"] for i in range(1, len(bars))]
    steps = [abs(bars[i]["close"] - bars[i - 1]["close"]) for i in range(1, len(bars))]
    med = st.median(steps)
    mad = st.median([abs(s - med) for s in steps]) or med * 0.01
    return {
        "stale_after_ms": int(st.median(gaps) * 5),     # 连续 5 个周期无更新即判陈旧
        "spike_abs": round(med + 8 * 1.4826 * mad, 6),  # 8 倍 MAD 以上判为尖峰
    }

输入是 /api/v1/market/equity/kline 的响应体,每根 bar 携带 openTime / closeTime(毫秒);interval 默认 1D,可取 1T 至 1M 共九档,limit 默认 100、范围 1 至 1000。跑一遍历史数据得到的阈值,比经验值更贴近该标的的真实波动结构。

三道防线与放置位置

校验放错位置,成本会翻倍。三条防线各管一类:

  • 采集端(最优先)。WebSocket 帧到达即校验。此处上下文最完整,且推流不按条计费,校验的边际成本为零。
  • 落库前。对 K 线做第 4 类自洽断言与时间戳单调性检查。写入前拒绝,避免污染历史数据集——它被污染后修复成本最高。
  • 消费前。告警与回测读取时再校验一次。理由不是冗余:历史数据可能被上游事后修正。

一条原则:校验不通过的数据应当被标记,而不是被删除。删除会掩盖失效率。

校验的成本与套餐配额

把校验放在采集端的关键收益在成本结构:TIQEX 对 WebSocket 只在握手时计入日配额与 RPM,之后的 tick 推送不再按条计费。对推流做逐条校验几乎不增加调用成本。

真正消耗配额的是异常后的补数。补数走 REST,两个上限需要注意:quote 的 codes 单次最多 50 个标的,kline 的 limit 上限 1000。补数应按批组织,遇到 429 时优先使用响应中的 Retry-After,配合带抖动的指数退避。

速率限制按套餐分档:免费版与试用 30 RPM,标准版 120,专业版 600,企业版 3000;WebSocket 连接数与标的上限依次为 1 / 10、3 / 25、10 / 50、50 / 200。配额与覆盖方面,免费版 300 次/日(加密 / 美股 / 金属延时),标准版 $99/月 50 万次,专业版 $299/月 500 万次且全市场实时,完整对照见定价页。

最后一条容易混淆:SLA 保证的是接口可用性,不是内容正确性。标准版 99.9% 与专业版 99.99% 描述的是服务可用性与延迟档位;内容层的正确性由本文的校验负责,两者需要分开监控。

常见问题(FAQ)

校验会不会拖慢推流?

不会成为瓶颈。六条断言都是常数级或对数级的本地计算,滚动窗口长度可配。需要控制的是每标的状态字典增长,超过数千标的时改用环形数组。单条 tick 的校验开销通常在微秒量级,远低于网络抖动。

阈值应该用固定值还是自适应?

自适应,但要分层:先按市场时段分层,再按标的用历史分布反推,最后按 K 线周期分开配置。全局常量一旦跨市场就会失效。

发现坏 tick 应该丢弃还是保留?

保留并标记。丢弃会让失效率无法统计,也无法判断上游是否在持续劣化。落库时增加一个 quality_flags 字段,由消费端按需过滤——先保住状态,再决定用途。

免费版能做这套校验吗?

可以,而且这是推荐起点。免费版提供加密 / 美股 / 金属的延时数据与 WebSocket 流式推送,300 次/日足够完成校验器开发与阈值标定。需要全市场实时数据时再升级,免费起步无需信用卡。

下一步

落地路径很短:把两个脚本拷进项目,用免费密钥对着一个标的跑 24 小时,统计六类违规的触发次数;再据此调整阈值,最后把校验接进收帧回调。不需要新依赖,也不需要改动数据模型——只是给每条记录补上一个质量标记。

行情管道的可靠性不取决于上游 SLA 有多高,而取决于数据变坏时你多快能知道。把这条判据前移到采集端,是最省成本的一步。

申请免费 API 密钥,把校验器接到真实数据上 →

本文引用的接口字段、错误码、速率限制与套餐覆盖均来自 tiqex.io 官方文档(更新于 2026-09-12)与定价页,访问时间 2026-09-22。示例参数仅用于说明工程实践,不构成投资建议或收益预测。