openpine_vm/
instance_builder.rs

1use std::collections::HashMap;
2
3use gc_arena::Arena;
4use openpine_compiler::{
5    CompileOptions,
6    loader::{CombinedLoader, LibraryLoader, Project},
7    program::Program,
8};
9use openpine_error::ErrorWithSourceFile;
10use serde::{Deserialize, Serialize};
11
12use crate::{
13    Error, Exception, ExecutionLimits, Instance, LastInfo, OutputMode, Series, StrategyConfig,
14    StrategyState, SymbolInfo, TimeFrame, TimeUnit, TradeSession,
15    bar_state::BarState,
16    context::ExecuteContext,
17    currency::{CurrencyConverter, default_currency_converter},
18    data_provider::{DataProvider, InternalProvider, PartialSymbolInfo},
19    inst_executor::Interrupt,
20    native_funcs::NativeFuncs,
21    script_info::{PartialScriptInfo, ScriptInfo, ScriptType},
22    state::{State, VariableValue},
23    visuals::{Chart, Color},
24};
25
26bitflags::bitflags! {
27    /// Instance execution options.
28    #[derive(Debug, Copy, Clone, Eq, PartialEq, Serialize, Deserialize)]
29    pub struct InputSessions: u8 {
30        /// Include regular trading session bars.
31        const REGULAR = 0b0001;
32        /// Include extended-hours bars (pre-market and after-hours).
33        const EXTENDED = 0b0010;
34        /// Include overnight session bars.
35        const OVERNIGHT = 0b0100;
36
37        /// Include all supported sessions.
38        const ALL = Self::REGULAR.bits() | Self::EXTENDED.bits() | Self::OVERNIGHT.bits();
39    }
40}
41
42impl InputSessions {
43    #[inline]
44    pub(crate) fn allow(&self, trade_session: TradeSession) -> bool {
45        match trade_session {
46            TradeSession::Regular => self.contains(InputSessions::REGULAR),
47            TradeSession::PreMarket | TradeSession::AfterHours => {
48                self.contains(InputSessions::EXTENDED)
49            }
50            TradeSession::Overnight => self.contains(InputSessions::OVERNIGHT),
51        }
52    }
53}
54
55/// Builder for compiling a script into an executable [`Instance`].
56pub struct InstanceBuilder<'a, L> {
57    compile_opts: CompileOptions<'a, L>,
58    input_values: HashMap<usize, serde_value::Value>,
59    timeframe: TimeFrame,
60    input_sessions: InputSessions,
61    symbol: String,
62    background_color: Option<Color>,
63    last_info: Option<LastInfo>,
64    currency_converter: Box<dyn CurrencyConverter>,
65    execution_limits: ExecutionLimits,
66    data_provider: Option<Box<dyn InternalProvider>>,
67    output_mode: OutputMode,
68}
69
70impl<'a, L> InstanceBuilder<'a, L> {
71    /// Sets the source path used for diagnostics.
72    pub fn with_path(mut self, path: &'a str) -> Self {
73        self.compile_opts = self.compile_opts.with_path(path);
74        self
75    }
76
77    /// Set the value of an input by its index.
78    ///
79    /// The `id` corresponds to the position of the input in the script's
80    /// `input.*()` declarations (0-based). Use
81    /// [`script_info()`] to inspect available inputs and
82    /// their types.
83    ///
84    /// # Examples
85    ///
86    /// ```no_run
87    /// use openpine_vm::{Candlestick, Instance, TimeFrame};
88    ///
89    /// # async fn example() -> Result<(), openpine_vm::Error> {
90    /// let source = r#"
91    ///     //@version=6
92    ///     indicator("With Inputs")
93    ///     int length = input.int(14, "Length")
94    ///     plot(ta.sma(close, length))
95    /// "#;
96    /// let provider = Vec::<Candlestick>::new();
97    /// let instance = Instance::builder(provider, source, TimeFrame::days(1), "NASDAQ:AAPL")
98    ///     .with_input_value(0, 20) // Override "Length" from 14 to 20
99    ///     .build()
100    ///     .await?;
101    /// # Ok(()) }
102    /// ```
103    pub fn with_input_value(mut self, id: usize, value: impl Serialize) -> Self {
104        let value = serde_value::to_value(value).unwrap_or(serde_value::Value::Unit);
105        self.input_values.insert(id, value);
106        self
107    }
108
109    /// Sets the locale passed into compilation/typechecking.
110    pub fn with_locale(mut self, locale: &'a str) -> Self {
111        self.compile_opts = self.compile_opts.with_locale(locale);
112        self
113    }
114
115    /// Configures which trading sessions are accepted as input.
116    pub fn with_input_sessions(mut self, sessions: InputSessions) -> Self {
117        self.input_sessions = sessions;
118        self
119    }
120
121    /// Overrides the chart background color used by visual outputs.
122    pub fn with_background_color(mut self, color: impl Into<Option<Color>>) -> Self {
123        self.background_color = color.into();
124        self
125    }
126
127    /// Provides the last-bar metadata used by some runtime features.
128    pub fn with_last_info(mut self, last_info: impl Into<Option<LastInfo>>) -> Self {
129        self.last_info = last_info.into();
130        self
131    }
132
133    /// Sets a custom currency converter for cross-currency backtesting.
134    ///
135    /// The converter is called by `strategy.convert_to_account()` and
136    /// `strategy.convert_to_symbol()`.  When not set, an identity converter
137    /// is used (returns the input value unchanged).
138    pub fn with_currency_converter(mut self, converter: Box<dyn CurrencyConverter>) -> Self {
139        self.currency_converter = converter;
140        self
141    }
142
143    /// Configures runtime execution limits for the VM.
144    ///
145    /// See [`ExecutionLimits`] for available options. When not called,
146    /// sensible defaults are used.
147    pub fn with_execution_limits(mut self, limits: ExecutionLimits) -> Self {
148        self.execution_limits = limits;
149        self
150    }
151
152    /// Sets the output mode for series graph data.
153    ///
154    /// - [`OutputMode::Chart`] (default): native functions write directly to
155    ///   the chart. No [`DrawEvent`](crate::DrawEvent) events are emitted.
156    /// - [`OutputMode::Stream`]: native functions emit
157    ///   [`DrawEvent`](crate::DrawEvent) events instead of updating the chart.
158    pub fn with_output_mode(mut self, mode: OutputMode) -> Self {
159        self.output_mode = mode;
160        self
161    }
162}
163
164impl<'a, L> InstanceBuilder<'a, L>
165where
166    L: LibraryLoader,
167{
168    /// Chains an additional library loader in front of the current loader.
169    pub fn with_library_loader<Q>(
170        self,
171        library_loader: Q,
172    ) -> InstanceBuilder<'a, CombinedLoader<Q, L>>
173    where
174        Q: LibraryLoader,
175    {
176        InstanceBuilder {
177            compile_opts: self.compile_opts.with_library_loader(library_loader),
178            input_values: self.input_values,
179            timeframe: self.timeframe,
180            input_sessions: self.input_sessions,
181            symbol: self.symbol,
182            background_color: self.background_color,
183            last_info: self.last_info,
184            currency_converter: self.currency_converter,
185            execution_limits: self.execution_limits,
186            data_provider: self.data_provider,
187
188            output_mode: self.output_mode,
189        }
190    }
191
192    /// Compiles the script and builds a runnable [`Instance`].
193    ///
194    /// This compiles the Pine Script source, resolves inputs, and prepares
195    /// the VM for execution. Returns an error if compilation fails or if
196    /// an invalid input value was provided.
197    ///
198    /// # Examples
199    ///
200    /// ```no_run
201    /// use openpine_vm::{Candlestick, Instance, TimeFrame};
202    ///
203    /// # async fn example() -> Result<(), openpine_vm::Error> {
204    /// let source = r#"
205    ///     //@version=6
206    ///     indicator("SMA Example")
207    ///     plot(ta.sma(close, 20))
208    /// "#;
209    /// let provider = Vec::<Candlestick>::new();
210    /// let instance = Instance::builder(provider, source, TimeFrame::days(1), "NASDAQ:AAPL")
211    ///     .build()
212    ///     .await?;
213    ///
214    /// // The instance is now ready to receive candlestick data via `run()`
215    /// println!("Script type: {:?}", instance.script_info().script_type);
216    /// # Ok(())
217    /// # }
218    /// ```
219    pub async fn build(self) -> Result<Instance, Error> {
220        let symbol_info = resolve_symbol_info(&self.symbol, self.data_provider.as_deref()).await?;
221        let program = Self::compile_program(self.compile_opts)?;
222        let script_info = get_script_info(&program, &symbol_info).await?;
223
224        if matches!(script_info.script_type, ScriptType::Library(_)) {
225            return Err(Error::LibraryScriptNotExecutable);
226        }
227
228        let mut arena = Arena::new(|mc| State::new(mc, &program, Some(&script_info)));
229
230        for (id, input_value) in self.input_values {
231            let input = script_info
232                .inputs
233                .get(id)
234                .ok_or(Error::InputValueNotFound(id))?;
235            arena.mutate_root(|mc, state: &mut State| {
236                let value = input
237                    .serialize_value(mc, &program, &input_value)
238                    .map_err(|err| Error::SetInputValue(id, err.to_string()))?;
239                state.inputs[id].value = value;
240                Ok::<_, Error>(())
241            })?;
242        }
243
244        let strategy_state = if let ScriptType::Strategy(strategy) = &script_info.script_type {
245            let config = StrategyConfig::new(Some(&symbol_info), strategy);
246            Some(Box::new(StrategyState::new(config)))
247        } else {
248            None
249        };
250
251        let mut chart = Chart::default();
252        chart.set_background_color(self.background_color);
253
254        Ok(Instance {
255            program,
256            arena,
257            timeframe: self.timeframe,
258            symbol_info,
259            candlesticks: Series::new(),
260            last_info: self.last_info,
261            bar_index: 0,
262            input_index: 0,
263            script_info,
264            chart,
265            events: vec![],
266            input_sessions: self.input_sessions,
267            strategy_state,
268            currency_converter: self.currency_converter,
269            execution_limits: self.execution_limits,
270            data_provider: self.data_provider,
271            candlestick_buffers: HashMap::new(),
272            security_sub_states: HashMap::new(),
273            security_lower_tf_sub_states: HashMap::new(),
274            last_bar_confirmed: true,
275            pending_security_capture: None,
276
277            output_mode: self.output_mode,
278        })
279    }
280
281    fn compile_program(compile_opts: CompileOptions<'a, L>) -> Result<Program, Error> {
282        let compile_opts = compile_opts
283            .with_library_loader(crate::builtins_loader())
284            .with_load_options(crate::builtins_load_options())
285            .with_native_func_registry(&NativeFuncs);
286        Ok(compile_opts.compile()?)
287    }
288}
289
290async fn resolve_symbol_info(
291    symbol: &str,
292    provider: Option<&dyn InternalProvider>,
293) -> Result<SymbolInfo, Error> {
294    let partial = match provider {
295        Some(p) => p
296            .symbol_info(symbol.to_string())
297            .await
298            .map_err(|e| Error::DataProvider(e.to_string()))?,
299        None => PartialSymbolInfo::default(),
300    };
301    Ok(SymbolInfo::from_partial(symbol, partial)?)
302}
303
304async fn get_script_info(program: &Program, symbol_info: &SymbolInfo) -> Result<ScriptInfo, Error> {
305    let mut script_info = PartialScriptInfo::default();
306
307    let mut candlesticks = Series::new();
308    candlesticks.append_new();
309
310    let mut arena = Arena::new(move |mc| {
311        let mut state = State::new(mc, program, None);
312        for var_value in state.variables.iter_mut() {
313            if let VariableValue::Series(series) = var_value {
314                series.values.append_new();
315            }
316        }
317        state
318    });
319
320    let res = crate::inst_executor::execute(
321        program.instruction(),
322        &mut ExecuteContext {
323            program,
324            arena: &mut arena,
325            candlesticks: &candlesticks,
326            last_info: None,
327            bar_state: BarState::History,
328            bar_index: 0,
329            input_index: 0,
330            partial_script_info: Some(&mut script_info),
331            script_info: None,
332            chart: &mut Chart::default(),
333            events: &mut vec![],
334            current_span: None,
335            module_stack: vec![],
336            timeframe: &TimeFrame::new(1, TimeUnit::Day),
337            symbol_info,
338            input_sessions: InputSessions::ALL,
339            strategy_state: None,
340            currency_converter: None,
341            loop_iterations_remaining: ExecutionLimits::default().max_loop_iterations_per_bar,
342            security_provider: None,
343            candlestick_buffers: &mut HashMap::new(),
344            security_sub_states: &mut HashMap::new(),
345            security_lower_tf_sub_states: &mut HashMap::new(),
346            security_depth: 0,
347            execution_limits: ExecutionLimits::default(),
348            security_capture: None,
349            output_mode: OutputMode::default(),
350        },
351    )
352    .await;
353
354    match res {
355        Ok(_) => {}
356        Err(Interrupt::RuntimeError { error, backtrace }) => {
357            return Err(Error::Exception(Exception::new(
358                ErrorWithSourceFile::new(
359                    openpine_error::Error::new(vec![error.span], error.value),
360                    program.source_files().clone(),
361                ),
362                backtrace,
363            )));
364        }
365        Err(_) => unreachable!("Unhandled interrupt in top-level execution"),
366    }
367
368    script_info.try_into()
369}
370
371/// Compile a script from source and return its [`ScriptInfo`] without
372/// building an [`Instance`] or requiring a data provider.
373///
374/// This is a lightweight alternative to
375/// [`Instance::script_info()`] for cases where you only need
376/// metadata (script type, inputs, alert conditions) and don't plan to
377/// execute the script.
378///
379/// Internally uses a dummy symbol (`NASDAQ:AAPL`) and timeframe (1 day);
380/// functions like `request.security()` return `na` during collection so
381/// no real data is fetched.
382pub fn script_info(source: &str) -> Result<ScriptInfo, Error> {
383    let compile_opts = CompileOptions::new(source)
384        .with_library_loader(crate::builtins_loader())
385        .with_load_options(crate::builtins_load_options())
386        .with_native_func_registry(&NativeFuncs);
387    let program = compile_opts.compile()?;
388    let symbol_info = SymbolInfo::from_partial("NASDAQ:AAPL", PartialSymbolInfo::default())?;
389    pollster::block_on(get_script_info(&program, &symbol_info))
390}
391
392/// Return [`ScriptInfo`] from an already-loaded [`Project`] without
393/// building an [`Instance`] or requiring a data provider.
394///
395/// Same as [`script_info()`] but skips parsing by reusing a pre-loaded
396/// project.
397pub fn script_info_from_project(project: &Project) -> Result<ScriptInfo, Error> {
398    let compile_opts = CompileOptions::new_from_project(project)
399        .with_library_loader(crate::builtins_loader())
400        .with_load_options(crate::builtins_load_options())
401        .with_native_func_registry(&NativeFuncs);
402    let program = compile_opts.compile()?;
403    let symbol_info = SymbolInfo::from_partial("NASDAQ:AAPL", PartialSymbolInfo::default())?;
404    pollster::block_on(get_script_info(&program, &symbol_info))
405}
406
407impl Instance {
408    /// Creates a new [`InstanceBuilder`] for the given provider, source,
409    /// timeframe and symbol string.
410    ///
411    /// `provider` supplies the main chart K-lines consumed by
412    /// [`Instance::run()`] and the data for any `request.security()` calls.
413    /// For simple single-symbol backtests pass a `Vec<Candlestick>` directly;
414    /// for multi-symbol or live data implement [`DataProvider`] directly.
415    ///
416    /// # Examples
417    ///
418    /// ```no_run
419    /// use openpine_vm::{Candlestick, Instance, TimeFrame, TradeSession};
420    ///
421    /// # async fn example() -> Result<(), openpine_vm::Error> {
422    /// let source = r#"
423    ///     //@version=6
424    ///     indicator("My Script")
425    ///     plot(close)
426    /// "#;
427    /// let bar = Candlestick::new(
428    ///     1_700_000_000_000,
429    ///     150.0,
430    ///     155.0,
431    ///     149.0,
432    ///     153.0,
433    ///     1_000_000.0,
434    ///     0.0,
435    ///     TradeSession::Regular,
436    /// );
437    /// let provider = vec![bar];
438    /// let mut instance = Instance::builder(provider, source, TimeFrame::days(1), "NASDAQ:AAPL")
439    ///     .with_path("my_script.pine")
440    ///     .build()
441    ///     .await?;
442    ///
443    /// instance
444    ///     .run_to_end("NASDAQ:AAPL", TimeFrame::days(1))
445    ///     .await?;
446    ///
447    /// let chart = instance.chart();
448    /// for (id, graph) in chart.series_graphs() {
449    ///     println!("Graph {:?}: {:?}", id, graph);
450    /// }
451    /// # Ok(())
452    /// # }
453    /// ```
454    #[inline]
455    pub fn builder<'a, P>(
456        provider: P,
457        source: &'a str,
458        timeframe: TimeFrame,
459        symbol: impl Into<String>,
460    ) -> InstanceBuilder<'a, ()>
461    where
462        P: DataProvider + 'static,
463    {
464        InstanceBuilder {
465            compile_opts: CompileOptions::new(source),
466            input_values: HashMap::new(),
467            timeframe,
468            input_sessions: InputSessions::ALL,
469            symbol: symbol.into(),
470            background_color: None,
471            last_info: None,
472            currency_converter: default_currency_converter(),
473            execution_limits: ExecutionLimits::default(),
474            data_provider: Some(Box::new(provider) as Box<dyn InternalProvider>),
475
476            output_mode: OutputMode::default(),
477        }
478    }
479
480    /// Creates a new [`InstanceBuilder`] from an already-loaded [`Project`].
481    ///
482    /// Skips the parse + load phase, going straight to optimize + compile
483    /// when [`InstanceBuilder::build()`] is called. This is useful when the
484    /// caller has already loaded the project (e.g. to share a cached
485    /// [`LoadResult`](openpine_compiler::loader::LoadResult) between
486    /// compilation and LSP analysis).
487    #[inline]
488    pub fn builder_from_project<'a, P>(
489        provider: P,
490        project: &'a Project,
491        timeframe: TimeFrame,
492        symbol: impl Into<String>,
493    ) -> InstanceBuilder<'a, ()>
494    where
495        P: DataProvider + 'static,
496    {
497        InstanceBuilder {
498            compile_opts: CompileOptions::new_from_project(project),
499            input_values: HashMap::new(),
500            timeframe,
501            input_sessions: InputSessions::ALL,
502            symbol: symbol.into(),
503            background_color: None,
504            last_info: None,
505            currency_converter: default_currency_converter(),
506            execution_limits: ExecutionLimits::default(),
507            data_provider: Some(Box::new(provider) as Box<dyn InternalProvider>),
508
509            output_mode: OutputMode::default(),
510        }
511    }
512}