從 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 個定時任務。
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 個 markdown | docs/*.md |
| Cron 排程 | 18 條 | crontab -l |
| Git commits | 106 個 | git rev-list --count HEAD |
| 最早 commit | 2026-01-26 v3.3 Ultimate | git log |
| 最近 commit | 2026-04-18 GO decision | git 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_TESTguard,程式碼裡沒實作;pytest 手動執行直接走真 Bot。 - 修復:實作
PYTEST_CURRENT_TEST檢查 +conftest.pyautouse fixture mock + 相關 test 的註解處理。5 個 phase 修完。 - 最重要的教訓:不要信文件說的「已實作」,要信測試能不能過。
三、T+2 三重鎖(概念層級)
T+2 意指台股「交易日 T 成交、T+2 交割」。賣出後要兩個工作日錢才到帳。系統設計必須區分「帳面現金」與「可用現金」,否則會用還沒到帳的錢重新買進。
Joseph 進入實盤前鎖的不是 T+2 本身,而是「實盤下單」這個動作——必須同時滿足三個獨立開關才執行:
| 層 | 位置 | 意義 | DRY RUN | Go-Live |
|---|---|---|---|---|
| 層 1 | config/config.json 策略許可旗標 | 系統是否進入實盤模式 | false | true |
| 層 2 | config/config.json 執行守衛旗標 | 額外禁止實盤執行,即使層 1 為真 | true | false |
| 層 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:50 | Boot 通知(系統起床) | 平日 |
| 08:00 | 盤前市場數據 | 平日 |
| 08:05 | TraderLive 自測 | 平日 |
| 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:30 | Git push(把日誌推到 repo 做版本化紀錄) | 平日 |
| 01:00 | refresh-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.jsonschema 合法 - 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 →
// 相關文章
- · 16 min read
我從 Claude 身上學到的 7 個協作原則:規格驅動、邊界優先、答完就是決定
三個月跟 Claude Code 協作做出 Joseph 投資代理人的 7 個原則——讓 AI 真的能自己跑完開發流程,而不是每一步都要釘在旁邊的保姆式協作。
#ai-collaboration#claude-code#spec-driven#methodology#joseph#series-2 - · 15 min read
砍掉三個專案後我才做出 Joseph:一個非工程師的三個月 AI 協作紀錄
花蓮零售業員工、非工程師、四個專案砍三個,從 CAIOS 到 Joseph 投資代理人 2026-04-20 Go-Live——這是一份誠實的決策歷程,不是成功學。
#ai-collaboration#claude-code#non-engineer#project-kill#joseph#series-1 - · 3 min read
Hello, BuildHub(中文版)
為什麼一個非工程師要開始寫每日開發紀錄,以及接下來的文章會是什麼樣子。
#meta#intro#claude-code