快照
快照可以在歷史回放完成後檢查點儲存 VM 狀態,下次啟動時即時恢復,避免昂貴的全量回放。
為什麼使用快照
在伺服器端部署中,每個執行的腳本通常需要處理數千根歷史 Bar 才能到達即時資料流。每次重新啟動都重新回放所有歷史 Bar 代價很高。快照透過將完整的 VM 狀態(變數、圖表資料、策略狀態和配置)序列化為二進位資料來解決此問題,可以持久化並在之後恢復。
冷啟動(首次執行)
├─ 建構 Instance
├─ 從 DataProvider 拉取所有歷史 Bar ← 較慢(數千根 Bar)
├─ save_state() → Vec<u8>
└─ 持久化到儲存
熱重啟(後續執行)
├─ 載入持久化資料
├─ restore_state(&bytes) → Instance ← 快速(無需回放)
└─ run() — 自動從下一根 Bar 繼續儲存狀態
在最後一根已確認的 Bar 之後呼叫 save_state() 產生可序列化的位元組資料。
簡單:全量回放後儲存
use navi_vm::{Instance, TimeFrame, snapshot::SnapshotError};
let mut instance = Instance::builder(provider, source, TimeFrame::days(1), "NASDAQ:AAPL")
.build().await?
.run_to_end("NASDAQ:AAPL", TimeFrame::days(1)).await?;
// 儲存 — 當前 Bar 必須已確認
let snapshot: Vec<u8> = instance.save_state()?;
// 持久化到資料庫、磁碟、物件儲存等
persist_to_storage(&snapshot);進階:透過 RunHandle 在串流途中 checkpoint
instance.run() 回傳的 RunHandle 實作了 DerefMut<Target = Instance>,可直接透過 handle 呼叫 save_state(),無需中斷執行。現在可以用 current_bar_confirmed() 先判斷當前 Bar 是否已確認,再只在可 checkpoint 的邊界儲存:
use navi_vm::{Event, Instance, TimeFrame};
let mut handle = instance.run("NASDAQ:AAPL", TimeFrame::days(1));
while let Some(result) = handle.next_event().await {
match result? {
Event::BarEnd => {
if handle.current_bar_confirmed() {
let snapshot = handle.save_state()?;
persist_to_storage(&snapshot);
}
}
_ => {}
}
}WARNING
當前 Bar 必須在儲存前確認。如果正處於即時 Bar 中,需要先確認它。若 Bar 未確認,save_state() 將傳回 SnapshotError::UnconfirmedBar。
當新的 realtime 時間戳到來時,Navi 會先發出上一根 realtime Bar 的 auto-confirm 事件,然後才開始新的 realtime Bar;這個 auto-confirm 對應的 BarEnd 就是安全的快照邊界,所以這裡 current_bar_confirmed() 會變成 true。
恢復狀態
使用 Instance::restore_state() 將快照位元組解碼為 RestoreBuilder,配置後呼叫 build():
let bytes: Vec<u8> = load_from_storage();
let instance = Instance::restore_state(&bytes)?
.with_data_provider(provider) // 必需:提供資料來源
.build()?
.run_to_end("NASDAQ:AAPL", TimeFrame::days(1)).await?;
// run() 自動從快照中斷處繼續K 線資料
K 線資料不會包含在快照中。當對恢復的實例呼叫 run() 時,VM 會自動將 last_bar_time + 1 傳遞給資料來源,僅取得新的 Bar。
RestoreBuilder 選項
| 方法 | 說明 |
|---|---|
with_data_provider(provider) | 注入 DataProvider 以支援 request.security() 和 Bar 資料 |
build() | 完成恢復並傳回 Instance |
失效處理
快照內嵌了編譯後的程式和格式版本號。快照僅對建立它的二進位檔案和腳本有效。以下情況必須捨棄快照並進行全量冷啟動回放:
- Navi 升級 — 快照格式可能已變更
- Navi 腳本原始碼變更 — 不同的編譯程式
- 輸入參數值變更 — 輸入會影響執行狀態
一個簡單的失效策略:將快照與 Navi 原始碼的雜湊值一起儲存。啟動時比較雜湊值;如果不同,刪除快照並從頭回放。
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
fn source_hash(source: &str) -> u64 {
let mut hasher = DefaultHasher::new();
source.hash(&mut hasher);
hasher.finish()
}
// 儲存
let hash = source_hash(source);
persist_snapshot(&snapshot, hash);
// 恢復
let (bytes, saved_hash) = load_snapshot();
if source_hash(source) != saved_hash {
// 腳本已變更 — 捨棄快照並冷啟動回放
} else {
let instance = Instance::restore_state(&bytes)?.build()?;
// ...
}錯誤處理
| 錯誤 | 原因 |
|---|---|
SnapshotError::UnconfirmedBar | 在當前 Bar 未確認時呼叫了 save_state() |
SnapshotError::Encode(msg) | Bincode 序列化失敗 |
SnapshotError::Decode(msg) | Bincode 反序列化失敗(資料損壞或截斷) |
SnapshotError::VersionMismatch { expected, got } | 快照由不同版本的 Navi 建立 |
快照 vs. reset()
快照和 instance.reset() 都可以讓你重新執行腳本,但用途不同:
| 快照 | reset() | |
|---|---|---|
| 保留已累積的狀態 | ✅ | ❌ |
| 後續啟動時跳過歷史回放 | ✅ | ❌ |
| 適合伺服器端熱重啟 | ✅ | ❌ |
| 適合參數掃描 | ❌ | ✅ |
| 從第 0 根 K 線重新執行 | ❌ | ✅ |
需要從頭重放同一批 K 線時(例如對比不同輸入參數),用 reset();需要在下次啟動時恢復到上次結束的位置時,用快照。
下一步
- JIT 編譯 — 啟用預編譯加速執行