[Bug 75] CRITICAL — Shioaji contracts 跨日不刷新,導致 reference / limit_up 用舊資料,停損 cover IOC 委託失敗
Labels: bug · critical · trading-safety · overnight-risk · shioaji-api
Severity: 🚨 Critical(直接造成停損失靈 → 部位 overnight 風險)
Affected versions: v2.0.0 ~ v2.0.4(估算自 v2.0.2 開始大量依賴 contract.reference)
Discovered: 2026-05-29(發現於微星 2377 真實交易)
TL;DR
程式跨日連續執行時(start_trading() 收盤後 sleep 到隔日 08:30 再遞迴呼叫自己),sys_state.api 從首次 api.login() 起就快取 contracts 資料,從未刷新。結果 contract.reference(昨日收盤價)凍結在首次登入那天的值,幾天後再用就是過時資料。
具體影響:
calculate_limit_up_price(yc) 算出的漲停價偏離真實值 2-3 元
- 停損 / 平倉的 IOC 穿價單 掛單價 > 交易所實際漲跌幅範圍
- 永豐回
價格超過漲跌幅範圍 → 委託失敗 → 停損失靈 → 部位 overnight
真實案例:微星 2377 @ 2026-05-29
| 項目 |
程式內 |
實際(永豐 app 查證) |
差異 |
| 昨日收盤(2026-05-28 close) |
126.5 |
123.5 |
+3.0(多算 2.4%) |
| 漲停價 |
139.0 |
135.5 |
+3.5 |
| IOC cover 掛單價 |
139 |
— |
超過 135.5 |
| 委託結果 |
失敗(價格超過漲跌幅範圍) |
— |
停損沒出場 |
漲停價公式(交易程式2.0.4.py:8427):
def calculate_limit_up_price(close_price):
lu = close_price * 1.10
return (lu // tick) * tick # tick = 0.5 for 100-500 price range
- 用錯誤 yc=126.5:
126.5 × 1.10 = 139.15 → floor(0.5) = 139.0
- 用正確 yc=123.5:
123.5 × 1.10 = 135.85 → floor(0.5) = 135.5
證據鏈:
intraday_kline_live 表內 2377 今日(2026-05-29)第一筆 09:00:00 寫入的 昨日收盤價 = 126.5,漲停價 = 139.0(由 _safe_api_reference() 從 Shioaji contract.reference 取得)
intraday_kline_history 2377 最新只有到 2026-05-25(5/26、5/27、5/28 都漏抓)
- 強烈暗示:程式從 2026-05-25 ~ 26 之間首次 login 後就持續執行,contracts 凍結在那刻的值(2026-05-25 收盤 ≈ 126.5)
重現步驟
- 啟動程式 →
api.login(api_key=..., secret_key=..., contracts_timeout=10000) 載入 contracts
- 讓程式自然進入
start_trading() 收盤分支,sleep 到隔天 08:30 自動遞迴
- 連續執行 2 天以上(中間沒手動切 live/sim 模式,沒重啟)
- 在第 N 天觀察任一持倉股的
sys_state.api.Contracts.Stocks[sym].reference
- 預期:= 昨日(第 N-1 天)收盤
- 實際:= 第 0 天(首次 login)的昨日收盤
期望行為 vs 實際行為
期望:每個交易日 09:00 開始時,所有股票的 contract.reference 應該是「前一個交易日的收盤價」
實際:contract.reference 凍結在「首次登入那天的前一日收盤」,跨日後從未更新
根因分析
start_trading() 的執行模型:
Day 0 啟動: api.login() → contracts loaded (snapshot of broker state at this moment)
↓
Day 0 09:00-13:30 盤中監控
↓
Day 0 13:30+ 收盤 → sleep to Day 1 08:30
↓
Day 1 08:30 → start_trading() 遞迴呼叫
↓
Day 1 09:00-13:30 盤中監控 ← contract.reference 還是 Day -1 close!
↓
... 持續累積過時
全 codebase grep fetch_contracts 零個結果 → 沒有任何地方主動刷新 contracts。
唯一會重抓的時機:_rebuild_shioaji_api_for_mode()(切換 live/sim 模式時走完整 logout + login),但這要 user 手動切。
影響範圍
跨所有依賴 contract.reference 或 contract.limit_up 的路徑:
| 模組 |
影響 |
| 停損 cover IOC |
掛單價超過真實漲停 → 委託失敗 → 停損失靈 ⚠ |
| 平倉超時 IOC |
同上 |
| 進場 IOC 穿價 |
用錯誤 limit_up 算穿價額 → 委託被拒 / 成交價偏 |
K 棒 漲停價 欄位 |
DB 寫入錯誤值,所有歷史指標都被污染 |
| 觸發判斷 rise% |
(price - yc) / yc 分母錯 → 觸發時機 / 強度判斷錯亂 |
| 籌碼差比對 / H2L early peak |
同上 |
| 接近跌停保護 |
limit_down 也錯 → 保護邏輯時機錯 |
最嚴重:停損失靈直接造成 overnight 隔夜風險,真實虧損可能放大數倍。
修補方案
1. 新增 helper _force_refresh_contracts(reason)(交易程式2.0.4.py:8435)
def _force_refresh_contracts(reason: str = "") -> bool:
try:
if sys_state.api is None: return False
_t = time.time()
sys_state.api.fetch_contracts(contract_download=True, contracts_timeout=10000)
logger.info(f"[contracts refresh] 完成, 耗時 {time.time()-_t:.2f}s (reason={reason})")
return True
except Exception as e:
logger.error(f"[contracts refresh] 失敗 (reason={reason}): {e}", exc_info=True)
return False
關鍵:contract_download=True 強制從遠端重抓(預設 False 只讀本地 cache,治標不治本)
2. 在 start_trading() 三個分支前都呼叫
| Branch |
line |
時機 |
| 盤中(market_start <= now < market_end) |
13793 |
CRITICAL — 進來時必跑,確保盤中用今日 reference |
| 盤前(pre_market_start <= now < market_start) |
13776 |
08:30+ 後 Shioaji 該已更新 → 提前抓 |
| 盤後補齊(收盤後跨日重啟到此分支) |
13736 |
確保補齊資料用今日 reference |
3. 文件 _safe_api_reference() 把責任明確化(line 8466)
def _safe_api_reference(api_obj, sym, fallback_close=None):
"""從 Shioaji 取昨日收盤 (contract.reference);API 失敗時退回 fallback_close (預設 0)。
v2.0.4 Bug 75 (2026-05-29): 此函式只讀 contract 快取, 確保 reference 新鮮度的責任在
呼叫端 — 必須先呼叫 _force_refresh_contracts() (在 start_trading 每次進盤中分支時).
"""
驗證方法
修補後重啟程式,觀察系統日誌應出現:
[contracts refresh] 開始強制重抓 (reason=start_trading 進盤中分支)
[contracts refresh] 完成, 耗時 X.XXs
✅ [contracts refresh] 已重抓 Shioaji 合約 (reference / limit_up / limit_down 更新為今日值, 耗時 X.Xs)
然後驗證 intraday_kline_live 表內持倉股的 昨日收盤價 是否吻合永豐 app 顯示的真實值:
SELECT symbol, time, 昨日收盤價, 漲停價
FROM intraday_kline_live
WHERE symbol='2377' AND time='09:00:00';
-- 期望:昨日收盤價=123.5, 漲停價=135.5(而非 126.5 / 139.0)
跨日驗證:讓程式跑 2 天以上,觀察第 2 天 09:00 的 reference 是否已更新為「第 1 天收盤」(而非凍結在「第 0 天收盤」)
應急 workaround(在 fix deploy 前)
- 每天早上 08:30 前手動重啟程式(login 會抓最新 contracts)
- 或盤前手動切換一次 live/sim 模式,走
_rebuild_shioaji_api_for_mode() 觸發完整重建
相關 code 位置
| 檔案 |
line |
角色 |
交易程式2.0.4.py |
8427 |
calculate_limit_up_price() — 漲停公式 |
交易程式2.0.4.py |
8435 |
_force_refresh_contracts() ← 新增 |
交易程式2.0.4.py |
8466 |
_safe_api_reference() — yc 來源 |
交易程式2.0.4.py |
8489 |
_safe_api_limit_up() — limit_up 來源(同樣依賴 contracts) |
交易程式2.0.4.py |
13736 |
盤後補齊分支 — 加 refresh |
交易程式2.0.4.py |
13776 |
盤前分支 — 加 refresh |
交易程式2.0.4.py |
13793 |
盤中分支 — 加 refresh ⚠️ |
交易程式2.0.4.py |
6632 |
_rebuild_shioaji_api_for_mode() 內 api.login() — 已含 contracts 抓取(切模式時) |
後續 follow-up(獨立 issue)
- [ ]
intraday_kline_history 持續性採集監控:DB 內最新只到 5/25,5/26 ~ 5/28 漏抓需排查 cron / 觸發機制
- [ ] 加入 sanity check:每次讀
contract.reference 時,跟 intraday_kline_history 最新一天的 close 做交叉驗證,差異 > 2% 警告
- [ ] UI 加「手動重抓合約」按鈕,給 user 急救用(不必重啟整支程式)
- [ ] 考慮 daily cron 自動
_force_refresh_contracts()(例如 08:25)— 不只靠 start_trading 分支進入點
Acceptance criteria
[Bug 75] CRITICAL — Shioaji contracts 跨日不刷新,導致
reference/limit_up用舊資料,停損 cover IOC 委託失敗Labels:
bug·critical·trading-safety·overnight-risk·shioaji-apiSeverity: 🚨 Critical(直接造成停損失靈 → 部位 overnight 風險)
Affected versions: v2.0.0 ~ v2.0.4(估算自 v2.0.2 開始大量依賴
contract.reference)Discovered: 2026-05-29(發現於微星 2377 真實交易)
TL;DR
程式跨日連續執行時(
start_trading()收盤後 sleep 到隔日 08:30 再遞迴呼叫自己),sys_state.api從首次api.login()起就快取 contracts 資料,從未刷新。結果contract.reference(昨日收盤價)凍結在首次登入那天的值,幾天後再用就是過時資料。具體影響:
calculate_limit_up_price(yc)算出的漲停價偏離真實值 2-3 元價格超過漲跌幅範圍→ 委託失敗 → 停損失靈 → 部位 overnight真實案例:微星 2377 @ 2026-05-29
126.5123.5139.0135.5139漲停價公式(
交易程式2.0.4.py:8427):126.5 × 1.10 = 139.15 → floor(0.5) = 139.0123.5 × 1.10 = 135.85 → floor(0.5) = 135.5證據鏈:
intraday_kline_live表內 2377 今日(2026-05-29)第一筆 09:00:00 寫入的昨日收盤價 = 126.5,漲停價 = 139.0(由_safe_api_reference()從 Shioajicontract.reference取得)intraday_kline_history2377 最新只有到 2026-05-25(5/26、5/27、5/28 都漏抓)重現步驟
api.login(api_key=..., secret_key=..., contracts_timeout=10000)載入 contractsstart_trading()收盤分支,sleep 到隔天 08:30 自動遞迴sys_state.api.Contracts.Stocks[sym].reference期望行為 vs 實際行為
期望:每個交易日 09:00 開始時,所有股票的
contract.reference應該是「前一個交易日的收盤價」實際:
contract.reference凍結在「首次登入那天的前一日收盤」,跨日後從未更新根因分析
start_trading()的執行模型:全 codebase grep
fetch_contracts零個結果 → 沒有任何地方主動刷新 contracts。唯一會重抓的時機:
_rebuild_shioaji_api_for_mode()(切換 live/sim 模式時走完整 logout + login),但這要 user 手動切。影響範圍
跨所有依賴
contract.reference或contract.limit_up的路徑:漲停價欄位(price - yc) / yc分母錯 → 觸發時機 / 強度判斷錯亂limit_down也錯 → 保護邏輯時機錯最嚴重:停損失靈直接造成 overnight 隔夜風險,真實虧損可能放大數倍。
修補方案
1. 新增 helper
_force_refresh_contracts(reason)(交易程式2.0.4.py:8435)關鍵:
contract_download=True強制從遠端重抓(預設False只讀本地 cache,治標不治本)2. 在
start_trading()三個分支前都呼叫3. 文件
_safe_api_reference()把責任明確化(line 8466)驗證方法
修補後重啟程式,觀察系統日誌應出現:
然後驗證
intraday_kline_live表內持倉股的昨日收盤價是否吻合永豐 app 顯示的真實值:跨日驗證:讓程式跑 2 天以上,觀察第 2 天 09:00 的 reference 是否已更新為「第 1 天收盤」(而非凍結在「第 0 天收盤」)
應急 workaround(在 fix deploy 前)
_rebuild_shioaji_api_for_mode()觸發完整重建相關 code 位置
交易程式2.0.4.pycalculate_limit_up_price()— 漲停公式交易程式2.0.4.py_force_refresh_contracts()← 新增交易程式2.0.4.py_safe_api_reference()— yc 來源交易程式2.0.4.py_safe_api_limit_up()— limit_up 來源(同樣依賴 contracts)交易程式2.0.4.py交易程式2.0.4.py交易程式2.0.4.py交易程式2.0.4.py_rebuild_shioaji_api_for_mode()內api.login()— 已含 contracts 抓取(切模式時)後續 follow-up(獨立 issue)
intraday_kline_history持續性採集監控:DB 內最新只到 5/25,5/26 ~ 5/28 漏抓需排查 cron / 觸發機制contract.reference時,跟intraday_kline_history最新一天的 close 做交叉驗證,差異 > 2% 警告_force_refresh_contracts()(例如 08:25)— 不只靠start_trading分支進入點Acceptance criteria
_force_refresh_contracts()helper 實作完成start_trading()分支都加上 refresh 呼叫py_compile通過價格超過漲跌幅範圍失敗