台股資料管線 QA 測試包 台股量化/資料團隊的靜默失敗案例庫

Case 02 — 明明 App 看得到,我的資料庫卻說「0 檔」:商品主檔只拉 TWSE 漏掉 TPEX 上櫃

類別:完整性與筆數 | 嚴重度:Critical | 對應檢查規則:universe_exchange_coverageknown_otc_probe_nonzerouniverse_count_floor


1. 真實症狀

查詢標的 6274 的認購權證,我的主檔(master cache)回答:0 檔。 程式沒有錯、cache 沒有壞、41,201 筆權證資料完整無缺。

打開券商 App 一看:6274 掛著 339 檔 Call。

再查兩檔:2337 主檔 0 檔(實際 193 檔)、2492 主檔 0 檔(實際 177 檔)。 不是資料髒,是整個上櫃(TPEX/OTC)市場從來沒進過主檔。主檔建置時只打了 TWSE OpenAPI 的上市權證清單 t187ap37_L,而上櫃標的對應的權證是上櫃權證, 登記在櫃買中心(TPEX)自己的 OpenAPI mopsfin_t187ap37_O —— 一個完全獨立的 機構、完全獨立的 API base URL。

最陰的是「0 檔」這個答案語意上完全合法:很多標的本來就真的沒有權證。 下游看到 0 不會懷疑,只會跳過。這個洞開著跑了數週,直到有人拿 App 對過來。

2. 為什麼不會報錯

3. 受影響結果(量化)

項目數字
缺漏筆數12,819 筆上櫃權證(修復後全檔 54,020 筆 = 上市 41,201 + 上櫃 12,819)
缺漏比例23.7% 的全市場權證不存在於主檔
個別標的假象6274:0 vs 339 | 2337:0 vs 193 | 2492:0 vs 177
受害標的類型系統性偏差 —— 上櫃多為中型/題材股,恰是權證交易最活躍的族群之一

衍生損害:篩選器對整個上櫃族群永遠回空 → 推薦清單系統性偏向大型上市股; 回測 universe 缺 23.7% → 統計結論帶結構性偏差且無法靠加大樣本修復。

4. 最小重現樣本

fixtures:fixtures/case_02_universe_twse_only.csv(壞:只有 TSE 來源) vs fixtures/case_02_universe_merged.csv(好:TWSE+TPEX 合併、含 exchange 欄)。 均為合成資料(代號結構仿真:上市權證多為 0 開頭、上櫃權證多為 7 開頭), 筆數比例對應真實事故的 41,201 / 12,819。

import csv
from collections import Counter

# 已知「一定有上櫃權證」的探針標的(維護一份小清單即可)
OTC_PROBES = {"6274", "2337", "2492"}

def load_universe(path):
    with open(path, newline="", encoding="utf-8") as f:
        return list(csv.DictReader(f))

for path in ("fixtures/case_02_universe_twse_only.csv",
             "fixtures/case_02_universe_merged.csv"):
    rows = load_universe(path)
    by_exchange = Counter(r["exchange"] for r in rows)
    probe_hits = {c: sum(1 for r in rows if r["underlying_code"] == c)
                  for c in OTC_PROBES}
    print(f"\n{path}")
    print(f"  總筆數={len(rows)}  exchange 分佈={dict(by_exchange)}")
    print(f"  探針標的權證數={probe_hits}")

    # invariant 1: 全市場主檔必須同時含 TSE 與 OTC
    assert {"TSE", "OTC"} <= set(by_exchange), \
        "COVERAGE_GAP: 主檔缺少整個交易所來源(只拉了 TWSE 沒拉 TPEX?)"
    # invariant 2: 已知有權證的上櫃標的,查詢結果不得為 0
    assert all(v > 0 for v in probe_hits.values()), \
        f"OTC_PROBE_ZERO: 探針標的在主檔查無權證 {probe_hits}"

執行結果:twse_only 檔在 invariant 1 就炸出 COVERAGE_GAPmerged 檔兩條全過。

5. 檢查條件(invariant)

I-1「交易所覆蓋」:宣稱「全市場」的主檔/universe,exchange(或等價 來源標記)欄的 distinct 值必須同時包含 TSE 與 OTC。建檔時就寫入來源標記, 沒有來源標記的主檔視為不可稽核。 → checks.yaml: universe_exchange_coverage

I-2「探針非零」:維護一份小型探針清單(3-5 檔確定有上櫃權證/上櫃掛牌的 標的),每次主檔重建後查詢,任何一檔回 0 即 fail。這是抓「合法的 0」最便宜 的手段。 → checks.yaml: known_otc_probe_nonzero

I-3「筆數下限」:主檔總筆數對照歷史基準(如 30 日中位數),單日掉幅 超過 10% 即告警 —— 23.7% 的缺口若有歷史基準比對,第一天就會被抓到。 → checks.yaml: universe_count_floor

6. 建議重試/回補策略

7. 驗收標準

  1. 對 fixtures 跑檢查:case_02_universe_twse_only.csv 必須觸發 universe_exchange_coverage(severity: critical);case_02_universe_merged.csv 三條規則全過。
  2. 探針清單查詢:6274/2337/2492(或自選的上櫃探針)在重建後的主檔中 權證數 > 0。
  3. 主檔 schema 含 exchange(或 source)欄位,且每筆非空。
  4. 兩源合併後總筆數落在歷史基準 ±10% 內;首次建置以官方公布檔數交叉核對 (量級參考:上市約 4 萬檔、上櫃約 1.3 萬檔,合計 5 萬檔級)。

8. 已知例外

這只是 20 個案例的其中一個

完整版覆蓋五大類、20 個台股資料管線靜默失敗案例,每個都配 invariant、 機器可讀檢查規則與合成測試資料。

看完整 20 案例清單與定價 →

下載免費樣本包(3 案例+checks.yaml+fixtures)