症狀:一個很短暫的「成功」
在模擬環境做全面點擊測試,各功能依序走過:初始載入、資產看板、網格策略 UI、 等差↔等比切換、單筆測試下單——全部正常。 最後做 Deploy All(4 筆委託),app 顯示「佈署完成!成功:4」。
正要收工,16 秒後看委託追蹤:四筆全部 ❌ 已取消。沒有按 Cancel All。 這個狀態從哪來的?
為什麼「成功」之後會「取消」
下委託的流程是非同步的。
place_order() 呼叫後,券商先回 PendingSubmit,
app 立刻把這筆記為「成功下出去了」,然後顯示「佈署完成!成功:4」。
但 PendingSubmit 只是「你的委託書送到我這了」,不是「我接受了」。
幾秒到十幾秒後,交易所驗證委託書——如果格式不對,就靜默拒絕,狀態變 CANCELLED。 這個拒絕是異步的,不會主動通知,要自己去查狀態。 app 的委託追蹤確實在追蹤,但使用者如果只看到最初的「成功:4」就離開,就錯過了拒絕。
根本原因:round(price, 2) 不等於台股升降單位
翻 src/strategy.py 的 generate_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_tick;
fragments.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」就收工,會錯過異步拒絕。