前言:大多數量化系統死在哪裡?
不是死在策略選錯。
死在「回測很美,實盤見鬼」這件事上。
這兩年我一直在思考一個問題:為什麼許多個人量化系統,回測 Sharpe 可以跑到 1.5 以上,Paper Trading 一開始表現還不錯,但只要真金白銀一下去,就開始走樣?
我認為根本原因不在策略,在於工程設計沒有把「可驗證性」當成第一公民。
這篇文章是我在設計一套日內動能策略 Paper Trading Engine(暫稱 VIP-5 L5 Engine)過程中,整理出來的工程觀點。不談機構級低延遲架構,不談 Kafka、Rust、lock-free ring buffer——因為個人系統根本用不到那些,引入只會增加除錯地獄的複雜度。
我只談一件事:怎麼設計一套 Paper Trading Engine,讓你的回測結果是可信的、可重演的、可驗證的。
---
一、最容易被忽略的問題:Look-Ahead Bias
先講最隱形的殺手。
Look-Ahead Bias(前瞻偏差)是指:你的回測程式,在計算「當下」訊號時,不小心用到了「未來」的資料。
這不是你故意的。它藏在細節裡:
pd.rolling(50).mean()算出來的 MA,你是用「今天」的值,還是「昨天收盤後」的值?- 你的 feature normalize 是對整段歷史資料做的,還是只對已知資料做的?
- 你的策略判斷用的是
bar_ts=now()還是bar_ts=close_of_last_bar?
根據 Quantopian 社群的歷史觀察,有前瞻偏差的策略,實盤表現平均比回測差 20-30%。
我的解法:從介面層把 Look-Ahead Bias 封死。
python
class BarProvider(ABC):
@abstractmethod
def get_bars(self, symbol: str, end: datetime, lookback: int) -> pd.DataFrame:
...
end 參數強制傳入。回測時永遠是 end = bar_ts(這根 bar 的收盤時間),策略層不可能看到這個時間點之後的資料。Live 時是 end = now()。策略邏輯完全不改。---
二、Single Source of Truth:JSONL + SQLite 二用設計
我見過太多量化系統,資料散落在各處:
- bars 在 CSV,signals 在 log 檔,fills 在資料庫,但三者的時間戳對不起來
- 出了問題,你根本無法 replay 當時到底發生什麼事
我的設計原則:每一筆決策都必須可以被事後完整重現。
具體做法是雙軌制:
| 資料類型 | 格式 | 原因 |
|---|---|---|
| bars | SQLite | 需要範圍查詢,WHERE ts BETWEEN ? AND ? |
| signals | JSONL | append-only,人工可讀,天然 audit trail |
| fills | JSONL | 稽核用,絕對不可修改 |
| portfolio snapshots | JSONL | 可完整重建部位 |
| risk events | JSONL | 每次 gate 判斷都寫,含拒絕原因 |
JSONL 的核心精神:每一行是一個獨立事實,永遠只 append,不修改。 單行損壞不影響其他行,
grep、jq、pandas.read_json(lines=True) 直接用,不需要 schema migration。這其實是 Event Sourcing 的思想在個人量化系統上的簡化落地版。
---
三、Trading Loop:7 步驟,每步都可重演
每 1 分鐘一次的 Loop(日盤),設計上每一步都必須留下可查核的 artifact:
[1] Load Bars → 標記 fetched_at、source='live'
[2] Compute Features → 輸出 FeatureRow,含 computed_at
[3] Strategy Signal → SignalEvent,含 trigger_rule、params_snapshot
[4] RiskGate → 每次都寫 risk_events.jsonl(通過或拒絕都寫)
[5] PaperExecution → 模擬成交,slippage + fee 參數化
[6] Update Portfolio → 寫 snapshots.jsonl
[7] Loop Artifact → 寫 loop_log.jsonl:elapsed_ms、signals、fills、errors
關鍵設計:每一步的輸出都帶兩個時間戳——
bar_ts(策略視角的時間)和 system_ts(實際執行時間)。這樣你可以測量每一步的實際延遲,也可以在回測時完整重播。---
四、風控門:不要等到虧損才想到
風控不是「有比沒有好」的點綴,它是 Engine 的核心結構之一。
我把風控設計成 5 個優先序的 Gate,依序檢查:
Gate 1(最優先):Kill Switch
- 當日虧損超過硬上限 → 停止 Loop、平所有 paper 部位、推 Telegram 告警
- 資料連續 stale 超過閾值 → PAUSE(不 HALT),每分鐘重試
Gate 2:Daily Risk Limit
- 當日最大虧損、最大回撤、最多交易次數
Gate 3:Cooldown
- 連虧 3 筆 → 強制冷卻 60 分鐘
- Cooldown 狀態持久化到檔案,避免 Loop 重啟後失效
Gate 4:Position Size
- 單股不超過帳戶 30%,超過則縮減 qty,不直接拒絕
Gate 5:通過
每次 Gate 判斷,無論通過或拒絕,一律寫入
risk_events.jsonl。---
五、Walk-Forward 驗證:你的策略真的有 edge,還是 overfit?
這是最多人跳過的步驟,也是最關鍵的一步。
Walk-Forward 的精神:把時間軸切成滾動窗口,每次只用「過去」的資料訓練,用「緊接著的未來」測試。重複 4-6 fold,看 OOS 的平均表現。
我設定的接受標準:
- OOS Sharpe > 0.5(含手續費)
- OOS 勝率 > 52%
- IS vs OOS 績效衰退 < 40%(衰退過大 = overfitting)
Parameter Stability Test 更是不可少:以最佳參數為中心,每個參數 ±20% 跑一次,畫出 Sharpe heat map。如果最佳參數是個「尖峰」,周圍快速崩潰,代表這組參數只是在擬合噪音,不可用。
Fee/Slippage Sweep:台股手續費 0.1425%、證交稅 0.3%、假設 10 bps 滑價,掃描到 slippage 20 bps 時策略是否還是正期望值。如果成本稍微增加就轉負,這個策略沒有足夠的 edge。
---
六、策略純函數化:最容易被忽略的工程細節
這是我認為個人量化系統最常犯、最難察覺的問題。
策略層(Signal Generator)必須是純函數:
- 給定相同的
FeatureRow+PortfolioState,永遠輸出相同的SignalEvent - 不讀全域變數、不讀資料庫、不寫 log、不做任何副作用
一旦策略有隱式狀態(例如:全域的
last_signal 變數、外部 DB 讀取、依賴上一次 Loop 的 cache),你的 replay 就不可信,你的回測就不可信。移植現有策略時,這是最容易翻車的地方。逐行檢查有沒有任何不由 FeatureRow 或 PortfolioState 傳入的外部依賴。
---
七、我不做什麼(刻意的)
最後說一件反直覺的事:一個好的工程設計,需要明確知道自己不做什麼。
這套 Engine 刻意不引入:
- Kafka / FIX / Rust:單機 paper 用不到,引入只增加除錯成本
- lock-free ring buffer:Python GIL 下無意義
- 微服務 / Docker 拆分:單機部署反而增加複雜度
- 即時 WebSocket 串流:1min bar 頻率不需要
- 多策略並行:先把一個策略跑穩
這不是能力問題,是優先序問題。在驗證策略真的有 edge 之前,任何基礎設施的過度投資都是浪費。
先讓系統能跑、能回測、能出報表、能告警。然後再談優化。
---
結語
量化交易的核心難題,不是找到好策略,而是知道你的策略是否真的好。
這需要一套工程設計嚴謹的 Engine:資料不能有前瞻偏差、每筆決策都可重演、風控不是裝飾、驗證方法不能只看 in-sample。
如果你也在走這條路,建議從最小可行的 Trading Loop 開始,讓每一步都留下可查核的 artifact,然後用 Walk-Forward 讓數字說話。
---
本文為研究小弟個人工程觀點,不構成任何投資建議。