Skip to content

BUG 75-跨日執行無正確獲取昨日收盤價問題 #8

Description

@OswallowO

[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

證據鏈:

  1. intraday_kline_live 表內 2377 今日(2026-05-29)第一筆 09:00:00 寫入的 昨日收盤價 = 126.5,漲停價 = 139.0(由 _safe_api_reference() 從 Shioaji contract.reference 取得)
  2. intraday_kline_history 2377 最新只有到 2026-05-25(5/26、5/27、5/28 都漏抓)
  3. 強烈暗示:程式從 2026-05-25 ~ 26 之間首次 login 後就持續執行,contracts 凍結在那刻的值(2026-05-25 收盤 ≈ 126.5)

重現步驟

  1. 啟動程式 → api.login(api_key=..., secret_key=..., contracts_timeout=10000) 載入 contracts
  2. 讓程式自然進入 start_trading() 收盤分支,sleep 到隔天 08:30 自動遞迴
  3. 連續執行 2 天以上(中間沒手動切 live/sim 模式,沒重啟)
  4. 在第 N 天觀察任一持倉股的 sys_state.api.Contracts.Stocks[sym].reference
  5. 預期:= 昨日(第 N-1 天)收盤
  6. 實際:= 第 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.referencecontract.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

  • _force_refresh_contracts() helper 實作完成
  • 三個 start_trading() 分支都加上 refresh 呼叫
  • py_compile 通過
  • 真實環境跑一個交易日,確認 cover IOC 不再因 價格超過漲跌幅範圍 失敗
  • 跨日驗證(連跑 ≥ 2 天)reference 每日刷新

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions