休市提醒日股將於 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。文中數字僅用於說明工程處理方式,不構成任何投資建議,也不對未來行情作任何預測。