症狀:一個很短暫的「成功」

在模擬環境做全面點擊測試,各功能依序走過:初始載入、資產看板、網格策略 UI、 等差↔等比切換、單筆測試下單——全部正常。 最後做 Deploy All(4 筆委託),app 顯示「佈署完成!成功:4」。

正要收工,16 秒後看委託追蹤:四筆全部 ❌ 已取消。沒有按 Cancel All。 這個狀態從哪來的?

為什麼「成功」之後會「取消」

下委託的流程是非同步的。 place_order() 呼叫後,券商先回 PendingSubmit, app 立刻把這筆記為「成功下出去了」,然後顯示「佈署完成!成功:4」。 但 PendingSubmit 只是「你的委託書送到我這了」,不是「我接受了」。

幾秒到十幾秒後,交易所驗證委託書——如果格式不對,就靜默拒絕,狀態變 CANCELLED。 這個拒絕是異步的,不會主動通知,要自己去查狀態。 app 的委託追蹤確實在追蹤,但使用者如果只看到最初的「成功:4」就離開,就錯過了拒絕。

根本原因:round(price, 2) 不等於台股升降單位

src/strategy.pygenerate_grids(),找到問題:

算網格價格的地方寫著 round(price, 2)。保留兩位小數。 但同一個檔案裡已經有 get_stock_tick()——這個函式會根據股價區間查 TWSE 的升降單位—— 只是 generate_grids() 沒有呼叫它。

問題在哪?台股的升降單位不是「保留兩位小數就好」。 以 00878 為例,這支 ETF 在 28 元附近,對應的升降單位是 0.05 元—— 也就是說,合法的委託價格必須是 0.05 的整數倍(28.00、28.05、28.10……)。

系統儲存的 anchor 中心價是 25.22,interval 是 1.00。 套進去就會算出 26.22、27.22、28.22、29.22—— 這些價格 round(*, 2) 看起來沒問題,但全部都不是 0.05 的倍數。 交易所逐筆驗,逐筆拒。

DB 裡的歷史把故事說清楚了

grid_state.db 裡查:423 筆 CANCELLED,15 筆 FILLED。

15 筆 FILLED 全在 2026-04-28——也就是 25.22 這個 anchor 設定**之前**。 2026-04-29 anchor 設定完成之後,共 5 個 deploy 批次,包含今天這次模擬, 100% CANCELLED,0 筆 FILLED。

這個 bug 從 2026-04-29 就存在,到今天才被發現。 原因是 app 說「成功」,使用者沒有理由繼續查; 而交易所的拒絕是靜默的,不發推播、不跳通知,只靜靜地把狀態改掉。

修法:snap_to_tick + step_price

新增 snap_to_tick(price):先呼叫 get_stock_tick() 取得升降單位, 再 round(price / tick) * tick,最後 round(*, 2) 清浮點殘差。

新增 step_price(base, direction, mode, interval): 補格走一格並 snap,避免重複在多處寫一樣的邏輯。

generate_grids() 兩處算價改用 snap_to_tickfragments.py 裡的補格 _step 改用 step_price()。 另外加了 seen_prices set 去重——間距小於 tick 時,相鄰格 snap 後可能同價, 去重避免重複委託。

新增 14 條回歸測試(tests/test_tick_alignment.py), 覆蓋各價位區間、等差/等比 ±1 步、anchor=25.22 回歸、小間距去重。修完試算: 30.30 / 30.20 / 30.10 / 30.00 / 29.90 / 29.80——全部 0.05 倍數,0 筆未對齊。

關鍵教訓

「成功」不代表「接受」:place_order 回 PendingSubmit 是「送達」不是「接受」。委託從提交到確認有一段異步窗口,UI 的「成功」是時序上的一個截面,不是最終狀態。

靜默拒絕比明顯錯誤更危險:交易所不會呼叫你的 callback 說「我拒絕了」,只是靜靜地改狀態。這種失敗模式要靠主動輪詢或 DB 分析才能發現,不是靠看 UI。

同檔案裡已有的函式要主動找get_stock_tick() 一直在 strategy.py 裡,generate_grids() 沒有呼叫它。重寫 round(price, 2) 之前先 grep 一下,現有的工具常常比寫新的好。

DB 狀態會說話:423 CANCELLED / 15 FILLED、15 筆 FILLED 全在 anchor 設定之前——這個分佈模式直接定位了「bug 從哪一天開始」。數據庫不是只用來存,也是排查的工具。

模擬環境測試要走完委託生命週期:Deploy All 之後等 16-30 秒再看委託追蹤,才算真正跑完一個委託週期。只看「成功:N」就收工,會錯過異步拒絕。

來源:個人開發日誌 2026-05-24 · stock_grid_bot 模擬環境測試 · CRITICAL fix · 423 筆歷史 CANCELLED / snap_to_tick + 14 條回歸測試