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

Case 03 — 颱風假那天,我的策略照樣「產生了 47 筆訊號」:非交易日排程照跑產出假訊號

類別:回補與自癒 | 嚴重度:High | 對應檢查規則:signal_date_is_trading_daycalendar_confirmed_absencescheduler_trading_day_gate


1. 真實症狀

7/10 台股因颱風全天休市。我的每日訊號排程不知道 —— 它只知道「今天是週五」, 照表操課:拉資料、跑模型、輸出 47 筆訊號,寫進追蹤資料庫,exit code 0, 一切「成功」。

那 47 筆訊號的依據是什麼?休市日根本沒有新行情,上游 API 回的是最近一個 交易日的舊資料(見 Case 01 的 stale 機制)——所以模型是拿 7/9 的行情, 產出標記為 7/10 的訊號。日期是假的,訊號自然全是假的。

清理時順藤摸瓜,發現更久的洞:另一條排程建立時忘了限定週一到週五, 從 4 月起每個週六日照跑,累積了 100 筆週末假訊號,安安靜靜躺在績效 統計裡三個月。兩個洞同一個根因:排程器的日曆不是交易所的日曆

2. 為什麼不會報錯

3. 受影響結果(量化)

項目數字
單一颱風休市日產出的假訊號47 筆(一天)
週末排程未限定平日的累積假訊號100 筆(約 3 個月無人發現)
合計污染追蹤資料庫147 筆需回頭 purge
績效統計影響假訊號以 stale 價結算,稀釋/扭曲勝率與平均報酬;本例清洗後主策略數字由 n=400 修正為 n=390

衍生損害:假訊號若接自動下單就是真金白銀的錯單;接推播就是假警報消耗 使用者信任;混進訓練集就是用不存在的交易日污染模型。

4. 最小重現樣本

fixtures:fixtures/case_03_signals_bad.csv(壞:含颱風日 7/10 與週六訊號)、 fixtures/case_03_signals_good.csv(好:全部落在交易日)、 fixtures/case_03_trading_calendar.csv(合成交易日曆,含 7/10 颱風休市標記)。 均為合成資料,筆數場景對應真實事故。

import csv
from datetime import date

def load_calendar(path):
    with open(path, newline="", encoding="utf-8") as f:
        return {r["date"]: r for r in csv.DictReader(f)}

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

cal = load_calendar("fixtures/case_03_trading_calendar.csv")

for path in ("fixtures/case_03_signals_bad.csv",
             "fixtures/case_03_signals_good.csv"):
    signals = load_signals(path)
    bad = []
    for s in signals:
        d = s["signal_date"]
        entry = cal.get(d)
        if entry is None:
            # 日曆沒這天 != 非交易日,可能只是日曆沒更新 -> 另行告警,不誤殺
            print(f"  WARN CALENDAR_GAP: {d} 不在交易日曆內,需先補日曆")
            continue
        if entry["is_trading_day"] != "1":
            bad.append((s["signal_id"], d, entry["note"]))
    print(f"{path}: {len(signals)} 筆訊號, 非交易日假訊號 {len(bad)} 筆")
    for sig_id, d, note in bad:
        print(f"  FAKE_SIGNAL: {sig_id} @ {d} ({note or 'non-trading day'})")
    assert not bad, "NON_TRADING_DAY_SIGNAL: 非交易日不得產生訊號"

執行結果:bad 檔抓出 7/10(颱風休市)與週六的假訊號並炸出 NON_TRADING_DAY_SIGNALgood 檔全過。注意 CALENDAR_GAP 分支 —— 「日曆查無此日」與「確認非交易日」是兩件事,混為一談會誤殺好資料(見第 8 節)。

5. 檢查條件(invariant)

I-1「訊號日必為交易日」:任何帶日期的產出(訊號、榜單、日報),其日期 必須存在於交易日曆且 is_trading_day == true。在寫入端(ingest)擋, 不只在排程端擋 —— 排程端擋板擋不住手動補跑與時鐘錯亂。 → checks.yaml: signal_date_is_trading_day

I-2「證實缺席才擋」:判定「今天休市」必須以正面證據為準(官方日曆 標記、或行情資料經探測確認當日全市場無成交),不能以「拉不到資料」推論 休市。資料未到 ≠ 市場沒開(見 Case 01 的公布時序)。 → checks.yaml: calendar_confirmed_absence

I-3「排程雙保險」:排程規則本身限定平日(MON-FRI)只是第一層;第二層 是 job 開頭查交易日曆,非交易日直接 exit 並記一筆「今日休市,跳過」的 明確 log —— 讓「沒跑」可稽核,與「掛了沒跑」區分開。 → checks.yaml: scheduler_trading_day_gate

6. 建議重試/回補策略

7. 驗收標準

  1. 對 fixtures 跑檢查:case_03_signals_bad.csv 必須抓出全部非交易日訊號 (颱風日 + 週末),case_03_signals_good.csv 零誤報。
  2. 演練「臨時休市」:把日曆中任一平日標記為休市後觸發排程,pipeline 必須 跳過產出並留下明確 skip log,且追蹤資料庫零新增。
  3. 演練「資料未到」:交易日但行情 API 暫時回空,pipeline 不得判定休市 (I-2),應走重試/延後路徑並告警。
  4. 歷史庫全表掃描:signal_date 不在交易日曆的筆數 = 0。

8. 已知例外

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

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

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

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