1mod diagnostics;
2mod frame;
3mod hir;
4mod intrinsic;
5mod parser;
6mod runtime;
7mod scene;
8mod types;
9mod warmup;
10
11pub use diagnostics::{Diagnostic, Severity, Span};
12pub use frame::{
13 BaseSeriesSource, ChartFrame, FrameField, FrameOutput, HorizontalLevelOutput, IndicatorFrame,
14 InstrumentContext, MarkerOutput, OhlcOutput, PlotOutput, RangeOutput, RegionOutput,
15 RegionUpdate, SegmentOutput, SeriesInputValue, TimeOrTick, TradeAggregate,
16};
17pub use hir::{
18 BarStateField, BinaryOperator, CallSiteId, ConstantId, ExprId, ExpressionKind, FunctionId,
19 HirFunction, HirProgram, IntrinsicId, LocalSlot, PlotId, SeriesSlot, SourceInputSlot,
20 StateSlot, StatementBlock, StatementBlockId, StatementId, StatementKind, TypedExpression,
21 TypedStatement, UnaryOperator,
22};
23use intrinsic::IntrinsicRegistry;
24pub use runtime::{IndicatorInstance, PlanningError, RuntimeError, RuntimeLimits};
25pub use scene::{
26 Color, IndicatorPresentation, OutputPrecision, PaneTarget, PlotDescriptor, PlotScale,
27 PlotStyle, PlotValueType, PresentationAttachmentMode, PresentationAxis, PresentationColumn,
28 PresentationColumnColor, PresentationDataKind, PresentationFill, PresentationGeometry,
29 PresentationInputSource, PresentationLabelMode, PresentationLayer, PresentationMark,
30 PresentationMarkKind, PresentationPrecision, PresentationReadout, PresentationScale,
31 PresentationSegment, PresentationStyle, PresentationViewKind, ReadoutBehavior,
32};
33pub use types::{
34 ArrayValue, IndicatorAuthor, IndicatorConfig, IndicatorDescriptor, IndicatorId,
35 InputDescriptor, InputOption, InputType, InputValue, Qualifier, SeriesInputDescriptor, Type,
36 Value, ValueType,
37};
38
39pub fn source_files_in_directory(
42 path: impl AsRef<std::path::Path>,
43) -> Result<(Vec<std::path::PathBuf>, Vec<String>), String> {
44 let root = path.as_ref();
45 let mut directories = vec![root.to_owned()];
46 let mut files = Vec::new();
47 let mut diagnostics = Vec::new();
48 while let Some(directory) = directories.pop() {
49 let entries = match std::fs::read_dir(&directory) {
50 Ok(entries) => entries,
51 Err(error) => {
52 let diagnostic =
53 format!("{}: could not read directory: {error}", directory.display());
54 if directory == root {
55 return Err(diagnostic);
56 }
57 diagnostics.push(diagnostic);
58 continue;
59 }
60 };
61 for entry in entries {
62 let entry = match entry {
63 Ok(entry) => entry,
64 Err(error) => {
65 diagnostics.push(format!(
66 "{}: could not read directory entry: {error}",
67 directory.display()
68 ));
69 continue;
70 }
71 };
72 let path = entry.path();
73 let file_type = match entry.file_type() {
74 Ok(file_type) => file_type,
75 Err(error) => {
76 diagnostics.push(format!(
77 "{}: could not inspect entry: {error}",
78 path.display()
79 ));
80 continue;
81 }
82 };
83 if file_type.is_dir() {
84 directories.push(path);
85 } else if file_type.is_file()
86 && path.extension().and_then(|extension| extension.to_str()) == Some("ind")
87 {
88 files.push(path);
89 }
90 }
91 }
92 files.sort();
93 diagnostics.sort();
94 Ok((files, diagnostics))
95}
96
97pub type Program = HirProgram;
98
99#[derive(Debug, Clone, Copy, PartialEq, Eq)]
101pub struct CompileLimits {
102 pub max_source_bytes: usize,
103 pub max_tokens: usize,
104 pub max_expressions: usize,
105 pub max_statements: usize,
106 pub max_functions: usize,
107 pub max_output_channels: usize,
108}
109
110impl Default for CompileLimits {
111 fn default() -> Self {
112 Self {
113 max_source_bytes: 1 << 20,
114 max_tokens: 100_000,
115 max_expressions: 100_000,
116 max_statements: 50_000,
117 max_functions: 256,
118 max_output_channels: 256,
119 }
120 }
121}
122
123#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
125pub struct DataRequirements {
126 pub ohlc: bool,
127 pub directional_volume: bool,
128 pub trades: bool,
129 pub open_interest: bool,
130}
131
132impl DataRequirements {
133 pub const fn needs_directional_volume(self) -> bool {
134 self.directional_volume
135 }
136
137 pub const fn needs_open_interest(self) -> bool {
138 self.open_interest
139 }
140}
141
142use std::path::Path;
143use std::sync::Arc;
144
145#[derive(Debug, Clone)]
147pub struct CompiledIndicator {
148 pub descriptor: IndicatorDescriptor,
149 pub requirements: DataRequirements,
150 pub program: Program,
151 pub(crate) registry: intrinsic::IntrinsicRegistry,
152}
153
154pub fn compile(source: &str, path: impl AsRef<Path>) -> Result<CompiledIndicator, Vec<Diagnostic>> {
156 compile_with_limits(source, path, CompileLimits::default())
157}
158
159pub fn compile_with_limits(
161 source: &str,
162 path: impl AsRef<Path>,
163 limits: CompileLimits,
164) -> Result<CompiledIndicator, Vec<Diagnostic>> {
165 let path = path.as_ref();
166 if source.len() > limits.max_source_bytes {
167 return Err(vec![Diagnostic::error(
168 path,
169 Span::new(0, source.len().min(limits.max_source_bytes)),
170 format!(
171 "source exceeds compile limit of {} bytes",
172 limits.max_source_bytes
173 ),
174 )]);
175 }
176 compile_with_registry(source, path, IntrinsicRegistry::standard(), limits)
177}
178
179pub fn compile_file(path: impl AsRef<Path>) -> Result<CompiledIndicator, Vec<Diagnostic>> {
181 compile_file_with_limits(path, CompileLimits::default())
182}
183
184pub fn compile_file_with_limits(
186 path: impl AsRef<Path>,
187 limits: CompileLimits,
188) -> Result<CompiledIndicator, Vec<Diagnostic>> {
189 let path = path.as_ref();
190 match std::fs::metadata(path) {
191 Ok(metadata) if metadata.len() > limits.max_source_bytes as u64 => {
192 return Err(vec![Diagnostic::error(
193 path,
194 Span::new(0, 0),
195 format!(
196 "source exceeds compile limit of {} bytes",
197 limits.max_source_bytes
198 ),
199 )]);
200 }
201 Err(error) => {
202 return Err(vec![Diagnostic::error(
203 path,
204 Span::new(0, 0),
205 format!("could not read source: {error}"),
206 )]);
207 }
208 Ok(_) => {}
209 }
210 let source = match std::fs::read_to_string(path) {
211 Ok(source) => source,
212 Err(error) => {
213 return Err(vec![Diagnostic::error(
214 path,
215 Span::new(0, 0),
216 format!("could not read source: {error}"),
217 )]);
218 }
219 };
220 compile_with_limits(&source, path, limits)
221}
222
223pub(crate) fn compile_with_registry(
224 source: &str,
225 path: impl AsRef<Path>,
226 registry: intrinsic::IntrinsicRegistry,
227 limits: CompileLimits,
228) -> Result<CompiledIndicator, Vec<Diagnostic>> {
229 parser::compile_with_registry(source, path.as_ref(), registry, limits)
230}
231
232impl CompiledIndicator {
233 pub fn warmup_bars(&self, config: &IndicatorConfig) -> Option<usize> {
237 self.warmup_bars_with_limits(config, RuntimeLimits::default())
238 }
239
240 pub fn warmup_bars_with_limits(
242 &self,
243 config: &IndicatorConfig,
244 limits: RuntimeLimits,
245 ) -> Option<usize> {
246 warmup::warmup_bars(
247 &self.program,
248 &self.registry,
249 &self.descriptor,
250 config,
251 limits.max_window_length,
252 )
253 .ok()
254 }
255
256 pub fn anchor_start(&self, config: &IndicatorConfig, reference_time: i64) -> Option<i64> {
260 warmup::anchor_start(
261 &self.program,
262 &self.registry,
263 &self.descriptor,
264 config,
265 reference_time,
266 )
267 .ok()
268 .flatten()
269 }
270
271 pub fn validate_history_analysis(&self, config: &IndicatorConfig) -> Result<(), PlanningError> {
273 warmup::warmup_bars(
274 &self.program,
275 &self.registry,
276 &self.descriptor,
277 config,
278 RuntimeLimits::default().max_window_length,
279 )?;
280 warmup::anchor_start(&self.program, &self.registry, &self.descriptor, config, 0)?;
281 Ok(())
282 }
283
284 pub fn requires_anchored_history(&self) -> bool {
286 warmup::requires_anchored_history(&self.program, &self.registry)
287 }
288
289 pub fn instantiate(&self, config: IndicatorConfig) -> Result<IndicatorInstance, PlanningError> {
291 self.instantiate_with_limits(config, RuntimeLimits::default())
292 }
293
294 pub fn instantiate_with_limits(
295 &self,
296 config: IndicatorConfig,
297 limits: RuntimeLimits,
298 ) -> Result<IndicatorInstance, PlanningError> {
299 IndicatorInstance::with_limits(Arc::new(self.clone()), config, limits)
300 }
301}
302
303#[cfg(test)]
304mod tests {
305 use super::*;
306 use std::fmt::Write as _;
307
308 fn frame(index: usize, time: i64, close: f64) -> IndicatorFrame {
309 IndicatorFrame {
310 index,
311 time,
312 basis: TimeOrTick::Time,
313 instrument: InstrumentContext {
314 min_tick: Some(0.1),
315 },
316 chart: ChartFrame {
317 open: Some(close),
318 high: Some(close),
319 low: Some(close),
320 close: Some(close),
321 volume: Some(1.0),
322 directional_volume_available: false,
323 buy_qty: 0.0,
324 sell_qty: 0.0,
325 trade: None,
326 },
327 open_interest: None,
328 series_inputs: Vec::new().into(),
329 }
330 }
331
332 fn market_frame(
333 index: usize,
334 high: Option<f64>,
335 low: Option<f64>,
336 close: Option<f64>,
337 volume: Option<f64>,
338 ) -> IndicatorFrame {
339 let close_value = close.unwrap_or(0.0);
340 let mut frame = frame(index, index as i64, close_value);
341 frame.chart.open = close;
342 frame.chart.high = high;
343 frame.chart.low = low;
344 frame.chart.close = close;
345 frame.chart.volume = volume;
346 frame
347 }
348
349 fn plot_value<'a>(output: &'a FrameOutput, id: &str) -> &'a Value {
350 output
351 .plots
352 .iter()
353 .find(|plot| plot.id == id)
354 .map(|plot| &plot.value)
355 .expect("plot output should be present")
356 }
357
358 #[test]
359 fn indicator_basis_restrictions() {
360 for (declaration, expected) in [
361 ("", None),
362 (", bases = [\"time\"]", Some(vec![TimeOrTick::Time])),
363 (", bases = [\"tick\"]", Some(vec![TimeOrTick::Tick])),
364 (
365 ", bases = [\"time\", \"tick\"]",
366 Some(vec![TimeOrTick::Time, TimeOrTick::Tick]),
367 ),
368 (
369 ", bases = [\"tick\", \"tick\"]",
370 Some(vec![TimeOrTick::Tick]),
371 ),
372 ] {
373 let source = format!(
374 "language(1)\nindicator(\"Bases\", id = \"test.bases\"{declaration})\nformat(precision = \"base_ticker_min_tick\")\nplot(chart.close)"
375 );
376 let compiled = compile(&source, "bases.ind").expect("valid basis restriction");
377 assert_eq!(compiled.descriptor.bases, expected);
378 }
379 for declaration in [
380 "[]",
381 "[\"range\"]",
382 "[1]",
383 "[\"time\", false]",
384 "\"time\"",
385 "chart.close",
386 ] {
387 let source = format!(
388 "language(1)\nindicator(\"Bases\", id = \"test.bases\", bases = {declaration})\nformat(precision = \"base_ticker_min_tick\")\nplot(chart.close)"
389 );
390 let error = compile(&source, "bases.ind").expect_err("invalid basis restriction");
391 assert!(
392 error
393 .iter()
394 .any(|diagnostic| diagnostic.message.contains("bases"))
395 );
396 }
397 }
398
399 #[test]
400 fn anchored_history_is_inferred_through_functions() {
401 for (body, expected) in [
402 ("plot(chart.close)", false),
403 ("plot(ta.avwap(chart.close, chart.volume, \"Day\"))", true),
404 (
405 "fn anchored(price) { return ta.avwap(price, chart.volume, \"Week\") }\nplot(anchored(chart.close))",
406 true,
407 ),
408 ] {
409 let source = format!(
410 "language(1)\nindicator(\"History\", id = \"test.history\")\nformat(precision = \"base_ticker_min_tick\")\n{body}"
411 );
412 let compiled = compile(&source, "history.ind").expect("history source should compile");
413 assert_eq!(compiled.requires_anchored_history(), expected);
414 }
415 }
416
417 #[test]
418 fn record_origin_alias_chain_is_bounded() {
419 const ALIAS_COUNT: usize = 24_000;
420 let mut source = String::from(
421 "language(1)\nindicator(\"Long aliases\", id = \"test.long-aliases\")\nformat(precision = \"base_ticker_min_tick\")\nalias0 = chart\n",
422 );
423 for index in 1..ALIAS_COUNT {
424 writeln!(&mut source, "alias{index} = alias{}", index - 1)
425 .expect("writing an in-memory source should succeed");
426 }
427 writeln!(
428 &mut source,
429 "plot(alias{}.close, id = \"value\")",
430 ALIAS_COUNT - 1
431 )
432 .expect("writing an in-memory source should succeed");
433
434 let result = std::thread::Builder::new()
435 .name("record-origin-regression".to_owned())
436 .stack_size(2 * 1024 * 1024)
437 .spawn(move || compile(&source, "long-aliases.ind"))
438 .expect("regression thread should start")
439 .join()
440 .expect("record-origin traversal should not overflow the stack");
441 assert!(
442 result.is_ok(),
443 "long immutable alias chains should compile: {result:?}"
444 );
445 }
446
447 #[test]
448 fn record_field_classification_preserves_builtin_and_dynamic_records() {
449 let chart_alias = compile(
450 r#"
451 language(1)
452 indicator("Chart alias", id = "test.chart-alias")
453 format(precision = "base_ticker_min_tick")
454 chart_alias = chart
455 plot(chart_alias.close, id = "value")
456 "#,
457 "chart-alias.ind",
458 )
459 .expect("an aliased chart should retain strict fields");
460 assert!(chart_alias.requirements.ohlc);
461
462 let trade_alias = compile(
463 r#"
464 language(1)
465 indicator("Trade aliases", id = "test.trade-aliases")
466 format(precision = "base_ticker_min_qty")
467 chart_alias = chart
468 chart_trade = chart.trade
469 aliased_trade = chart_alias.trade
470 plot(chart_trade.count, id = "direct")
471 plot(aliased_trade.count, id = "aliased")
472 "#,
473 "trade-aliases.ind",
474 )
475 .expect("chart.trade aliases should retain strict trade fields");
476 assert!(trade_alias.requirements.trades);
477
478 for source in [
479 r#"
480 language(1)
481 indicator("Invalid chart field", id = "test.invalid-chart-field")
482 format(precision = "base_ticker_min_tick")
483 chart_alias = chart
484 plot(chart_alias.missing, id = "value")
485 "#,
486 r#"
487 language(1)
488 indicator("Invalid trade field", id = "test.invalid-trade-field")
489 format(precision = "base_ticker_min_qty")
490 trade_alias = chart.trade
491 plot(trade_alias.close, id = "value")
492 "#,
493 ] {
494 let diagnostics = compile(source, "invalid-record-field.ind")
495 .expect_err("unknown built-in fields should remain strict");
496 assert!(
497 diagnostics
498 .iter()
499 .any(|diagnostic| { diagnostic.message.contains("unknown record field") })
500 );
501 }
502
503 let nested = compile(
504 r#"
505 language(1)
506 indicator("Nested user record", id = "test.nested-user-record")
507 format(precision = "base_ticker_min_tick")
508 value = { trade: { close: 7 } }
509 plot(value.trade.close, id = "value")
510 "#,
511 "nested-user-record.ind",
512 )
513 .expect("user records named trade should stay dynamic");
514 let mut nested_instance = nested
515 .instantiate(IndicatorConfig::default())
516 .expect("nested user record should instantiate");
517 let nested_output = nested_instance
518 .evaluate_closed(frame(0, 1, 10.0))
519 .expect("nested user record should evaluate");
520 assert_eq!(plot_value(&nested_output, "value"), &Value::Int(7));
521
522 let mutable = compile(
523 r#"
524 language(1)
525 indicator("Mutable record", id = "test.mutable-record")
526 format(precision = "base_ticker_min_tick")
527 value = chart
528 value := { close: 9 }
529 plot(value.close, id = "value")
530 "#,
531 "mutable-record.ind",
532 )
533 .expect("mutable records should use dynamic field access");
534 let mut mutable_instance = mutable
535 .instantiate(IndicatorConfig::default())
536 .expect("mutable record should instantiate");
537 let mutable_output = mutable_instance
538 .evaluate_closed(frame(0, 1, 10.0))
539 .expect("mutable record should evaluate");
540 assert_eq!(plot_value(&mutable_output, "value"), &Value::Int(9));
541 }
542
543 #[test]
544 fn rsi_uses_wilder_seed_and_exact_zero_edges() {
545 let source = r#"
546 language(1)
547 indicator("RSI", id = "test.rsi")
548 format(precision = "base_ticker_min_tick")
549 period = input.int(3, "Period", min = 1, max = 100)
550 plot(ta.rsi(chart.close, period), id = "value")
551 "#;
552 let compiled = compile(source, "rsi.ind").expect("RSI source should compile");
553 assert_eq!(compiled.warmup_bars(&IndicatorConfig::default()), Some(4));
554
555 let evaluate = |values: &[Option<f64>]| {
556 let mut instance = compiled
557 .clone()
558 .instantiate(IndicatorConfig::default())
559 .expect("RSI inputs should bind");
560 values
561 .iter()
562 .enumerate()
563 .map(|(index, close)| {
564 plot_value(
565 &instance
566 .evaluate_closed(market_frame(index, *close, *close, *close, Some(1.0)))
567 .expect("RSI frame should evaluate"),
568 "value",
569 )
570 .clone()
571 })
572 .collect::<Vec<_>>()
573 };
574
575 let alternating = evaluate(&[Some(1.0), Some(2.0), Some(1.0), Some(2.0), Some(1.0)]);
576 assert_eq!(&alternating[..3], &[Value::Na, Value::Na, Value::Na]);
577 assert!(
578 matches!(alternating[3], Value::Float(value) if (value - 66.6666666667).abs() < 1e-9)
579 );
580 assert!(
581 matches!(alternating[4], Value::Float(value) if (value - 44.4444444444).abs() < 1e-9)
582 );
583
584 for (values, expected) in [
585 (&[Some(1.0), Some(2.0), Some(3.0), Some(4.0)][..], 100.0),
586 (&[Some(4.0), Some(3.0), Some(2.0), Some(1.0)][..], 0.0),
587 (&[Some(1.0), Some(1.0), Some(1.0), Some(1.0)][..], 50.0),
588 (
589 &[
590 Some(1.0),
591 Some(1.0 + 1e-12),
592 Some(1.0 + 2e-12),
593 Some(1.0 + 3e-12),
594 ][..],
595 100.0,
596 ),
597 ] {
598 let output = evaluate(values);
599 assert!(matches!(output[3], Value::Float(value) if (value - expected).abs() < 1e-9));
600 }
601 }
602
603 #[test]
604 fn rsi_gaps_retain_state_and_previews_do_not_commit() {
605 let source = r#"
606 language(1)
607 indicator("RSI gaps", id = "test.rsi-gaps")
608 format(precision = "base_ticker_min_tick")
609 period = input.int(3, "Period", min = 1, max = 100)
610 plot(ta.rsi(chart.close, period), id = "value")
611 "#;
612 let compiled = compile(source, "rsi-gaps.ind").expect("RSI source should compile");
613
614 let mut instance = compiled
615 .clone()
616 .instantiate(IndicatorConfig::default())
617 .expect("RSI inputs should bind");
618 for (index, close) in [Some(1.0), Some(2.0)].into_iter().enumerate() {
619 instance
620 .evaluate_closed(market_frame(index, close, close, close, Some(1.0)))
621 .expect("seed frame should evaluate");
622 }
623 let preview = instance
624 .preview_open(market_frame(2, Some(1.0), Some(1.0), Some(1.0), Some(1.0)))
625 .expect("RSI preview should evaluate");
626 assert_eq!(plot_value(&preview, "value"), &Value::Na);
627 let second_preview = instance
628 .preview_open(market_frame(2, Some(2.0), Some(2.0), Some(2.0), Some(1.0)))
629 .expect("repeated RSI preview should evaluate");
630 assert_eq!(plot_value(&second_preview, "value"), &Value::Na);
631 let committed = instance
632 .evaluate_closed(market_frame(2, Some(1.0), Some(1.0), Some(1.0), Some(1.0)))
633 .expect("RSI commit should evaluate");
634 assert_eq!(plot_value(&committed, "value"), &Value::Na);
635 let committed = instance
636 .evaluate_closed(market_frame(3, Some(2.0), Some(2.0), Some(2.0), Some(1.0)))
637 .expect("RSI seeded output should evaluate");
638 assert!(
639 matches!(plot_value(&committed, "value"), Value::Float(value) if (*value - 66.6666666667).abs() < 1e-9)
640 );
641
642 let mut gap_instance = compiled
643 .instantiate(IndicatorConfig::default())
644 .expect("RSI gap inputs should bind");
645 let values = [Some(1.0), Some(2.0), None, Some(1.0), Some(2.0), Some(1.0)];
646 let outputs = values
647 .iter()
648 .enumerate()
649 .map(|(index, close)| {
650 plot_value(
651 &gap_instance
652 .evaluate_closed(market_frame(index, *close, *close, *close, Some(1.0)))
653 .expect("RSI gap frame should evaluate"),
654 "value",
655 )
656 .clone()
657 })
658 .collect::<Vec<_>>();
659 assert_eq!(outputs[2], Value::Na);
660 assert!(matches!(outputs[5], Value::Float(value) if (value - 66.6666666667).abs() < 1e-9));
661
662 let mut seeded_gap_instance = compiled
663 .instantiate(IndicatorConfig::default())
664 .expect("seeded RSI gap inputs should bind");
665 for (index, close) in [Some(1.0), Some(2.0), Some(1.0), Some(2.0), None, Some(3.0)]
666 .into_iter()
667 .enumerate()
668 {
669 let output = seeded_gap_instance
670 .evaluate_closed(market_frame(index, close, close, close, Some(1.0)))
671 .expect("seeded RSI gap frame should evaluate");
672 if index == 4 || index == 5 {
673 assert_eq!(plot_value(&output, "value"), &Value::Na);
674 }
675 }
676 let output = seeded_gap_instance
677 .evaluate_closed(market_frame(6, Some(2.0), Some(2.0), Some(2.0), Some(1.0)))
678 .expect("post-gap RSI frame should evaluate");
679 assert!(
680 matches!(plot_value(&output, "value"), Value::Float(value) if (value - 44.4444444444).abs() < 1e-9)
681 );
682 }
683
684 #[test]
685 fn pivots_confirm_after_the_right_leg_and_accept_ties() {
686 let source = r#"
687 language(1)
688 indicator("Pivots", id = "test.pivots")
689 format(precision = "base_ticker_min_tick")
690 left = input.int(1, "Left", min = 1, max = 10)
691 right = input.int(1, "Right", min = 1, max = 10)
692 plot(ta.pivothigh(chart.high, left, right), id = "high")
693 plot(ta.pivotlow(chart.low, left, right), id = "low")
694 "#;
695 let compiled = compile(source, "pivots.ind").expect("pivots should compile");
696 assert_eq!(compiled.warmup_bars(&IndicatorConfig::default()), Some(3));
697 let mut instance = compiled
698 .instantiate(IndicatorConfig::default())
699 .expect("inputs should bind");
700
701 let values = [(1.0, 3.0), (3.0, 3.0), (2.0, 2.0)];
702 let first = instance
703 .evaluate_closed(market_frame(0, Some(1.0), Some(3.0), Some(1.0), Some(1.0)))
704 .expect("first frame should evaluate");
705 assert_eq!(plot_value(&first, "high"), &Value::Na);
706 assert_eq!(plot_value(&first, "low"), &Value::Na);
707 let second = instance
708 .evaluate_closed(market_frame(1, Some(3.0), Some(3.0), Some(3.0), Some(1.0)))
709 .expect("second frame should evaluate");
710 assert_eq!(plot_value(&second, "high"), &Value::Na);
711 let third = instance
712 .evaluate_closed(market_frame(2, Some(2.0), Some(2.0), Some(2.0), Some(1.0)))
713 .expect("third frame should evaluate");
714 assert_eq!(plot_value(&third, "high"), &Value::Float(values[1].0));
715 assert_eq!(plot_value(&third, "low"), &Value::Na);
716
717 let mut preview_instance = compiled
718 .instantiate(IndicatorConfig::default())
719 .expect("preview inputs should bind");
720 preview_instance
721 .evaluate_closed(market_frame(0, Some(1.0), Some(1.0), Some(1.0), Some(1.0)))
722 .expect("first preview frame should evaluate");
723 preview_instance
724 .evaluate_closed(market_frame(1, Some(3.0), Some(3.0), Some(3.0), Some(1.0)))
725 .expect("second preview frame should evaluate");
726 let preview = preview_instance
727 .preview_open(market_frame(2, Some(2.0), Some(2.0), Some(2.0), Some(1.0)))
728 .expect("pivot preview should evaluate");
729 assert_eq!(plot_value(&preview, "high"), &Value::Float(3.0));
730 let closed_after_preview = preview_instance
731 .evaluate_closed(market_frame(2, Some(1.0), Some(1.0), Some(1.0), Some(1.0)))
732 .expect("closed frame after preview should evaluate");
733 assert_eq!(
734 plot_value(&closed_after_preview, "high"),
735 &Value::Float(3.0)
736 );
737 }
738
739 #[test]
740 fn pivots_return_na_for_missing_samples_and_dynamic_legs() {
741 let source = r#"
742 language(1)
743 indicator("Dynamic pivots", id = "test.dynamic-pivots")
744 format(precision = "base_ticker_min_tick")
745 left = input.int(1, "Left", min = 1, max = 10)
746 right = input.int(2, "Right", min = 1, max = 10)
747 plot(ta.pivothigh(chart.high, left, right), id = "high")
748 "#;
749 let compiled = compile(source, "dynamic-pivots.ind").expect("pivots should compile");
750 assert_eq!(compiled.warmup_bars(&IndicatorConfig::default()), Some(4));
751 let mut instance = compiled
752 .instantiate(IndicatorConfig::default().with("right", InputValue::Int(2)))
753 .expect("inputs should bind");
754 for (index, high) in [Some(1.0), None, Some(2.0), Some(1.0)]
755 .into_iter()
756 .enumerate()
757 {
758 let output = instance
759 .evaluate_closed(market_frame(index, high, high, high, Some(1.0)))
760 .expect("frame should evaluate");
761 assert_eq!(plot_value(&output, "high"), &Value::Na);
762 }
763
764 let mut instance = compiled
765 .instantiate(IndicatorConfig::default().with("right", InputValue::Int(1)))
766 .expect("dynamic input should bind");
767 for (index, high) in [Some(1.0), Some(4.0), Some(2.0)].into_iter().enumerate() {
768 let output = instance
769 .evaluate_closed(market_frame(index, high, high, high, Some(1.0)))
770 .expect("frame should evaluate");
771 if index == 2 {
772 assert_eq!(plot_value(&output, "high"), &Value::Float(4.0));
773 }
774 }
775 }
776
777 #[test]
778 fn pivots_reject_non_positive_legs_and_bounded_windows() {
779 let invalid = r#"
780 language(1)
781 indicator("Invalid pivot", id = "test.invalid-pivot")
782 format(precision = "base_ticker_min_tick")
783 plot(ta.pivothigh(chart.high, 0, 1), id = "high")
784 "#;
785 let invalid =
786 compile(invalid, "invalid-pivot.ind").expect("leg values are checked at runtime");
787 let result = invalid
788 .instantiate(IndicatorConfig::default())
789 .expect("invalid leg source should still plan")
790 .evaluate_closed(frame(0, 0, 1.0));
791 assert!(result.is_err());
792
793 let bounded = r#"
794 language(1)
795 indicator("Bounded pivot", id = "test.bounded-pivot")
796 format(precision = "base_ticker_min_tick")
797 left = input.int(2, "Left", min = 1, max = 10)
798 right = input.int(2, "Right", min = 1, max = 10)
799 plot(ta.pivotlow(chart.low, left, right), id = "low")
800 "#;
801 let compiled = compile(bounded, "bounded-pivot.ind").expect("pivot should compile");
802 let limits = RuntimeLimits {
803 max_window_length: 3,
804 ..Default::default()
805 };
806 let result = compiled
807 .instantiate_with_limits(IndicatorConfig::default(), limits)
808 .expect("bounded source should plan")
809 .evaluate_closed(frame(0, 0, 1.0));
810 assert!(result.is_err());
811 }
812
813 #[test]
814 fn regions_emit_loop_snapshots_last_write_wins_and_preview_isolated() {
815 let source = r#"
816 language(1)
817 indicator("Regions", id = "test.regions")
818 display(view = "primary_overlay")
819 plot(na, id = "anchor", display = "none", pane = "main", layer = true)
820 state threshold = chart.close
821 if chart.close > threshold {
822 for index = 0 to 1 {
823 region(key = 7, upper = 10 + index, lower = 5,
824 left_index = 0, right_index = bar_index,
825 extend_right = true, id = "zones")
826 }
827 }
828 threshold := chart.close
829 "#;
830 let compiled = compile(source, "regions.ind").expect("regions should compile");
831 let mut instance = compiled
832 .instantiate(IndicatorConfig::default())
833 .expect("inputs should bind");
834 let first = instance
835 .evaluate_closed(frame(0, 0, 10.0))
836 .expect("first frame should evaluate");
837 assert!(first.regions.is_empty());
838
839 let preview = instance
840 .preview_open(frame(1, 1, 20.0))
841 .expect("preview should evaluate");
842 assert_eq!(preview.regions.len(), 1);
843 assert_eq!(preview.regions[0].key, 7);
844 assert_eq!(preview.regions[0].upper, 11.0);
845
846 let closed = instance
847 .evaluate_closed(frame(1, 1, 15.0))
848 .expect("closed frame should evaluate");
849 assert_eq!(closed.regions.len(), 1);
850 assert_eq!(closed.regions[0].upper, 11.0);
851
852 let removed = instance
853 .evaluate_closed(frame(2, 2, 5.0))
854 .expect("region removal should evaluate");
855 assert!(removed.regions.is_empty());
856 }
857
858 #[test]
859 fn persistent_regions_emit_updates_and_last_write_wins() {
860 let source = r#"
861 language(1)
862 indicator("Persistent regions", id = "test.persistent-regions")
863 display(view = "primary_overlay")
864 plot(na, id = "anchor", display = "none", pane = "main", layer = true)
865 if chart.close > 0 {
866 region(key = 7, upper = 10, lower = 5,
867 left_index = 0, right_index = bar_index,
868 extend_right = true, persistent = true, id = "zones")
869 region(key = 7, upper = 12, lower = 5,
870 left_index = 0, right_index = bar_index,
871 extend_right = false, persistent = true, id = "zones")
872 region_remove(id = "zones", key = 8)
873 }
874 "#;
875 let compiled = compile(source, "persistent-regions.ind")
876 .expect("persistent region source should compile");
877 let mut instance = compiled
878 .instantiate(IndicatorConfig::default())
879 .expect("inputs should bind");
880 let output = instance
881 .evaluate_closed(frame(0, 0, 1.0))
882 .expect("persistent region should evaluate");
883 assert_eq!(output.regions.len(), 0);
884 assert_eq!(output.region_updates.len(), 2);
885 assert!(matches!(
886 &output.region_updates[0],
887 RegionUpdate::Upsert(region)
888 if region.key == 7 && region.upper == 12.0 && !region.extend_right
889 ));
890 assert!(matches!(
891 &output.region_updates[1],
892 RegionUpdate::Remove { id, key } if id == "zones" && *key == 8
893 ));
894
895 let omitted = instance
896 .evaluate_closed(frame(1, 1, -1.0))
897 .expect("omitted persistent regions should evaluate");
898 assert!(omitted.region_updates.is_empty());
899 }
900
901 #[test]
902 fn arrays_support_tuple_storage_push_get_and_size() {
903 let source = r#"
904 language(1)
905 indicator("Array zones", id = "test.array-zones")
906 format(precision = "base_ticker_min_tick")
907 active = array.new()
908 active := array.push(active, (7, 10.0, 5.0, 3, 2))
909 active := array.push(active, (8, 20.5, 15.0, 4, 1))
910 (key, upper, lower, left, count) = array.get(active, 1)
911 plot(array.size(active), id = "size")
912 plot(upper, id = "upper")
913 plot(left, id = "left")
914 plot(count, id = "count")
915 "#;
916 let compiled = compile(source, "array-zones.ind").expect("array source should compile");
917 let mut instance = compiled
918 .instantiate(IndicatorConfig::default())
919 .expect("inputs should bind");
920 let output = instance
921 .evaluate_closed(frame(0, 0, 1.0))
922 .expect("array source should evaluate");
923 assert_eq!(plot_value(&output, "size"), &Value::Int(2));
924 assert_eq!(plot_value(&output, "upper"), &Value::Float(20.5));
925 assert_eq!(plot_value(&output, "left"), &Value::Int(4));
926 assert_eq!(plot_value(&output, "count"), &Value::Int(1));
927 }
928
929 #[test]
930 fn array_push_evaluates_side_effecting_arguments_once_when_aliased() {
931 let source = r#"
932 language(1)
933 indicator("Array alias", id = "test.array-alias")
934 format(precision = "base_ticker_min_tick")
935 active = array.new()
936 alias = active
937 active := array.push(active, (7, ta.cum(1), 0.0, 0, 0))
938 (_, upper, _, _, _) = array.get(active, 0)
939 plot(upper, id = "upper")
940 "#;
941 let compiled = compile(source, "array-alias.ind").expect("aliased array should compile");
942 let mut instance = compiled
943 .instantiate(IndicatorConfig::default())
944 .expect("inputs should bind");
945 let output = instance
946 .evaluate_closed(frame(0, 0, 1.0))
947 .expect("aliased array push should evaluate");
948 assert_eq!(plot_value(&output, "upper"), &Value::Float(1.0));
949 }
950
951 #[test]
952 fn self_pushed_arrays_obey_collection_limits() {
953 let source = r#"
954 language(1)
955 indicator("Bounded array", id = "test.bounded-array")
956 format(precision = "base_ticker_min_tick")
957 active = array.new()
958 active := array.push(active, (7, 10.0))
959 plot(array.size(active), id = "size")
960 "#;
961 let compiled = compile(source, "bounded-array.ind").expect("array source should compile");
962 let limits = RuntimeLimits {
963 max_collection_elements: 0,
964 ..RuntimeLimits::default()
965 };
966 let mut instance = compiled
967 .instantiate_with_limits(IndicatorConfig::default(), limits)
968 .expect("inputs should bind");
969 assert_eq!(
970 instance
971 .evaluate_closed(frame(0, 0, 1.0))
972 .expect_err("self-pushed array should hit the collection limit"),
973 RuntimeError::ResourceLimitExceeded("collection elements")
974 );
975 }
976
977 #[test]
978 fn large_array_reads_stay_within_untrusted_execution_limits() {
979 let source = r#"
980 language(1)
981 indicator("Large array reads", id = "test.large-array-reads")
982 format(precision = "base_ticker_min_tick")
983 values = array.new()
984 for index = 0 to 255 {
985 values := array.push(values, (index, index))
986 }
987 for index = 0 to array.size(values) - 1 {
988 (key, value) = array.get(values, index)
989 }
990 plot(array.size(values), id = "size")
991 "#;
992 let compiled =
993 compile(source, "large-array-reads.ind").expect("array source should compile");
994 let mut instance = compiled
995 .instantiate_with_limits(IndicatorConfig::default(), RuntimeLimits::untrusted())
996 .expect("inputs should bind");
997 let output = instance
998 .evaluate_closed(frame(0, 0, 1.0))
999 .expect("large array reads should remain within limits");
1000 assert_eq!(plot_value(&output, "size"), &Value::Int(256));
1001 }
1002
1003 #[test]
1004 fn flat_open_zones_emit_only_creation_and_completion_updates() {
1005 let compiled = compile(
1006 include_str!("../tests/fixtures/flat-open-zones.ind"),
1007 "flat-open-zones.ind",
1008 )
1009 .expect("flat open source should compile");
1010 let mut instance = compiled
1011 .instantiate(IndicatorConfig::default())
1012 .expect("flat open source should instantiate");
1013 let flat_frame = |index: usize, open: f64, high: f64, low: f64, close: f64| {
1014 let mut frame = frame(index, index as i64, close);
1015 frame.chart.open = Some(open);
1016 frame.chart.high = Some(high);
1017 frame.chart.low = Some(low);
1018 frame.chart.close = Some(close);
1019 frame
1020 };
1021
1022 let mut invalidated = compiled
1023 .instantiate(IndicatorConfig::default())
1024 .expect("flat open source should instantiate");
1025 assert!(
1026 invalidated
1027 .evaluate_closed(flat_frame(0, 100.0, 100.1, 99.95, 99.95))
1028 .expect("invalidated setup bar should evaluate")
1029 .region_updates
1030 .is_empty()
1031 );
1032 assert!(
1033 invalidated
1034 .evaluate_closed(flat_frame(1, 100.0, 101.0, 99.9, 101.0))
1035 .expect("invalidated creation bar should evaluate")
1036 .region_updates
1037 .is_empty()
1038 );
1039
1040 assert!(
1041 instance
1042 .evaluate_closed(flat_frame(0, 100.0, 101.0, 99.0, 100.0))
1043 .expect("first bar should evaluate")
1044 .region_updates
1045 .is_empty()
1046 );
1047 let created = instance
1048 .evaluate_closed(flat_frame(1, 100.0, 101.0, 99.9, 101.0))
1049 .expect("creation bar should evaluate");
1050 assert!(matches!(
1051 created.region_updates.as_ref(),
1052 [RegionUpdate::Upsert(region)] if region.left_index == 1 && region.extend_right
1053 ));
1054 for index in 2..4 {
1055 assert!(
1056 instance
1057 .evaluate_closed(flat_frame(index, 100.5, 102.0, 99.5, 99.5))
1058 .expect("inside bar should evaluate")
1059 .region_updates
1060 .is_empty()
1061 );
1062 }
1063 let completed = instance
1064 .evaluate_closed(flat_frame(4, 100.5, 102.0, 99.5, 99.5))
1065 .expect("completion bar should evaluate");
1066 assert!(matches!(
1067 completed.region_updates.as_ref(),
1068 [RegionUpdate::Remove { id, .. }] if id == "flat_open_zones"
1069 ));
1070 }
1071
1072 #[test]
1073 #[ignore = "slow 10k-bar history stress test; run explicitly"]
1074 fn flat_open_zones_handle_dense_10k_history_under_untrusted_limits() {
1075 let compiled = compile(
1076 include_str!("../tests/fixtures/flat-open-zones.ind"),
1077 "flat-open-zones.ind",
1078 )
1079 .expect("flat open source should compile");
1080 let mut instance = compiled
1081 .instantiate_with_limits(IndicatorConfig::default(), RuntimeLimits::untrusted())
1082 .expect("flat open source should instantiate");
1083 for index in 0..10_000 {
1084 let open = 100.0 + index as f64 * 0.2;
1085 let mut bar = frame(index, index as i64, open + 0.5);
1086 bar.chart.open = Some(open);
1087 bar.chart.high = Some(open + 1.0);
1088 bar.chart.low = Some(open - 0.1);
1089 bar.chart.close = Some(open + 0.5);
1090 instance
1091 .evaluate_closed(bar)
1092 .expect("dense flat-open history should remain within limits");
1093 }
1094 }
1095
1096 #[test]
1097 #[ignore = "slow 10k-bar history stress test; run explicitly"]
1098 fn volume_pivot_zones_handle_dense_10k_history_under_untrusted_limits() {
1099 let compiled = compile(
1100 include_str!("../tests/fixtures/volume-pivot-zones.ind"),
1101 "volume-pivot-zones.ind",
1102 )
1103 .expect("volume pivot source should compile");
1104 let mut instance = compiled
1105 .instantiate_with_limits(IndicatorConfig::default(), RuntimeLimits::untrusted())
1106 .expect("volume pivot source should instantiate");
1107 for index in 0..10_000 {
1108 let base = 100.0 + index as f64 * 0.2;
1109 instance
1110 .evaluate_closed(market_frame(
1111 index,
1112 Some(base + 1.0),
1113 Some(base),
1114 Some(base + 0.75),
1115 Some(if index % 11 == 5 { 1_000.0 } else { 1.0 }),
1116 ))
1117 .expect("dense volume-pivot history should remain within limits");
1118 }
1119 }
1120
1121 #[test]
1122 fn flat_open_zones_remove_oldest_active_zones_at_the_configured_cap() {
1123 let compiled = compile(
1124 include_str!("../tests/fixtures/flat-open-zones.ind"),
1125 "flat-open-zones.ind",
1126 )
1127 .expect("flat open source should compile");
1128 let mut instance = compiled
1129 .instantiate_with_limits(
1130 IndicatorConfig::default().with("max_active_zones", InputValue::Int(16)),
1131 RuntimeLimits::untrusted(),
1132 )
1133 .expect("flat open source should instantiate");
1134 let mut removed = false;
1135 for index in 0..64 {
1136 let open = 100.0 + index as f64 * 0.2;
1137 let mut bar = frame(index, index as i64, open + 0.5);
1138 bar.chart.open = Some(open);
1139 bar.chart.high = Some(open + 1.0);
1140 bar.chart.low = Some(open - 0.1);
1141 bar.chart.close = Some(open + 0.5);
1142 let output = instance
1143 .evaluate_closed(bar)
1144 .expect("capped flat-open history should evaluate");
1145 removed |= output
1146 .region_updates
1147 .iter()
1148 .any(|update| matches!(update, RegionUpdate::Remove { .. }));
1149 }
1150 assert!(removed, "the active-zone cap should retire old regions");
1151 }
1152
1153 #[test]
1154 fn flat_open_zones_apply_one_cap_across_bullish_and_bearish_zones() {
1155 let compiled = compile(
1156 include_str!("../tests/fixtures/flat-open-zones.ind"),
1157 "flat-open-zones.ind",
1158 )
1159 .expect("flat open source should compile");
1160 let mut instance = compiled
1161 .instantiate_with_limits(
1162 IndicatorConfig::default().with("max_active_zones", InputValue::Int(16)),
1163 RuntimeLimits::untrusted(),
1164 )
1165 .expect("flat open source should instantiate");
1166 let mut removed = false;
1167 let mut first = frame(0, 0, 100.0);
1168 first.chart.open = Some(100.0);
1169 first.chart.high = Some(100.1);
1170 first.chart.low = Some(99.95);
1171 first.chart.close = Some(100.0);
1172 instance
1173 .evaluate_closed(first)
1174 .expect("setup bar should evaluate");
1175 for index in 1..24 {
1176 let bullish = index % 2 == 1;
1177 let mut bar = frame(index, index as i64, 100.0);
1178 bar.chart.open = Some(100.0);
1179 bar.chart.high = Some(100.1);
1180 bar.chart.low = Some(if bullish { 100.0 } else { 99.95 });
1181 bar.chart.close = Some(if bullish { 100.05 } else { 99.95 });
1182 let output = instance
1183 .evaluate_closed(bar)
1184 .expect("mixed flat open should evaluate");
1185 removed |= output
1186 .region_updates
1187 .iter()
1188 .any(|update| matches!(update, RegionUpdate::Remove { .. }));
1189 }
1190 assert!(
1191 removed,
1192 "the global active-zone cap should retire old regions"
1193 );
1194 }
1195
1196 #[test]
1197 fn volume_pivot_zones_remove_oldest_active_zones_at_the_configured_cap() {
1198 let compiled = compile(
1199 include_str!("../tests/fixtures/volume-pivot-zones.ind"),
1200 "volume-pivot-zones.ind",
1201 )
1202 .expect("volume pivot source should compile");
1203 let mut instance = compiled
1204 .instantiate_with_limits(
1205 IndicatorConfig::default().with("max_active_zones", InputValue::Int(16)),
1206 RuntimeLimits::untrusted(),
1207 )
1208 .expect("volume pivot source should instantiate");
1209 let mut removed = false;
1210 for index in 0..300 {
1211 let base = 100.0 + index as f64 * 0.2;
1212 let output = instance
1213 .evaluate_closed(market_frame(
1214 index,
1215 Some(base + 1.0),
1216 Some(base),
1217 Some(base + 0.75),
1218 Some(if index % 11 == 5 { 1_000.0 } else { 1.0 }),
1219 ))
1220 .expect("dense volume-pivot history should evaluate");
1221 removed |= output
1222 .region_updates
1223 .iter()
1224 .any(|update| matches!(update, RegionUpdate::Remove { .. }));
1225 }
1226 assert!(removed, "the active-zone cap should retire old regions");
1227 }
1228
1229 #[test]
1230 fn history_indexing_reads_an_immutable_series_local() {
1231 let source = r#"
1232 language(1)
1233 indicator("Lagged value", id = "test.lagged")
1234 format(precision = "base_ticker_min_tick")
1235 value = chart.close * 2
1236 plot(value[1], id = "lagged")
1237 "#;
1238 let compiled = compile(source, "lagged.ind").expect("source should compile");
1239 let mut instance = compiled
1240 .instantiate(IndicatorConfig::default())
1241 .expect("inputs should bind");
1242
1243 assert_eq!(
1244 plot_value(
1245 &instance
1246 .evaluate_closed(frame(0, 1, 10.0))
1247 .expect("first frame should evaluate"),
1248 "lagged"
1249 ),
1250 &Value::Na
1251 );
1252 assert_eq!(
1253 plot_value(
1254 &instance
1255 .evaluate_closed(frame(1, 2, 20.0))
1256 .expect("second frame should evaluate"),
1257 "lagged"
1258 ),
1259 &Value::Float(20.0)
1260 );
1261 }
1262
1263 #[test]
1264 fn history_indexing_rejects_unretained_sources() {
1265 for source in [
1266 r#"
1267 language(1)
1268 indicator("Mutable history", id = "test.mutable-history")
1269 format(precision = "base_ticker_min_tick")
1270 value = chart.close
1271 value := value
1272 plot(value[1], id = "lagged")
1273 "#,
1274 r#"
1275 language(1)
1276 indicator("Parameter history", id = "test.parameter-history")
1277 format(precision = "base_ticker_min_tick")
1278 plot(lag(chart.close), id = "value")
1279 fn lag(value) {
1280 return value[1]
1281 }
1282 "#,
1283 r#"
1284 language(1)
1285 indicator("Call history", id = "test.call-history")
1286 format(precision = "base_ticker_min_tick")
1287 plot(identity(chart.close)[1], id = "lagged")
1288 fn identity(value) {
1289 return value
1290 }
1291 "#,
1292 ] {
1293 let diagnostics = compile(source, "unretained-history.ind")
1294 .expect_err("unretained history should fail compilation");
1295 assert!(
1296 diagnostics
1297 .iter()
1298 .any(|diagnostic| { diagnostic.message.contains("immutable retained series") })
1299 );
1300 }
1301 }
1302
1303 #[test]
1304 fn history_validation_is_order_independent_for_local_mutations() {
1305 for source in [
1306 r#"
1307 language(1)
1308 indicator("Later mutation", id = "test.later-mutation")
1309 format(precision = "base_ticker_min_tick")
1310 value = chart.close
1311 plot(value[1], id = "lagged")
1312 value := value
1313 "#,
1314 r#"
1315 language(1)
1316 indicator("Branch mutation", id = "test.branch-mutation")
1317 format(precision = "base_ticker_min_tick")
1318 value = chart.close
1319 plot(value[1], id = "lagged")
1320 if true {
1321 value := value
1322 }
1323 "#,
1324 r#"
1325 language(1)
1326 indicator("Destructure mutation", id = "test.destructure-mutation")
1327 format(precision = "base_ticker_min_tick")
1328 value = chart.close
1329 other = chart.open
1330 plot(value[1], id = "lagged")
1331 (value, other) := (value, other)
1332 "#,
1333 r#"
1334 language(1)
1335 indicator("Loop mutation", id = "test.loop-mutation")
1336 format(precision = "base_ticker_min_tick")
1337 value = chart.close
1338 plot(value[1], id = "lagged")
1339 for index = 0 to 1 {
1340 value := value
1341 }
1342 "#,
1343 r#"
1344 language(1)
1345 indicator("Loop target", id = "test.loop-target")
1346 format(precision = "base_ticker_min_tick")
1347 for index = 0 to 1 {
1348 lagged = index[1]
1349 }
1350 "#,
1351 ] {
1352 let diagnostics = compile(source, "order-independent-history.ind")
1353 .expect_err("mutable history should fail compilation");
1354 assert!(
1355 diagnostics
1356 .iter()
1357 .any(|diagnostic| diagnostic.message.contains("immutable retained series"))
1358 );
1359 }
1360 }
1361
1362 #[test]
1363 fn parser_rejects_deep_input_without_recursing() {
1364 let deeply_nested = [
1365 "(".repeat(257) + "chart.close" + &")".repeat(257),
1366 "+".repeat(257) + "chart.close",
1367 "chart.close".to_owned() + &" + chart.close".repeat(257),
1368 "(chart.close)".to_owned() + &".field".repeat(257),
1369 "chart.close".to_owned() + &"[0]".repeat(257),
1370 {
1371 let mut expression = "chart.close".to_owned();
1372 for _ in 0..257 {
1373 expression = format!("chart.close + ({expression})");
1374 }
1375 expression
1376 },
1377 {
1378 let mut expression = "chart.close".to_owned();
1379 for _ in 0..257 {
1380 expression = format!("math.abs({expression})");
1381 }
1382 expression
1383 },
1384 {
1385 let mut expression = "chart.close".to_owned();
1386 for _ in 0..257 {
1387 expression = format!("[{expression}]");
1388 }
1389 expression
1390 },
1391 {
1392 let mut expression = "chart.close".to_owned();
1393 for _ in 0..257 {
1394 expression = format!("switch chart.close {{ default => {expression} }}");
1395 }
1396 expression
1397 },
1398 ];
1399 for (index, expression) in deeply_nested.iter().enumerate() {
1400 let source = format!(
1401 "language(1)\nindicator(\"Deep {index}\", id = \"test.deep-{index}\")\nformat(precision = \"base_ticker_min_tick\")\nplot({expression}, id = \"value\")"
1402 );
1403 let diagnostics =
1404 compile(&source, "deep-parser.ind").expect_err("deep source should be rejected");
1405 assert!(
1406 diagnostics
1407 .iter()
1408 .any(|diagnostic| diagnostic.message.contains("parser nesting limit")),
1409 "missing nesting diagnostic for expression {index}: {diagnostics:?}"
1410 );
1411 }
1412
1413 let mut source = String::from(
1414 "language(1)\nindicator(\"Deep statements\", id = \"test.deep-statements\")\nformat(precision = \"base_ticker_min_tick\")\n",
1415 );
1416 for _ in 0..257 {
1417 source.push_str("if true { ");
1418 }
1419 source.push_str("plot(chart.close, id = \"value\")");
1420 for _ in 0..257 {
1421 source.push_str(" }");
1422 }
1423 let diagnostics = compile(&source, "deep-statements.ind")
1424 .expect_err("deep statements should be rejected");
1425 assert!(
1426 diagnostics
1427 .iter()
1428 .any(|diagnostic| diagnostic.message.contains("parser nesting limit"))
1429 );
1430 }
1431
1432 #[test]
1433 fn compile_limits_reject_large_sources_and_variadic_underflow() {
1434 let diagnostics = compile_with_limits(
1435 "language(1)",
1436 "large-source.ind",
1437 CompileLimits {
1438 max_source_bytes: 4,
1439 ..CompileLimits::default()
1440 },
1441 )
1442 .expect_err("source limit should be enforced before lexing");
1443 assert!(
1444 diagnostics[0]
1445 .message
1446 .contains("source exceeds compile limit")
1447 );
1448
1449 for source in [
1450 r#"
1451 language(1)
1452 indicator("Missing arguments", id = "test.missing-arguments")
1453 format(precision = "base_ticker_min_tick")
1454 plot(math.max(), id = "value")
1455 "#,
1456 r#"
1457 language(1)
1458 indicator("Missing color", id = "test.missing-color")
1459 format(precision = "base_ticker_min_tick")
1460 plot(chart.close, id = "value", color = color.rgb(1, 2))
1461 "#,
1462 ] {
1463 let diagnostics = compile(source, "variadic-underflow.ind")
1464 .expect_err("variadic minimum arity should be diagnosed");
1465 assert!(
1466 diagnostics
1467 .iter()
1468 .any(|diagnostic| { diagnostic.message.contains("at least") })
1469 );
1470 }
1471 }
1472
1473 #[test]
1474 fn source_inputs_support_history_ta_and_host_overrides() {
1475 let source = r#"
1476 language(1)
1477 indicator("Source input", id = "test.source-input", sourceable = true)
1478 display(attachment = "panel")
1479 format(precision = "base_ticker_min_tick")
1480 source = input.source(chart.close, "Source")
1481 plot(source, id = "current")
1482 plot(source[1], id = "lagged")
1483 plot(ta.sma(source, 2), id = "average")
1484 "#;
1485 let compiled = compile(source, "source-input.ind").expect("source input should compile");
1486 assert!(compiled.descriptor.sourceable);
1487 assert_eq!(compiled.descriptor.series_inputs.len(), 1);
1488 assert_eq!(compiled.descriptor.series_inputs[0].id, "source");
1489 assert_eq!(compiled.descriptor.series_inputs[0].label, "Source");
1490 assert_eq!(
1491 compiled.descriptor.series_inputs[0].default_field,
1492 FrameField::ChartClose
1493 );
1494 assert!(compiled.descriptor.series_inputs[0].allow_indicator);
1495 assert_eq!(
1496 compiled.descriptor.presentation.attachment,
1497 Some(PresentationAttachmentMode::Panel)
1498 );
1499 assert_eq!(compiled.warmup_bars(&IndicatorConfig::default()), Some(2));
1500 let mut instance = compiled
1501 .instantiate(IndicatorConfig::default())
1502 .expect("source input should bind");
1503
1504 let first = instance
1505 .evaluate_closed(frame(0, 1, 10.0))
1506 .expect("first frame should evaluate");
1507 assert_eq!(plot_value(&first, "current"), &Value::Float(10.0));
1508 assert_eq!(plot_value(&first, "lagged"), &Value::Na);
1509 assert_eq!(plot_value(&first, "average"), &Value::Na);
1510
1511 let second = instance
1512 .evaluate_closed(frame(1, 2, 20.0))
1513 .expect("fallback frame should evaluate");
1514 assert_eq!(plot_value(&second, "current"), &Value::Float(20.0));
1515 assert_eq!(plot_value(&second, "lagged"), &Value::Float(10.0));
1516 assert_eq!(plot_value(&second, "average"), &Value::Float(15.0));
1517
1518 let mut bound = frame(2, 3, 30.0);
1519 bound.series_inputs = vec![SeriesInputValue::Value(Some(100.0))].into();
1520 let bound = instance
1521 .evaluate_closed(bound)
1522 .expect("bound frame should evaluate");
1523 assert_eq!(plot_value(&bound, "current"), &Value::Float(100.0));
1524 assert_eq!(plot_value(&bound, "lagged"), &Value::Float(20.0));
1525 assert_eq!(plot_value(&bound, "average"), &Value::Float(60.0));
1526
1527 let mut unavailable = frame(3, 4, 40.0);
1528 unavailable.series_inputs = vec![SeriesInputValue::Value(None)].into();
1529 let unavailable = instance
1530 .evaluate_closed(unavailable)
1531 .expect("unavailable frame should evaluate");
1532 assert_eq!(plot_value(&unavailable, "current"), &Value::Na);
1533 assert_eq!(plot_value(&unavailable, "lagged"), &Value::Float(100.0));
1534 assert_eq!(plot_value(&unavailable, "average"), &Value::Na);
1535
1536 let mut selected = frame(4, 5, 50.0);
1537 selected.chart.high = Some(9.0);
1538 selected.chart.low = Some(3.0);
1539 selected.series_inputs = vec![SeriesInputValue::Base(BaseSeriesSource::Hl2)].into();
1540 let selected = instance
1541 .evaluate_closed(selected)
1542 .expect("base source selection should evaluate");
1543 assert_eq!(plot_value(&selected, "current"), &Value::Float(6.0));
1544 }
1545
1546 #[test]
1547 fn chart_composite_sources_compile_and_evaluate() {
1548 let source = r#"
1549 language(1)
1550 indicator("Composite sources", id = "test.composite-sources")
1551 display(attachment = "panel")
1552 format(precision = "base_ticker_min_tick")
1553 source = input.source(chart.hlc3, "Source")
1554 plot(chart.hl2, id = "hl2")
1555 plot(chart.hlc3, id = "hlc3")
1556 plot(chart.ohlc4, id = "ohlc4")
1557 plot(chart.hlcc4, id = "hlcc4")
1558 plot(source, id = "source")
1559 "#;
1560 let compiled = compile(source, "composite-sources.ind")
1561 .expect("composite chart sources should compile");
1562 assert!(compiled.requirements.ohlc);
1563 assert_eq!(compiled.descriptor.series_inputs.len(), 1);
1564 assert_eq!(compiled.descriptor.series_inputs[0].id, "source");
1565 assert_eq!(compiled.descriptor.series_inputs[0].label, "Source");
1566 assert_eq!(
1567 compiled.descriptor.series_inputs[0].default_field,
1568 FrameField::ChartHlc3
1569 );
1570
1571 let mut complete = frame(0, 1, 5.0);
1572 complete.chart.open = Some(1.0);
1573 complete.chart.high = Some(8.0);
1574 complete.chart.low = Some(3.0);
1575 let mut instance = compiled
1576 .instantiate(IndicatorConfig::default())
1577 .expect("composite source instance should bind");
1578 let output = instance
1579 .evaluate_closed(complete)
1580 .expect("complete composite frame should evaluate");
1581 assert_eq!(plot_value(&output, "hl2"), &Value::Float(5.5));
1582 assert_eq!(plot_value(&output, "hlc3"), &Value::Float(16.0 / 3.0));
1583 assert_eq!(plot_value(&output, "ohlc4"), &Value::Float(17.0 / 4.0));
1584 assert_eq!(plot_value(&output, "hlcc4"), &Value::Float(21.0 / 4.0));
1585 assert_eq!(plot_value(&output, "source"), &Value::Float(16.0 / 3.0));
1586
1587 let mut incomplete = frame(1, 2, 5.0);
1588 incomplete.chart.high = Some(8.0);
1589 incomplete.chart.low = None;
1590 let output = instance
1591 .evaluate_closed(incomplete)
1592 .expect("incomplete composite frame should evaluate");
1593 assert_eq!(plot_value(&output, "hl2"), &Value::Na);
1594 assert_eq!(plot_value(&output, "hlc3"), &Value::Na);
1595 assert_eq!(plot_value(&output, "ohlc4"), &Value::Na);
1596 assert_eq!(plot_value(&output, "hlcc4"), &Value::Na);
1597 assert_eq!(plot_value(&output, "source"), &Value::Na);
1598
1599 let mut nonfinite = frame(2, 3, 5.0);
1600 nonfinite.chart.high = Some(f64::NAN);
1601 let output = instance
1602 .evaluate_closed(nonfinite)
1603 .expect("non-finite composite frame should evaluate");
1604 assert_eq!(plot_value(&output, "source"), &Value::Na);
1605 }
1606
1607 #[test]
1608 fn source_input_requires_a_direct_chart_series_default() {
1609 let diagnostics = compile(
1610 r#"
1611 language(1)
1612 indicator("Invalid source", id = "test.invalid-source")
1613 display(attachment = "panel")
1614 format(precision = "base_ticker_min_tick")
1615 source = input.source(chart.close + 1, "Source")
1616 plot(source, id = "value")
1617 "#,
1618 "invalid-source.ind",
1619 )
1620 .expect_err("source defaults must be direct chart fields");
1621 assert!(diagnostics.iter().any(|diagnostic| {
1622 diagnostic
1623 .message
1624 .contains("input.source default must be chart.open")
1625 }));
1626 }
1627
1628 #[test]
1629 fn source_policies_map_and_allow_multiple_engine_inputs() {
1630 let policy = compile(
1631 r#"
1632 language(1)
1633 indicator("Policies", id = "test.policies", sourceable = false)
1634 display(attachment = "panel")
1635 format(precision = "base_ticker_min_tick")
1636 base = input.source(chart.close, "Base", allow_indicator = false)
1637 source = input.source(chart.close, "Source")
1638 plot(base + source, id = "value")
1639 "#,
1640 "policies.ind",
1641 )
1642 .expect("source policies should compile");
1643 assert!(!policy.descriptor.sourceable);
1644 assert_eq!(policy.descriptor.series_inputs.len(), 2);
1645 assert!(!policy.descriptor.series_inputs[0].allow_indicator);
1646 assert!(policy.descriptor.series_inputs[1].allow_indicator);
1647
1648 let multiple = compile(
1649 r#"
1650 language(1)
1651 indicator("Multiple sources", id = "test.multiple-sources")
1652 display(attachment = "panel")
1653 format(precision = "base_ticker_min_tick")
1654 left = input.source(chart.close, "Left")
1655 right = input.source(chart.open, "Right")
1656 plot(left - right, id = "value")
1657 "#,
1658 "multiple-sources.ind",
1659 )
1660 .expect("multiple source inputs should compile");
1661 assert_eq!(multiple.descriptor.series_inputs.len(), 2);
1662 assert!(
1663 multiple
1664 .descriptor
1665 .series_inputs
1666 .iter()
1667 .all(|input| input.allow_indicator)
1668 );
1669 }
1670
1671 #[test]
1672 fn sourceable_defaults_to_false_and_requires_visible_numeric_output() {
1673 let default = compile(
1674 r#"
1675 language(1)
1676 indicator("Default source policy", id = "test.default-source-policy")
1677 format(precision = "base_ticker_min_tick")
1678 plot(chart.close, id = "value")
1679 "#,
1680 "default-source-policy.ind",
1681 )
1682 .expect("default source policy should compile");
1683 assert!(!default.descriptor.sourceable);
1684 assert!(default.descriptor.series_inputs.is_empty());
1685
1686 let hidden = compile(
1687 r#"
1688 language(1)
1689 indicator("Private source", id = "test.private-source", sourceable = true)
1690 display(view = "primary_overlay")
1691 plot(na, id = "private", pane = "main", layer = false)
1692 "#,
1693 "private-source.ind",
1694 )
1695 .expect_err("sourceable indicators need a visible numeric output");
1696 assert!(hidden.iter().any(|diagnostic| {
1697 diagnostic
1698 .message
1699 .contains("sourceable = true requires at least one visible numeric plot layer")
1700 }));
1701 }
1702
1703 #[test]
1704 fn base_only_source_inputs_can_use_non_plot_views() {
1705 let table = compile(
1706 r#"
1707 language(1)
1708 indicator("Base table", id = "test.base-table")
1709 display(view = "table")
1710 source = input.source(chart.close, "Source", allow_indicator = false)
1711 plot(source, id = "value", layer = false)
1712 column(title = "Value", source = "value")
1713 "#,
1714 "base-table.ind",
1715 )
1716 .expect("base-only table source should compile");
1717 assert_eq!(
1718 table.descriptor.presentation.view,
1719 PresentationViewKind::Table
1720 );
1721
1722 let overlay = compile(
1723 r#"
1724 language(1)
1725 indicator("Base overlay", id = "test.base-overlay")
1726 display(view = "primary_overlay")
1727 source = input.source(chart.close, "Source", allow_indicator = false)
1728 plot(source, id = "value", pane = "main")
1729 "#,
1730 "base-overlay.ind",
1731 )
1732 .expect("base-only primary overlay source should compile");
1733 assert_eq!(
1734 overlay.descriptor.presentation.view,
1735 PresentationViewKind::PrimaryOverlay
1736 );
1737 }
1738
1739 #[test]
1740 fn indicator_enabled_source_inputs_require_plot_views_and_visible_numeric_layers() {
1741 let table = compile(
1742 r#"
1743 language(1)
1744 indicator("Invalid table", id = "test.invalid-table")
1745 display(view = "table")
1746 source = input.source(chart.close, "Source")
1747 plot(source, id = "value", layer = false)
1748 column(title = "Value", source = "value")
1749 "#,
1750 "invalid-table.ind",
1751 )
1752 .expect_err("indicator-enabled table source should fail");
1753 assert!(table.iter().any(|diagnostic| {
1754 diagnostic
1755 .message
1756 .contains("allow_indicator = true requires display(view = \"plot\")")
1757 }));
1758 assert!(table.iter().any(|diagnostic| {
1759 diagnostic
1760 .message
1761 .contains("requires at least one visible numeric plot layer")
1762 }));
1763
1764 let overlay = compile(
1765 r#"
1766 language(1)
1767 indicator("Invalid overlay", id = "test.invalid-overlay")
1768 display(view = "primary_overlay")
1769 source = input.source(chart.close, "Source")
1770 plot(source, id = "value", pane = "main")
1771 "#,
1772 "invalid-overlay.ind",
1773 )
1774 .expect_err("indicator-enabled primary overlay source should fail");
1775 assert!(overlay.iter().any(|diagnostic| {
1776 diagnostic
1777 .message
1778 .contains("allow_indicator = true requires display(view = \"plot\")")
1779 }));
1780 }
1781
1782 #[test]
1783 fn source_indicators_require_an_explicit_attachment_mode() {
1784 let overlay = compile(
1785 r#"
1786 language(1)
1787 indicator("Overlay source", id = "test.overlay-source")
1788 display(attachment = "overlay")
1789 format(precision = "base_ticker_min_tick")
1790 source = input.source(chart.close, "Source")
1791 plot(source, id = "value")
1792 "#,
1793 "overlay-source.ind",
1794 )
1795 .expect("overlay source should compile");
1796 assert_eq!(
1797 overlay.descriptor.presentation.attachment,
1798 Some(PresentationAttachmentMode::Overlay)
1799 );
1800
1801 let missing = compile(
1802 r#"
1803 language(1)
1804 indicator("Missing attachment", id = "test.missing-attachment")
1805 format(precision = "base_ticker_min_tick")
1806 source = input.source(chart.close, "Source")
1807 plot(source, id = "value")
1808 "#,
1809 "missing-attachment.ind",
1810 )
1811 .expect_err("source indicators must declare an attachment mode");
1812 assert!(missing.iter().any(|diagnostic| {
1813 diagnostic
1814 .message
1815 .contains("display must declare attachment")
1816 }));
1817
1818 let invalid = compile(
1819 r#"
1820 language(1)
1821 indicator("Invalid attachment", id = "test.invalid-attachment")
1822 display(attachment = "split")
1823 format(precision = "base_ticker_min_tick")
1824 source = input.source(chart.close, "Source")
1825 plot(source, id = "value")
1826 "#,
1827 "invalid-attachment.ind",
1828 )
1829 .expect_err("unknown attachment modes must be rejected");
1830 assert!(
1831 invalid
1832 .iter()
1833 .any(|diagnostic| { diagnostic.message.contains("unknown attachment split") })
1834 );
1835 }
1836
1837 #[test]
1838 fn named_panes_and_numeric_layer_targets_are_validated_by_the_compiler() {
1839 let named = compile(
1840 r#"
1841 language(1)
1842 indicator("Named", id = "test.named")
1843 format(precision = "base_ticker_min_tick")
1844 plot(chart.close, id = "value", pane = "named:7")
1845 "#,
1846 "named.ind",
1847 )
1848 .expect("named pane should compile");
1849 assert_eq!(named.descriptor.plots[0].pane, PaneTarget::Named(7));
1850 assert_eq!(
1851 named.descriptor.presentation.layers[0].pane,
1852 PaneTarget::Named(7)
1853 );
1854
1855 let primary_overlay = compile(
1856 r#"
1857 language(1)
1858 indicator("Overlay", id = "test.named-overlay")
1859 display(view = "primary_overlay")
1860 plot(chart.close, id = "value", pane = "named:7")
1861 "#,
1862 "named-overlay.ind",
1863 )
1864 .expect_err("primary overlays must target the main pane");
1865 assert!(primary_overlay.iter().any(|diagnostic| {
1866 diagnostic.message.contains("primary_overlay")
1867 && diagnostic.message.contains("pane main")
1868 }));
1869
1870 let mixed = compile(
1871 r#"
1872 language(1)
1873 indicator("Mixed", id = "test.mixed")
1874 format(precision = "base_ticker_min_tick")
1875 plot(chart.close, id = "main", pane = "main")
1876 plot(chart.close, id = "indicator", pane = "indicator")
1877 "#,
1878 "mixed.ind",
1879 )
1880 .expect_err("numeric layers must share a pane target");
1881 assert!(mixed.iter().any(|diagnostic| {
1882 diagnostic
1883 .message
1884 .contains("all numeric plot layers in one indicator must use the same pane")
1885 }));
1886 }
1887
1888 #[test]
1889 fn preview_restarts_from_the_last_committed_state() {
1890 let source = r#"
1891 language(1)
1892 indicator("Committed state", id = "test.state")
1893 format(precision = "base_ticker_min_tick")
1894 state total = 0
1895 total := total + chart.close
1896 plot(total, id = "total")
1897 "#;
1898 let compiled = compile(source, "state.ind").expect("source should compile");
1899 let mut instance = compiled
1900 .instantiate(IndicatorConfig::default())
1901 .expect("inputs should bind");
1902 instance
1903 .evaluate_closed(frame(0, 1, 1.0))
1904 .expect("first frame should evaluate");
1905 instance
1906 .evaluate_closed(frame(1, 2, 2.0))
1907 .expect("second frame should evaluate");
1908
1909 let first_preview = instance
1910 .preview_open(frame(2, 3, 10.0))
1911 .expect("preview should evaluate");
1912 let second_preview = instance
1913 .preview_open(frame(2, 3, 20.0))
1914 .expect("preview should evaluate");
1915 assert_eq!(plot_value(&first_preview, "total"), &Value::Float(13.0));
1916 assert_eq!(plot_value(&second_preview, "total"), &Value::Float(23.0));
1917
1918 let committed = instance
1919 .evaluate_closed(frame(2, 3, 20.0))
1920 .expect("final open frame should commit");
1921 assert_eq!(plot_value(&committed, "total"), &Value::Float(23.0));
1922 }
1923
1924 #[test]
1925 fn cumulative_volume_skips_missing_directional_samples() {
1926 let source = r#"
1927 language(1)
1928 indicator("CVD", id = "test.cvd")
1929 format(precision = "base_ticker_min_qty")
1930 delta = select(chart.directional_volume_available, chart.buy_qty - chart.sell_qty, na)
1931 value = ta.cum(delta)
1932 plot(value, id = "value")
1933 "#;
1934 let compiled = compile(source, "cvd.ind").expect("source should compile");
1935 let mut instance = compiled
1936 .instantiate(IndicatorConfig::default())
1937 .expect("inputs should bind");
1938
1939 let mut first = frame(0, 1, 10.0);
1940 first.chart.directional_volume_available = true;
1941 first.chart.buy_qty = 5.0;
1942 first.chart.sell_qty = 2.0;
1943 let first_output = instance
1944 .evaluate_closed(first)
1945 .expect("first directional frame should evaluate");
1946 assert_eq!(plot_value(&first_output, "value"), &Value::Float(3.0));
1947
1948 let mut second = frame(1, 2, 10.0);
1949 second.chart.directional_volume_available = true;
1950 second.chart.buy_qty = 4.0;
1951 second.chart.sell_qty = 1.0;
1952 let second_output = instance
1953 .evaluate_closed(second)
1954 .expect("second directional frame should evaluate");
1955 assert_eq!(plot_value(&second_output, "value"), &Value::Float(6.0));
1956
1957 let missing_output = instance
1958 .evaluate_closed(frame(2, 3, 10.0))
1959 .expect("missing directional frame should evaluate");
1960 assert_eq!(plot_value(&missing_output, "value"), &Value::Na);
1961
1962 let mut third = frame(3, 4, 10.0);
1963 third.chart.directional_volume_available = true;
1964 third.chart.buy_qty = 3.0;
1965 third.chart.sell_qty = 1.0;
1966 let third_output = instance
1967 .evaluate_closed(third)
1968 .expect("directional frame after gap should evaluate");
1969 assert_eq!(plot_value(&third_output, "value"), &Value::Float(8.0));
1970
1971 let mut fourth = frame(4, 5, 10.0);
1972 fourth.chart.directional_volume_available = true;
1973 fourth.chart.buy_qty = 2.0;
1974 fourth.chart.sell_qty = 1.0;
1975 let fourth_output = instance
1976 .evaluate_closed(fourth)
1977 .expect("directional frame should resume after gap");
1978 assert_eq!(plot_value(&fourth_output, "value"), &Value::Float(9.0));
1979 }
1980
1981 #[test]
1982 fn optional_trade_inputs_fall_back_to_candle_volume() {
1983 let source = r#"
1984 language(1)
1985 indicator("Volume", id = "test.volume", optional_trades = true)
1986 format(precision = "base_ticker_min_qty")
1987 has_directional_volume = chart.directional_volume_available
1988 buy = select(has_directional_volume, chart.buy_qty, chart.volume / 2)
1989 sell = select(has_directional_volume, chart.sell_qty, chart.volume / 2)
1990 signed = select(has_directional_volume, buy - sell, na)
1991 plot(buy, id = "buy")
1992 plot(signed, id = "signed")
1993 "#;
1994 let compiled = compile(source, "volume.ind").expect("source should compile");
1995 assert!(compiled.descriptor.optional_trades);
1996 assert!(compiled.requirements.needs_directional_volume());
1997 assert!(!compiled.requirements.trades);
1998 let mut instance = compiled
1999 .instantiate(IndicatorConfig::default())
2000 .expect("inputs should bind");
2001
2002 let output = instance
2003 .evaluate_closed(frame(0, 1, 10.0))
2004 .expect("candle-only frame should evaluate");
2005 assert_eq!(plot_value(&output, "buy"), &Value::Float(0.5));
2006 assert_eq!(plot_value(&output, "signed"), &Value::Na);
2007
2008 let mut flow_frame = frame(1, 2, 10.0);
2009 flow_frame.chart.directional_volume_available = true;
2010 flow_frame.chart.buy_qty = 3.0;
2011 flow_frame.chart.trade = Some(TradeAggregate {
2012 buy_qty: 3.0,
2013 sell_qty: 0.0,
2014 buy_count: 2,
2015 sell_count: 0,
2016 });
2017 let output = instance
2018 .evaluate_closed(flow_frame)
2019 .expect("trade frame should evaluate");
2020 assert_eq!(plot_value(&output, "buy"), &Value::Float(3.0));
2021 assert_eq!(plot_value(&output, "signed"), &Value::Float(3.0));
2022 }
2023
2024 #[test]
2025 fn chart_trade_fields_use_the_base_chart() {
2026 let source = r#"
2027 language(1)
2028 indicator("Base trades", id = "test.base-trades")
2029 format(precision = "base_ticker_min_qty")
2030 plot(chart.trade.available, id = "chart_available")
2031 plot(chart.trade.buy_qty, id = "chart_buy")
2032 plot(chart.trade.sell_qty, id = "chart_sell")
2033 plot(chart.trade.buy_count, id = "chart_cbuy")
2034 plot(chart.trade.sell_count, id = "chart_csell")
2035 plot(chart.trade.count, id = "chart_trades")
2036 plot(chart.directional_volume_available, id = "directional_available")
2037 plot(chart.buy_qty, id = "buy_qty")
2038 plot(chart.sell_qty, id = "sell_qty")
2039 "#;
2040 let compiled = compile(source, "base-trades.ind").expect("source should compile");
2041 assert!(compiled.requirements.trades);
2042 assert!(compiled.requirements.needs_directional_volume());
2043
2044 let mut instance = compiled
2045 .instantiate(IndicatorConfig::default())
2046 .expect("inputs should bind");
2047 let trade = TradeAggregate {
2048 buy_qty: 7.0,
2049 sell_qty: 3.0,
2050 buy_count: 3,
2051 sell_count: 1,
2052 };
2053 let mut populated = frame(0, 1, 10.0);
2054 populated.chart.directional_volume_available = true;
2055 populated.chart.buy_qty = trade.buy_qty;
2056 populated.chart.sell_qty = trade.sell_qty;
2057 populated.chart.trade = Some(trade);
2058
2059 let populated_output = instance
2060 .evaluate_closed(populated)
2061 .expect("populated trade frame should evaluate");
2062 assert_eq!(
2063 plot_value(&populated_output, "chart_available"),
2064 &Value::Bool(true)
2065 );
2066 assert_eq!(
2067 plot_value(&populated_output, "chart_buy"),
2068 &Value::Float(7.0)
2069 );
2070 assert_eq!(
2071 plot_value(&populated_output, "chart_sell"),
2072 &Value::Float(3.0)
2073 );
2074 assert_eq!(plot_value(&populated_output, "chart_cbuy"), &Value::Int(3));
2075 assert_eq!(plot_value(&populated_output, "chart_csell"), &Value::Int(1));
2076 assert_eq!(
2077 plot_value(&populated_output, "chart_trades"),
2078 &Value::Int(4)
2079 );
2080 assert_eq!(
2081 plot_value(&populated_output, "directional_available"),
2082 &Value::Bool(true)
2083 );
2084 assert_eq!(plot_value(&populated_output, "buy_qty"), &Value::Float(7.0));
2085 assert_eq!(
2086 plot_value(&populated_output, "sell_qty"),
2087 &Value::Float(3.0)
2088 );
2089
2090 for (name, expression) in [
2091 ("bare-close", "close"),
2092 ("bare-directional", "directional_volume_available"),
2093 ("vbuy", "vbuy"),
2094 ("vsell", "vsell"),
2095 ("trades", "trades"),
2096 ("cbuy", "cbuy"),
2097 ("csell", "csell"),
2098 ] {
2099 let source = format!(
2100 "language(1)\nindicator(\"Removed\", id = \"community.removed-{name}\")\nformat(precision = \"base_ticker_min_qty\")\nplot({expression}, id = \"value\")"
2101 );
2102 assert!(
2103 compile(&source, format!("{name}.ind")).is_err(),
2104 "{name} should fail"
2105 );
2106 }
2107 }
2108
2109 #[test]
2110 fn signed_table_columns_require_typed_color_bindings() {
2111 let source = r#"
2112 language(1)
2113 indicator("Signed table", id = "test.signed-table")
2114 display(view = "table")
2115 delta = chart.close - chart.open
2116 plot(delta, id = "delta")
2117 style("positive", title = "Positive values")
2118 style("negative", title = "Negative values")
2119 column(title = "Delta", source = "delta", color_mode = "signed",
2120 positive_color_slot = "positive", negative_color_slot = "negative")
2121 "#;
2122 let compiled = compile(source, "signed-table.ind").expect("table should compile");
2123 assert_eq!(compiled.descriptor.presentation.columns.len(), 1);
2124 assert!(matches!(
2125 compiled.descriptor.presentation.columns[0].color,
2126 PresentationColumnColor::Signed {
2127 ref positive_slot,
2128 ref negative_slot
2129 } if positive_slot == "positive" && negative_slot == "negative"
2130 ));
2131
2132 for (source, expected_message) in [
2133 (
2134 r#"
2135 language(1)
2136 indicator("Invalid table", id = "test.invalid-table")
2137 display(view = "table")
2138 plot(chart.close, id = "value")
2139 style("positive")
2140 style("negative")
2141 column(title = "Value", source = "value", color_mode = "signed",
2142 color_slot = "positive", positive_color_slot = "positive",
2143 negative_color_slot = "negative")
2144 "#,
2145 "cannot provide color_slot",
2146 ),
2147 (
2148 r#"
2149 language(1)
2150 indicator("Invalid table", id = "test.invalid-table")
2151 display(view = "table")
2152 plot(chart.close, id = "value")
2153 style("positive")
2154 column(title = "Value", source = "value", color_mode = "signed",
2155 positive_color_slot = "positive")
2156 "#,
2157 "require negative_color_slot",
2158 ),
2159 ] {
2160 let diagnostics = compile(source, "invalid-table.ind")
2161 .expect_err("invalid table colors should be diagnosed");
2162 assert!(
2163 diagnostics
2164 .iter()
2165 .any(|diagnostic| diagnostic.message.contains(expected_message))
2166 );
2167 }
2168 }
2169
2170 #[test]
2171 fn unsupported_execution_paths_are_rejected_at_compile_time() {
2172 for (name, expression) in [
2173 (
2174 "request",
2175 "request.security(\"ETHUSDT\", \"1h\", chart.close)",
2176 ),
2177 ("drawing", "box.new(1, 2, 3, 4)"),
2178 ("map", "map.new()"),
2179 ] {
2180 let source = format!(
2181 "language(1)\nindicator(\"Unsupported\", id = \"test.{name}\")\nvalue = {expression}\nplot(value)"
2182 );
2183 let diagnostics = compile(&source, format!("{name}.ind"))
2184 .expect_err("unsupported paths must fail compilation");
2185 assert!(
2186 diagnostics
2187 .iter()
2188 .any(|diagnostic| { diagnostic.message.contains("not available") })
2189 );
2190 }
2191
2192 let diagnostics = compile(
2193 "language(1)\nindicator(\"Intrabar\", id = \"test.intrabar\")\nvarip ticks = 0\nplot(ticks)",
2194 "intrabar.ind",
2195 )
2196 .expect_err("varip must fail compilation");
2197 assert!(
2198 diagnostics
2199 .iter()
2200 .any(|diagnostic| diagnostic.message.contains("varip"))
2201 );
2202 }
2203
2204 #[test]
2205 fn presentation_scene_is_compiled_with_owned_defaults() {
2206 let source = r#"
2207 language(1)
2208 indicator("Scene", id = "community.scene")
2209 display(panel_title = "Panel", layer_name = "Layer")
2210 format(scale = "fit_visible_include_zero", precision = "fixed_step:0.5",
2211 axis_labels = "abbreviated", header_labels = "commas", max_decimals = 2)
2212 period = input.int(3, "Period", min = 1, max = 20, step = 1)
2213 value = ta.sma(chart.close, period)
2214 upper = value + 1
2215 lower = value - 1
2216 rising = value > value[1]
2217 plot(value, id = "value", title = "Value", precision = "decimals:2",
2218 width = 1.5, offset = 1, color_slot = "base", style_by = "rising",
2219 true_color_slot = "bull", false_color_slot = "bear")
2220 plot(upper, id = "upper", title = "Upper", color_slot = "band")
2221 plot(lower, id = "lower", title = "Lower", color_slot = "band")
2222 plot(rising, id = "rising", title = "Rising", layer = false)
2223 style("base", title = "Base", role = "neutral", shade = "base")
2224 style("bull", title = "Bull", role = "success", shade = "base")
2225 style("bear", title = "Bear", role = "danger", shade = "base")
2226 style("band", title = "Band", role = "secondary", shade = "soft")
2227 mark(title = "Histogram", source = "value", kind = "histogram", color_slot = "base",
2228 highlight_by = "rising", highlight_color_slot = "bull",
2229 requires = "directional_volume", fallback = "line")
2230 mark(title = "Line", source = "value", kind = "line", color_slot = "base")
2231 fill(upper = "upper", lower = "lower", color_slot = "band", opacity = 0.2,
2232 style_by = "rising", true_color_slot = "bull", false_color_slot = "bear")
2233 readout(title = "Signal", source = "value", color_slot = "base")
2234 segment(value, upper, bar_index, bar_index, id = "projection",
2235 title = "Projection", color_slot = "band", width = 1.5)
2236 "#;
2237 let compiled = compile(source, "scene.ind").expect("presentation scene should compile");
2238 let presentation = &compiled.descriptor.presentation;
2239 assert!(!compiled.requirements.needs_directional_volume());
2240 assert_eq!(
2241 presentation.value_precision,
2242 Some(PresentationPrecision::FixedStep(0.5))
2243 );
2244 assert_eq!(
2245 compiled.descriptor.plots[0].output_precision,
2246 Some(OutputPrecision::Decimals(2))
2247 );
2248 assert_eq!(presentation.panel_title.as_deref(), Some("Panel"));
2249 assert_eq!(presentation.layer_name.as_deref(), Some("Layer"));
2250 assert_eq!(presentation.styles.len(), 4);
2251 assert_eq!(presentation.layers.len(), 3);
2252 assert_eq!(
2253 presentation
2254 .layers
2255 .iter()
2256 .map(|layer| layer.channel.as_str())
2257 .collect::<Vec<_>>(),
2258 vec!["value", "upper", "lower"]
2259 );
2260 assert_eq!(
2261 compiled.descriptor.plots[0].color_slot.as_deref(),
2262 Some("base")
2263 );
2264 assert_eq!(compiled.descriptor.plots[0].offset_bars, 1);
2265 assert_eq!(
2266 compiled.descriptor.plots[3].readout,
2267 ReadoutBehavior::Hidden,
2268 "boolean plots are output-only by default"
2269 );
2270 assert_eq!(presentation.marks[0].kind, PresentationMarkKind::Histogram);
2271 assert_eq!(
2272 presentation.marks[0].required_input,
2273 Some(PresentationInputSource::DirectionalVolume)
2274 );
2275 assert_eq!(
2276 presentation.marks[0].fallback_mark,
2277 Some(PresentationMarkKind::Line)
2278 );
2279 assert_eq!(
2280 presentation.marks[0].highlight_by.as_deref(),
2281 Some("rising")
2282 );
2283 assert_eq!(
2284 presentation.marks[0].highlight_color_slot.as_deref(),
2285 Some("bull")
2286 );
2287 assert_eq!(presentation.fills[0].style_by.as_deref(), Some("rising"));
2288 assert_eq!(presentation.fills[0].opacity, 0.2);
2289 assert_eq!(presentation.readouts[0].channel, "value");
2290 assert_eq!(presentation.segments[0].id, "projection");
2291 }
2292
2293 #[test]
2294 fn mark_highlight_bindings_are_validated() {
2295 let cases = [
2296 (
2297 "numeric",
2298 r#"highlight_by = "value", highlight_color_slot = "warning""#,
2299 "highlight_by must reference a boolean output",
2300 ),
2301 (
2302 "missing-color",
2303 r#"highlight_by = "rising""#,
2304 "highlight_by requires highlight_color_slot",
2305 ),
2306 (
2307 "line",
2308 r#"highlight_by = "rising", highlight_color_slot = "warning""#,
2309 "highlight_by requires a bar mark",
2310 ),
2311 ];
2312
2313 for (name, binding, expected_message) in cases {
2314 let kind = if name == "line" { "line" } else { "histogram" };
2315 let source = format!(
2316 r#"
2317 language(1)
2318 indicator("Highlight", id = "community.highlight-{name}")
2319 format(precision = "base_ticker_min_tick")
2320 value = chart.close
2321 rising = chart.close > chart.open
2322 style("base")
2323 style("warning", role = "warning")
2324 plot(value, id = "value", title = "Value", color_slot = "base")
2325 plot(rising, id = "rising", layer = false)
2326 mark(title = "Mark", source = "value", kind = "{kind}", color_slot = "base", {binding})
2327 "#
2328 );
2329 let diagnostics = compile(&source, format!("{name}.ind"))
2330 .expect_err("invalid mark highlight binding should be diagnosed");
2331 assert!(
2332 diagnostics
2333 .iter()
2334 .any(|diagnostic| diagnostic.message.contains(expected_message)),
2335 "expected {expected_message}, got {diagnostics:?}"
2336 );
2337 }
2338 }
2339
2340 #[test]
2341 fn numeric_plot_panels_require_format_precision() {
2342 let diagnostics = compile(
2343 "language(1)\nindicator(\"Missing precision\", id = \"test.missing-precision\")\nplot(chart.close, id = \"value\")\n",
2344 "missing-precision.ind",
2345 )
2346 .expect_err("numeric plot panels should require value precision");
2347 assert!(
2348 diagnostics
2349 .iter()
2350 .any(|diagnostic| { diagnostic.message.contains("must declare format(precision") })
2351 );
2352
2353 let explicit = compile(
2354 "language(1)\nindicator(\"Explicit precision\", id = \"test.explicit-precision\")\nformat(precision = \"fixed_step:0.01\")\nplot(chart.close, id = \"value\")\n",
2355 "explicit-precision.ind",
2356 )
2357 .expect("numeric plot panels with value precision should compile");
2358 assert_eq!(
2359 explicit.descriptor.presentation.value_precision,
2360 Some(PresentationPrecision::FixedStep(0.01))
2361 );
2362
2363 compile(
2364 "language(1)\nindicator(\"Overlay\", id = \"test.overlay\")\ndisplay(view = \"primary_overlay\")\nplot(chart.close, id = \"value\")\n",
2365 "overlay.ind",
2366 )
2367 .expect("primary overlays should not require panel precision");
2368
2369 compile(
2370 "language(1)\nindicator(\"Table\", id = \"test.table\")\ndisplay(view = \"table\")\nplot(chart.close, id = \"value\", display = \"none\", overlay = false, layer = false)\ncolumn(title = \"Value\", source = \"value\")\n",
2371 "table.ind",
2372 )
2373 .expect("table views should not require panel precision");
2374 }
2375
2376 #[test]
2377 fn display_and_format_declarations_are_unique_and_precede_outputs() {
2378 for (source, message) in [
2379 (
2380 "language(1)\nindicator(\"Duplicate display\", id = \"test.duplicate-display\")\ndisplay()\ndisplay()\nplot(chart.close, id = \"value\")\n",
2381 "duplicate display declaration",
2382 ),
2383 (
2384 "language(1)\nindicator(\"Duplicate format\", id = \"test.duplicate-format\")\nformat(precision = \"base_ticker_min_tick\")\nformat(precision = \"base_ticker_min_tick\")\nplot(chart.close, id = \"value\")\n",
2385 "duplicate format declaration",
2386 ),
2387 (
2388 "language(1)\nindicator(\"Late display\", id = \"test.late-display\")\nformat(precision = \"base_ticker_min_tick\")\nplot(chart.close, id = \"value\")\ndisplay()\n",
2389 "display declarations must appear before output declarations",
2390 ),
2391 (
2392 "language(1)\nindicator(\"Late format\", id = \"test.late-format\")\nplot(chart.close, id = \"value\")\nformat(precision = \"base_ticker_min_tick\")\n",
2393 "format declarations must appear before output declarations",
2394 ),
2395 ] {
2396 let diagnostics = compile(source, "metadata.ind")
2397 .expect_err("invalid metadata declarations should be diagnosed");
2398 assert!(
2399 diagnostics
2400 .iter()
2401 .any(|diagnostic| diagnostic.message.contains(message))
2402 );
2403 }
2404 }
2405
2406 #[test]
2407 fn generic_plot_accepts_scalar_styles_and_structured_constructors() {
2408 let scalar_source = r#"
2409 language(1)
2410 indicator("Scalar styles", id = "test.scalar-styles")
2411 format(precision = "base_ticker_min_tick")
2412 plot(chart.close, id = "line", style = "line")
2413 plot(chart.close, id = "step", style = "step")
2414 plot(chart.close, id = "histogram", style = "histogram")
2415 plot(chart.close, id = "area", style = "area")
2416 "#;
2417 compile(scalar_source, "scalar-styles.ind").expect("scalar plot styles should compile");
2418
2419 let structured_source = r#"
2420 language(1)
2421 indicator("Structured plots", id = "test.structured-plots")
2422 display(view = "primary_overlay")
2423 format(precision = "base_ticker_min_tick")
2424 plotcandle(chart.open, chart.high, chart.low, chart.close, id = "candles")
2425 plotohlc(chart.open, chart.high, chart.low, chart.close, id = "ohlc")
2426 plotrange(chart.high, chart.low, id = "range")
2427 broken_cloud(chart.high, chart.low, id = "broken")
2428 "#;
2429 compile(structured_source, "structured-plots.ind")
2430 .expect("structured plot constructors should compile");
2431 }
2432
2433 #[test]
2434 fn generic_plot_rejects_structured_styles() {
2435 for (style, constructors) in [
2436 ("candles", "plotcandle or plotohlc"),
2437 ("bars", "plotcandle or plotohlc"),
2438 ("cloud", "plotrange or broken_cloud"),
2439 ("broken_cloud", "plotrange or broken_cloud"),
2440 ] {
2441 let source = format!(
2442 "language(1)\nindicator(\"Invalid plot\", id = \"test.invalid-{style}\")\nformat(precision = \"base_ticker_min_tick\")\nplot(chart.close, id = \"value\", style = \"{style}\")"
2443 );
2444 let diagnostics = compile(&source, format!("invalid-{style}.ind"))
2445 .expect_err("structured styles should not be accepted by generic plot");
2446 assert!(diagnostics.iter().any(|diagnostic| {
2447 diagnostic
2448 .message
2449 .contains(&format!("plot style '{style}'"))
2450 && diagnostic.message.contains("requires a structured plot")
2451 && diagnostic.message.contains(constructors)
2452 }));
2453 }
2454 }
2455
2456 #[test]
2457 fn unknown_presentation_input_is_rejected_without_becoming_an_indicator_requirement() {
2458 let diagnostics = compile(
2459 r#"
2460 language(1)
2461 indicator("Unknown input", id = "community.unknown-input")
2462 format(precision = "base_ticker_min_tick")
2463 value = chart.close
2464 style("base", title = "Base")
2465 plot(value, id = "value", title = "Value", color_slot = "base")
2466 mark(title = "Bars", source = "value", kind = "histogram", color_slot = "base",
2467 requires = "not_a_real_input")
2468 "#,
2469 "unknown-input.ind",
2470 )
2471 .expect_err("unknown presentation input should be diagnosed");
2472
2473 assert!(diagnostics.iter().any(|diagnostic| {
2474 diagnostic
2475 .message
2476 .contains("unknown presentation input not_a_real_input")
2477 }));
2478 }
2479
2480 #[test]
2481 fn invalid_presentation_fallbacks_are_rejected() {
2482 let cases = [
2483 (
2484 "unknown",
2485 r#"fallback = "not_a_real_mark""#,
2486 "unknown presentation mark not_a_real_mark",
2487 ),
2488 (
2489 "missing",
2490 r#"fallback = "line""#,
2491 "references unknown fallback mark",
2492 ),
2493 (
2494 "self",
2495 r#"fallback = "histogram""#,
2496 "cannot fall back to itself",
2497 ),
2498 ];
2499
2500 for (name, fallback, expected_message) in cases {
2501 let source = format!(
2502 r#"
2503 language(1)
2504 indicator("Fallback", id = "community.fallback-{name}")
2505 format(precision = "base_ticker_min_tick")
2506 value = chart.close
2507 style("base")
2508 plot(value, id = "value", title = "Value", color_slot = "base")
2509 mark(title = "Histogram", source = "value", kind = "histogram",
2510 color_slot = "base", {fallback})
2511 "#
2512 );
2513 let diagnostics = compile(&source, format!("{name}.ind"))
2514 .expect_err("invalid presentation fallback should be diagnosed");
2515 assert!(
2516 diagnostics
2517 .iter()
2518 .any(|diagnostic| diagnostic.message.contains(expected_message))
2519 );
2520 }
2521
2522 let diagnostics = compile(
2523 r#"
2524 language(1)
2525 indicator("Input fallback", id = "community.input-fallback")
2526 format(precision = "base_ticker_min_tick")
2527 value = chart.close
2528 style("base")
2529 plot(value, id = "value", title = "Value", color_slot = "base")
2530 mark(title = "Overlay", source = "value", kind = "histogram",
2531 color_slot = "base", requires = "directional_volume", fallback = "line")
2532 mark(title = "Line", source = "value", kind = "line", color_slot = "base",
2533 requires = "trades")
2534 "#,
2535 "input-fallback.ind",
2536 )
2537 .expect_err("input-dependent fallback should be diagnosed");
2538 assert!(diagnostics.iter().any(|diagnostic| {
2539 diagnostic
2540 .message
2541 .contains("fallback mark Line cannot require input")
2542 }));
2543 }
2544
2545 #[test]
2546 fn sparse_plot_declarations_materialize_the_same_layer_defaults() {
2547 let explicit = compile(
2548 r#"
2549 language(1)
2550 indicator("Sparse", id = "community.sparse")
2551 display(view = "plot", panel_title = "Sparse", layer_name = "Sparse")
2552 format(precision = "base_ticker_min_tick", scale = "fit_visible")
2553 value = chart.close
2554 style("value", title = "value", role = "neutral", shade = "base")
2555 plot(value, id = "value", title = "Value", pane = "indicator",
2556 scale = "price", style = "line", display = "value", width = 1,
2557 offset = 0, color_slot = "value", overlay = true,
2558 data_kind = "scalar", mark = "line", axis = "secondary", layer = true)
2559 "#,
2560 "explicit.ind",
2561 )
2562 .expect("explicit defaults should compile");
2563 let sparse = compile(
2564 r#"
2565 language(1)
2566 indicator("Sparse", id = "community.sparse")
2567 format(precision = "base_ticker_min_tick")
2568 value = chart.close
2569 style("value")
2570 plot(value, id = "value", title = "Value")
2571 "#,
2572 "sparse.ind",
2573 )
2574 .expect("sparse declarations should compile");
2575
2576 assert_eq!(
2577 explicit.descriptor.presentation.layers,
2578 sparse.descriptor.presentation.layers
2579 );
2580 assert_eq!(
2581 explicit.descriptor.presentation.styles,
2582 sparse.descriptor.presentation.styles
2583 );
2584 assert_eq!(
2585 explicit.descriptor.plots[0].pane,
2586 sparse.descriptor.plots[0].pane
2587 );
2588 assert_eq!(
2589 explicit.descriptor.plots[0].scale,
2590 sparse.descriptor.plots[0].scale
2591 );
2592 assert_eq!(
2593 explicit.descriptor.plots[0].width,
2594 sparse.descriptor.plots[0].width
2595 );
2596 assert_eq!(
2597 explicit.descriptor.plots[0].overlay,
2598 sparse.descriptor.plots[0].overlay
2599 );
2600 assert_eq!(
2601 explicit.descriptor.plots[0].layer,
2602 sparse.descriptor.plots[0].layer
2603 );
2604 }
2605
2606 #[test]
2607 fn na_comparisons_propagate_and_switch_falls_back() {
2608 let source = r#"
2609 language(1)
2610 indicator("Missing values", id = "community.missing-values")
2611 format(precision = "base_ticker_min_tick")
2612 plot(na == na, id = "equal")
2613 plot(switch na { na => 1, default => 2 }, id = "switch")
2614 "#;
2615 let compiled = compile(source, "missing-values.ind").expect("source should compile");
2616 let mut instance = compiled
2617 .instantiate(IndicatorConfig::default())
2618 .expect("inputs should bind");
2619 let output = instance
2620 .evaluate_closed(frame(0, 1, 10.0))
2621 .expect("frame should evaluate");
2622
2623 assert_eq!(plot_value(&output, "equal"), &Value::Na);
2624 assert_eq!(plot_value(&output, "switch"), &Value::Int(2));
2625 }
2626
2627 #[test]
2628 fn state_initializers_retry_na_and_warm_up() {
2629 let source = r#"
2630 language(1)
2631 indicator("State seed", id = "community.state-seed")
2632 format(precision = "base_ticker_min_tick")
2633 seed = ta.sma(chart.close, 2)
2634 state average = seed
2635 plot(average, id = "average")
2636 "#;
2637 let compiled = compile(source, "state-seed.ind").expect("source should compile");
2638 assert_eq!(compiled.warmup_bars(&IndicatorConfig::default()), Some(2));
2639 let mut instance = compiled
2640 .instantiate(IndicatorConfig::default())
2641 .expect("inputs should bind");
2642
2643 assert_eq!(
2644 plot_value(
2645 &instance
2646 .evaluate_closed(frame(0, 1, 10.0))
2647 .expect("first frame should evaluate"),
2648 "average"
2649 ),
2650 &Value::Na
2651 );
2652 assert_eq!(
2653 plot_value(
2654 &instance
2655 .evaluate_closed(frame(1, 2, 20.0))
2656 .expect("second frame should evaluate"),
2657 "average"
2658 ),
2659 &Value::Float(15.0)
2660 );
2661 }
2662
2663 #[test]
2664 fn explicit_na_state_updates_remain_authoritative() {
2665 let source = r#"
2666 language(1)
2667 indicator("Explicit missing", id = "community.explicit-missing")
2668 format(precision = "base_ticker_min_tick")
2669 state value = chart.close
2670 if bar_index == 0 {
2671 value := na
2672 }
2673 plot(value, id = "value")
2674 "#;
2675 let compiled = compile(source, "explicit-missing.ind").expect("source should compile");
2676 let mut instance = compiled
2677 .instantiate(IndicatorConfig::default())
2678 .expect("inputs should bind");
2679 instance
2680 .evaluate_closed(frame(0, 1, 10.0))
2681 .expect("first frame should evaluate");
2682 let output = instance
2683 .evaluate_closed(frame(1, 2, 20.0))
2684 .expect("second frame should evaluate");
2685
2686 assert_eq!(plot_value(&output, "value"), &Value::Na);
2687 }
2688
2689 #[test]
2690 fn mutable_bindings_keep_compatible_types_across_updates_and_branches() {
2691 for source in [
2692 r#"
2693 language(1)
2694 indicator("Invalid state", id = "test.invalid-state")
2695 state value = 0
2696 value := true
2697 plot(value, id = "value")
2698 "#,
2699 r#"
2700 language(1)
2701 indicator("Invalid local", id = "test.invalid-local")
2702 value = 0
2703 value := true
2704 plot(value, id = "value")
2705 "#,
2706 r#"
2707 language(1)
2708 indicator("Invalid conditional", id = "test.invalid-conditional")
2709 state value = 0
2710 if true {
2711 value := true
2712 }
2713 plot(value, id = "value")
2714 "#,
2715 ] {
2716 let diagnostics = compile(source, "invalid-binding.ind")
2717 .expect_err("incompatible binding updates should fail");
2718 assert!(
2719 diagnostics
2720 .iter()
2721 .any(|diagnostic| diagnostic.message.contains("incompatible types"))
2722 );
2723 }
2724
2725 let source = r#"
2726 language(1)
2727 indicator("Widened state", id = "test.widened-state")
2728 format(precision = "base_ticker_min_tick")
2729 state value = 0
2730 value := chart.close
2731 value := na
2732 plot(value, id = "value")
2733 "#;
2734 let compiled = compile(source, "widened-state.ind").expect("compatible updates compile");
2735 assert_eq!(
2736 compiled.descriptor.plots[0].value_type,
2737 PlotValueType::Number
2738 );
2739
2740 let source = r#"
2741 language(1)
2742 indicator("Conditional value", id = "test.conditional-value")
2743 state value = na
2744 if true {
2745 value := true
2746 } else {
2747 value := false
2748 }
2749 plot(value, id = "value")
2750 "#;
2751 let compiled = compile(source, "conditional-value.ind").expect("Any should resolve");
2752 assert_eq!(
2753 compiled.descriptor.plots[0].value_type,
2754 PlotValueType::Boolean
2755 );
2756
2757 for source in [
2758 r#"
2759 language(1)
2760 indicator("While value", id = "test.while-value")
2761 state value = na
2762 while false {
2763 value := true
2764 }
2765 plot(value, id = "value")
2766 "#,
2767 r#"
2768 language(1)
2769 indicator("For value", id = "test.for-value")
2770 state value = na
2771 for index = 0 to 1 {
2772 value := true
2773 }
2774 plot(value, id = "value")
2775 "#,
2776 ] {
2777 let compiled = compile(source, "loop-value.ind").expect("loop joins compile");
2778 assert_eq!(
2779 compiled.descriptor.plots[0].value_type,
2780 PlotValueType::Boolean
2781 );
2782 }
2783 }
2784
2785 #[test]
2786 fn result_branches_and_binary_operands_are_checked() {
2787 for source in [
2788 r#"
2789 language(1)
2790 indicator("Invalid select", id = "test.invalid-select")
2791 plot(select(true, 1, false), id = "value")
2792 "#,
2793 r#"
2794 language(1)
2795 indicator("Invalid coalesce", id = "test.invalid-coalesce")
2796 plot(coalesce(1, false), id = "value")
2797 "#,
2798 r#"
2799 language(1)
2800 indicator("Invalid switch", id = "test.invalid-switch")
2801 plot(switch 1 { 1 => 1, default => false }, id = "value")
2802 "#,
2803 r#"
2804 language(1)
2805 indicator("Invalid order", id = "test.invalid-order")
2806 plot(true < false, id = "value")
2807 "#,
2808 r#"
2809 language(1)
2810 indicator("Invalid logical", id = "test.invalid-logical")
2811 plot(1 && true, id = "value")
2812 "#,
2813 ] {
2814 let diagnostics = compile(source, "invalid-expression.ind")
2815 .expect_err("invalid expression types should fail");
2816 assert!(
2817 diagnostics
2818 .iter()
2819 .any(|diagnostic| diagnostic.message.contains("incompatible")
2820 || diagnostic.message.contains("operands"))
2821 );
2822 }
2823
2824 let source = r#"
2825 language(1)
2826 indicator("Valid comparisons", id = "test.valid-comparisons")
2827 plot(chart.close < chart.open, id = "numeric")
2828 plot(chart.time < chart.time[1], id = "time")
2829 "#;
2830 let compiled = compile(source, "valid-comparisons.ind").expect("valid comparisons compile");
2831 assert_eq!(
2832 compiled.descriptor.plots[0].value_type,
2833 PlotValueType::Boolean
2834 );
2835 assert_eq!(
2836 compiled.descriptor.plots[1].value_type,
2837 PlotValueType::Boolean
2838 );
2839 }
2840
2841 #[test]
2842 fn cumulative_overflow_becomes_missing_without_poisoning_state() {
2843 let source = r#"
2844 language(1)
2845 indicator("Cumulative overflow", id = "test.cumulative-overflow")
2846 format(precision = "base_ticker_min_tick")
2847 plot(ta.cum(chart.close), id = "value")
2848 "#;
2849 let compiled = compile(source, "cumulative-overflow.ind").expect("source should compile");
2850 let mut instance = compiled
2851 .instantiate(IndicatorConfig::default())
2852 .expect("inputs should bind");
2853
2854 assert_eq!(
2855 plot_value(
2856 &instance
2857 .evaluate_closed(frame(0, 1, f64::MAX))
2858 .expect("first frame should evaluate"),
2859 "value"
2860 ),
2861 &Value::Float(f64::MAX)
2862 );
2863 assert_eq!(
2864 plot_value(
2865 &instance
2866 .evaluate_closed(frame(1, 2, f64::MAX))
2867 .expect("overflow should remain recoverable"),
2868 "value"
2869 ),
2870 &Value::Na
2871 );
2872 assert_eq!(
2873 plot_value(
2874 &instance
2875 .evaluate_closed(frame(2, 3, -f64::MAX))
2876 .expect("state should remain usable"),
2877 "value"
2878 ),
2879 &Value::Float(0.0)
2880 );
2881 }
2882
2883 #[test]
2884 fn functions_are_numeric_and_typed_before_their_bodies_compile() {
2885 let source = r#"
2886 language(1)
2887 indicator("Function", id = "community.function")
2888 format(precision = "base_ticker_min_tick")
2889 plot(identity(chart.close), id = "value")
2890 fn identity(value) {
2891 return value
2892 }
2893 "#;
2894 let compiled = compile(source, "function.ind").expect("numeric function should compile");
2895 let mut instance = compiled
2896 .instantiate(IndicatorConfig::default())
2897 .expect("inputs should bind");
2898 let output = instance
2899 .evaluate_closed(frame(0, 1, 10.0))
2900 .expect("frame should evaluate");
2901 assert_eq!(plot_value(&output, "value"), &Value::Float(10.0));
2902
2903 let diagnostics = compile(
2904 r#"
2905 language(1)
2906 indicator("Invalid function", id = "community.invalid-function")
2907 format(precision = "base_ticker_min_tick")
2908 plot(identity(chart.close > chart.open), id = "value")
2909 fn identity(value) {
2910 return value
2911 }
2912 "#,
2913 "invalid-function.ind",
2914 )
2915 .expect_err("boolean function arguments should be rejected");
2916 assert!(
2917 diagnostics
2918 .iter()
2919 .any(|diagnostic| diagnostic.message.contains("must be numeric or na"))
2920 );
2921 }
2922
2923 #[test]
2924 fn input_arguments_and_runtime_values_are_validated() {
2925 for (source, message) in [
2926 (
2927 r#"
2928 language(1)
2929 indicator("Unknown input argument", id = "community.unknown-input")
2930 value = input.int(1, "Value", unknown = 2)
2931 plot(value, id = "value")
2932 "#,
2933 "unknown argument",
2934 ),
2935 (
2936 r#"
2937 language(1)
2938 indicator("Duplicate options", id = "community.duplicate-options")
2939 value = input.string("a", "Value", options = ["a", "a"])
2940 plot(chart.close, id = "value")
2941 "#,
2942 "is duplicated",
2943 ),
2944 ] {
2945 let diagnostics = compile(source, "invalid-input.ind")
2946 .expect_err("invalid input declarations should be diagnosed");
2947 assert!(
2948 diagnostics
2949 .iter()
2950 .any(|diagnostic| diagnostic.message.contains(message))
2951 );
2952 }
2953
2954 let compiled = compile(
2955 r#"
2956 language(1)
2957 indicator("Bounded input", id = "community.bounded-input")
2958 format(precision = "base_ticker_min_tick")
2959 value = input.int(1, "Value", min = 1, max = 5)
2960 plot(chart.close, id = "value")
2961 "#,
2962 "bounded-input.ind",
2963 )
2964 .expect("bounded input should compile");
2965 let error = compiled
2966 .instantiate(IndicatorConfig::default().with("value", InputValue::Int(6)))
2967 .expect_err("out-of-range input should be rejected");
2968 assert_eq!(error, PlanningError::InvalidInput("value".to_owned()));
2969
2970 let compiled = compile(
2971 r#"
2972 language(1)
2973 indicator("Choice input", id = "community.choice-input")
2974 format(precision = "base_ticker_min_tick")
2975 method = input.string("a", "Method", options = ["a", "b"])
2976 plot(chart.close, id = "value")
2977 "#,
2978 "choice-input.ind",
2979 )
2980 .expect("choice input should compile");
2981 let error = compiled
2982 .instantiate(
2983 IndicatorConfig::default().with("method", InputValue::String("other".to_owned())),
2984 )
2985 .expect_err("unknown option should be rejected");
2986 assert_eq!(error, PlanningError::InvalidInput("method".to_owned()));
2987 }
2988
2989 #[test]
2990 fn transient_collection_values_obey_heap_limits() {
2991 let source = r#"
2992 language(1)
2993 indicator("Collection", id = "community.collection")
2994 format(precision = "base_ticker_min_tick")
2995 plot(array.size(array.push(array.new(), "12345")), id = "size")
2996 "#;
2997 let compiled = compile(source, "collection.ind").expect("source should compile");
2998 let limits = RuntimeLimits {
2999 max_heap_bytes: 4,
3000 ..RuntimeLimits::default()
3001 };
3002 let mut instance = compiled
3003 .instantiate_with_limits(IndicatorConfig::default(), limits)
3004 .expect("inputs should bind");
3005 assert_eq!(
3006 instance
3007 .evaluate_closed(frame(0, 1, 10.0))
3008 .expect_err("temporary string collection should hit the heap limit"),
3009 RuntimeError::ResourceLimitExceeded("heap bytes")
3010 );
3011 }
3012
3013 #[test]
3014 fn ta_window_limits_are_checked_without_counting_ta_state_as_collections() {
3015 let source = r#"
3016 language(1)
3017 indicator("Window", id = "community.window")
3018 format(precision = "base_ticker_min_tick")
3019 value = ta.sma(chart.close, 6)
3020 plot(value, id = "value")
3021 "#;
3022 let compiled = compile(source, "window.ind").expect("source should compile");
3023 let limits = RuntimeLimits {
3024 max_window_length: 5,
3025 ..RuntimeLimits::default()
3026 };
3027 let mut instance = compiled
3028 .instantiate_with_limits(IndicatorConfig::default(), limits)
3029 .expect("inputs should bind");
3030 assert_eq!(
3031 instance
3032 .evaluate_closed(frame(0, 1, 10.0))
3033 .expect_err("oversized TA window should hit the window limit"),
3034 RuntimeError::ResourceLimitExceeded("window length")
3035 );
3036
3037 let source = source.replace("ta.sma(chart.close, 6)", "ta.sma(chart.close, 2)");
3038 let compiled = compile(&source, "window-collection.ind").expect("source should compile");
3039 let limits = RuntimeLimits {
3040 max_collection_elements: 0,
3041 ..RuntimeLimits::default()
3042 };
3043 let mut instance = compiled
3044 .instantiate_with_limits(IndicatorConfig::default(), limits)
3045 .expect("inputs should bind");
3046 instance
3047 .evaluate_closed(frame(0, 1, 10.0))
3048 .expect("TA state should not consume collection elements");
3049 }
3050
3051 #[test]
3052 fn numeric_contracts_preserve_integers_and_match_warmup() {
3053 let source = r#"language(1)
3054indicator("Numbers", id = "test.numbers")
3055format(precision = "fixed_step:0.1")
3056length = 2 + 2
3057plot(9007199254740992 + 1, id = "integer")
3058plot(1 / 2, id = "division")
3059plot(9223372036854775807 + 1, id = "overflow")
3060plot(9007199254740992 < 9007199254740993, id = "order")
3061plot(9007199254740993 != 9007199254740992.0, id = "mixed")
3062plot(ta.pivothigh(chart.high, length, 2), id = "pivot")"#;
3063 let compiled = compile(source, "numbers.ind").unwrap();
3064 assert_eq!(compiled.warmup_bars(&IndicatorConfig::default()), Some(7));
3065 assert_eq!(compiled.program.plots[1].value_type, PlotValueType::Number);
3066 let mut instance = compiled.instantiate(IndicatorConfig::default()).unwrap();
3067 for index in 0..7 {
3068 let output = instance
3069 .evaluate_closed(frame(index, index as i64, 10.0))
3070 .unwrap();
3071 assert_eq!(
3072 plot_value(&output, "integer"),
3073 &Value::Int(9007199254740993)
3074 );
3075 assert_eq!(plot_value(&output, "division"), &Value::Float(0.5));
3076 assert_eq!(plot_value(&output, "overflow"), &Value::Na);
3077 assert_eq!(plot_value(&output, "order"), &Value::Bool(true));
3078 assert_eq!(plot_value(&output, "mixed"), &Value::Bool(true));
3079 if index == 6 {
3080 assert_eq!(plot_value(&output, "pivot"), &Value::Float(10.0));
3081 }
3082 }
3083 assert!(matches!(
3084 instance.evaluate_closed(frame(6, 6, 99.0)),
3085 Err(RuntimeError::InvalidValue {
3086 operation: "closed frame order"
3087 })
3088 ));
3089 assert_eq!(instance.committed_index(), Some(6));
3090 instance.preview_open(frame(7, 7, 20.0)).unwrap();
3091 instance.evaluate_closed(frame(7, 7, 20.0)).unwrap();
3092 for (integer, float, order) in [
3093 (i64::MAX, 9223372036854775808.0, std::cmp::Ordering::Less),
3094 (i64::MIN, -9223372036854775808.0, std::cmp::Ordering::Equal),
3095 (-1, -1.5, std::cmp::Ordering::Greater),
3096 (1, 1.5, std::cmp::Ordering::Less),
3097 ] {
3098 assert_eq!(
3099 Value::Int(integer).numeric_compare(&Value::Float(float)),
3100 Some(order)
3101 );
3102 assert_eq!(
3103 Value::Float(float).numeric_compare(&Value::Int(integer)),
3104 Some(order.reverse())
3105 );
3106 }
3107 for (left, operator, right, expected) in [
3108 (i64::MIN, hir::BinaryOperator::Subtract, 1, Value::Na),
3109 (i64::MAX, hir::BinaryOperator::Multiply, 2, Value::Na),
3110 (i64::MIN, hir::BinaryOperator::Remainder, -1, Value::Na),
3111 (9, hir::BinaryOperator::Remainder, 4, Value::Int(1)),
3112 (9, hir::BinaryOperator::Divide, 0, Value::Na),
3113 (9, hir::BinaryOperator::Remainder, 0, Value::Na),
3114 ] {
3115 assert_eq!(
3116 Value::Int(left).binary(operator, Value::Int(right)),
3117 expected
3118 );
3119 }
3120 for kind in ["int", "time"] {
3121 let source = format!(
3122 "language(1)\nindicator(\"Invalid\", id = \"test.invalid\")\nvalue = input.{kind}(2.9)\nplot(chart.close)"
3123 );
3124 assert!(
3125 compile(&source, "invalid.ind")
3126 .unwrap_err()
3127 .iter()
3128 .any(|error| error.message.contains("default has the wrong type"))
3129 );
3130 }
3131 }
3132
3133 #[test]
3134 fn dynamic_output_boundaries_reject_wrong_kinds() {
3135 for body in [
3136 "plot(array.get(array.new(array.new(1, 2)), 0))",
3137 "plotrange(array.get(array.new(false), 0), chart.low)",
3138 "plotcandle(chart.open, chart.high, chart.low, array.get(array.new(\"wrong\"), 0))",
3139 "display(view = \"primary_overlay\")\nplot(na)\nregion(key = array.get(array.new(1.5), 0), upper = chart.high, lower = chart.low, left_index = 0, right_index = 0)",
3140 "display(view = \"primary_overlay\")\nplot(na)\nregion(key = 1, upper = chart.high, lower = chart.low, left_index = 0, right_index = 0, visible = array.get(array.new(1), 0))",
3141 "plot(chart.close, color = array.get(array.new(1), 0))",
3142 ] {
3143 let source = format!(
3144 "language(1)\nindicator(\"Boundary\", id = \"test.boundary\")\nformat(precision = \"fixed_step:1\")\n{body}"
3145 );
3146 let compiled = compile(&source, "boundary.ind").unwrap();
3147 let error = compiled
3148 .instantiate(IndicatorConfig::default())
3149 .unwrap()
3150 .evaluate_closed(frame(0, 0, 10.0))
3151 .unwrap_err();
3152 assert!(
3153 matches!(error, RuntimeError::InvalidOutput { .. }),
3154 "{body}: {error}"
3155 );
3156 }
3157 let compiled = compile("language(1)\nindicator(\"Valid\", id = \"test.valid\")\nformat(precision = \"fixed_step:1\")\nplot(array.get(array.new(1, na), 0), id = \"number\")\nplot(na, id = \"missing\")", "valid.ind").unwrap();
3158 let output = compiled
3159 .instantiate(IndicatorConfig::default())
3160 .unwrap()
3161 .evaluate_closed(frame(0, 0, 10.0))
3162 .unwrap();
3163 assert_eq!(plot_value(&output, "number"), &Value::Int(1));
3164 assert_eq!(plot_value(&output, "missing"), &Value::Na);
3165 }
3166
3167 #[test]
3168 fn stateful_ta_requires_per_bar_execution_paths() {
3169 for body in [
3170 "plot(select(chart.close > 0, ta.sma(chart.close, 2), na))",
3171 "value = na\nfor index = 0 to 1 { value := ta.sma(chart.close, 2) }\nplot(value)",
3172 "plot(coalesce(chart.close, ta.ema(chart.close, 2)))",
3173 "plot(chart.close > 0 && ta.ema(chart.close, 2) > 0)",
3174 "state seed = ta.sma(chart.close, 2)\nplot(seed)",
3175 "fn average(value) { return ta.ema(value, 2) }\nplot(select(chart.close > 0, average(chart.close), na))",
3176 "fn inner(value) { return ta.ema(value, 2) }\nfn average(value) { return inner(value) }\nplot(select(chart.close > 0, average(chart.close), na))",
3177 "fn average(value) { if value > 0 { return 0 } return ta.ema(value, 2) }\nplot(average(chart.close))",
3178 "fn average(value = ta.ema(chart.close, 2)) { return value }\nplot(average())",
3179 "plot(ta.sma(chart.close, 2)[1])",
3180 "display(view = \"primary_overlay\")\nplot(na)\nregion(key = 1, upper = ta.sma(chart.high, 2), lower = chart.low, left_index = 0, right_index = 0, visible = chart.close > 0)",
3181 ] {
3182 let source = format!(
3183 "language(1)\nindicator(\"Schedule\", id = \"test.schedule\")\nformat(precision = \"fixed_step:1\")\n{body}"
3184 );
3185 let errors = compile(&source, "schedule.ind").unwrap_err();
3186 assert!(
3187 errors.iter().any(|error| error
3188 .message
3189 .contains("stateful TA must execute once per bar")
3190 && error.span.end > error.span.start),
3191 "{body}: {errors:?}"
3192 );
3193 }
3194 let source = "language(1)\nindicator(\"Fixed\", id = \"test.fixed\")\nformat(precision = \"fixed_step:1\")\nenabled = input.bool(true)\naverage = select(enabled, ta.sma(chart.close, 2), ta.ema(chart.close, 2))\nstate seed = average\nplot(select(chart.close > 0, average, na))";
3195 let compiled = compile(source, "fixed.ind").unwrap();
3196 let mut instance = compiled.instantiate(IndicatorConfig::default()).unwrap();
3197 instance.evaluate_closed(frame(0, 0, 10.0)).unwrap();
3198 instance.evaluate_closed(frame(1, 1, -100.0)).unwrap();
3199 let output = instance.evaluate_closed(frame(2, 2, 20.0)).unwrap();
3200 assert_eq!(plot_value(&output, "plot_0"), &Value::Float(-40.0));
3201 }
3202
3203 #[test]
3204 fn work_budget_charges_scans_and_collection_copies() {
3205 let mut work = usize::MAX;
3206 assert!(!runtime::charge_work(&mut work, usize::MAX, 1));
3207 let compiled = compile("language(1)\nindicator(\"Work\", id = \"test.work\")\nformat(precision = \"fixed_step:1\")\nplot(ta.sma(chart.close, 100))", "work.ind").unwrap();
3208 let limits = RuntimeLimits {
3209 max_evaluated_nodes: 250,
3210 ..RuntimeLimits::untrusted()
3211 };
3212 let mut instance = compiled
3213 .instantiate_with_limits(IndicatorConfig::default(), limits)
3214 .unwrap();
3215 assert!((0..100).any(|index| matches!(
3216 instance.evaluate_closed(frame(index, index as i64, 10.0)),
3217 Err(RuntimeError::ExecutionLimitExceeded)
3218 )));
3219 let destructure = format!(
3220 "values = array.new(\"{}\", 0)\nfor index = 0 to 11 {{ (first, second) = values }}\nplot(chart.close)",
3221 "x".repeat(1024)
3222 );
3223 for (body, expected) in [
3224 (
3225 "values = array.new()\nfor index = 0 to 127 { values := array.push(values, index) }\nplot(array.size(values))",
3226 true,
3227 ),
3228 (
3229 "values = array.new()\nfor index = 0 to 127 { previous = values\n values := array.push(values, index) }\nplot(array.size(values))",
3230 false,
3231 ),
3232 (destructure.as_str(), false),
3233 ] {
3234 let source = format!(
3235 "language(1)\nindicator(\"Copy\", id = \"test.copy\")\nformat(precision = \"fixed_step:1\")\n{body}"
3236 );
3237 let compiled = compile(&source, "copy.ind").unwrap();
3238 let limits = RuntimeLimits {
3239 max_evaluated_nodes: 4000,
3240 ..RuntimeLimits::untrusted()
3241 };
3242 let result = compiled
3243 .instantiate_with_limits(IndicatorConfig::default(), limits)
3244 .unwrap()
3245 .evaluate_closed(frame(0, 0, 10.0));
3246 assert_eq!(result.is_ok(), expected, "{body}: {result:?}");
3247 if !expected {
3248 assert!(matches!(result, Err(RuntimeError::ExecutionLimitExceeded)));
3249 }
3250 }
3251 let compiled = compile("language(1)\nindicator(\"Depth\", id = \"test.depth\")\nformat(precision = \"fixed_step:1\")\nvalues = array.new()\nfor index = 0 to 256 { values := array.new(values) }\nplot(array.size(values))", "depth.ind").unwrap();
3252 assert!(matches!(
3253 compiled
3254 .instantiate(IndicatorConfig::default())
3255 .unwrap()
3256 .evaluate_closed(frame(0, 0, 10.0)),
3257 Err(RuntimeError::ResourceLimitExceeded("collection nesting"))
3258 ));
3259 }
3260}