快照
快照可以在历史回放完成后检查点保存 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 编译 — 启用预编译加速执行