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 许可证发布。