問題
台股選股工具的月營收資料永遠落後一期。
8 月營收 9/10 公布,9/12 查 DB 還是只有 7 月——而且不只一檔,全 watchlist 298 檔都沒進來,monthly_revenue 最後寫入停在 9/1。
最難發現的地方:排程 log 每小時都印 [fetch-financials] 完成,exit code 0,沒有任何錯誤。
原因
抓取前會先查 DB 決定哪些要抓(skip-if-fresh)。門檻由 expected_latest() 算:
# financial_check.py:93
last_m_yr, last_m_mo = (yr, mo - 2) if mo > 2 else (yr - 1, 12 + mo - 2)
month_threshold = last_m_yr * 100 + last_m_mo # 永遠「上上月」
9 月 → 門檻 202607。DB 已有 7 月 → monthly_ok = True → 整個 9 月都不抓 8 月。
等到 10 月,門檻才進位到 202608 去抓 8 月——但那時 9 月營收又已公布。
不是偶發延遲,是系統性落後一期。
而同一個檔案裡,latest_available_monthly() 早就有正確規則:
# financial_check.py:115
# 台股月營收法定每月 10 日前公布上月,保守抓「11 日(含)後可用上月」
if day >= 11:
last_yr, last_mo = (yr, mo - 1) if mo > 1 else (yr - 1, 12)
它有 5 個邊界測試(跨年、2 月、公布日前後)。expected_latest() 一個測試都沒有。
兩份邏輯表達的是同一件事——「哪一期資料已經可以拿到」——各寫各的,只有一份被測試釘住。
解法
刪掉硬算的那份,沿用有測試的那份:
month_threshold = latest_available_monthly(today)
TDD 走完:先寫測試,看它以 assert 202607 == 202608 失敗(失敗訊息本身就是 bug),再改實作。
補的三個測試裡,最關鍵的是這個:
def test_expected_latest_monthly_matches_latest_available():
for d in (date(2026, 1, 10), date(2026, 1, 11), date(2026, 2, 10),
date(2026, 5, 11), date(2026, 9, 12), date(2026, 12, 31)):
assert expected_latest(d)[0] == latest_available_monthly(d), d
它不驗證任何業務規則,只釘住「這兩個函式不准再分岔」。跨年、2 月、公布日前後的邊界一次涵蓋。
307 → 310 passed。修後補回 8 月營收 291 檔。
關鍵洞察
同一個語意在程式裡出現第二份實作,就一定會分岔。 分岔之後,有測試的那份是對的,沒測試的那份靜默錯下去,而且錯的通常是被更多人呼叫的那份。
與其靠 code review 抓,不如寫一個「斷言兩者相等」的測試。這種測試不驗證業務規則,只釘住不變量,成本三行。
第二條:批次任務印「完成」不等於有做事。
skip-if-fresh 這類流程,log 的「完成」只證明流程跑完,不證明資料進來了。同一個專案上個月才踩過另一版:MOPS 重大訊息的下游過濾寫錯,API 抓回全市場快照卻只留 30 檔,一樣沒報錯,一樣漏了 94% 的資料。
兩次都是下游判斷錯誤造成的資料遺失,上游不會報錯。
驗證方式不能是讀 log,要直接查資料:
SELECT MAX(fetched_at) FROM monthly_revenue;
SELECT year, month, COUNT(*) FROM monthly_revenue WHERE year=2026 GROUP BY year, month;
排程類的健康檢查應該盯「最新資料的時間戳」,不是盯 exit code。