行情數據缺失值的處理,是行情接入裡最容易被跳過、也最容易在生產環境暴露的一環。多數團隊把精力放在鑑權、重連和限流上,默認“接口返回的每個字段都有值”;等到某個標的在某個時段返回了 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 繼續算指標。
缺失的五個來源與對應處置
把缺失當成一個籠統概念,就無法設計重試策略。按來源拆開,處置方式完全不同:
- 休市與時段外。不同市場的交易時段不同,加密市場則接近 7×24,覆蓋範圍見 加密貨幣市場頁。時段外沒有 tick 是正常現象,應按時段過濾,而不是判為故障。
- 標的當日無成交。產生合法零值,字段有值、數值為零,正常落庫。
- 上游行情源暫時不可用。對應 HTTP 502
UPSTREAM_UNAVAILABLE,屬於可重試錯誤,應指數退避後重試,期間不得寫入零值。 - 權益未覆蓋。對應 HTTP 403 的
MARKET_NOT_ENABLED、API_KEY_SCOPE_DENIED等,屬於配置問題,重試無效,應直接告警。 - 配額與限流。對應 HTTP 429 的
DAILY_LIMIT_EXCEEDED、RATE_LIMIT_EXCEEDED等,需要降頻或改用推送。注意 REST 的quote/kline與 WebSocket 握手都計入日配額與 RPM,而推送的 tick 不重複計次。
這五類裡,只有第三類是“等一下就好”。把第三類和第四類用同一個重試循環處理,結果通常是配額被無意義地燒掉,而真正的配置錯誤被埋在一堆重試日誌裡。
工程落地:把三態寫進數據模型
下面的函數把一次 quote 請求的結果歸一成四態之一,並保留原始錯誤碼。它是後續所有判斷的唯一入口,輸出可以直接落庫或投遞給下游。
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 線側的校驗邏輯同樣簡單:只檢查字段是否存在,不檢查數值是否為零。
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 判據、一條錯誤碼透傳路徑。收益是下游所有指標、告警和回測都能建立在可區分的數據之上。註冊即可獲得免費密鑰,免費版無需信用卡,可先用延時行情把校驗邏輯跑通。
本文所述接口字段、錯誤碼、配額與套餐覆蓋範圍,均取自 tiqex.io 官方文檔(文檔更新日期 2026-09-12)與定價頁,訪問日期 2026-09-20。文中數字僅用於說明工程處理方式,不構成任何投資建議,也不對未來行情作任何預測。