Executing Scripts
Candlestick
The Candlestick struct represents one OHLCV bar:
use navi_vm::{Candlestick, market::TradeSession};
let candle = Candlestick::new(
1700000000000, // time: epoch milliseconds
150.0, // open
155.0, // high
149.0, // low
153.0, // close
1_000_000.0, // volume
0.0, // turnover
TradeSession::Regular, // trade session
);Candlestick also provides derived price calculations:
candle.hl2(); // (high + low) / 2
candle.hlc3(); // (high + low + close) / 3
candle.ohlc4(); // (open + high + low + close) / 4
candle.hlcc4(); // (high + low + close + close) / 4TimeFrame
TimeFrame specifies the chart period. It can be created using convenience methods or parsed from a string:
use navi_vm::TimeFrame;
// Convenience constructors
let tf = TimeFrame::days(1);
let tf = TimeFrame::minutes(5);
let tf = TimeFrame::weeks(1);
let tf = TimeFrame::months(1);
let tf = TimeFrame::seconds(30);
// Parse from string shorthand
let tf: TimeFrame = "D".parse().unwrap(); // 1 day
let tf: TimeFrame = "5".parse().unwrap(); // 5 minutes
let tf: TimeFrame = "60".parse().unwrap(); // 60 minutes (1 hour)
let tf: TimeFrame = "W".parse().unwrap(); // 1 week
let tf: TimeFrame = "3M".parse().unwrap(); // 3 months
// Inspect
let seconds = tf.in_seconds(); // Option<u64>
let display = tf.to_string(); // "D", "5", "W", etc.SymbolInfo
SymbolInfo carries metadata about the trading instrument. You no longer construct it manually — just pass the symbol string to Instance::builder and the builder resolves the info automatically (from DataProvider::symbol_info if a provider is configured, otherwise from built-in defaults).
Supported market prefixes: NYSE, NASDAQ, SHSE, SZSE, HKEX, SGX.
To supply custom symbol metadata (description, currency, min tick, etc.), implement DataProvider::symbol_info and return a PartialSymbolInfo. See Instance Builder for details.
Providing Data
Historical Backtest
Pass a Vec<Candlestick> directly to Instance::builder(). The VM will pull from it when you call run_to_end():
use navi_vm::TimeFrame;
let instance = Instance::builder(historical_data, source, TimeFrame::days(1), "NASDAQ:AAPL")
.build().await?
.run_to_end("NASDAQ:AAPL", TimeFrame::days(1)).await?;For lazy or streaming data, collect into a Vec<Candlestick> first, or implement a custom DataProvider:
// Collect from an iterator
let bars: Vec<Candlestick> = load_bars_from_db().collect();
let mut instance = Instance::builder(bars, source, timeframe, "NASDAQ:AAPL")
.build().await?;Live Streaming
For live data, implement DataProvider and yield CandlestickItem::Bar(candle) for each bar. All bars emitted before CandlestickItem::HistoryEnd must be fully closed historical bars — HistoryEnd is a hard boundary meaning "all history is confirmed". Emit HistoryEnd once all confirmed history has been sent. Items after HistoryEnd are treated as realtime bars — they may carry the same timestamp as the last historical bar (if the current period was still forming at query time) or a later timestamp (new bar). The VM automatically handles rollback between realtime updates and confirms the bar when the stream ends or a new timestamp arrives.
Event Stream
Instance::run() returns a RunHandle that yields events as the script executes bar by bar.
RunHandle
RunHandle owns the Instance while the run is active. Iterate events with handle.next_event().await, call instance methods directly through the handle via DerefMut, and recover the instance afterwards with handle.into_instance().
When realtime timestamps roll over (Bar(tA) followed by Bar(tB) where tB > tA), the stream preserves the same event order as before — confirm tA, then start tB — but those two steps are exposed separately so callers can checkpoint after the confirm boundary.
Event Types
| Event | When emitted |
|---|---|
ScriptInfo(Arc<ScriptInfo>) | Stream start: script metadata (title, overlay flag, inputs, declared series, alerts). Emitted once before any bar (Stream mode only). |
Warning(Warnings) | Compilation warnings, emitted once after ScriptInfo and before any bar (when warnings are present). |
BarStart(BarStartEvent) | Before each bar's execution begins. Contains bar_index, candlestick (full OHLCV), and bar_state (History, RealtimeNew, RealtimeUpdate, or RealtimeConfirmed). |
BarEnd | After each bar's execution completes (strategy processing, script body, etc.). Always paired with a preceding BarStart. |
HistoryEnd | Once, when the DataProvider yields CandlestickItem::HistoryEnd. Signals the transition from historical replay to live data. Not preceded by BarStart. |
Log(LogEvent) | During bar execution, when the script calls log.info(), log.warning(), or log.error(). Emitted between BarStart and BarEnd. |
Alert(AlertEvent) | During bar execution, when the script calls alert() or an alert_condition() triggers. Emitted between BarStart and BarEnd. |
Draw(DrawEvent) | A drawing instruction (Stream mode only). Wraps SeriesEvent, GraphEvent, or FilledOrder. See Reading Outputs. |
Strategy(StrategyEvent) | A strategy event (Stream mode only). Orders, trades, equity snapshots. See Reading Outputs. |
Event Ordering
For a typical historical-then-realtime session, the stream yields:
BarStart(0) → Log/Alert/Draw... → BarEnd ← history bar 0
BarStart(1) → Log/Alert/Draw... → BarEnd ← history bar 1
...
HistoryEnd
BarStart(N) → ... → BarEnd ← first tick of bar N (RealtimeNew)
BarStart(N) → ... → BarEnd ← same-timestamp tick update (RealtimeUpdate)
BarStart(N) → ... → BarEnd ← bar N confirmed (RealtimeConfirmed)
BarStart(N+1) → ... → BarEnd ← first tick of bar N+1 (RealtimeNew)
BarStart(N+1) → ... → BarEnd ← stream ended, final confirm (RealtimeConfirmed)Usage
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::ScriptInfo(info) => {
println!("script: {:?}", info.script_type);
}
Event::BarStart(bs) => {
println!("bar {} started in phase {:?}", bs.bar_index, bs.bar_state);
}
Event::BarEnd => { /* bar finished */ }
Event::HistoryEnd => {
println!("history replay complete, live bars follow");
}
Event::Log(log) => println!("[{:?}] {}", log.level, log.message),
Event::Alert(alert) => println!("ALERT: {}", alert.message),
Event::Draw(draw) => { /* drawing data, Stream mode only */ }
Event::Strategy(strat) => { /* strategy events, Stream mode only */ }
_ => {}
}
}If you don't need the event stream, use the convenience method run_to_end() which consumes the stream internally and returns the finished instance:
let instance = instance.run_to_end("NASDAQ:AAPL", TimeFrame::days(1)).await?;
// Results available via instance.chart(), instance.strategy_report(), etc.Resetting State
instance.reset() discards all runtime state and returns the instance to its post-build condition — as if no bars had ever been executed — without recompiling the script or rebuilding the object.
let mut instance = instance.run_to_end("NASDAQ:AAPL", TimeFrame::days(1)).await?;
// Reset: wipe all bars, chart, strategy state, etc.
instance.reset();
// Re-run with the same script and provider from bar 0
let instance = instance.run_to_end("NASDAQ:AAPL", TimeFrame::days(1)).await?;When to use reset() vs. a new Instance
reset()— preferred when the same compiled program and data provider are reused repeatedly (e.g. parameter sweeps, re-running after changing input overrides). Avoids the cost of recompilation and arena allocation.- New
Instance— required when the script source, symbol, or timeframe changes, or when you need different input value overrides applied at build time. - Snapshot — preferred when you want to skip the historical replay on subsequent starts without losing accumulated state. See Snapshots.
Caveats
Input value overrides set via .with_input_value() on the builder are applied to the GC arena during build() and are not stored on the instance afterward. After reset(), inputs revert to their script-declared defaults (e.g. input.int(14, "Length") returns to 14).
Next Steps
- Reading Outputs — plots, labels, strategy orders
- Event Compactor — tracking realtime bar lifecycle and filtering events
- Error Handling — handling compile and runtime errors