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

行情数据缺失值三种语义:null、0 与 delayed 标记该怎么读

行情接口里的“没有值”并不只有一种含义。TIQEX 的 quote 响应用 delayed 与 unavailable 两个布尔标记区分延时回退与不可用,而 0 是合法数值。三者混用是数据质量事故的起点。本文按字段口径拆开,并给出可直接运行的三态处理代码。

行情数据缺失值的处理,是行情接入里最容易被跳过、也最容易在生产环境暴露的一环。多数团队把精力放在鉴权、重连和限流上,默认“接口返回的每个字段都有值”;等到某个标的在某个时段返回了 null 或 0,下游的指标、告警和回测会同时失真。问题的根源在于:行情响应里的“没有值”并不只有一种含义。TIQEX 的 quote 响应把它明确拆成两个布尔标记 delayed 与 unavailable,而 0 是一个完全合法的数值。三者混用,就是绝大多数数据质量事故的起点。本文按字段口径逐个拆开,并给出可直接运行的处理代码。

一个常见事故:把 null 当成 0 之后

场景很典型。某监控服务每 5 秒拉一次 /api/v1/market/quote,对 last 字段做阈值比较。某天上游行情源短暂不可用,接口返回 HTTP 502 与错误码 UPSTREAM_UNAVAILABLE;采集侧取不到值就写成 0。于是所有标的价格在同一分钟内变成 0,跌停告警触发了几百条,值班同学花了四十分钟才确认这是数据问题而非行情问题。

这个链条上有两个独立的错误。第一,把“请求失败”降级成了“数值为零”,丢失了错误的可区分性;第二,把缺失与合法零值压进了同一个表示。只要数据模型里 0 同时承担“价格为零”和“没有数据”两种含义,下游任何判断都无法自救。

正确的做法不是写更多兜底分支,而是在入口处就把状态建模清楚:一份行情记录至少有三种状态——有值、延时回退、不可用,外加一种错误状态。区分清楚之后,下游只需要问“这条记录能不能用于实时决策”,而不是猜这个 0 是什么意思。

三种状态:有值、延时回退、不可用

TIQEX 的 quote 响应在 data 里对每个标的返回一组字段,其中与数据可用性直接相关的是 source、delayed、unavailable 与 timestampMs。文档把 delayed 与 unavailable 描述为“延时回退或不可用标记”,两者是不同布尔量:delayed=true 表示有数据、但按当前权益只能拿到延时档位;unavailable=true 表示当前没有可用数据。

需要注意,data 的形态会随请求变化:一次请求多个代码时(codes 最多 50 个,逗号分隔)返回数组,单个代码时可能是对象。下面的处理函数对两种形态都做了归一。

状态判据正确处置
有值(实时)succ=true、delayed 与 unavailable 均非真、last 有值进入实时链路,可用于告警与看板
延时回退delayed=true,字段齐全可用于分析、回测、展示;不得用于实时触发
不可用unavailable=true写空值并打标,不参与任何计算
请求失败HTTP 非 200,或 succ=false,code 有值按错误码分类重试或告警,不写入数据表

这张表的关键在于第三行与第四行:不可用是业务状态,失败是传输状态。前者可以正常落库并留痕,后者必须走错误处理路径。把两者混为一谈,就会出现“静默丢数据”。

为什么 0 是合法值

在 K 线数据里,0 的合法性更明显。/api/v1/market/equity/kline 返回的 bars[] 包含 open / high / low / close / volume,以及 vwap / trades / session 等辅助字段。某个时间区间内该标的一笔成交都没有,volume=0 就是事实,不是缺失。把它当成缺失并过滤掉,等于人为删掉了一段真实的低流动性区间;把它当成缺失并填成上一根的成交量,等于凭空制造了不存在的成交。

判断字段是否缺失,唯一可靠的依据是“字段是否为 null 或不存在”,而不是“字段是否等于 0”。这条规则在成交量、笔数(trades)这类天然可能为零的字段上尤其重要。K 线各字段的完整含义可参考术语条目 K 线 OHLCV 字段详解,本文不再重复。

与之相对,high / low 出现 null 就是明确的信号:这一根 bar 没有任何有效报价,此时整根 bar 都应视为不可用,而不是只把价格补成 0 继续算指标。

缺失的五个来源与对应处置

把缺失当成一个笼统概念,就无法设计重试策略。按来源拆开,处置方式完全不同:

  1. 休市与时段外。不同市场的交易时段不同,加密市场则接近 7×24,覆盖范围见 加密货币市场页。时段外没有 tick 是正常现象,应按时段过滤,而不是判为故障。
  2. 标的当日无成交。产生合法零值,字段有值、数值为零,正常落库。
  3. 上游行情源暂时不可用。对应 HTTP 502 UPSTREAM_UNAVAILABLE,属于可重试错误,应指数退避后重试,期间不得写入零值。
  4. 权益未覆盖。对应 HTTP 403 的 MARKET_NOT_ENABLED、API_KEY_SCOPE_DENIED 等,属于配置问题,重试无效,应直接告警。
  5. 配额与限流。对应 HTTP 429 的 DAILY_LIMIT_EXCEEDED、RATE_LIMIT_EXCEEDED 等,需要降频或改用推送。注意 REST 的 quote / kline 与 WebSocket 握手都计入日配额与 RPM,而推送的 tick 不重复计次。

这五类里,只有第三类是“等一下就好”。把第三类和第四类用同一个重试循环处理,结果通常是配额被无意义地烧掉,而真正的配置错误被埋在一堆重试日志里。

工程落地:把三态写进数据模型

下面的函数把一次 quote 请求的结果归一成四态之一,并保留原始错误码。它是后续所有判断的唯一入口,输出可以直接落库或投递给下游。

Python
import requests

API = "https://api.tiqex.io/api/v1/market/quote"
HEADERS = {"X-API-Key": "YOUR_API_KEY"}


def fetch_quotes(codes):
    """返回 (state, payload)。state 取 live / delayed / unavailable / error。"""
    resp = requests.get(API, params={"codes": ",".join(codes)},
                        headers=HEADERS, timeout=10)
    try:
        body = resp.json()
    except ValueError:
        return "error", {"code": "BAD_JSON", "msg": resp.text[:200]}

    if resp.status_code != 200 or not body.get("succ"):
        # 传输层失败:保留机器可读错误码,绝不降级为零值
        return "error", {"code": body.get("code"), "msg": body.get("msg")}

    data = body["data"]
    rows = data if isinstance(data, list) else [data]
    out = []
    for q in rows:
        if q.get("unavailable"):
            state = "unavailable"
        elif q.get("delayed"):
            state = "delayed"
        elif q.get("last") is None:
            state = "unavailable"
        else:
            state = "live"
        out.append({"symbol": q.get("code") or q.get("symbol"),
                    "state": state, "last": q.get("last"),
                    "at": q.get("timestampMs")})
    return "ok", out


if __name__ == "__main__":
    print(fetch_quotes(["EURUSD", "XAUUSD"]))

注意 last is None 这一条:它处理“接口成功、但该标的确实没有最新价”的情况。归入 unavailable 而不是造一个 0,下游阈值判断自然就不会误触发。

K 线侧的校验逻辑同样简单:只检查字段是否存在,不检查数值是否为零。

Python
import requests

KLINE = "https://api.tiqex.io/api/v1/market/equity/kline"
HEADERS = {"X-API-Key": "YOUR_API_KEY"}


def fetch_bars(symbol="AAPL", interval="1d", limit=30):
    resp = requests.get(KLINE, params={"symbol": symbol,
                                       "interval": interval,
                                       "limit": limit},
                        headers=HEADERS, timeout=10)
    body = resp.json()
    if not body.get("succ"):
        raise RuntimeError(body.get("code"), body.get("msg"))
    return body["data"]["bars"]


def usable(bar):
    """字段齐全即可用;volume 为 0 是合法状态,不是缺失。"""
    return all(bar.get(k) is not None
               for k in ("open", "high", "low", "close", "volume"))


if __name__ == "__main__":
    bars = fetch_bars()
    good = [b for b in bars if usable(b)]
    print("总 bar 数:", len(bars), "字段完整:", len(good))
    print("零成交 bar:", sum(1 for b in good if b["volume"] == 0))
    print("样例:", good[-1])

在实时链路上,同样的原则适用于 WebSocket:wss://api.tiqex.io/api/v1/market/stream 的帧按 type 区分,subscribed 是订阅确认,tick 是数据,error 携带 msg 与 code(例如连接数超限的 WS_CONN_LIMIT)。把 error 帧当成数据帧解析,是另一条静默丢数据路径。订阅与心跳的完整写法见 WebSocket 实时行情接入指南。

延时数据什么时候可以接受

区分出 delayed 之后,还需要回答“延时数据能不能用”。判断标准不是延迟多久,而是这条数据被用来做什么决策。

  • 可以接受:历史研究、回测数据集构建、图表展示、日报与周报、非实时的风控巡检。这些场景对时效不敏感,延时数据不影响结论。
  • 不可以接受:实时告警、自动下单触发、盘口挂单撤单判断、任何以“当前状态”为前提的决策。延时行情反映的是过去某一时刻的状态。

需要注意的是,延时是权益问题而非接口问题:免费版覆盖加密、美股、金属的延时行情,标准版在加密与金属上提供实时、其余市场为延时,专业版才是全市场实时。各档位覆盖范围与配额以 定价页 为准。工程上的建议是让延时标记一路传递到消费端,而不是在接入层悄悄丢弃,否则你无法回答“上周那次误触发里有多少条是延时数据”这个问题。

常见问题(FAQ)

字段返回 0 一定是缺失吗?

不是。零是合法数值,成交量、成交笔数在无成交区间就应该是 0。只有字段为 null 或字段不存在才是缺失。判据应写成 is None,不要写成 not value——后者会把 0 和空字符串一起判为缺失。

delayed 和 unavailable 可以合并判断吗?

不可以。两者语义不同:delayed=true 表示有数据但档位为延时,可以用于分析;unavailable=true 表示当前没有可用数据,不能参与任何计算。合并判断会导致两类问题被同一套逻辑处理,要么误拦可用数据,要么把不可用数据放进实时链路。

请求失败时应该写入 0 还是留空?

都不应该。失败不是数据状态,而是传输状态。正确做法是把错误码单独记录,本次不写入行情表;只有在业务状态为 unavailable 时才写入带标记的空值,这样“没有数据”与“没有请求成功”在数据表里可区分。

怎么确认自己遇到的缺失属于哪一类?

先看 HTTP 状态码与响应信封里的 code。502 UPSTREAM_UNAVAILABLE 是可重试的上游问题;403 系列的 MARKET_NOT_ENABLED、API_KEY_SCOPE_DENIED 是权益配置问题;429 系列是配额问题。因此入口处必须保留原始错误码,不能统一压成一句“取数失败”。

下一步

把三态建模落到代码里,成本很低:一个归一函数、一条 is None 判据、一条错误码透传路径。收益是下游所有指标、告警和回测都能建立在可区分的数据之上。注册即可获得免费密钥,免费版无需信用卡,可先用延时行情把校验逻辑跑通。

获取免费 API 密钥,开始接入 →

本文所述接口字段、错误码、配额与套餐覆盖范围,均取自 tiqex.io 官方文档(文档更新日期 2026-09-12)与定价页,访问日期 2026-09-20。文中数字仅用于说明工程处理方式,不构成任何投资建议,也不对未来行情作任何预测。