Skip to content

Executing Scripts

Candlestick

The Candlestick struct represents one OHLCV bar:

rust
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:

rust
candle.hl2();    // (high + low) / 2
candle.hlc3();   // (high + low + close) / 3
candle.ohlc4();  // (open + high + low + close) / 4
candle.hlcc4();  // (high + low + close + close) / 4

TimeFrame

TimeFrame specifies the chart period. It can be created using convenience methods or parsed from a string:

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

rust
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:

rust
// 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

EventWhen 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).
BarEndAfter each bar's execution completes (strategy processing, script body, etc.). Always paired with a preceding BarStart.
HistoryEndOnce, 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

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::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:

rust
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.

rust
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

Released under the MIT License.