行情數據質量校驗的難點不在“數據有沒有到”,而在“到了的數據對不對”。鏈路中斷會報錯,錯誤的值不會:一次價格尖峰、一段時間戳回退、一組交叉盤口,都能安靜穿過採集、落庫與消費三層,直到在下游的假告警或回測結果裡顯形。本文按 TIQEX 已文檔化的字段口徑給出六類靜默失效的判據,並附一個可直接運行的 Python 校驗器。
一個不會報警的故障
設想一條已在生產運行的管道:WebSocket 連上了,type=subscribed 回執正常,tick 幀以穩定速率到達,連接數、幀率、HTTP 狀態碼全綠。但日誌顯示,某個標的的 tick.at 已停在十二分鐘前——上游推送早已停止,客戶端收到的只是同一個舊值的重複幀。
這類故障的共性是傳輸層健康、內容層已錯,而多數團隊只監控前者。更隱蔽的是,即使狀態碼為 2xx,業務層仍可能失敗:TIQEX 的錯誤文檔明確指出,響應信封的 succ 字段在 2xx 下仍可能為 false,因此兩者必須同時檢查。只看 HTTP 200 就放行,等於把內容層的判據全部讓給下游。
結論:生產級管道需要兩條獨立的健康度信號——傳輸健康度與內容健康度(值域、時間、自洽性)。本文只討論後者。
靜默失效為什麼比中斷更貴
中斷是顯式失敗:觸發重試與告警,代價可見且有界——幾秒到幾分鐘的缺口,加一次人工確認。靜默失效不觸發任何機制,代價沿鏈路放大:
- 假信號成本。一條尖峰進入告警系統,觸發一次人工核查。被侵蝕的是告警通道的信噪比——假告警佔比上升時,真實信號被忽略的概率同步上升。
- 重算成本。錯誤值一旦進入回測數據集或報表,修復不是改一行記錄,而是重跑整條管道並重新分發結論。
- 信任成本。最貴的一段。團隊開始對全部數據打折扣,決策退回人工判斷。
作為量級參照,假設一次假告警核查為十五分鐘、一次回測重跑為四小時——這是示例參數,不是實測數字。真正值得注意的是成本的不對稱:檢測本身幾乎免費。六條斷言都在本地對已到達的數據執行,不發出 API 調用,不消耗配額。用零邊際成本換取下游可信度,正是把校驗前移到採集端的理由。
六類靜默失效與最小檢測判據
以下六類覆蓋了“值不為空但已經錯了”的絕大多數情況,共同點是都能用一條廉價斷言檢出,無需統計模型或人工標註。
| # | 失效類型 | 典型表現 | 最小檢測判據 | 主要誤報來源 |
|---|---|---|---|---|
| 1 | 陳舊值 stale | 幀持續到達,at 不前進 | now − at > k × 典型間隔 | 休市時段 |
| 2 | 時間戳回退 | at 小於上一條 | 按標的維護水位線 | 亂序到達 |
| 3 | 價格尖峰 | last 偏離中位數一個數量級 | |last − median| > k × 1.4826 × MAD | 真實跳空 |
| 4 | K 線不自洽 | 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 實時行情接入指南。
# 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 線和日線上,誤報率會差一個數量級。可行做法是從標的自身的歷史分佈反推,並按標的緩存:
# 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 有多高,而取決於數據變壞時你多快能知道。把這條判據前移到採集端,是最省成本的一步。
本文引用的接口字段、錯誤碼、速率限制與套餐覆蓋均來自 tiqex.io 官方文檔(更新於 2026-09-12)與定價頁,訪問時間 2026-09-22。示例參數僅用於說明工程實踐,不構成投資建議或收益預測。