「我們的回測跑出來很漂亮,但上了實盤之後,怎麼差這麼多?」
這句話幾乎每個做量化策略的團隊都說過。問題是,絕大多數人說的是「感覺差很多」——沒有具體數字,也沒有拆解落差來源的方法。感覺不能被優化,感覺只能被擔心。
這篇文章想說的是:我們怎麼把這個「感覺」轉成一個可以跟客戶解釋、可以追蹤、可以改善的工程問題。過程中有一個具體的初步驗證數字:同一張多單,在回測引擎與即時引擎規則下的獲利相差 6.7 倍。
這個問題為什麼不好解 #
「回測與實盤落差」是量化交易的老問題,文獻討論很多,但停在概念層的居多。
最常見的解釋是「過度擬合」(overfitting)——策略在歷史資料上調得太好,對未來泛化能力差。這個說法不能說錯,但它把落差歸因到策略設計層,而忽略了一個更根本的工程層問題:兩套引擎在計算同一件事時,用的根本不是同一個路徑。
這不是擬合問題,是架構問題。
回測引擎的工作方式,是一次拿到一段完整的歷史 K 棒資料,然後跑過去計算訊號。這代表它在計算某根 K 棒的 rolling 指標時,其實暗中「看到了」這根棒之後的資料——因為 rolling window 是對整段資料做的,而整段資料已經全部在記憶體裡。
即時引擎不同。它每一根 K 棒都只能看到「截至當下」的資料。rolling 指標的計算天生是增量的,不是全局的。兩者看似在做同一件事(計算 20 根均線),但計算的起點資料量不一樣,在策略啟動初期尤其明顯——即時引擎在前幾根棒的指標根本還不穩定,而回測引擎從第一根就拿到完整的數字。
除此之外,成交價的假設也不同:
- 回測引擎常見假設是「當根訊號觸發,下一根開盤成交」
- 即時引擎必須面對「這根收盤前幾秒觸發,嘗試在收盤前送單」的現實
後者的成交價,在市場波動時,與「下一根開盤」之間的差距可以很可觀。
這兩個差異疊加,就產生了「回測賺、實盤不賺」的系統性落差。而因為這是架構層的問題,光靠調整策略參數是解不掉的。
兩套引擎,兩種世界觀 #
在動手開發解法之前,我們先把落差的來源整理成一張清單,強迫自己把「感覺差很多」拆成幾個可以分別量測的維度:
| 維度 | 回測引擎 | 即時引擎 | 落差性質 |
|---|---|---|---|
| 資料視野 | 完整段一次計算 | 截至當根逐步累積 | 指標數值系統性偏差 |
| rolling 指標穩定性 | 從第一根就穩定 | 前期資料不足,指標震盪 | 策略啟動期的訊號可信度差 |
| 成交價假設 | 下一根開盤 | 當根收盤(或排隊) | 每筆交易的進出場點不同 |
| 觸價時序 | 事後計算,精確命中 | 即時比對,存在延遲窗口 | 實際觸發時間可能差一根棒 |
把這張表攤出來之後,問題就從「感覺差很多」變成「哪幾個維度、各差多少」。這是能被工程處理的問題形式。
但光有表格還不夠。客戶想知道的是:在我這張單子上,這個落差是多少點、多少元?
即時重播頁面的設計決策 #
要回答這個問題,有個直覺但錯誤的做法:拿回測引擎的輸出跟實盤紀錄對比。這樣做的問題是,實盤紀錄本身就有執行時序的雜訊(延遲、部分成交、人工干預),你無法確定差距是「兩套引擎的設計差異」還是「那天實盤運氣不好」。
正確的做法是用第三條路重新跑一次:用與實盤完全相同的執行路徑,把歷史 K 棒資料逐根餵進去,模擬當時即時引擎的運算結果。這就是「即時重播」頁面的核心設計。
具體流程如下:
- 選定對象:選策略版本、商品代碼、日期(可以精確到某一個盤)
- 後端模擬:後端把那天的歷史 K 棒一根一根按時間序餵給策略——每根 K 棒送進去時,策略只能看到截至那根棒為止的所有歷史資料,不能多也不能少。進出場的觸發邏輯、觸價規則、成交假設,全部跟實盤程式碼共用同一份,不是重寫一套。
- 前端呈現:K 線圖上標出進出場點,讓使用者可以視覺化確認邏輯是否符合預期。
- 並排比對表:最關鍵的部分。頁面下方有一張表格,左欄是「回測引擎在同段時間的交易結果」,右欄是「即時重播(即時引擎路徑)的交易結果」,每筆交易都列出點數差與損益差。
這個設計有一個重要原則:後端程式碼必須跟實盤共用,不能為了「重播功能」寫一套模擬版本。因為一旦分岔,你驗證的就不是實盤行為,而是你對實盤行為的理解。兩者之間的差距,往往才是最大的陷阱。
踩過的坑 #
這個功能做起來並不複雜,但有幾個地方的設計決策不直覺,值得說清楚。
坑一:rolling 指標的「重置問題」
最初版本的重播邏輯,是從選定日期的第一根棒開始餵資料。結果發現,即時引擎在盤初的指標跟實盤完全對不上——因為實盤的指標是從更早的資料持續累積過來的,而重播從零開始跑,前幾根的 rolling 指標根本不準。
解法是在重播開始之前,先把那天開盤前的足夠多的歷史 K 棒「暖機」餵進去,讓指標值達到穩定狀態,再從選定的開始時間點進行真正的重播。暖機需要多長的歷史?取決於策略用到的最長 rolling window,要確保所有指標都跑過至少一個完整的 window 長度。
坑二:人工干預的處理
客戶有時會在實盤途中手動干預(比如在某個時間點手動平倉)。即時重播不知道這件事,會繼續持倉到策略訊號出場,造成比對表上出現「實盤比重播少一筆交易」的混亂。
我們的做法是在比對表上標注「實盤有人工干預,以下比對以系統訊號為準」,讓使用者知道這筆不是引擎差異造成的。人工干預的記錄從交易紀錄表拉,不是靠人手動標記。
坑三:成交價假設的透明化
回測引擎的成交假設「下一根開盤」,在歷史資料中是確定的數字,但在即時重播裡,我們的成交假設跟實盤一樣(觸價當根或下一根,視送單時序而定)。這代表兩欄的成交價假設本質就不同,不是「誰比較接近正確」,而是「它們本來就在算不同的東西」。
這一點要在介面上說清楚,否則客戶會以為「比對表的差距就是系統 bug」,而實際上有一部分是設計上不可消除的假設差異。
第一次量化結果:6.7 倍 #
以初次驗證為例,選取了某天夜盤的一張多單做比對。
這張單在回測引擎的結算下獲利可觀,在即時重播(即時引擎路徑)下的獲利則只有前者的約 15%——也就是說,回測結果是即時引擎結果的 6.7 倍。
落差的構成主要來自兩個地方:
- 進場點不同:回測引擎在指標觸發的那根棒用「下一根開盤」進場,抓到了一個相對有利的位置;即時引擎在收盤前幾秒送出委託,成交價比那個「下一根開盤」貴了幾個 tick。
- 出場點不同:出場訊號的觸發時序有半根棒的差異,導致實際出場點不同,而那半根棒的行情方向對多單不利。
6.7 倍聽起來很誇張,但這個數字本身不代表策略失效——它代表的是「在這個市況下,這個策略的回測數字高估了 6.7 倍的獲利」。知道這個倍數,才能對客戶的績效預期做正確的校正。
更重要的是,這個數字現在是可追蹤的。每次策略更新後,可以跑一次即時重播,確認這個倍數有沒有縮小——縮小代表策略改善有效,沒縮小代表問題不在策略本身。
從可視化到可溝通 #
這個功能做出來之前,每次客戶問「為什麼回測跟實盤差這麼多」,我們能說的只是「兩套引擎計算路徑不同,這是正常的」。這個答案是對的,但沒有說服力,因為沒有數字。
有了即時重播之後,我們可以說:「在 8/11 夜盤這張多單上,兩套引擎的獲利差距是 X 點、Y 元,主要來源是進場點差了 Z tick,以及出場訊號晚了半根棒。」這是完全不同的溝通品質。
對客戶來說,這個功能也有一個心理上的作用:他們可以自己選日期、自己跑比對,不需要相信我們的解釋。這種「可自查」的設計,往往比任何說明文件都更有說服力。
這個工法可以搬移到哪 #
「即時重播」這個概念,本質上是:
用生產環境的執行路徑,在歷史資料上做確定性重現。
這個工法不限於量化交易。任何「有離線計算結果與線上即時計算結果」的系統,都可能存在類似的引擎落差:
- 推薦系統:離線評估的推薦效果 vs 線上 A/B 實際點擊率,兩者的差距不一定是模型問題,可能是線上特徵的計算方式跟離線評估不一致。
- 風控模型:離線訓練的模型 AUC vs 線上實際攔截率,落差來源可能是線上特徵延遲導致的資料版本差異。
- 庫存補貨預測:離線預測的補貨量 vs 實際採購決策,落差可能來自即時庫存資料比訓練資料的更新頻率低。
共同結構都是:「離線用了線上做不到的資訊優勢,導致離線評估過於樂觀。」驗證方法也都相同:用與線上完全相同的計算路徑,在歷史資料上重跑一次,比對結果差距。
在正式上線任何依賴「離線評估」做決策的系統之前,建議先量化這個落差。否則部署後看到的每一次績效不如預期,都無法判斷是模型失效、是市場變化、還是從一開始就沒有估對基準線。
有類似的系統評估需求,或想討論如何在你的環境建立這類驗證機制,歡迎與我們聯繫。
