Skip to content

快照

快照可以在歷史回放完成後檢查點儲存 VM 狀態,下次啟動時即時恢復,避免昂貴的全量回放。

為什麼使用快照

在伺服器端部署中,每個執行的腳本通常需要處理數千根歷史 Bar 才能到達即時資料流。每次重新啟動都重新回放所有歷史 Bar 代價很高。快照透過將完整的 VM 狀態(變數、圖表資料、策略狀態和配置)序列化為二進位資料來解決此問題,可以持久化並在之後恢復。

text
冷啟動(首次執行)
  ├─ 建構 Instance
  ├─ 從 DataProvider 拉取所有歷史 Bar  ← 較慢(數千根 Bar)
  ├─ save_state() → Vec<u8>
  └─ 持久化到儲存

熱重啟(後續執行)
  ├─ 載入持久化資料
  ├─ restore_state(&bytes) → Instance    ← 快速(無需回放)
  └─ run() — 自動從下一根 Bar 繼續

儲存狀態

在最後一根已確認的 Bar 之後呼叫 save_state() 產生可序列化的位元組資料。

簡單:全量回放後儲存

rust
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 的邊界儲存:

rust
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()

rust
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 原始碼的雜湊值一起儲存。啟動時比較雜湊值;如果不同,刪除快照並從頭回放。

rust
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();需要在下次啟動時恢復到上次結束的位置時,用快照。

下一步

基於 MIT 許可證發佈。