· 16 min read

從 9 次 DRY RUN 失敗到 Go-Live:Joseph 的 T+2 三重鎖、458 測試、18 個 cron

Joseph 投資代理人 2026-04-20 進入實盤觀察期。這篇寫從 DRY RUN 反覆爆雷到 Go-Live 通過三個 Gate 的全部技術細節:9 個失敗模式、T+2 三重鎖設計、Shioaji 踩坑、18 個定時任務。

#joseph#claude-code#dry-run#triple-lock#shioaji#automation#trading-system#series-3

TL;DR 給趕時間的人

Joseph 是一個自動化投資執行子系統,2026-04-20 週一 08:00 台北時間通過三個 Gate 後進入實盤觀察期。技術概況:25,941 行 Python、40 個測試檔 / 458 個測試、18 條 cron 排程、106 個 commit(最早 2026-01-26 v3.3)、Go-Live 前累計驗證 9 個 DRY RUN 失敗模式。本篇寫所有可公開的技術細節——但不洩漏三重鎖實作、憑證、持股、下單紀錄。


本系列三篇總覽

  • [第一篇] 身份故事篇:砍掉三個專案才做出 Joseph
  • [第二篇] 方法論:讓 AI 獨立開發的 7 條原則
  • [第三篇(本文)] 實戰紀錄:DRY RUN 失敗、T+2 三重鎖、Shioaji、cron 排程

一、系統規模快照(Go-Live 當下)

指標數值來源
Python 原始碼25,941 行(排除 venv / 快取 / archive)find . -name "*.py"
測試檔案40 個tests/test_*.py
測試函式458 個(Go-Live 宣告,實際 pytest collected)docs/GO_DECISION_20260420.md
文件74 個 markdowndocs/*.md
Cron 排程18 條crontab -l
Git commits106 個git rev-list --count HEAD
最早 commit2026-01-26 v3.3 Ultimategit log
最近 commit2026-04-18 GO decisiongit log
核心模組core/ 123 個檔、layers/ 四層分層find core / layers

二、9 個 DRY RUN 失敗模式

Joseph 進入實盤前跑了三個批次 DRY RUN——4/74/10 初期、4/13 正式、4/154/17 延長。每個批次都在模擬真實下單但不真的下。以下是 9 個被抓到的失敗模式,時序排列:

1. 資金全零(2026-04-10)

  • 現象ShadowExecutor 初始化後回傳 cash=$0、pool weights 全零,系統完全無法出單。
  • 根因:缺 async get_balance() / get_positions() 非同步查詢 + fallback。
  • 修復:補齊非同步查詢、增加 fallback 機制。
  • 留下的教訓:非同步介面的 None 要有明確語意——是「還沒回」還是「真的沒有」。

2. Scanner 時段邊界錯誤(2026-04-10)

  • 現象:08:30 被判定為「closed」,Scanner 不執行。
  • 根因:時段判定邊界值設錯。
  • 修復:調整為 08:30 ≤ t < 13:30 開放。

3. Equity=0 時 allocations 全零(2026-04-10)

  • 現象equity 計算為 0 時,allocation 模組把所有 pool weight 算成 0,無法交易。
  • 根因:Strategy Engine 缺 equity fallback。
  • 修復:新增 equity = cash + marked_value 的 fallback 計算。

4. shadow_ledger 混入 33 筆 E2E 測試殘留(2026-04-13)

  • 現象:holding_days 顯示 38 天,但系統才跑 1 天。
  • 根因tests/test_e2e_dryrun_0410.py 跑完沒清理,把 2026-03-06 的假交易寫進 shadow_ledger.json
  • 修復FIX_REPORT_0413.md 執行 shadow_ledger 重置(cash=10000, positions=[], history=[])。

5. Gmail HTML 附件中文亂碼(2026-04-13)

  • 現象:iOS Safari 打開 Gmail HTML 附件,中文變亂碼。
  • 根因:缺 <meta charset="UTF-8"> + MIMEBase encoding 錯誤。
  • 修復core/report_formatter.py 加 charset meta、core/reporter.py 改用 MIMEText(utf-8)

6. holding_days 計算異常(2026-04-13)

  • 現象:holding_days=38。
  • 根因:第 4 項的衍生問題(first_buy_time=2026-03-06 是測試殘留)。
  • 修復:第 4 項修完自動解決。

7. cron_wrapper.sh 路徑錯誤(2026-04-13)

  • 現象:所有 cron 執行失敗,log 寫「No such file or directory」。
  • 根因scripts/cron_wrapper.sh 寫死舊帳號路徑 /home/uop3364,但機器已遷移到 /home/molinkailazy
  • 修復:改絕對路徑為新帳號。
  • 學到:硬編碼路徑是債,遷移一次就知道。

8. cron_scan.py 重複 import(2026-04-13)

  • 現象:函數內重複 from pathlib import Path
  • 根因:編寫遺漏。
  • 修復:移除重複 import(line 27)。
  • 代表意義:純粹機械疏忽也會被 DRY RUN 抓出來——這就是為什麼 DRY RUN 值得跑。

9. Telegram 非交易時段誤發通知(2026-04-18 00:01)

  • 現象:凌晨 00:01 Bot 突然發了 6 筆 Circuit Breaker alert + 1 筆 error 通知。
  • 根因:文件宣稱有 PYTEST_CURRENT_TEST guard,程式碼裡沒實作;pytest 手動執行直接走真 Bot。
  • 修復:實作 PYTEST_CURRENT_TEST 檢查 + conftest.py autouse fixture mock + 相關 test 的註解處理。5 個 phase 修完。
  • 最重要的教訓不要信文件說的「已實作」,要信測試能不能過

三、T+2 三重鎖(概念層級)

T+2 意指台股「交易日 T 成交、T+2 交割」。賣出後要兩個工作日錢才到帳。系統設計必須區分「帳面現金」與「可用現金」,否則會用還沒到帳的錢重新買進。

Joseph 進入實盤前鎖的不是 T+2 本身,而是「實盤下單」這個動作——必須同時滿足三個獨立開關才執行:

位置意義DRY RUNGo-Live
層 1config/config.json 策略許可旗標系統是否進入實盤模式falsetrue
層 2config/config.json 執行守衛旗標額外禁止實盤執行,即使層 1 為真truefalse
層 3.env 環境變數授權人類操作者當次授權(非程式碼檔)未設設為 1

為什麼要三層?

  • 層 1 單獨存在可能被代碼變更誤觸
  • 層 1 + 層 2 都在程式碼裡,風險仍高
  • 層 3 放環境變數、不進 git,代表「人類當次知情同意」這一層

實作層驗證點在 core/trader_live.py_verify_triple_lock()。缺任何一層 → 拋 LiveTradingForbiddenError(錯誤訊息不洩漏任何憑證)。

不會公開:三層的具體鍵名、值格式、驗證順序、錯誤訊息內文。這些不是安全性(第一道防線),但是減少失誤(不會有人無意間貼錯配置)。


四、Shioaji 踩坑 4 例

Shioaji 是永豐金提供的量化下單 API。Joseph 進入實盤的最後兩週踩了以下四個坑:

1. T+2 邏輯完全缺席(2026-04-16)

  • 發現api.account_balance() / list_positions() / settlements() 沒接;舊的 capital_manager.py 被 archive,新骨架沒補對等模組。
  • 修復:Task #6-PreLive(2026-04-18)補齊 pending_settlements.py + 新版 capital_manager.py + TraderLive 橋接層。

2. 下單前 gate 用 shadow_ledger 而非即時餘額(2026-04-17)

  • 發現:shadow_executor 內 inline gate 檢查用 shadow_ledger.current_cash 而非 Shioaji 即時餘額。DRY RUN 階段可以,實盤會違反 Rule 1.3(「以即時帳戶餘額為準」)。
  • 修復:Task #6 新增 capital_manager._can_afford() + Shioaji bridge 層。

3. SELL 進度立即入帳(shadow 與 T+2 不一致)

  • 發現:shadow ledger 「語意即時結算」——賣出後立刻把錢算回現金池;但真實 T+2 要 D+2 才到帳,D+1 可能用未到帳的錢再買進。
  • 修復:Task #6 引入 pending_settlements 追蹤實際交割日;計算 available_cash 時扣除未交割金額。

4. 登入失敗靜默(呼叫端風險)

  • 發現:Shioaji 登入失敗時舊版本回傳 {"success": False, "error": ...} 而非 raise,呼叫端可能靜默失敗。
  • 修復:Task #6 引入 ShioajiAPIFailedError / ShioajiOrderRejected 等異常類別;Task #7 補 _classify_order_status() 二次判讀 + 兜底 _fail_dict

這四件事後來成為 Go-Live 前 Gate 11 TraderLive 實盤下單能力驗收的基礎——72 個 unit test 覆蓋 FILLED / CANCELLED / REJECTED / FAILED 四種終態 + polling + fallback,全綠才放行。


五、18 個 cron 排程(平日 / 週間節奏)

Joseph 在 GCP VM 上跑 cron。時間全部 UTC+8 台北時間。

時間任務排程
07:50Boot 通知(系統起床)平日
08:00盤前市場數據平日
08:05TraderLive 自測平日
08:55盤前橋接健康檢查平日
09:00開盤掃描平日
09:00~12:00盤中市場數據更新(每小時)平日
10:00~13:00盤中掃描(每小時)平日
13:30收盤處理平日
13:30日績效回顧平日
13:35收盤市場數據平日
13:40投入率檢查平日
13:45月報平日
14:00週報週五
14:00備份平日
14:30Git push(把日誌推到 repo 做版本化紀錄)平日
01:00refresh-history週日

共 18 條(小時級任務展開後更多,這邊以 schedule entries 計)。

為什麼 08:05 要跑 TraderLive 自測? 因為如果 TraderLive 的實盤能力在開盤前 55 分鐘壞掉,09:00 掃描就算出單也送不出。自測負責在開盤前抓這種情況、發 Telegram 警示。

為什麼 08:55 還要再跑一次健康檢查? 冗餘。08:05 是第一道(早期發現),08:55 是最後一道(開盤前 5 分鐘)。如果 08:55 失敗,當日關閉下單 gate、只做觀察。


六、Go-Live 前通過的 3 個 Gate(2026-04-18 預演)

Gate 4:技術健康

  • ✅ 5/5 PASS
  • 內容:系統依賴齊備、cron.log 低噪音、cache validator 通過、cron 排程健全、log rotation 就緒。

Gate 10:T+2 守衛

  • ✅ 3 PASS + 1 預期 WARN
  • 10.1 stress test 6/6 通過
  • 10.2 / 10.3 pending_settlements.json schema 合法
  • 10.4 settle_heartbeat 會在 08:05 premarket 後產生 (4/18 預演時還沒到那個時間點,故 WARN 為預期)

Gate 11:TraderLive 實盤下單能力

  • ✅ 72 tests PASS
  • _call_shioaji_place_order 全終態覆蓋 + polling + fallback
  • Shioaji 登入成功、帳戶確認

三 Gate 全通 → 2026-04-20 週一 08:00 啟動。


七、風險、免責、不會公開的東西

這篇不提供以下內容

  • 任何具體持股、進場價、部位大小、下單決策
  • 三重鎖的具體鍵名、值格式、驗證順序
  • Shioaji API key、帳戶編號、憑證路徑、.env 內容
  • shadow_ledger 實際交易紀錄

風險與定位

  • Joseph 是個人投資紀錄與執行子系統,不是投資建議、不是訊號服務、不是教人複製交易
  • 本系列三篇寫作目的是記錄「一個非工程師如何用 AI 做出可維護的真實系統」,不是推廣任何投資方法
  • 進入實盤不代表績效正向。Joseph 目前只是通過工程驗證績效驗證要用數月以上時間觀察
  • 本文對任何第三方跟著做出的投資行為不承擔任何責任

FAQ

Q1:458 測試是 pytest 的 collected count 嗎?還是測試函式數? GO_DECISION_20260420.md 寫的是 458(Go-Live 前實際跑通的 pytest 數)。repo 裡用 grep -c "^def test_" 數到的測試函式是 416 個——差異在 parametrize 展開後的 case 數。兩個都是真的,語境不同。

Q2:T+2 三重鎖的每一層鍵名是什麼? 不會公開。公開會增加誤觸風險(有人 copy-paste 對不上值),對第三方也沒用。

Q3:DRY RUN 一共跑幾天? 三個批次:4/74/10(初期,爆出前 3 個失敗模式)、4/13(正式,第 48 個)、4/15~4/17(延長,穩定性驗證,第 9 個是 4/18 Telegram 誤發)。

Q4:Shioaji 踩坑那幾項,是 Shioaji 的問題還是 Joseph 的問題? 都是 Joseph 的問題。Shioaji 的 API 行為完全合理,只是 Joseph 早期骨架沒接對應模組、語意沒對齊 T+2。把責任推給第三方 API 是最廉價的藉口。

Q5:18 條 cron 不會衝突嗎? 會。所以有 cron_wrapper.sh 和 lock file 機制。同一任務若上一次還沒跑完,下一次會 skip 並記 log,不會疊加執行。

Q6:Go-Live 後會有第 10、11、12 個失敗模式嗎? 幾乎肯定會。實盤環境有 DRY RUN 永遠碰不到的情境(成交失敗、部分成交、市場休市、主機斷線)。我會在之後寫 post-live 紀錄文,把新的失敗模式逐一記錄。

Q7:為什麼 Shioaji 下單有 4 種終態? FILLED(成交)/ CANCELLED(撤單)/ REJECTED(被交易所拒絕)/ FAILED(API 層錯誤)。exhaustive match 是 Task #7 強制的——只要有個終態沒寫,就拋 UnknownOrderStatusError,絕對不讓「未知狀態」靜默溜過。

Q8:這個系統會開源嗎? 目前不會。理由:(1) 帳戶與風險邏輯跟個人情況強相關,別人直接用風險極高;(2) 三重鎖與憑證防護需要逐案調整,開源會讓抄襲者失去這層保護;(3) 我也還沒有底氣說這套架構值得被抄。如果未來 Joseph 在實盤跑 12 個月以上且績效驗證,會考慮把非敏感模組拆出來。


結語

這篇是三篇裡最硬的一篇,刻意塞滿數字——因為「真實的系統」跟「想像中的系統」唯一的差別就是能不能被數字驗證。Joseph 今天進入實盤不代表它永遠活著,但它至少活在可驗證、可修復、可回溯的狀態。

如果你也在用 AI 做自己的系統,希望這三篇能省你一點時間。砍掉三個案子不丟臉,編造成功故事才丟臉。


回到系列起點第一篇:砍掉三個專案後我才做出 Joseph →

方法論複習第二篇:我從 Claude 身上學到的 7 個協作原則 →