Runbook:永豐 ETF 持股資料日期修復(issue #1435)
本流程修復舊版 SinopacProvider 將 scheduler 查詢日誤寫成持股
as_of_date 的資料。永豐 PCF 頁面的「資料日期」是唯一日期依據;不得固定
減一天,也不得使用 FinMind 代替。
0. 前置條件
- issue #1435 的 parser 修正已合併並部署。
- country-schema cutover 已完成,以下 tables 必須存在:
taiwan.etf_fundstaiwan.etf_holdingstaiwan.etf_holdings_syncarchive_taiwanschema- 執行環境可讀永豐公開 PCF 頁面。
- 正式 apply 前必須停止會寫入 ETF holdings 的 scheduler,避免 dry-run、備份 與 apply 之間出現新寫入。
若環境仍只有 public.etf_*,停止本流程,先完成 country-schema cutover;
不得臨時把修復腳本改回舊 schema。
1. 首次 dry-run 與 scope capture(唯讀)
在已部署的新 backend image 執行:
python -m scripts.repair_sinopac_holding_dates \
--database-url "$DATABASE_URL" \
--capture-live-scope \
--report-path /app/logs/sinopac-date-repair-dry-run.json
--capture-live-scope 是首次稽核必要的明確動作:腳本只在這次從 DB
擷取 scope,並把 scope 與 dry-run 結果寫入指定報告。--report-path 必須是
本次新輸出的檔案;腳本會在任何 provider/DB 操作前以 exclusive create
預留該路徑,若檔案已存在就中止。後續 apply、驗證與 replay 不得覆寫這份
原始報告。
scope 的語意:
scope_source應為live-db-capture;首次輸出中的scope_report_sha256為null,因為報告不能包含自身完整檔案的 hash。 核准時另行記錄原始報告檔案的 SHA-256;後續輸出會記錄這個輸入檔案的 hash。- scope 是
provider=sinopac的 ETF 及其當時已儲存的 query dates;來源as_of_date不得在這次 capture 後被重新當成 query date。 --etf、--start-date、--end-date只可縮小 live scope;它們不是新的 scope 身分,也不能把部分報告宣稱為全量稽核。輸出報告的filters會完整 記錄這三種參數;正式全量 capture 時etfs=[]且兩個日期都應為null。- 對 captured scope 中的每個 query date 逐檔逐日查詢永豐歷史頁面。
- 解析每次回應的真實「資料日期」及完整持股。
- 計算 query date → source date 映射及預計替換日期。
- 完全不寫 DB。
若無法取得完整 scope、scope provenance 不明、報告遺失或 hash 無法重算, 必須 fail closed;不得自行從目前 live DB 重新發現日期來繼續。
2. Dry-run 審查 gate
報告必須符合以下條件才能提出 apply 核准:
safe_to_apply=trueunresolved=[]affected_etfs與 DB 中所有永豐 ETF 清單一致scope_source=live-db-capture、scope_report_sha256=null,並另行記錄原始 報告檔案可重算的 SHA-256filters={"etfs":[],"start_date":null,"end_date":null};若不是,該報告 只能代表明示的縮小範圍,不得核准為全量修復query_snapshot_count與 captured scope 的 query-date 集合一致- 每個
query_to_source映射都有來源日期與前後 row count holding_dates_to_replace、sync_dates_to_replace的範圍合理- 已記錄
plan_sha256
以下任一情況會阻擋 apply:
- 永豐頁面無法取得或資料日期缺失/無效/歧義
- 回應資料日期晚於 query date
- 同一 source date 的多個回應內容衝突
- 來源頁面解析為空,但 DB 的同一 source date 已有持股
- DB 在 dry-run 與 apply 之間發生 drift
將完整報告、精確環境/database、預計影響筆數、備份 label 與 rollback SQL 提交使用者審查。未收到明確核准不得執行下一節。
3. 正式 apply(只使用原始 scope)
先停止 scheduler,確認沒有其他 ETF holdings writer,再使用剛核准報告中的 hash:
python -m scripts.repair_sinopac_holding_dates \
--database-url "$DATABASE_URL" \
--apply \
--scope-report /app/logs/sinopac-date-repair-dry-run.json \
--expected-plan-sha256 "<DRY_RUN_PLAN_SHA256>" \
--backup-label "sinopac_yyyymmdd_01" \
--report-path /app/logs/sinopac-date-repair-applied.json
--scope-report 必須指向原始 dry-run JSON,--report-path 必須是不同的
apply 輸出檔。apply 不得帶 --capture-live-scope,也不得從 live DB
rediscover scope;它只驗證原始 scope 報告、重新抓取該 scope 的來源並重算
plan_sha256。腳本會把原始 scope 報告的檔案 SHA-256 記錄在 apply 輸出的
scope_report_sha256,供操作者與已核准的原始報告 checksum 核對;計畫 hash
不符則直接中止。
開始改資料前,腳本在 archive_taiwan 建立三個不可覆寫的備份表:
sinopac_funds_<label>sinopac_holdings_<label>sinopac_sync_<label>
備份 commit 完成後才開始第二個 transaction。該 transaction 會:
- 重新 lock/read 受影響 rows 並再次比對 plan hash。
- 將各 source date 替換成永豐當次歷史回應的完整快照。
- query-only 日期移除錯標 holdings,sync 改為 retryable
empty。 - source date sync 記為
success與正確 fetched count。 - transaction 內重新讀回;任何 holdings/sync 差異都 rollback。
4. 修復後驗證與 replay(只使用原始 scope)
驗證必須繼續傳入同一份原始 scope,並輸出到另一個新檔案;不得省略
--scope-report,也不得用目前 live DB 重新發現 scope:
python -m scripts.repair_sinopac_holding_dates \
--database-url "$DATABASE_URL" \
--scope-report /app/logs/sinopac-date-repair-dry-run.json \
--report-path /app/logs/sinopac-date-repair-verify.json
需要重播或調查時,仍使用同一 --scope-report,另指定不同的輸出報告;禁止
使用 --capture-live-scope。已完整修正時:
holding_dates_to_replace=[]sync_dates_to_replace=[]unresolved=[]
若原始 scope 報告或核准時記錄的檔案 SHA-256 無法復原,停止流程並保留現有 資料與備份;不得以 live DB 的當前日期集合補建 scope。只有找回同一份原始 報告並核對 checksum 後,才可繼續驗證或 replay。
再執行唯讀 DB 與 API 驗證:
SELECT f.etf_stock_code, h.as_of_date, COUNT(*) AS holding_rows
FROM taiwan.etf_funds AS f
JOIN taiwan.etf_holdings AS h USING (etf_stock_code)
WHERE f.provider = 'sinopac'
GROUP BY f.etf_stock_code, h.as_of_date
ORDER BY f.etf_stock_code, h.as_of_date;
SELECT etf_stock_code, as_of_date, status, fetched_count, error_message
FROM taiwan.etf_holdings_sync
WHERE provider = 'sinopac'
ORDER BY etf_stock_code, as_of_date;
確認主動式 ETF 買賣排行不再選到尚未開盤的錯誤日期,才可重新啟動 scheduler。
5. Rollback
apply 報告的 backup.rollback_sql 是該次 backup label 的精確 rollback。若
修復後驗證失敗,保持 scheduler 停止,核對 table 名與 row counts 後執行該
SQL。Rollback 會刪除備份範圍內目前的永豐 holdings/sync,再從
archive_taiwan 還原原始 rows。
Rollback 後重新執行 DB/API 驗證,並保留三個 archive tables 與兩份 JSON 報告供調查;不得在未驗證前刪除備份。