Skip to main content

navi_vm/
instance_builder.rs

1//! Builder for constructing an [`Instance`] from Navi source.
2
3use std::{collections::HashMap, sync::Arc};
4
5use gc_arena::Arena;
6use navi_compiler::{
7    CompileOptions,
8    loader::{CombinedLoader, LibraryLoader, Project},
9};
10use navi_program::Program;
11pub use navi_types::{InputSessions, StrategyConfigOverride};
12use navi_types::{Market, Tz};
13use serde::Serialize;
14
15use crate::{
16    BarState, Error, ExecutionLimits, Instance, LastInfo, OutputMode, Series, StrategyConfig,
17    StrategyState, SymbolInfo, TimeFrame, TimeUnit,
18    context::ExecuteContext,
19    data_provider::{DataProvider, InternalProvider, PartialSymbolInfo},
20    executor::Interrupt,
21    script_info::{Input, InputExt, PartialScriptInfo, ScriptInfo, ScriptType},
22    state::{State, VariableValue},
23    visuals::{Chart, Color},
24};
25
26/// Runtime fields shared by all builder variants — everything
27/// `finalize_instance` needs, with no compilation-specific state.
28struct InstanceBuilderCore {
29    locale: String,
30    input_values: HashMap<usize, serde_value::Value>,
31    timeframe: TimeFrame,
32    input_sessions: InputSessions,
33    symbol: String,
34    background_color: Option<Color>,
35    last_info: Option<LastInfo>,
36    execution_limits: ExecutionLimits,
37    data_provider: Option<Box<dyn InternalProvider>>,
38    output_mode: OutputMode,
39    mtf_wait: std::time::Duration,
40    strategy_config_override: Option<StrategyConfigOverride>,
41    #[cfg(feature = "jit")]
42    enable_jit: bool,
43}
44
45impl InstanceBuilderCore {
46    fn new<P: DataProvider + 'static>(
47        provider: P,
48        timeframe: TimeFrame,
49        symbol: impl Into<String>,
50    ) -> Self {
51        Self {
52            locale: "en".to_string(),
53            input_values: HashMap::new(),
54            timeframe,
55            input_sessions: InputSessions::ALL,
56            symbol: symbol.into(),
57            background_color: None,
58            last_info: None,
59            execution_limits: ExecutionLimits::default(),
60            data_provider: Some(Box::new(provider) as Box<dyn InternalProvider>),
61            output_mode: OutputMode::default(),
62            mtf_wait: std::time::Duration::ZERO,
63            strategy_config_override: None,
64            #[cfg(feature = "jit")]
65            enable_jit: false,
66        }
67    }
68}
69
70// ---------------------------------------------------------------------------
71// InstanceBuilder — compiles source then builds an Instance
72// ---------------------------------------------------------------------------
73
74/// Builder that compiles Navi source and produces an executable [`Instance`].
75///
76/// Obtain a builder via [`Instance::builder`].
77///
78/// The generic parameter `L` tracks the chain of library loaders registered
79/// via [`with_library_loader`](InstanceBuilder::with_library_loader). Start
80/// with `InstanceBuilder<()>` (the default returned by [`Instance::builder`])
81/// and each call to `with_library_loader` prepends a new loader while
82/// preserving the fallback chain in the type.
83pub struct InstanceBuilder<L = ()> {
84    core: InstanceBuilderCore,
85    source: String,
86    dialect: navi_compiler::loader::Dialect,
87    library_loader: L,
88}
89
90impl<L> InstanceBuilder<L> {
91    /// Sets the locale used for i18n diagnostic messages.
92    pub fn with_locale(mut self, locale: impl Into<String>) -> Self {
93        self.core.locale = locale.into();
94        self
95    }
96
97    /// Sets the source dialect used to parse the script (Pine or Navi).
98    pub fn with_dialect(mut self, dialect: navi_compiler::loader::Dialect) -> Self {
99        self.dialect = dialect;
100        self
101    }
102
103    /// Set the value of an input by its index.
104    pub fn with_input_value(mut self, id: usize, value: impl Serialize) -> Self {
105        let value = serde_value::to_value(value).unwrap_or(serde_value::Value::Unit);
106        self.core.input_values.insert(id, value);
107        self
108    }
109
110    /// Configures which trading sessions are accepted as input.
111    pub fn with_input_sessions(mut self, sessions: InputSessions) -> Self {
112        self.core.input_sessions = sessions;
113        self
114    }
115
116    /// Overrides the chart background color used by visual outputs.
117    pub fn with_background_color(mut self, color: impl Into<Option<Color>>) -> Self {
118        self.core.background_color = color.into();
119        self
120    }
121
122    /// Provides the last-bar metadata used by some runtime features.
123    pub fn with_last_info(mut self, last_info: impl Into<Option<LastInfo>>) -> Self {
124        self.core.last_info = last_info.into();
125        self
126    }
127
128    /// Configures runtime execution limits for the VM.
129    pub fn with_execution_limits(mut self, limits: ExecutionLimits) -> Self {
130        self.core.execution_limits = limits;
131        self
132    }
133
134    /// Sets the output mode for series graph data.
135    pub fn with_output_mode(mut self, mode: OutputMode) -> Self {
136        self.core.output_mode = mode;
137        self
138    }
139
140    /// Sets the fixed delay applied before executing each realtime bar.
141    pub fn with_mtf_wait(mut self, duration: std::time::Duration) -> Self {
142        self.core.mtf_wait = duration;
143        self
144    }
145
146    /// Overrides strategy execution parameters declared in the script source.
147    ///
148    /// Fields set to `Some(…)` take precedence over the `strategy()` call
149    /// arguments. Fields left as `None` fall back to the script defaults.
150    /// Has no effect on non-strategy scripts.
151    pub fn with_strategy_config_override(mut self, ov: StrategyConfigOverride) -> Self {
152        self.core.strategy_config_override = Some(ov);
153        self
154    }
155
156    /// Enables JIT compilation of bytecode to native machine code.
157    #[cfg(feature = "jit")]
158    pub fn with_jit(mut self, enable: bool) -> Self {
159        self.core.enable_jit = enable;
160        self
161    }
162}
163
164impl<L: LibraryLoader> InstanceBuilder<L> {
165    /// Prepends a library loader to the resolution chain.
166    ///
167    /// The new loader is tried first; on failure it falls back to whichever
168    /// loader was registered previously. The built-in prelude/stdlib loader is
169    /// always appended as the final fallback regardless of how many custom
170    /// loaders are registered.
171    ///
172    /// Call this method multiple times to build a priority stack:
173    ///
174    /// ```rust,ignore
175    /// // Resolution order: loader_b → loader_a → builtins
176    /// let instance = Instance::builder(provider, source, timeframe, symbol)
177    ///     .with_library_loader(loader_a)
178    ///     .with_library_loader(loader_b)
179    ///     .build()
180    ///     .await?;
181    /// ```
182    pub fn with_library_loader<Q: LibraryLoader>(
183        self,
184        loader: Q,
185    ) -> InstanceBuilder<CombinedLoader<Q, L>> {
186        InstanceBuilder {
187            core: self.core,
188            source: self.source,
189            dialect: self.dialect,
190            library_loader: loader.combine(self.library_loader),
191        }
192    }
193
194    /// Compiles the source and builds a runnable [`Instance`].
195    pub async fn build(self) -> Result<Instance, Error> {
196        let program = compile_source(
197            &self.source,
198            &self.core.locale,
199            self.dialect,
200            self.library_loader,
201        )?;
202        finalize_instance(self.core, program).await
203    }
204}
205
206impl Instance {
207    /// Creates a new [`InstanceBuilder`] for the given source code.
208    ///
209    /// `provider` supplies the main chart K-lines and any `request.security()`
210    /// data. For simple single-symbol backtests pass a `Vec<Candlestick>`
211    /// directly; for live or multi-symbol data implement [`DataProvider`].
212    ///
213    /// The source is compiled when [`InstanceBuilder::build`] is called.
214    pub fn builder<P>(
215        provider: P,
216        source: impl Into<String>,
217        timeframe: TimeFrame,
218        symbol: impl Into<String>,
219    ) -> InstanceBuilder
220    where
221        P: DataProvider + 'static,
222    {
223        InstanceBuilder {
224            core: InstanceBuilderCore::new(provider, timeframe, symbol),
225            source: source.into(),
226            dialect: navi_compiler::loader::Dialect::Navi,
227            library_loader: (),
228        }
229    }
230}
231
232// ---------------------------------------------------------------------------
233// InstanceBuilderFromProgram — uses a pre-compiled Program
234// ---------------------------------------------------------------------------
235
236/// Builder that produces an [`Instance`] from an already-compiled [`Program`],
237/// skipping source compilation entirely.
238///
239/// Obtain a builder via [`Instance::builder_from_program`].
240pub struct InstanceBuilderFromProgram {
241    program: Program,
242    core: InstanceBuilderCore,
243}
244
245impl InstanceBuilderFromProgram {
246    /// Sets the locale used for i18n diagnostic messages.
247    pub fn with_locale(mut self, locale: impl Into<String>) -> Self {
248        self.core.locale = locale.into();
249        self
250    }
251
252    /// Set the value of an input by its index.
253    pub fn with_input_value(mut self, id: usize, value: impl Serialize) -> Self {
254        let value = serde_value::to_value(value).unwrap_or(serde_value::Value::Unit);
255        self.core.input_values.insert(id, value);
256        self
257    }
258
259    /// Configures which trading sessions are accepted as input.
260    pub fn with_input_sessions(mut self, sessions: InputSessions) -> Self {
261        self.core.input_sessions = sessions;
262        self
263    }
264
265    /// Overrides the chart background color used by visual outputs.
266    pub fn with_background_color(mut self, color: impl Into<Option<Color>>) -> Self {
267        self.core.background_color = color.into();
268        self
269    }
270
271    /// Provides the last-bar metadata used by some runtime features.
272    pub fn with_last_info(mut self, last_info: impl Into<Option<LastInfo>>) -> Self {
273        self.core.last_info = last_info.into();
274        self
275    }
276
277    /// Configures runtime execution limits for the VM.
278    pub fn with_execution_limits(mut self, limits: ExecutionLimits) -> Self {
279        self.core.execution_limits = limits;
280        self
281    }
282
283    /// Sets the output mode for series graph data.
284    pub fn with_output_mode(mut self, mode: OutputMode) -> Self {
285        self.core.output_mode = mode;
286        self
287    }
288
289    /// Sets the fixed delay applied before executing each realtime bar.
290    pub fn with_mtf_wait(mut self, duration: std::time::Duration) -> Self {
291        self.core.mtf_wait = duration;
292        self
293    }
294
295    /// Overrides strategy execution parameters declared in the script source.
296    ///
297    /// Fields set to `Some(…)` take precedence over the `strategy()` call
298    /// arguments. Fields left as `None` fall back to the script defaults.
299    /// Has no effect on non-strategy scripts.
300    pub fn with_strategy_config_override(mut self, ov: StrategyConfigOverride) -> Self {
301        self.core.strategy_config_override = Some(ov);
302        self
303    }
304
305    /// Enables JIT compilation of bytecode to native machine code.
306    #[cfg(feature = "jit")]
307    pub fn with_jit(mut self, enable: bool) -> Self {
308        self.core.enable_jit = enable;
309        self
310    }
311
312    /// Builds a runnable [`Instance`] using the pre-compiled program.
313    pub async fn build(self) -> Result<Instance, Error> {
314        finalize_instance(self.core, self.program).await
315    }
316}
317
318impl Instance {
319    /// Creates a new [`InstanceBuilderFromProgram`] from an already-compiled
320    /// [`Program`], skipping source compilation.
321    ///
322    /// Use this when you compile a program once and want to reuse it across
323    /// multiple instances (e.g. different providers or input configurations).
324    ///
325    /// `provider` supplies the main chart K-lines and any `request.security()`
326    /// data.
327    pub fn builder_from_program<P>(
328        program: Program,
329        provider: P,
330        timeframe: TimeFrame,
331        symbol: impl Into<String>,
332    ) -> InstanceBuilderFromProgram
333    where
334        P: DataProvider + 'static,
335    {
336        InstanceBuilderFromProgram {
337            program,
338            core: InstanceBuilderCore::new(provider, timeframe, symbol),
339        }
340    }
341}
342
343// ---------------------------------------------------------------------------
344// Internal helpers
345// ---------------------------------------------------------------------------
346
347fn compile_source<L: LibraryLoader>(
348    source: &str,
349    locale: &str,
350    dialect: navi_compiler::loader::Dialect,
351    extra_loader: L,
352) -> Result<Program, Error> {
353    let opts = CompileOptions::new(source)
354        .with_locale(locale)
355        .with_dialect(dialect)
356        .with_library_loader(navi_builtins::builtins_loader())
357        .with_library_loader(extra_loader)
358        .with_load_options(navi_builtins::load_options())
359        .with_native_func_registry(&crate::native_funcs::NativeFuncs);
360    Ok(opts.compile()?)
361}
362
363async fn finalize_instance(core: InstanceBuilderCore, program: Program) -> Result<Instance, Error> {
364    let symbol_info = resolve_symbol_info(
365        &core.symbol,
366        core.data_provider.as_deref(),
367        program.syminfo_fields(),
368    )
369    .await?;
370    let script_info = Arc::new(get_script_info(&program, &symbol_info, &core.locale).await?);
371
372    if matches!(script_info.script_type, ScriptType::Library(_)) {
373        return Err(Error::LibraryScriptNotExecutable);
374    }
375
376    let mut arena = Arena::new(|mc| State::new(mc, &program, Some(script_info.as_ref())));
377
378    let tz = symbol_to_tz(&core.symbol);
379    for (id, input_value) in core.input_values {
380        let input = script_info
381            .inputs
382            .get(id)
383            .ok_or(Error::InputValueNotFound { id })?;
384        let input_value = if let Input::Time(_) = input {
385            if let serde_value::Value::String(ref s) = input_value {
386                let ts_ms = parse_datetime_to_ms(s, tz).ok_or_else(|| Error::SetInputValue {
387                    id,
388                    error: format!(
389                        "invalid datetime string (expected \"YYYY-MM-DD HH:mm:ss\"): {s}"
390                    ),
391                })?;
392                serde_value::to_value(ts_ms).unwrap_or(input_value)
393            } else {
394                input_value
395            }
396        } else {
397            input_value
398        };
399        arena.mutate_root(|mc, state: &mut State| {
400            let value = input
401                .serialize_value(mc, &program, &input_value)
402                .map_err(|err| Error::SetInputValue {
403                    id,
404                    error: err.to_string(),
405                })?;
406            state.inputs[id].value = value;
407            Ok::<_, Error>(())
408        })?;
409    }
410
411    let strategy_state = if let ScriptType::Strategy(strategy) = &script_info.script_type {
412        let mut config = StrategyConfig::new(strategy);
413        if let Some(ref ov) = core.strategy_config_override {
414            config = config.apply_override(ov);
415        }
416        let mintick = symbol_info.min_tick();
417        let min_contract = symbol_info.min_contract().unwrap_or(0.0);
418        Some(Box::new(StrategyState::new(config, mintick, min_contract)))
419    } else {
420        None
421    };
422
423    #[cfg(feature = "jit")]
424    let jit_code = if core.enable_jit {
425        Some(
426            crate::jit::compile(program.ops())
427                .map_err(|message| Error::JitCompilation { message })?,
428        )
429    } else {
430        None
431    };
432    #[cfg(feature = "jit")]
433    let jit_state = if jit_code.is_some() {
434        Some(crate::jit::JitExecState::new())
435    } else {
436        None
437    };
438
439    let mut chart = Chart::default();
440    chart.set_background_color(core.background_color);
441
442    Ok(Instance {
443        program,
444        locale: core.locale,
445        arena,
446        timeframe: core.timeframe,
447        main_timeframe: core.timeframe,
448        symbol_info,
449        main_symbol: core.symbol,
450        candlesticks: Series::new(),
451        last_info: core.last_info,
452        bar_index: 0,
453        input_index: 0,
454        script_max_bars_back: script_info.script_type.max_bars_back(),
455        script_info,
456        chart,
457        events: vec![],
458        input_sessions: core.input_sessions,
459        strategy_state,
460        strategy_config_override: core.strategy_config_override,
461        aux_series_buffers: HashMap::new(),
462        execution_limits: core.execution_limits,
463        data_provider: core.data_provider,
464        candlestick_buffers: HashMap::new(),
465        security_sub_states: HashMap::new(),
466        security_lower_tf_sub_states: HashMap::new(),
467        last_bar_confirmed: true,
468        pending_security_capture: None,
469        output_mode: core.output_mode,
470        candlestick_offset: 0,
471        candlestick_max_bars_back: None,
472        candlestick_observed_max_lookback: 1,
473        global_observed_max_lookback: 0,
474        last_bar_time: 0,
475        mtf_wait: core.mtf_wait,
476        exec_state_pool: Vec::new(),
477        #[cfg(feature = "jit")]
478        jit_code,
479        #[cfg(feature = "jit")]
480        jit_state,
481    })
482}
483
484async fn resolve_symbol_info(
485    symbol: &str,
486    provider: Option<&dyn InternalProvider>,
487    fields: navi_types::SyminfoFields,
488) -> Result<SymbolInfo, Error> {
489    let partial = match provider {
490        Some(p) => p
491            .symbol_info(symbol.to_string(), fields)
492            .await
493            .map_err(|e| Error::DataProvider {
494                message: e.to_string(),
495            })?,
496        None => PartialSymbolInfo::default(),
497    };
498    partial.into_symbol_info(symbol)
499}
500
501async fn get_script_info(
502    program: &Program,
503    symbol_info: &SymbolInfo,
504    locale: &str,
505) -> Result<ScriptInfo, Error> {
506    let mut script_info = PartialScriptInfo::default();
507
508    let mut candlesticks = Series::new();
509    candlesticks.append_new();
510
511    let mut arena = Arena::new(move |mc| {
512        let mut state = State::new(mc, program, None);
513        for var_value in state.variables.iter_mut() {
514            if let VariableValue::Series(series) = var_value {
515                series.values.append_new();
516            }
517        }
518        state
519    });
520
521    let mut candlestick_observed_max_lookback = 1usize;
522    let mut global_observed_max_lookback = 0usize;
523    let mut exec_state_pool = Vec::new();
524    let main_timeframe = TimeFrame::new(1, TimeUnit::Day);
525    let main_symbol = symbol_info.symbol().as_str().to_owned();
526    let res = crate::executor::execute(
527        program.ops(),
528        &mut ExecuteContext {
529            program,
530            locale,
531            arena: &mut arena,
532            candlesticks: &candlesticks,
533            last_info: None,
534            bar_state: BarState::History,
535            bar_index: 0,
536            candlestick_offset: 0,
537            input_index: 0,
538            partial_script_info: Some(&mut script_info),
539            script_info: None,
540            chart: &mut Chart::default(),
541            events: &mut vec![],
542            current_span: None,
543            module_stack: vec![],
544            timeframe: &main_timeframe,
545            main_timeframe: &main_timeframe,
546            symbol_info,
547            main_symbol: &main_symbol,
548            input_sessions: InputSessions::ALL,
549            strategy_state: None,
550            aux_series_buffers: &mut HashMap::new(),
551            loop_iterations_remaining: ExecutionLimits::default().max_loop_iterations_per_bar,
552            security_provider: None,
553            candlestick_buffers: &mut HashMap::new(),
554            security_sub_states: &mut HashMap::new(),
555            security_lower_tf_sub_states: &mut HashMap::new(),
556            security_depth: 0,
557            execution_limits: ExecutionLimits::default(),
558            security_capture: None,
559            output_mode: OutputMode::default(),
560            candlestick_max_bars_back_override: None,
561            candlestick_observed_max_lookback: &mut candlestick_observed_max_lookback,
562            global_observed_max_lookback: &mut global_observed_max_lookback,
563            exec_state_pool: &mut exec_state_pool,
564        },
565    )
566    .await;
567
568    match res {
569        Ok(_) => {}
570        Err(Interrupt::RuntimeError { error, backtrace }) => {
571            return Err(Error::Exception(crate::error::build_runtime_error(
572                error.value,
573                error.span,
574                backtrace,
575                program.source_files(),
576            )));
577        }
578    }
579
580    script_info.timezone = symbol_info.timezone();
581    script_info.try_into()
582}
583
584fn script_info_from_program(program: &Program) -> Result<ScriptInfo, Error> {
585    let symbol_info = PartialSymbolInfo::default().into_symbol_info("NASDAQ:AAPL")?;
586    pollster::block_on(get_script_info(program, &symbol_info, "en"))
587}
588
589/// Compile a script from source and return its [`ScriptInfo`].
590pub fn script_info(source: &str) -> Result<ScriptInfo, Error> {
591    let program = compile_source(source, "en", navi_compiler::loader::Dialect::Navi, ())?;
592    script_info_from_program(&program)
593}
594
595/// Return [`ScriptInfo`] from an already-loaded [`Project`].
596pub fn script_info_from_project(project: &Project) -> Result<ScriptInfo, Error> {
597    let opts = CompileOptions::new_from_project(project)
598        .with_locale("en")
599        .with_library_loader(navi_builtins::builtins_loader())
600        .with_load_options(navi_builtins::load_options())
601        .with_native_func_registry(&crate::native_funcs::NativeFuncs);
602    let program = opts.compile()?;
603    script_info_from_program(&program)
604}
605
606fn symbol_to_tz(symbol: &str) -> Tz {
607    symbol
608        .split(':')
609        .next()
610        .and_then(|p| p.parse::<Market>().ok())
611        .map(|m| m.timezone())
612        .unwrap_or(Tz::NewYork)
613}
614
615fn parse_datetime_to_ms(s: &str, tz: Tz) -> Option<i64> {
616    use time::{PrimitiveDateTime, macros::format_description};
617    use time_tz::PrimitiveDateTimeExt;
618    let fmt = format_description!("[year]-[month]-[day] [hour]:[minute]:[second]");
619    let naive = PrimitiveDateTime::parse(s, &fmt).ok()?;
620    let offset_dt = naive.assume_timezone(&tz).take_first()?;
621    Some(offset_dt.unix_timestamp() * 1000)
622}