模組 03:設計哲學與品質屬性理解每個設計決策背後的「為什麼」— 這決定了系統的靈魂
上一個模組我們看了系統「長什麼樣」。這個模組我們要深入理解它「為什麼」要這樣做。NautilusTrader 的每一個設計選擇都不是隨便做的,都有明確的交易場景做依據。
一、五大架構設計模式
本節涉及較深的軟體工程與系統架構概念。如果你是量化交易新手,可以先粗略瀏覽或直接跳過本節,這完全不會影響你後續學習如何撰寫策略。
NautilusTrader 使用了五種經過驗證的軟件工程設計模式。不用擔心,我們會用交易場景來解釋每一個。
flowchart LR
root(("🐚 NautilusTrader<br/>設計哲學"))
subgraph ddd["💎 領域驅動 (DDD)"]
direction TB
ddd1["用交易語言寫代碼"]
ddd2["Price, Order, Position"]
end
subgraph eda["⚡ 事件驅動 (EDA)"]
direction TB
eda1["一切皆事件"]
eda2["即時回應變化"]
end
subgraph hexa["🔌 六角形架構"]
direction TB
hexa1["核心與外部隔離"]
hexa2["可替換的適配器"]
end
subgraph crash["💥 Crash-Only"]
direction TB
crash1["崩潰即恢復"]
crash2["快速重啟"]
end
subgraph fail["🚫 Fail-Fast"]
direction TB
fail1["錯誤數據立即拒絕"]
fail2["不讓壞數據擴散"]
end
root ==> ddd
root ==> eda
root ==> hexa
root ==> crash
root ==> fail
style root fill:#0f1419,stroke:#4fc3f7,stroke-width:2px
style ddd fill:#0f1419,stroke:#ab47bc,stroke-width:1px
style eda fill:#0f1419,stroke:#ab47bc,stroke-width:1px
style hexa fill:#0f1419,stroke:#ab47bc,stroke-width:1px
style crash fill:#0f1419,stroke:#ab47bc,stroke-width:1px
style fail fill:#0f1419,stroke:#ab47bc,stroke-width:1px
1.1 領域驅動設計 (DDD)
核心思想:代碼應該用交易員能理解的「語言」來寫,而不是用程式設計師的抽象概念。
在 NautilusTrader 中,你會看到這些名字:
| 交易概念 | 代碼類型 | 說明 |
|---|---|---|
| 價格 | Price | 不是普通的浮點數,而是帶精度的定點數,避免 0.1 + 0.2 ≠ 0.3 的問題 |
| 數量 | Quantity | 同樣是定點數,保證計算精確 |
| 訂單 | Order | 完整的訂單對象,追蹤從建立到完成的每個狀態 |
| 持倉 | Position | 由多個事件聚合而成,即時反映盈虧 |
| 金融工具 | Instrument | 股票、期貨、期權等,每種都有專門的子類 |
| 帳戶 | Account | 現金帳戶、保證金帳戶,各有不同的結算邏輯 |
因為交易員看代碼時,能直接看懂。你不需要猜「float price」代表什麼精度。Price("1.23450", precision=5) 一目了然:這是一個 5
位小數精度的價格。
1.2 事件驅動架構
核心思想:系統不是按順序跑的,而是「有事件發生時才行動」。
想像一下兩種不同的交易方式:
傳統方式(輪詢)
每秒查一次:「價格變了嗎?」「有新 K 線嗎?」「訂單成交了嗎?」
→ 浪費大量資源在「什麼都沒發生」的時候
事件驅動方式
系統告訴你:「新 K 線來了!」「你的訂單成交了!」
→ 只在有事件時才行動,高效且即時
在 NautilusTrader 中,你的策略會「訂閱」感興趣的事件,然後等系統推送:
sequenceDiagram
participant S as 🎯 你的策略
participant MB as 📡 消息總線
Note over S: on_start() 啟動時
S->>MB: 訂閱 BTC/USDT 的 1 分鐘 K 線
S->>MB: 訂閱 BTC/USDT 的報價
Note over S: 等待事件...
MB->>S: on_bar(bar) — 新 K 線到了!
S->>S: 計算指標、判斷信號
MB->>S: on_quote_tick(tick) — 新報價到了!
S->>S: 更新即時數據
MB->>S: on_order_filled(event) — 訂單成交了!
S->>S: 處理成交邏輯
1.3 六角形架構(Ports and Adapters)
核心思想:核心業務邏輯和外部世界完全隔離。透過「端口」(Port) 和「適配器」(Adapter) 連接。
flowchart LR
subgraph 外部_左["外部數據"]
binance_data["Binance WS"]
ib_data["IB API"]
file_data["歷史文件"]
end
subgraph 端口_左["📥 數據端口"]
dc["DataClient<br/>(介面)"]
end
subgraph 核心["🧠 核心業務邏輯"]
de["DataEngine"]
strategy["你的策略"]
ee["ExecutionEngine"]
end
subgraph 端口_右["📤 執行端口"]
ec["ExecutionClient<br/>(介面)"]
end
subgraph 外部_右["外部交易所"]
binance_exec["Binance API"]
ib_exec["IB API"]
sim_exec["模擬器"]
end
binance_data --> dc
ib_data --> dc
file_data --> dc
dc --> de --> strategy --> ee --> ec
ec --> binance_exec
ec --> ib_exec
ec --> sim_exec
這就是為什麼你的策略代碼在回測和實盤中完全一樣:策略只和「端口」互動,不知道背後是模擬器還是 Binance。換交易所只需要換一個適配器。
1.4 Crash-Only 設計
核心思想:與其花大量精力寫「優雅關閉」的代碼,不如確保系統能從崩潰中快速恢復。
統一恢復路徑
啟動和崩潰恢復走同一條代碼路徑。因為每次啟動都在「恢復」,所以這條路徑被充分測試。
外部化狀態
關鍵狀態(訂單、持倉)持久化到外部存儲(如 Redis)。崩潰也不會丟失狀態。
快速重啟
設計為能在崩潰後毫秒級重啟,最小化停機時間。
冪等操作
操作設計為可以安全重試。重啟後重新執行不會產生副作用。
想像凌晨 3 點你的交易系統崩潰了。Crash-Only 設計意味著系統會自動重啟,從 Redis 恢復所有訂單和持倉狀態,然後繼續運行。你可能連睡夢中都不知道發生過崩潰。
1.5 Fail-Fast 數據完整性策略
核心思想:在交易系統中,腐壞的數據比沒有數據更危險。發現錯誤數據時立即停止,絕不讓它擴散。
flowchart LR
subgraph 壞數據場景["😱 如果沒有 Fail-Fast"]
bad_price["收到 NaN 價格"] --> calc["計算指標"] --> signal["產生錯誤信號"] --> order["下錯誤的訂單"] --> loss["💸
虧損"]
end
flowchart LR
subgraph 好的場景["✅ Fail-Fast 保護"]
bad_price2["收到 NaN 價格"] --> reject["❌ 立即拒絕並報錯"] --> log["記錄錯誤日誌"] --> safe["系統安全"]
end
以下情況系統會立即停止(Panic):
| 情況 | 例子 | 為什麼要停止 |
|---|---|---|
| 算術溢出 | 時間戳相加超過 u64 上限 | 結果會是錯誤的數值 |
| 無效數據 | 價格是 NaN 或 Infinity | 會導致後續所有計算錯誤 |
| 類型轉換失敗 | 負數量、負時間戳 | 這些在物理上不可能存在 |
| 格式錯誤 | 無法解析的價格字串 | 說明數據源有問題 |
二、品質屬性優先級
在設計系統時,不同的需求會互相衝突。NautilusTrader 按以下優先級做取捨:
flowchart LR
r["🥇 可靠性 (可靠第一)"] ==> p["🥈 性能 (速度其次)"]
p ==> m["🥉 模組化 (結構優雅)"]
m ==> t["4️⃣ 其它品質屬性 (測試、維護、部署)"]
這個排序解釋了很多設計選擇。比如:系統在遇到可疑數據時會選擇崩潰(可靠性優先)而不是嘗試修復繼續運行(可用性優先)。因為在交易中,一個錯誤的決策比短暫的停機更危險。
三、組件狀態機
NautilusTrader 中的每個組件(引擎、策略、適配器)都遵循相同的狀態機模式。這意味著它們有明確定義的「生命階段」:
stateDiagram-v2
[*] --> PRE_INITIALIZED : 剛建立
PRE_INITIALIZED --> READY : register() 註冊
READY --> STARTING : start() 啟動中
STARTING --> RUNNING : 啟動完成
RUNNING --> STOPPING : stop() 停止中
STOPPING --> STOPPED : 停止完成
STOPPED --> STARTING : start() 重新啟動
STOPPED --> RESETTING : reset() 重置中
RESETTING --> READY : 重置完成
RUNNING --> DEGRADING : degrade() 降級中
DEGRADING --> DEGRADED : 進入降級狀態
RUNNING --> FAULTING : fault() 故障中
FAULTING --> FAULTED : 進入故障狀態
DEGRADED --> STOPPING : stop()
DEGRADED --> FAULTING : fault()
STOPPED --> DISPOSING : dispose() 釋放中
FAULTED --> DISPOSING : dispose()
DISPOSING --> DISPOSED : 已釋放
DISPOSED --> [*]
穩定狀態 vs 過渡狀態
| 狀態 | 類型 | 說明 |
|---|---|---|
| PRE_INITIALIZED | 穩定 | 剛建立,還沒配置好 |
| READY | 穩定 | 已配置,可以啟動 |
| RUNNING | 穩定 | 正常運行中 |
| STOPPED | 穩定 | 已停止 |
| DEGRADED | 穩定 | 降級運行(部分功能受限) |
| FAULTED | 穩定 | 故障狀態 |
| DISPOSED | 穩定 | 已釋放所有資源 |
| STARTING, STOPPING, … | 過渡 | 正在轉換中,不會長時間停留 |
當交易所的 WebSocket 斷線時,對應的適配器會從 RUNNING 轉為 DEGRADED(降級)。系統繼續用緩存中的數據運行,同時嘗試重新連接。如果重連成功,回到 RUNNING。如果重連失敗,可能轉為 FAULTED。
四、Actor vs Component 特性
在 Rust 的底層實現中,系統區分了兩種「角色」:
Actor(演員)
功能:接收和處理消息
方法:handle(message)
特點:輕量、專注於消息處理
例子:節流器
(Throttler)
Component(組件)
功能:管理生命週期
方法:start(), stop(), reset()
特點:有狀態機、有生命週期
例子:DataEngine,
ExecutionEngine
有些東西同時具備兩種特性:
flowchart LR
subgraph actors["🎭 Actor 特性 (消息處理)"]
direction TB
throttler["Throttler (節流器)"]
end
subgraph hybrid["🧩 雙重特性"]
direction TB
strategy["Strategy (你的策略)"]
end
subgraph components["🔧 Component 特性 (生命週期)"]
direction TB
de["DataEngine (數據引擎)"]
ee["ExecutionEngine (執行引擎)"]
end
actors --- hybrid --- components
分離的好處是靈活組合。你的策略需要接收消息(Actor)也需要被啟動/停止(Component),所以它兩者都是。但節流器只是一個消息轉發器,它不需要複雜的生命週期管理,所以只做 Actor 就夠了。
五、線程模型
這是一個非常重要的技術決策,直接影響系統的性能和行為:
NautilusTrader 的核心處理邏輯在單一線程上運行。這包括消息總線上的所有消息調度、策略邏輯、風控檢查、訂單管理。
flowchart LR
subgraph single["🧵 單線程核心 (確定性執行)"]
direction TB
logic["消息調度 / 策略邏輯<br/>風控檢查 / 持倉更新"]
end
subgraph multi["🔀 多線程 IO (高併發服務)"]
direction TB
io["WebSocket / API / DB<br/>數據持久化"]
end
multi ==>|"事件推送"| single
single ==>|"指令下發"| multi
style single fill:#0f1419,stroke:#4fc3f7,stroke-width:2px
style multi fill:#0f1419,stroke:#ab47bc,stroke-width:1px
單線程核心的好處
• 事件順序確定性保證
• 不需要加鎖(無鎖競爭)
• 回測和實盤行為一致
• 更容易調試
多線程 IO 的好處
• 網絡操作不阻塞核心邏輯
• WebSocket 連接在獨立線程
• 數據庫操作不影響策略速度
• 充分利用多核 CPU
這個設計靈感來自 LMAX 交易所的 Disruptor 架構。LMAX 是一家真實的外匯交易所,它用單線程處理每秒600 萬筆交易,證明了單線程架構在交易場景中的卓越性能。
六、流程限制:一個進程一個節點
不能在同一個進程中同時運行多個 TradingNode 或
BacktestNode。原因是系統內部有全局共享的單例資源(日誌系統、全局運行時等)。
正確做法:
- 在一個 TradingNode 中增加多個策略 ✅
- 用多個進程分別運行不同的節點 ✅
- 依次運行多個回測(先結束再開始下一個)✅
七、互動練習
以下場景中,NautilusTrader 會怎麼做?請選擇並解釋原因:
- 收到一個價格為
-5.00的報價 → 系統會 ____
A) 忽略這筆數據繼續運行
B) 立即崩潰報錯 - WebSocket 連接斷開 3 秒 → 適配器狀態變為 ____
A) FAULTED
B) DEGRADED - 策略代碼要從 Binance 切到 OKX → 需要修改 ____
A) 只改配置文件
B) 重寫策略代碼
以下 NautilusTrader 的特性分別體現了哪種設計模式?
- 訂單對象叫
Order而不是DataPacket→ ________ - 策略的
on_bar()回調在有新 K 線時自動觸發 → ________ - 系統崩潰後從 Redis 恢復狀態 → ________
- 同一策略可以切換不同交易所而不改代碼 → ________
八、本模組重點回顧
- 五大設計模式:DDD、事件驅動、六角形架構、Crash-Only、Fail-Fast
- 品質優先級:可靠性 > 性能 > 模組化 > 可測試性 > 可維護性 > 可部署性
- 組件狀態機:有 7 個穩定狀態和 5 個過渡狀態,所有組件遵循相同規則
- 單線程核心:保證事件順序確定性;網絡 IO 在後台線程
- Fail-Fast:壞數據立即被拒絕,不會污染系統
- 六角形架構:這是實現「回測 = 實盤」的核心技術手段