1use 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
26struct 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
70pub 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 pub fn with_locale(mut self, locale: impl Into<String>) -> Self {
93 self.core.locale = locale.into();
94 self
95 }
96
97 pub fn with_dialect(mut self, dialect: navi_compiler::loader::Dialect) -> Self {
99 self.dialect = dialect;
100 self
101 }
102
103 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 pub fn with_input_sessions(mut self, sessions: InputSessions) -> Self {
112 self.core.input_sessions = sessions;
113 self
114 }
115
116 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 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 pub fn with_execution_limits(mut self, limits: ExecutionLimits) -> Self {
130 self.core.execution_limits = limits;
131 self
132 }
133
134 pub fn with_output_mode(mut self, mode: OutputMode) -> Self {
136 self.core.output_mode = mode;
137 self
138 }
139
140 pub fn with_mtf_wait(mut self, duration: std::time::Duration) -> Self {
142 self.core.mtf_wait = duration;
143 self
144 }
145
146 pub fn with_strategy_config_override(mut self, ov: StrategyConfigOverride) -> Self {
152 self.core.strategy_config_override = Some(ov);
153 self
154 }
155
156 #[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 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 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 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
232pub struct InstanceBuilderFromProgram {
241 program: Program,
242 core: InstanceBuilderCore,
243}
244
245impl InstanceBuilderFromProgram {
246 pub fn with_locale(mut self, locale: impl Into<String>) -> Self {
248 self.core.locale = locale.into();
249 self
250 }
251
252 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 pub fn with_input_sessions(mut self, sessions: InputSessions) -> Self {
261 self.core.input_sessions = sessions;
262 self
263 }
264
265 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 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 pub fn with_execution_limits(mut self, limits: ExecutionLimits) -> Self {
279 self.core.execution_limits = limits;
280 self
281 }
282
283 pub fn with_output_mode(mut self, mode: OutputMode) -> Self {
285 self.core.output_mode = mode;
286 self
287 }
288
289 pub fn with_mtf_wait(mut self, duration: std::time::Duration) -> Self {
291 self.core.mtf_wait = duration;
292 self
293 }
294
295 pub fn with_strategy_config_override(mut self, ov: StrategyConfigOverride) -> Self {
301 self.core.strategy_config_override = Some(ov);
302 self
303 }
304
305 #[cfg(feature = "jit")]
307 pub fn with_jit(mut self, enable: bool) -> Self {
308 self.core.enable_jit = enable;
309 self
310 }
311
312 pub async fn build(self) -> Result<Instance, Error> {
314 finalize_instance(self.core, self.program).await
315 }
316}
317
318impl Instance {
319 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
343fn 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
589pub 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
595pub 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}