1use super::language::PackageOrigin;
2use super::region_intersects_source_range;
3use super::registry::{IndicatorConfig, IndicatorId, IndicatorRegistration};
4use super::{
5 AvailabilityContext, IndicatorAttachmentMode, IndicatorAuthor, IndicatorColorSpec,
6 IndicatorConfigError, IndicatorDefinition, IndicatorFillGeometry, IndicatorFillSpec,
7 IndicatorHistoryRequirement, IndicatorInputCoverageState, IndicatorInputRequirement,
8 IndicatorInputSource, IndicatorMarkSpec, IndicatorOutputLayerSpec, IndicatorOverlayChannelSpec,
9 IndicatorPanelRecipe, IndicatorReadoutScope, IndicatorReadoutSpec, IndicatorSegmentSpec,
10 IndicatorSetting, IndicatorSettingChoice, IndicatorSettingControl, IndicatorSettingValue,
11 IndicatorSettingsContext, IndicatorSourceInput, IndicatorUnsupportedReason, OutputChannelSpec,
12 OutputValue, OutputValueType, TableColumnColor, TableColumnFormat, TableColumnSpec,
13};
14use crate::chart::Basis;
15use crate::chart::kline::composition::{
16 AxisBinding, BarMode, HistogramMode, LayerDataKind, MarkKind, PanelScaleMode,
17 PanelValueLabelMode, PanelValueLabelPolicy, PanelValuePrecision,
18};
19use crate::config::theme::{ThemeColorRole, ThemeColorShade};
20use exchange::UnixMs;
21use indicator_engine::{
22 Color, CompiledIndicator, FrameOutput, HorizontalLevelOutput,
23 IndicatorConfig as EngineIndicatorConfig, IndicatorFrame, IndicatorInstance, InputValue,
24 MarkerOutput, OhlcOutput, PlanningError, PlotDescriptor, PlotValueType, RangeOutput,
25 RegionOutput, RegionUpdate, RuntimeError, RuntimeLimits, SegmentOutput, Value, ValueType,
26};
27use rustc_hash::FxHashMap;
28use std::collections::BTreeMap;
29use std::mem::size_of;
30use std::ops::Range;
31use std::str::FromStr;
32use std::sync::Arc;
33use url::Url;
34
35const MAX_RETAINED_OUTPUT_BYTES: usize = 32 * 1024 * 1024;
36
37struct PresentationParts {
38 panel_recipe: IndicatorPanelRecipe,
39 mark_specs: Vec<IndicatorMarkSpec>,
40 segment_specs: Vec<IndicatorSegmentSpec>,
41 overlay_channels: Vec<IndicatorOverlayChannelSpec>,
42 fill_specs: Vec<IndicatorFillSpec>,
43 region_style_slots: Vec<String>,
44}
45
46#[derive(Debug, Clone, Default)]
47struct IndicatorOutputStore {
48 budget: RetainedOutputBudget,
49 series: FxHashMap<String, FxHashMap<usize, SeriesOutput>>,
50 markers: BTreeMap<String, BTreeMap<usize, MarkerOutput>>,
51 segments: BTreeMap<String, BTreeMap<usize, SegmentOutput>>,
52 levels: BTreeMap<String, HorizontalLevelOutput>,
53 ohlc: BTreeMap<String, BTreeMap<usize, OhlcOutput>>,
54 ranges: BTreeMap<String, BTreeMap<usize, RangeOutput>>,
55 regions: BTreeMap<String, BTreeMap<i64, RegionOutput>>,
56 active_region_bytes: usize,
57 persistent_regions: PersistentRegionStore,
58 preview_persistent_regions: Option<PersistentRegionStore>,
59}
60
61#[derive(Debug, Clone, Default)]
62struct RetainedOutputBudget {
63 bytes: usize,
64}
65
66impl RetainedOutputBudget {
67 fn reserve(&mut self, bytes: usize) -> Result<(), RuntimeError> {
68 let total = self
69 .bytes
70 .checked_add(bytes)
71 .ok_or(retained_output_error())?;
72 if total > MAX_RETAINED_OUTPUT_BYTES {
73 return Err(retained_output_error());
74 }
75 self.bytes = total;
76 Ok(())
77 }
78
79 fn release(&mut self, bytes: usize) -> Result<(), RuntimeError> {
80 self.bytes = self
81 .bytes
82 .checked_sub(bytes)
83 .ok_or(retained_output_error())?;
84 Ok(())
85 }
86}
87
88fn retained_output_error() -> RuntimeError {
89 RuntimeError::ResourceLimitExceeded("retained output bytes")
90}
91
92fn retained_record_bytes<T>(strings: &[&str]) -> Result<usize, RuntimeError> {
94 strings.iter().try_fold(size_of::<T>(), |bytes, text| {
95 bytes.checked_add(text.len()).ok_or(retained_output_error())
96 })
97}
98
99fn marker_retained_bytes(marker: &MarkerOutput) -> Result<usize, RuntimeError> {
100 retained_record_bytes::<(usize, MarkerOutput)>(&[
101 &marker.id,
102 marker.text.as_deref().unwrap_or_default(),
103 ])
104}
105
106fn segment_retained_bytes(segment: &SegmentOutput) -> Result<usize, RuntimeError> {
107 retained_record_bytes::<(usize, SegmentOutput)>(&[
108 &segment.id,
109 segment.text.as_deref().unwrap_or_default(),
110 ])
111}
112
113fn region_retained_bytes(region: &RegionOutput) -> Result<usize, RuntimeError> {
114 retained_record_bytes::<(i64, RegionOutput)>(&[
115 ®ion.id,
116 region.border_color_slot.as_deref().unwrap_or_default(),
117 region.fill_color_slot.as_deref().unwrap_or_default(),
118 ])
119}
120
121#[derive(Debug, Clone)]
122struct SeriesOutput {
123 value: Value,
124 color: Option<Color>,
125}
126
127#[derive(Debug, Clone)]
128struct PersistentRegion {
129 region: RegionOutput,
130 sequence: usize,
131}
132
133#[derive(Debug, Clone)]
134struct PersistentRegionStore {
135 retained_bytes: usize,
136 values: BTreeMap<(String, i64), PersistentRegion>,
137 order: BTreeMap<usize, (String, i64)>,
138 next_sequence: usize,
139 limit: usize,
140}
141
142impl Default for PersistentRegionStore {
143 fn default() -> Self {
144 Self {
145 retained_bytes: 0,
146 values: BTreeMap::new(),
147 order: BTreeMap::new(),
148 next_sequence: 0,
149 limit: RuntimeLimits::untrusted().max_output_items,
150 }
151 }
152}
153
154impl PersistentRegionStore {
155 fn region_bytes(region: &RegionOutput) -> Result<usize, RuntimeError> {
156 retained_record_bytes::<(((String, i64), PersistentRegion), (usize, (String, i64)))>(&[
157 ®ion.id,
158 ®ion.id,
159 ®ion.id,
160 region.border_color_slot.as_deref().unwrap_or_default(),
161 region.fill_color_slot.as_deref().unwrap_or_default(),
162 ])
163 }
164
165 fn upsert(
166 &mut self,
167 region: &RegionOutput,
168 budget: &mut RetainedOutputBudget,
169 ) -> Result<(), RuntimeError> {
170 let key = (region.id.clone(), region.key);
171 if let Some(existing) = self.values.get_mut(&key) {
172 let previous_bytes = Self::region_bytes(&existing.region)?;
173 budget.release(previous_bytes)?;
174 self.retained_bytes = self
175 .retained_bytes
176 .checked_sub(previous_bytes)
177 .ok_or(retained_output_error())?;
178 let bytes = Self::region_bytes(region)?;
179 budget.reserve(bytes)?;
180 self.retained_bytes = self
181 .retained_bytes
182 .checked_add(bytes)
183 .ok_or(retained_output_error())?;
184 existing.region = region.clone();
185 return Ok(());
186 }
187
188 if self.values.len() >= self.limit
189 && let Some((_, oldest_key)) = self.order.first_key_value()
190 {
191 let oldest_key = oldest_key.clone();
192 self.remove(&oldest_key.0, oldest_key.1, budget)?;
193 }
194
195 let bytes = Self::region_bytes(region)?;
196 budget.reserve(bytes)?;
197
198 self.retained_bytes = self
199 .retained_bytes
200 .checked_add(bytes)
201 .ok_or(retained_output_error())?;
202 let sequence = self.next_sequence;
203 self.next_sequence = self
204 .next_sequence
205 .checked_add(1)
206 .ok_or(retained_output_error())?;
207 self.values.insert(
208 key.clone(),
209 PersistentRegion {
210 region: region.clone(),
211 sequence,
212 },
213 );
214 self.order.insert(sequence, key);
215 Ok(())
216 }
217
218 fn remove(
219 &mut self,
220 id: &str,
221 key: i64,
222 budget: &mut RetainedOutputBudget,
223 ) -> Result<(), RuntimeError> {
224 let key = (id.to_owned(), key);
225 if let Some(region) = self.values.remove(&key) {
226 self.order.remove(®ion.sequence);
227 let bytes = Self::region_bytes(®ion.region)?;
228 budget.release(bytes)?;
229 self.retained_bytes = self
230 .retained_bytes
231 .checked_sub(bytes)
232 .ok_or(retained_output_error())?;
233 }
234 Ok(())
235 }
236
237 fn apply_updates(
238 &mut self,
239 updates: &[RegionUpdate],
240 budget: &mut RetainedOutputBudget,
241 ) -> Result<(), RuntimeError> {
242 for update in updates {
243 match update {
244 RegionUpdate::Upsert(region) => self.upsert(region, budget)?,
245 RegionUpdate::Remove { id, key } => self.remove(id, *key, budget)?,
246 }
247 }
248 Ok(())
249 }
250}
251
252impl IndicatorOutputStore {
253 fn series_value(&self, plot: &str, index: usize) -> Option<&Value> {
254 Some(&self.series.get(plot)?.get(&index)?.value)
255 }
256
257 fn series_color(&self, plot: &str, index: usize) -> Option<Color> {
258 self.series.get(plot)?.get(&index)?.color
259 }
260
261 fn marker_values_at(&self, index: usize) -> Vec<MarkerOutput> {
262 self.markers
263 .values()
264 .filter_map(|values| values.get(&index))
265 .cloned()
266 .collect()
267 }
268
269 fn segment_values(&self) -> impl Iterator<Item = &SegmentOutput> {
270 self.segments.values().flat_map(|values| values.values())
271 }
272
273 fn level_values(&self) -> Vec<HorizontalLevelOutput> {
274 self.levels.values().cloned().collect()
275 }
276
277 fn ohlc_value(&self, plot: &str, index: usize) -> Option<OhlcOutput> {
278 self.ohlc.get(plot)?.get(&index).cloned()
279 }
280
281 fn range_value(&self, plot: &str, index: usize) -> Option<RangeOutput> {
282 self.ranges.get(plot)?.get(&index).cloned()
283 }
284
285 fn region_values_in_range(&self, source_range: Option<&Range<usize>>) -> Vec<RegionOutput> {
286 let include = |region: &RegionOutput| {
287 source_range.is_none_or(|range| region_intersects_source_range(region, range))
288 };
289
290 let mut regions = self
291 .regions
292 .iter()
293 .flat_map(|(id, values)| values.iter().map(move |(key, region)| (id, key, region)))
294 .filter(|(_, _, region)| include(region))
295 .map(|(id, key, region)| ((id.clone(), *key), region.clone()))
296 .collect::<BTreeMap<_, _>>();
297 let persistent_regions = self
298 .preview_persistent_regions
299 .as_ref()
300 .unwrap_or(&self.persistent_regions);
301
302 for ((id, key), value) in &persistent_regions.values {
303 if !include(&value.region) {
304 continue;
305 }
306 regions.insert((id.clone(), *key), value.region.clone());
307 }
308 regions.into_values().collect()
309 }
310
311 #[cfg(test)]
312 fn region_values(&self) -> Vec<RegionOutput> {
313 self.region_values_in_range(None)
314 }
315
316 fn apply_output(&mut self, output: FrameOutput, preview: bool) -> Result<(), RuntimeError> {
317 if let Some(previous) = self.preview_persistent_regions.take() {
318 self.budget.release(previous.retained_bytes)?;
319 }
320 self.budget.release(self.active_region_bytes)?;
321 self.active_region_bytes = 0;
322 self.regions.clear();
323
324 for values in self.series.values_mut() {
325 if values.remove(&output.index).is_some() {
326 self.budget.release(size_of::<(usize, SeriesOutput)>())?;
327 }
328 }
329 for values in self.markers.values_mut() {
330 if let Some(previous) = values.remove(&output.index) {
331 self.budget.release(marker_retained_bytes(&previous)?)?;
332 }
333 }
334 for values in self.segments.values_mut() {
335 if let Some(previous) = values.remove(&output.index) {
336 self.budget.release(segment_retained_bytes(&previous)?)?;
337 }
338 }
339 for values in self.ohlc.values_mut() {
340 if let Some(previous) = values.remove(&output.index) {
341 self.budget
342 .release(retained_record_bytes::<(usize, OhlcOutput)>(&[
343 &previous.id
344 ])?)?;
345 }
346 }
347 for values in self.ranges.values_mut() {
348 if let Some(previous) = values.remove(&output.index) {
349 self.budget
350 .release(retained_record_bytes::<(usize, RangeOutput)>(&[
351 &previous.id
352 ])?)?;
353 }
354 }
355
356 for plot in output.plots.iter() {
357 if self
358 .series
359 .get_mut(&plot.id)
360 .and_then(|values| values.remove(&output.index))
361 .is_some()
362 {
363 self.budget.release(size_of::<(usize, SeriesOutput)>())?;
364 }
365
366 if !self.series.contains_key(&plot.id) {
367 self.budget.reserve(retained_record_bytes::<(
368 String,
369 FxHashMap<usize, SeriesOutput>,
370 )>(&[&plot.id])?)?;
371 }
372
373 self.budget.reserve(size_of::<(usize, SeriesOutput)>())?;
374 self.series.entry(plot.id.clone()).or_default().insert(
375 output.index,
376 SeriesOutput {
377 value: plot.value.clone(),
378 color: plot.color,
379 },
380 );
381 }
382
383 for marker in output.markers.iter() {
384 if let Some(previous) = self
385 .markers
386 .get_mut(&marker.id)
387 .and_then(|values| values.remove(&output.index))
388 {
389 self.budget.release(marker_retained_bytes(&previous)?)?;
390 }
391 if !self.markers.contains_key(&marker.id) {
392 self.budget.reserve(retained_record_bytes::<(
393 String,
394 BTreeMap<usize, MarkerOutput>,
395 )>(&[&marker.id])?)?;
396 }
397
398 self.budget.reserve(marker_retained_bytes(marker)?)?;
399 self.markers
400 .entry(marker.id.clone())
401 .or_default()
402 .insert(output.index, marker.clone());
403 }
404
405 for segment in output.segments.iter() {
406 if let Some(previous) = self
407 .segments
408 .get_mut(&segment.id)
409 .and_then(|values| values.remove(&output.index))
410 {
411 self.budget.release(segment_retained_bytes(&previous)?)?;
412 }
413
414 if !self.segments.contains_key(&segment.id) {
415 self.budget.reserve(retained_record_bytes::<(
416 String,
417 BTreeMap<usize, SegmentOutput>,
418 )>(&[&segment.id])?)?;
419 }
420
421 self.budget.reserve(segment_retained_bytes(segment)?)?;
422 self.segments
423 .entry(segment.id.clone())
424 .or_default()
425 .insert(output.index, segment.clone());
426 }
427
428 for level in output.levels.iter() {
429 if let Some(previous) = self.levels.remove(&level.id) {
430 self.budget
431 .release(retained_record_bytes::<(String, HorizontalLevelOutput)>(
432 &[&previous.id, &previous.id],
433 )?)?;
434 }
435
436 self.budget
437 .reserve(retained_record_bytes::<(String, HorizontalLevelOutput)>(
438 &[&level.id, &level.id],
439 )?)?;
440 self.levels.insert(level.id.clone(), level.clone());
441 }
442
443 for ohlc in output.ohlc.iter() {
444 if let Some(previous) = self
445 .ohlc
446 .get_mut(&ohlc.id)
447 .and_then(|values| values.remove(&output.index))
448 {
449 self.budget
450 .release(retained_record_bytes::<(usize, OhlcOutput)>(&[
451 &previous.id
452 ])?)?;
453 }
454
455 if !self.ohlc.contains_key(&ohlc.id) {
456 self.budget.reserve(retained_record_bytes::<(
457 String,
458 BTreeMap<usize, OhlcOutput>,
459 )>(&[&ohlc.id])?)?;
460 }
461
462 self.budget
463 .reserve(retained_record_bytes::<(usize, OhlcOutput)>(&[&ohlc.id])?)?;
464 self.ohlc
465 .entry(ohlc.id.clone())
466 .or_default()
467 .insert(output.index, ohlc.clone());
468 }
469
470 for range in output.ranges.iter() {
471 if let Some(previous) = self
472 .ranges
473 .get_mut(&range.id)
474 .and_then(|values| values.remove(&output.index))
475 {
476 self.budget
477 .release(retained_record_bytes::<(usize, RangeOutput)>(&[
478 &previous.id
479 ])?)?;
480 }
481
482 if !self.ranges.contains_key(&range.id) {
483 self.budget.reserve(retained_record_bytes::<(
484 String,
485 BTreeMap<usize, RangeOutput>,
486 )>(&[&range.id])?)?;
487 }
488
489 self.budget
490 .reserve(retained_record_bytes::<(usize, RangeOutput)>(&[&range.id])?)?;
491 self.ranges
492 .entry(range.id.clone())
493 .or_default()
494 .insert(output.index, range.clone());
495 }
496
497 let previous_bytes = self.budget.bytes;
498 for region in output.regions.iter() {
499 if let Some(previous) = self
500 .regions
501 .get_mut(®ion.id)
502 .and_then(|values| values.remove(®ion.key))
503 {
504 self.budget.release(region_retained_bytes(&previous)?)?;
505 }
506 if !self.regions.contains_key(®ion.id) {
507 self.budget.reserve(retained_record_bytes::<(
508 String,
509 BTreeMap<i64, RegionOutput>,
510 )>(&[®ion.id])?)?;
511 }
512 self.budget.reserve(region_retained_bytes(region)?)?;
513 self.regions
514 .entry(region.id.clone())
515 .or_default()
516 .insert(region.key, region.clone());
517 }
518
519 self.active_region_bytes = self
520 .budget
521 .bytes
522 .checked_sub(previous_bytes)
523 .ok_or(retained_output_error())?;
524 if preview {
525 if !output.region_updates.is_empty() {
526 self.budget
527 .reserve(self.persistent_regions.retained_bytes)?;
528 let mut persistent_regions = self.persistent_regions.clone();
529 persistent_regions.apply_updates(&output.region_updates, &mut self.budget)?;
530 self.preview_persistent_regions = Some(persistent_regions);
531 }
532 } else {
533 self.persistent_regions
534 .apply_updates(&output.region_updates, &mut self.budget)?;
535 }
536 Ok(())
537 }
538}
539
540fn validated_http_url(
541 value: &str,
542 indicator: impl std::fmt::Display,
543 field: &str,
544) -> Result<String, String> {
545 let url = Url::parse(value)
546 .map_err(|_| format!("{indicator}: indicator {field} must be a valid URL"))?;
547
548 if !matches!(url.scheme(), "http" | "https") || url.host_str().is_none() {
549 return Err(format!(
550 "{indicator}: indicator {field} must be an absolute http(s) URL"
551 ));
552 }
553 Ok(value.to_owned())
554}
555
556fn definition_from_compiled(compiled: &CompiledIndicator) -> Result<IndicatorDefinition, String> {
557 let requirements = compiled.requirements;
558 let mut inputs = Vec::new();
559 if requirements.ohlc {
560 inputs.push(IndicatorInputRequirement::required(
561 IndicatorInputSource::Klines,
562 ));
563 }
564 if requirements.needs_directional_volume() {
565 let requirement = if compiled.descriptor.optional_trades {
566 IndicatorInputRequirement::optional(IndicatorInputSource::DirectionalVolume)
567 } else {
568 IndicatorInputRequirement::required(IndicatorInputSource::DirectionalVolume)
569 };
570 inputs.push(requirement);
571 }
572 if requirements.trades {
573 inputs.push(IndicatorInputRequirement::required(
574 IndicatorInputSource::Trades,
575 ));
576 }
577 if requirements.open_interest {
578 inputs.push(IndicatorInputRequirement::required(
579 IndicatorInputSource::OpenInterest,
580 ));
581 }
582
583 let style_slots = descriptor_styles(compiled)?;
584 let outputs = descriptor_outputs(compiled);
585 let author = compiled
586 .descriptor
587 .author
588 .as_ref()
589 .map(|author| -> Result<IndicatorAuthor, String> {
590 let url = validated_http_url(&author.url, &compiled.descriptor.id, "author_url")?;
591 Ok(IndicatorAuthor {
592 name: author.name.clone(),
593 url,
594 })
595 })
596 .transpose()?;
597 let source = compiled
598 .descriptor
599 .source
600 .as_ref()
601 .map(|source| validated_http_url(source, &compiled.descriptor.id, "source"))
602 .transpose()?;
603 let PresentationParts {
604 panel_recipe,
605 mark_specs,
606 segment_specs,
607 overlay_channels,
608 fill_specs,
609 region_style_slots,
610 } = descriptor_view(compiled, &style_slots)?;
611
612 let definition = IndicatorDefinition::new(
613 compiled.descriptor.title.clone(),
614 inputs,
615 outputs,
616 panel_recipe,
617 )
618 .with_author(author)
619 .with_description(compiled.descriptor.description.clone())
620 .with_source(source)
621 .with_sourceable(compiled.descriptor.sourceable)
622 .with_source_inputs(
623 compiled
624 .descriptor
625 .series_inputs
626 .iter()
627 .map(|input| IndicatorSourceInput {
628 id: input.id.clone(),
629 label: input.label.clone(),
630 default_label: input
631 .default_field
632 .source_input_label()
633 .unwrap_or("Declared default")
634 .to_owned(),
635 allow_indicator: input.allow_indicator,
636 })
637 .collect::<Vec<_>>(),
638 )
639 .with_attachment_mode(
640 compiled
641 .descriptor
642 .presentation
643 .attachment
644 .map(IndicatorAttachmentMode::from)
645 .unwrap_or(IndicatorAttachmentMode::Panel),
646 )
647 .with_style_slots(style_slots)
648 .with_region_style_slots(region_style_slots)
649 .with_display_specs(mark_specs, overlay_channels, fill_specs)
650 .with_segment_specs(segment_specs);
651
652 definition.validate().map_err(|error| {
653 format!(
654 "{}: invalid indicator definition: {error:?}",
655 compiled.descriptor.id
656 )
657 })?;
658 Ok(definition)
659}
660
661fn descriptor_styles(compiled: &CompiledIndicator) -> Result<Vec<IndicatorColorSpec>, String> {
662 compiled
663 .descriptor
664 .presentation
665 .styles
666 .iter()
667 .map(|style| {
668 Ok(IndicatorColorSpec::new(
669 style.id.clone(),
670 style.label.clone(),
671 ThemeColorRole::from_str(&style.role).map_err(|error| error.to_string())?,
672 ThemeColorShade::from_str(&style.shade).map_err(|error| error.to_string())?,
673 )
674 .with_required_input(style.required_input.map(IndicatorInputSource::from)))
675 })
676 .collect::<Result<Vec<_>, String>>()
677}
678
679impl From<&PlotDescriptor> for OutputValueType {
680 fn from(plot: &PlotDescriptor) -> Self {
681 match plot.value_type {
682 PlotValueType::Number => Self::Number,
683 PlotValueType::Boolean => Self::Boolean,
684 PlotValueType::Ohlc => Self::Ohlc,
685 PlotValueType::Range => Self::Range,
686 }
687 }
688}
689
690impl From<&PlotDescriptor> for OutputChannelSpec {
691 fn from(plot: &PlotDescriptor) -> Self {
692 Self {
693 id: plot.id.clone(),
694 label: plot.label.clone(),
695 value_type: plot.into(),
696 precision: plot
697 .output_precision
698 .unwrap_or(indicator_engine::OutputPrecision::Instrument),
699 line_width: plot.width,
700 readout: plot.readout,
701 style_slot: plot.color_slot.clone(),
702 offset_bars: plot.offset_bars,
703 style_by: plot.style_by.clone(),
704 independent_scale: matches!(plot.scale, indicator_engine::PlotScale::Independent),
705 }
706 }
707}
708
709fn descriptor_outputs(compiled: &CompiledIndicator) -> Vec<OutputChannelSpec> {
710 compiled
711 .descriptor
712 .plots
713 .iter()
714 .map(OutputChannelSpec::from)
715 .collect()
716}
717
718fn descriptor_view(
719 compiled: &CompiledIndicator,
720 style_slots: &[IndicatorColorSpec],
721) -> Result<PresentationParts, String> {
722 let presentation = &compiled.descriptor.presentation;
723 let layers = presentation
724 .layers
725 .iter()
726 .map(|layer| {
727 let plot = compiled
728 .descriptor
729 .plots
730 .iter()
731 .find(|plot| plot.id == layer.channel)
732 .ok_or_else(|| {
733 format!(
734 "{}: presentation layer references unknown output '{}'",
735 compiled.descriptor.id, layer.channel
736 )
737 })?;
738 if matches!(plot.value_type, PlotValueType::Boolean) {
739 return Err(format!(
740 "{}: boolean output '{}' cannot be rendered as a layer",
741 compiled.descriptor.id, layer.channel
742 ));
743 }
744 let data_kind = layer.data_kind.ok_or_else(|| {
745 format!(
746 "{}: presentation layer '{}' is missing its data kind",
747 compiled.descriptor.id, layer.channel
748 )
749 })?;
750 let mark = layer.mark.ok_or_else(|| {
751 format!(
752 "{}: presentation layer '{}' is missing its mark",
753 compiled.descriptor.id, layer.channel
754 )
755 })?;
756 let axis = layer.axis.ok_or_else(|| {
757 format!(
758 "{}: presentation layer '{}' is missing its axis",
759 compiled.descriptor.id, layer.channel
760 )
761 })?;
762 Ok(IndicatorOutputLayerSpec {
763 channel_id: layer.channel.clone(),
764 layer_name: layer.name.clone(),
765 pane: layer.pane,
766 data_kind: data_kind.into(),
767 mark: mark.into(),
768 axis: axis.into(),
769 })
770 })
771 .collect::<Result<Vec<_>, String>>()?;
772 let mut marks = presentation
773 .marks
774 .iter()
775 .map(|mark| IndicatorMarkSpec {
776 label: mark.label.clone(),
777 mark: mark.kind.into(),
778 source_channel: mark.source_channel.clone(),
779 required_input: mark.required_input.map(IndicatorInputSource::from),
780 fallback_mark: mark.fallback_mark.map(MarkKind::from),
781 color_slot: mark.color_slot.clone(),
782 true_color_slot: mark.true_color_slot.clone(),
783 false_color_slot: mark.false_color_slot.clone(),
784 highlight_by: mark.highlight_by.clone(),
785 highlight_color_slot: mark.highlight_color_slot.clone(),
786 })
787 .collect::<Vec<_>>();
788 for plot in &compiled.descriptor.plots {
793 let (Some(true_slot), Some(false_slot)) = (
794 plot.true_color_slot.as_deref(),
795 plot.false_color_slot.as_deref(),
796 ) else {
797 continue;
798 };
799 if plot.style_by.is_none() || marks.iter().any(|mark| mark.source_channel == plot.id) {
800 continue;
801 }
802 let color_slot = plot.color_slot.as_deref().unwrap_or(true_slot).to_owned();
803 let mark = presentation
804 .layers
805 .iter()
806 .find(|layer| layer.channel == plot.id)
807 .and_then(|layer| layer.mark)
808 .or(plot.presentation_mark)
809 .unwrap_or(indicator_engine::PresentationMarkKind::Line);
810 marks.push(IndicatorMarkSpec {
811 label: plot.label.clone(),
812 mark: mark.into(),
813 source_channel: plot.id.clone(),
814 required_input: None,
815 fallback_mark: None,
816 color_slot,
817 true_color_slot: Some(true_slot.to_owned()),
818 false_color_slot: Some(false_slot.to_owned()),
819 highlight_by: None,
820 highlight_color_slot: None,
821 });
822 }
823 let mut segments = presentation
824 .segments
825 .iter()
826 .map(|segment| IndicatorSegmentSpec {
827 id: segment.id.clone(),
828 label: segment.label.clone(),
829 color_slot: segment.color_slot.clone(),
830 line_width: segment.line_width,
831 })
832 .collect::<Vec<_>>();
833 let overlays = compiled
834 .descriptor
835 .plots
836 .iter()
837 .filter(|plot| plot.overlay && plot.value_type == PlotValueType::Number)
838 .filter_map(|plot| {
839 let color_slot = plot.color_slot.clone()?;
840 Some(IndicatorOverlayChannelSpec {
841 label: plot.label.clone(),
842 source: plot.id.clone(),
843 line_width: plot.width,
844 color_role: style_role_for_plot(style_slots, &color_slot),
845 color_slot,
846 style_by: plot.style_by.clone(),
847 })
848 })
849 .collect::<Vec<_>>();
850 let fills = presentation
851 .fills
852 .iter()
853 .map(|fill| IndicatorFillSpec {
854 upper: fill.upper.clone(),
855 lower: fill.lower.clone(),
856 upper_offset_bars: fill.upper_offset_bars,
857 lower_offset_bars: fill.lower_offset_bars,
858 style_by: fill.style_by.clone(),
859 geometry: match fill.geometry {
860 indicator_engine::PresentationGeometry::PanelBand => {
861 IndicatorFillGeometry::PanelBand
862 }
863 indicator_engine::PresentationGeometry::SeriesBand => {
864 IndicatorFillGeometry::SeriesBand
865 }
866 },
867 color_slot: fill.color_slot.clone(),
868 true_color_slot: fill.true_color_slot.clone(),
869 false_color_slot: fill.false_color_slot.clone(),
870 opacity: fill.opacity,
871 })
872 .collect::<Vec<_>>();
873 let readouts = presentation
874 .readouts
875 .iter()
876 .map(|readout| IndicatorReadoutSpec {
877 label: readout.label.clone(),
878 source: readout.channel.clone(),
879 color_role: style_role_for_plot(style_slots, &readout.color_slot),
880 color_slot: readout.color_slot.clone(),
881 scope: readout
882 .mark
883 .map(|mark| IndicatorReadoutScope::Mark(mark.into()))
884 .unwrap_or(IndicatorReadoutScope::Always),
885 })
886 .collect::<Vec<_>>();
887 let has_numeric_output = compiled
888 .descriptor
889 .plots
890 .iter()
891 .any(|plot| !matches!(plot.value_type, PlotValueType::Boolean));
892 let view_kind = if matches!(
893 presentation.view,
894 indicator_engine::PresentationViewKind::Plot
895 ) && !has_numeric_output
896 && presentation.columns.is_empty()
897 {
898 indicator_engine::PresentationViewKind::Table
899 } else {
900 presentation.view
901 };
902 let panel_title = presentation
903 .panel_title
904 .clone()
905 .unwrap_or_else(|| compiled.descriptor.title.clone());
906 let layer_name = presentation
907 .layer_name
908 .clone()
909 .unwrap_or_else(|| compiled.descriptor.title.clone());
910 let panel_recipe = match view_kind {
911 indicator_engine::PresentationViewKind::Table => IndicatorPanelRecipe::Table {
912 panel_title,
913 columns: presentation
914 .columns
915 .iter()
916 .map(|column| TableColumnSpec {
917 label: column.label.clone(),
918 value: column.channel.clone(),
919 format: parse_table_format(&column.format),
920 color: match &column.color {
921 indicator_engine::PresentationColumnColor::Static { slot } => {
922 TableColumnColor::Static { slot: slot.clone() }
923 }
924 indicator_engine::PresentationColumnColor::Signed {
925 positive_slot,
926 negative_slot,
927 } => TableColumnColor::Signed {
928 positive_slot: positive_slot.clone(),
929 negative_slot: negative_slot.clone(),
930 },
931 },
932 })
933 .collect(),
934 },
935 indicator_engine::PresentationViewKind::PrimaryOverlay => {
936 if layers.is_empty() {
937 return Err(format!(
938 "{}: presentation has no numeric output layers",
939 compiled.descriptor.id
940 ));
941 }
942 IndicatorPanelRecipe::PrimaryOverlay { layer_name, layers }
943 }
944 indicator_engine::PresentationViewKind::Plot => {
945 let value_precision = presentation
946 .value_precision
947 .map(PanelValuePrecision::from)
948 .ok_or_else(|| {
949 format!(
950 "{}: numeric plot presentation requires format(precision = ...)",
951 compiled.descriptor.id
952 )
953 })?;
954 IndicatorPanelRecipe::Plot {
955 panel_title,
956 layer_name,
957 value_precision,
958 value_label_policy: PanelValueLabelPolicy {
959 axis_mode: presentation
960 .axis_label_mode
961 .map(PanelValueLabelMode::from)
962 .unwrap_or(PanelValueLabelMode::Compact),
963 header_mode: presentation
964 .header_label_mode
965 .map(PanelValueLabelMode::from)
966 .unwrap_or(PanelValueLabelMode::Commas),
967 max_decimals: presentation.max_decimals,
968 },
969 preferred_scale: presentation
970 .preferred_scale
971 .map(PanelScaleMode::from)
972 .unwrap_or(PanelScaleMode::FitVisible),
973 layers,
974 readouts,
975 }
976 }
977 };
978 if matches!(view_kind, indicator_engine::PresentationViewKind::Table) {
979 marks.clear();
980 segments.clear();
981 }
982 Ok(PresentationParts {
983 panel_recipe,
984 mark_specs: marks,
985 segment_specs: segments,
986 overlay_channels: overlays,
987 fill_specs: fills,
988 region_style_slots: presentation.region_style_slots.clone(),
989 })
990}
991
992fn style_role_for_plot(style_slots: &[IndicatorColorSpec], slot: &str) -> ThemeColorRole {
993 style_slots
994 .iter()
995 .find(|style| style.id == slot)
996 .map(|style| style.default_role)
997 .unwrap_or(ThemeColorRole::Neutral)
998}
999
1000impl From<indicator_engine::PresentationDataKind> for LayerDataKind {
1001 fn from(kind: indicator_engine::PresentationDataKind) -> Self {
1002 match kind {
1003 indicator_engine::PresentationDataKind::Scalar => Self::Scalar,
1004 indicator_engine::PresentationDataKind::Histogram => Self::Histogram,
1005 indicator_engine::PresentationDataKind::Ohlc => Self::Ohlc,
1006 indicator_engine::PresentationDataKind::Range => Self::Range,
1007 }
1008 }
1009}
1010
1011impl From<indicator_engine::PresentationAttachmentMode> for IndicatorAttachmentMode {
1012 fn from(mode: indicator_engine::PresentationAttachmentMode) -> Self {
1013 match mode {
1014 indicator_engine::PresentationAttachmentMode::Overlay => Self::Overlay,
1015 indicator_engine::PresentationAttachmentMode::Panel => Self::Panel,
1016 }
1017 }
1018}
1019
1020impl From<indicator_engine::PresentationMarkKind> for MarkKind {
1021 fn from(mark: indicator_engine::PresentationMarkKind) -> Self {
1022 match mark {
1023 indicator_engine::PresentationMarkKind::Candle => Self::Candle,
1024 indicator_engine::PresentationMarkKind::RegularBar => Self::Bar(BarMode::Regular),
1025 indicator_engine::PresentationMarkKind::Histogram => {
1026 Self::Bar(BarMode::Histogram(HistogramMode::Plain))
1027 }
1028 indicator_engine::PresentationMarkKind::HistogramSignedOverlay => {
1029 Self::Bar(BarMode::Histogram(HistogramMode::SignedOverlay))
1030 }
1031 indicator_engine::PresentationMarkKind::Line => Self::Line,
1032 indicator_engine::PresentationMarkKind::Step => Self::StepLine,
1033 indicator_engine::PresentationMarkKind::Area => Self::Area,
1034 indicator_engine::PresentationMarkKind::Point => Self::Point,
1035 indicator_engine::PresentationMarkKind::Cross => Self::Cross,
1036 indicator_engine::PresentationMarkKind::Segment => Self::Segment,
1037 indicator_engine::PresentationMarkKind::HorizontalLevel => Self::HorizontalLevel,
1038 }
1039 }
1040}
1041
1042impl From<indicator_engine::PresentationInputSource> for IndicatorInputSource {
1043 fn from(source: indicator_engine::PresentationInputSource) -> Self {
1044 match source {
1045 indicator_engine::PresentationInputSource::Klines => Self::Klines,
1046 indicator_engine::PresentationInputSource::DirectionalVolume => Self::DirectionalVolume,
1047 indicator_engine::PresentationInputSource::Trades => Self::Trades,
1048 indicator_engine::PresentationInputSource::OpenInterest => Self::OpenInterest,
1049 }
1050 }
1051}
1052
1053impl From<indicator_engine::PresentationAxis> for AxisBinding {
1054 fn from(axis: indicator_engine::PresentationAxis) -> Self {
1055 match axis {
1056 indicator_engine::PresentationAxis::Primary => Self::Primary,
1057 indicator_engine::PresentationAxis::Secondary => Self::Secondary,
1058 }
1059 }
1060}
1061
1062impl From<indicator_engine::PresentationPrecision> for PanelValuePrecision {
1063 fn from(precision: indicator_engine::PresentationPrecision) -> Self {
1064 match precision {
1065 indicator_engine::PresentationPrecision::BaseTickerMinTick => Self::BaseTickerMinTick,
1066 indicator_engine::PresentationPrecision::BaseTickerMinQty => Self::BaseTickerMinQty,
1067 indicator_engine::PresentationPrecision::FixedStep(step) => Self::FixedStep(step),
1068 }
1069 }
1070}
1071
1072impl From<indicator_engine::PresentationLabelMode> for PanelValueLabelMode {
1073 fn from(mode: indicator_engine::PresentationLabelMode) -> Self {
1074 match mode {
1075 indicator_engine::PresentationLabelMode::Compact => Self::Compact,
1076 indicator_engine::PresentationLabelMode::Commas => Self::Commas,
1077 indicator_engine::PresentationLabelMode::Abbreviated => Self::Abbreviated,
1078 }
1079 }
1080}
1081
1082impl From<indicator_engine::PresentationScale> for PanelScaleMode {
1083 fn from(scale: indicator_engine::PresentationScale) -> Self {
1084 match scale {
1085 indicator_engine::PresentationScale::Absolute => Self::Absolute,
1086 indicator_engine::PresentationScale::FitVisible => Self::FitVisible,
1087 indicator_engine::PresentationScale::FitVisibleIncludeZero => {
1088 Self::FitVisibleIncludeZero
1089 }
1090 indicator_engine::PresentationScale::Logarithmic => Self::Logarithmic,
1091 indicator_engine::PresentationScale::PercentFromBase => Self::PercentFromBase,
1092 }
1093 }
1094}
1095
1096fn parse_table_format(value: &str) -> TableColumnFormat {
1097 match value.to_ascii_lowercase().as_str() {
1098 "integer" => TableColumnFormat::Integer,
1099 "percent" => TableColumnFormat::Percent,
1100 "boolean" => TableColumnFormat::Boolean,
1101 _ => TableColumnFormat::Quantity,
1102 }
1103}
1104
1105fn input_value_to_json(value: &InputValue) -> serde_json::Value {
1106 match value {
1107 InputValue::Int(value) => (*value).into(),
1108 InputValue::Float(value) => (*value).into(),
1109 InputValue::Bool(value) => (*value).into(),
1110 InputValue::String(value) => value.clone().into(),
1111 InputValue::Color(value) => (*value).into(),
1112 InputValue::Time(value) => (*value).into(),
1113 }
1114}
1115
1116fn integer_bound(value: Option<f64>, round: fn(f64) -> f64) -> Option<i64> {
1117 let value = value.filter(|value| value.is_finite()).map(round)?;
1118 Some(if value <= i64::MIN as f64 {
1119 i64::MIN
1120 } else if value >= i64::MAX as f64 {
1121 i64::MAX
1122 } else {
1123 value as i64
1124 })
1125}
1126
1127fn setting_from_input(
1128 input: &indicator_engine::InputDescriptor,
1129 config: &serde_json::Value,
1130) -> IndicatorSetting {
1131 let configured = config
1132 .as_object()
1133 .and_then(|values| values.get(&input.id))
1134 .and_then(|value| input_value_from_json(&input.value_type, value))
1135 .and_then(|value| input.accepts(&value).then_some(value))
1136 .unwrap_or_else(|| input.default.clone());
1137 let (value, control) = match configured {
1138 InputValue::Int(value) => (
1139 IndicatorSettingValue::I64(value),
1140 IndicatorSettingControl::SliderI64 {
1141 min: integer_bound(input.min, f64::ceil),
1142 max: integer_bound(input.max, f64::floor),
1143 },
1144 ),
1145 InputValue::Float(value) => (
1146 IndicatorSettingValue::F64(value),
1147 IndicatorSettingControl::SliderF64 {
1148 min: input.min,
1149 max: input.max,
1150 step: input.step.unwrap_or(0.01).max(f64::EPSILON),
1151 },
1152 ),
1153 InputValue::Bool(value) => (
1154 IndicatorSettingValue::Bool(value),
1155 IndicatorSettingControl::Toggle,
1156 ),
1157 InputValue::String(value) => {
1158 if input.options.is_empty() {
1159 (
1160 IndicatorSettingValue::Text(value),
1161 IndicatorSettingControl::Text,
1162 )
1163 } else {
1164 let choices = input
1165 .options
1166 .iter()
1167 .map(|option| IndicatorSettingChoice {
1168 id: option.id.clone(),
1169 label: option.label.clone(),
1170 })
1171 .collect();
1172 (
1173 IndicatorSettingValue::Choice(value),
1174 IndicatorSettingControl::Choices(choices),
1175 )
1176 }
1177 }
1178 InputValue::Color(value) => (
1179 IndicatorSettingValue::Color(value),
1180 IndicatorSettingControl::Color,
1181 ),
1182 InputValue::Time(value) => (
1183 IndicatorSettingValue::I64(value),
1184 IndicatorSettingControl::SliderI64 {
1185 min: integer_bound(input.min, f64::ceil),
1186 max: integer_bound(input.max, f64::floor),
1187 },
1188 ),
1189 };
1190 IndicatorSetting {
1191 id: input.id.clone(),
1192 label: input.label.clone(),
1193 tooltip: input.tooltip.clone(),
1194 group: input.group.clone(),
1195 value,
1196 control,
1197 }
1198}
1199
1200fn setting_value_to_json(
1201 input: &indicator_engine::InputDescriptor,
1202 value: IndicatorSettingValue,
1203) -> Option<serde_json::Value> {
1204 match (input.value_type, value) {
1205 (indicator_engine::ValueType::Int, IndicatorSettingValue::I64(value)) => {
1206 Some(serde_json::Value::from(value))
1207 }
1208 (indicator_engine::ValueType::Float, IndicatorSettingValue::F64(value)) => {
1209 Some(serde_json::Value::from(value))
1210 }
1211 (indicator_engine::ValueType::Bool, IndicatorSettingValue::Bool(value)) => {
1212 Some(serde_json::Value::from(value))
1213 }
1214 (indicator_engine::ValueType::String, IndicatorSettingValue::Text(value))
1215 | (indicator_engine::ValueType::String, IndicatorSettingValue::Choice(value)) => {
1216 Some(serde_json::Value::from(value))
1217 }
1218 (indicator_engine::ValueType::Time, IndicatorSettingValue::I64(value)) => {
1219 Some(serde_json::Value::from(value))
1220 }
1221 (indicator_engine::ValueType::Color, IndicatorSettingValue::Color(value)) => {
1222 Some(serde_json::Value::from(value))
1223 }
1224 _ => None,
1225 }
1226}
1227
1228pub fn registration(
1232 compiled: CompiledIndicator,
1233 origin: PackageOrigin,
1234) -> Result<IndicatorRegistration, Vec<String>> {
1235 let descriptor = compiled.descriptor.clone();
1236 let origin_label = match origin {
1237 PackageOrigin::Bundled => "bundled",
1238 PackageOrigin::Installed => "installed",
1239 };
1240 let id = IndicatorId::parse(descriptor.id.as_str()).map_err(|error| {
1241 vec![format!(
1242 "{origin_label} indicator source has invalid ID: {error}"
1243 )]
1244 })?;
1245 let definition = definition_from_compiled(&compiled).map_err(|error| vec![error])?;
1246 let registration =
1247 IndicatorRegistration::from_parts(id, origin, definition, Arc::new(compiled));
1248 registration
1249 .validate_config(®istration.default_config())
1250 .map_err(|error| {
1251 vec![format!(
1252 "{origin_label} indicator source '{}': {error}",
1253 registration.id.id()
1254 )]
1255 })?;
1256 Ok(registration)
1257}
1258
1259impl IndicatorRegistration {
1260 pub fn default_config(&self) -> IndicatorConfig {
1261 let values = self
1262 .compiled
1263 .descriptor
1264 .inputs
1265 .iter()
1266 .map(|input| (input.id.clone(), input_value_to_json(&input.default)))
1267 .collect();
1268 IndicatorConfig::from_value(serde_json::Value::Object(values))
1269 }
1270
1271 pub fn normalize_config(&self, config: IndicatorConfig) -> IndicatorConfig {
1272 let mut values = config.value().as_object().cloned().unwrap_or_default();
1273 for input in &self.compiled.descriptor.inputs {
1274 values
1275 .entry(input.id.clone())
1276 .or_insert_with(|| input_value_to_json(&input.default));
1277 }
1278 IndicatorConfig::from_value(serde_json::Value::Object(values))
1279 }
1280
1281 pub fn settings(
1282 &self,
1283 config: IndicatorConfig,
1284 context: IndicatorSettingsContext,
1285 ) -> Result<Vec<IndicatorSetting>, IndicatorConfigError> {
1286 let values = config.value();
1287 let inputs = &self.compiled.descriptor.inputs;
1288 Ok(inputs
1289 .iter()
1290 .filter(|input| input_is_visible(input, inputs, values, &context.input_coverage))
1291 .map(|input| setting_from_input(input, values))
1292 .collect())
1293 }
1294
1295 pub fn with_setting(
1296 &self,
1297 config: IndicatorConfig,
1298 id: &str,
1299 value: IndicatorSettingValue,
1300 ) -> Option<IndicatorConfig> {
1301 let mut object = config.value().as_object().cloned().unwrap_or_default();
1302 let input = self
1303 .compiled
1304 .descriptor
1305 .inputs
1306 .iter()
1307 .find(|input| input.id == id)?;
1308 let value = setting_value_to_json(input, value)?;
1309 object.insert(id.to_owned(), value);
1310 Some(IndicatorConfig::from_value(serde_json::Value::Object(
1311 object,
1312 )))
1313 }
1314
1315 pub fn history_requirement(
1316 &self,
1317 config: IndicatorConfig,
1318 basis: Basis,
1319 reference_time: exchange::UnixMs,
1320 ) -> IndicatorHistoryRequirement {
1321 self.history_requirement_with_warmup(config, basis, reference_time, None)
1322 }
1323
1324 pub(crate) fn history_requirement_with_warmup(
1325 &self,
1326 config: IndicatorConfig,
1327 basis: Basis,
1328 reference_time: exchange::UnixMs,
1329 warmup_bars: Option<u64>,
1330 ) -> IndicatorHistoryRequirement {
1331 let Ok(next_config) = config_to_runtime(&config, &self.compiled.descriptor) else {
1332 return IndicatorHistoryRequirement::default();
1333 };
1334 let warmup = warmup_bars
1335 .unwrap_or_else(|| self.compiled.warmup_bars(&next_config).unwrap_or(0) as u64);
1336 let start = self
1337 .compiled
1338 .anchor_start(&next_config, reference_time.as_u64() as i64);
1339 let bars = match (start, basis) {
1340 (Some(start), Basis::Time(timeframe)) => {
1341 let elapsed = (reference_time.as_u64() as i64 - start).max(0) as u64;
1342 let dt = timeframe.to_milliseconds().max(1);
1343 warmup.max(elapsed.div_ceil(dt).saturating_add(1))
1344 }
1345 _ => warmup,
1346 };
1347 IndicatorHistoryRequirement::new(start.map(|start| UnixMs::new(start as u64)), bars, warmup)
1348 }
1349
1350 pub fn warmup_bars(&self, config: &IndicatorConfig) -> Option<u64> {
1351 let next_config = config_to_runtime(config, &self.compiled.descriptor).ok()?;
1352 self.compiled
1353 .warmup_bars(&next_config)
1354 .map(|warmup| warmup as u64)
1355 }
1356
1357 pub fn validate_config(&self, config: &IndicatorConfig) -> Result<(), IndicatorConfigError> {
1358 let next_config = config_to_runtime(config, &self.compiled.descriptor)?;
1359 self.compiled
1360 .validate_history_analysis(&next_config)
1361 .map_err(|error| IndicatorConfigError::Deserialize(error.to_string()))
1362 }
1363
1364 pub fn unsupported_reason(
1365 &self,
1366 context: AvailabilityContext,
1367 ) -> Option<IndicatorUnsupportedReason> {
1368 let basis = match context.basis {
1369 Basis::Time(_) => indicator_engine::TimeOrTick::Time,
1370 Basis::Tick(_) => indicator_engine::TimeOrTick::Tick,
1371 };
1372 if self
1373 .compiled
1374 .descriptor
1375 .bases
1376 .as_ref()
1377 .is_some_and(|bases| !bases.contains(&basis))
1378 {
1379 return Some(IndicatorUnsupportedReason::BasisNotSupported);
1380 }
1381 if basis == indicator_engine::TimeOrTick::Tick && self.compiled.requires_anchored_history()
1382 {
1383 return Some(IndicatorUnsupportedReason::IncompleteHistory);
1384 }
1385 if self.compiled.requirements.open_interest {
1386 let Basis::Time(timeframe) = context.basis else {
1387 return Some(IndicatorUnsupportedReason::BasisNotSupported);
1388 };
1389 if !crate::chart::kline::fetch_plan::supports_open_interest_exchange(
1390 context.base_ticker.exchange(),
1391 ) {
1392 return Some(IndicatorUnsupportedReason::SourceNotSupported);
1393 }
1394 if !crate::chart::kline::fetch_plan::supports_open_interest_timeframe(timeframe) {
1395 return Some(IndicatorUnsupportedReason::ResolutionNotSupported);
1396 }
1397 }
1398 None
1399 }
1400
1401 pub(crate) fn create_runtime(
1402 &self,
1403 config: IndicatorConfig,
1404 ) -> Result<ScriptIndicatorRuntime, IndicatorConfigError> {
1405 ScriptIndicatorRuntime::new(self.compiled.clone(), config)
1406 }
1407}
1408
1409enum ScriptRuntimeState {
1410 Active(Box<IndicatorInstance>),
1411 Quarantined(String),
1412}
1413
1414pub(crate) struct ScriptIndicatorRuntime {
1415 compiled: Arc<CompiledIndicator>,
1416 state: ScriptRuntimeState,
1417 outputs: IndicatorOutputStore,
1418}
1419
1420impl ScriptIndicatorRuntime {
1421 fn new(
1422 compiled: Arc<CompiledIndicator>,
1423 config: IndicatorConfig,
1424 ) -> Result<Self, IndicatorConfigError> {
1425 let instance = Self::instantiate(&compiled, &config)?;
1426 Ok(Self {
1427 compiled,
1428 state: ScriptRuntimeState::Active(Box::new(instance)),
1429 outputs: IndicatorOutputStore::default(),
1430 })
1431 }
1432
1433 fn quarantine(&mut self, phase: &str, error: impl std::fmt::Display) {
1434 if matches!(self.state, ScriptRuntimeState::Active(_)) {
1435 let diagnostic = format!("{phase}: {error}");
1436 log::error!(
1437 "indicator {} quarantined: {diagnostic}",
1438 self.compiled.descriptor.id
1439 );
1440 self.state = ScriptRuntimeState::Quarantined(diagnostic);
1441 }
1442 self.outputs = IndicatorOutputStore::default();
1443 }
1444
1445 fn instantiate(
1446 compiled: &Arc<CompiledIndicator>,
1447 config: &IndicatorConfig,
1448 ) -> Result<IndicatorInstance, IndicatorConfigError> {
1449 let next_config = config_to_runtime(config, &compiled.descriptor)?;
1450 IndicatorInstance::with_limits(
1451 Arc::clone(compiled),
1452 next_config,
1453 RuntimeLimits::untrusted(),
1454 )
1455 .map_err(|error| {
1456 let message = match error {
1457 PlanningError::HistoryAnalysisWorkLimitExceeded
1458 | PlanningError::HistoryAnalysisDepthLimitExceeded => error.to_string(),
1459 _ => format!("indicator could not be planned: {error}"),
1460 };
1461 IndicatorConfigError::Deserialize(message)
1462 })
1463 }
1464}
1465
1466impl ScriptIndicatorRuntime {
1467 pub(super) fn committed_index(&self) -> Option<usize> {
1468 match &self.state {
1469 ScriptRuntimeState::Active(instance) => instance.committed_index(),
1470 ScriptRuntimeState::Quarantined(_) => None,
1471 }
1472 }
1473
1474 pub(super) fn evaluate_closed(&mut self, frame: &IndicatorFrame) {
1475 let ScriptRuntimeState::Active(instance) = &mut self.state else {
1476 return;
1477 };
1478 let result = instance
1479 .evaluate_closed(frame.clone())
1480 .and_then(|output| self.outputs.apply_output(output, false));
1481 if let Err(error) = result {
1482 self.quarantine(&format!("closed bar {} failed", frame.index), error);
1483 }
1484 }
1485
1486 pub(super) fn preview_open(&mut self, frame: &IndicatorFrame) {
1487 let ScriptRuntimeState::Active(instance) = &mut self.state else {
1488 return;
1489 };
1490 let result = instance
1491 .preview_open(frame.clone())
1492 .and_then(|output| self.outputs.apply_output(output, true));
1493 if let Err(error) = result {
1494 self.quarantine(&format!("preview bar {} failed", frame.index), error);
1495 }
1496 }
1497
1498 pub(super) fn output_value(&self, channel_id: &str, index: usize) -> Option<OutputValue> {
1499 self.outputs
1500 .series_value(channel_id, index)
1501 .and_then(|value| match value {
1502 Value::Float(value) => finite_number(*value).map(OutputValue::Number),
1503 Value::Int(value) => finite_number(*value as f64).map(OutputValue::Number),
1504 Value::Bool(value) => Some(OutputValue::Boolean(*value)),
1505 _ => None,
1506 })
1507 }
1508
1509 pub(super) fn output_color(
1510 &self,
1511 channel_id: &str,
1512 index: usize,
1513 ) -> Option<indicator_engine::Color> {
1514 self.outputs.series_color(channel_id, index)
1515 }
1516
1517 pub(super) fn marker_outputs_at(&self, index: usize) -> Vec<MarkerOutput> {
1518 self.outputs.marker_values_at(index)
1519 }
1520
1521 pub(super) fn segment_outputs(&self) -> impl Iterator<Item = &SegmentOutput> {
1522 self.outputs.segment_values()
1523 }
1524
1525 pub(super) fn level_outputs(&self) -> Vec<HorizontalLevelOutput> {
1526 self.outputs.level_values()
1527 }
1528
1529 pub(super) fn ohlc_output(&self, channel_id: &str, index: usize) -> Option<OhlcOutput> {
1530 self.outputs.ohlc_value(channel_id, index)
1531 }
1532
1533 pub(super) fn range_output(&self, channel_id: &str, index: usize) -> Option<RangeOutput> {
1534 self.outputs.range_value(channel_id, index)
1535 }
1536
1537 pub(super) fn region_outputs_in_range(
1538 &self,
1539 source_range: Option<Range<usize>>,
1540 ) -> Vec<RegionOutput> {
1541 self.outputs.region_values_in_range(source_range.as_ref())
1542 }
1543
1544 pub(super) fn diagnostic(&self) -> Option<&str> {
1545 match &self.state {
1546 ScriptRuntimeState::Active(_) => None,
1547 ScriptRuntimeState::Quarantined(diagnostic) => Some(diagnostic),
1548 }
1549 }
1550}
1551
1552fn finite_number(value: f64) -> Option<f64> {
1553 value.is_finite().then_some(value)
1554}
1555
1556fn config_to_runtime(
1557 config: &IndicatorConfig,
1558 descriptor: &indicator_engine::IndicatorDescriptor,
1559) -> Result<EngineIndicatorConfig, IndicatorConfigError> {
1560 let object = config.value().as_object();
1561 let mut values = BTreeMap::new();
1562 for input in &descriptor.inputs {
1563 let value = match object.and_then(|object| object.get(&input.id)) {
1564 Some(value) => validated_input_value(input, value)?,
1565 None => input.default.clone(),
1566 };
1567 values.insert(input.id.clone(), value);
1568 }
1569 Ok(EngineIndicatorConfig { values })
1570}
1571
1572fn input_value_from_json(value_type: &ValueType, value: &serde_json::Value) -> Option<InputValue> {
1573 match value_type {
1574 ValueType::Int => value.as_i64().map(InputValue::Int),
1575 ValueType::Float => value
1576 .as_f64()
1577 .filter(|value| value.is_finite())
1578 .map(InputValue::Float),
1579 ValueType::Bool => value.as_bool().map(InputValue::Bool),
1580 ValueType::String => value
1581 .as_str()
1582 .map(|value| InputValue::String(value.to_owned())),
1583 ValueType::Color => value
1584 .as_i64()
1585 .and_then(|value| u32::try_from(value).ok())
1586 .map(InputValue::Color),
1587 ValueType::Time => value.as_i64().map(InputValue::Time),
1588 _ => None,
1589 }
1590}
1591
1592fn input_is_visible(
1593 input: &indicator_engine::InputDescriptor,
1594 inputs: &[indicator_engine::InputDescriptor],
1595 config: &serde_json::Value,
1596 input_coverage: &super::IndicatorInputCoverage,
1597) -> bool {
1598 if input
1599 .required_input
1600 .map(IndicatorInputSource::from)
1601 .is_some_and(|source| {
1602 matches!(
1603 input_coverage.state(source),
1604 IndicatorInputCoverageState::Missing
1605 )
1606 })
1607 {
1608 return false;
1609 }
1610 let Some(dependency) = input.visible_when.as_deref() else {
1611 return true;
1612 };
1613 let Some(controller) = inputs.iter().find(|candidate| candidate.id == dependency) else {
1614 return true;
1615 };
1616 config
1617 .as_object()
1618 .and_then(|values| values.get(dependency))
1619 .and_then(serde_json::Value::as_bool)
1620 .unwrap_or(matches!(&controller.default, InputValue::Bool(true)))
1621}
1622
1623fn validated_input_value(
1624 input: &indicator_engine::InputDescriptor,
1625 value: &serde_json::Value,
1626) -> Result<InputValue, IndicatorConfigError> {
1627 let parsed = input_value_from_json(&input.value_type, value).ok_or_else(|| {
1628 IndicatorConfigError::Deserialize(format!("invalid value for input '{}'", input.id))
1629 })?;
1630 if !input.accepts(&parsed) {
1631 return Err(IndicatorConfigError::Deserialize(format!(
1632 "invalid value for input '{}'",
1633 input.id
1634 )));
1635 }
1636 Ok(parsed)
1637}
1638
1639#[cfg(test)]
1640mod tests {
1641 use super::*;
1642 use crate::chart::kline::IndicatorInputCoverage;
1643
1644 fn settings_context() -> IndicatorSettingsContext {
1645 IndicatorSettingsContext {
1646 basis: Basis::Time(exchange::Timeframe::M1),
1647 input_coverage: IndicatorInputCoverage::default(),
1648 }
1649 }
1650
1651 fn input_coverage(directional_volume: bool) -> IndicatorInputCoverage {
1652 IndicatorInputCoverage::from_frames(&[IndicatorFrame {
1653 index: 0,
1654 time: 0,
1655 basis: indicator_engine::TimeOrTick::Time,
1656 instrument: indicator_engine::InstrumentContext { min_tick: None },
1657 chart: indicator_engine::ChartFrame {
1658 open: Some(1.0),
1659 high: Some(1.0),
1660 low: Some(1.0),
1661 close: Some(1.0),
1662 volume: Some(1.0),
1663 directional_volume_available: directional_volume,
1664 buy_qty: 1.0,
1665 sell_qty: 0.0,
1666 trade: None,
1667 },
1668 open_interest: None,
1669 series_inputs: Vec::new().into(),
1670 }])
1671 }
1672
1673 #[test]
1674 fn basis_restrictions_and_anchored_history_availability() {
1675 let ticker = exchange::TickerInfo::new(
1676 exchange::Ticker::new("BTCUSDT", exchange::adapter::Exchange::BinanceLinear),
1677 0.1,
1678 0.001,
1679 None,
1680 );
1681 for (source, origin, tick_reason) in [
1682 (
1683 r#"language(1)
1684indicator("Trade Count", id = "catalog.trade-count", sourceable = true, max_bars_back = 1, bases = ["time"])
1685display(layer_name = "Trades")
1686format(precision = "fixed_step:1", axis_labels = "abbreviated", header_labels = "commas", scale = "fit_visible_include_zero", max_decimals = 0)
1687value = chart.trade.count
1688plot(value, id = "value", title = "Trade count", precision = "decimals:0", style = "histogram", color_slot = "main", layer_title = "Trades", overlay = false)
1689
1690style("main", title = "Line")
1691style("bars", title = "Bars", role = "secondary")
1692mark(title = "Line", source = "value", kind = "line", color_slot = "main")
1693mark(title = "Bars", source = "value", kind = "histogram", color_slot = "bars")
1694"#,
1695 PackageOrigin::Installed,
1696 Some(IndicatorUnsupportedReason::BasisNotSupported),
1697 ),
1698 (
1699 include_str!("../../../../indicators/sources/anchored-vwap.ind"),
1700 PackageOrigin::Bundled,
1701 Some(IndicatorUnsupportedReason::IncompleteHistory),
1702 ),
1703 (
1704 "language(1)\nindicator(\"Restricted anchor\", id = \"catalog.restricted-anchor\", bases = [\"time\"])\nformat(precision = \"base_ticker_min_tick\")\nplot(ta.avwap(chart.close, chart.volume, \"Day\"))",
1705 PackageOrigin::Installed,
1706 Some(IndicatorUnsupportedReason::BasisNotSupported),
1707 ),
1708 (
1709 "language(1)\nindicator(\"Plain\", id = \"catalog.plain\")\nformat(precision = \"base_ticker_min_tick\")\nplot(chart.close)",
1710 PackageOrigin::Installed,
1711 None,
1712 ),
1713 ] {
1714 let compiled = indicator_engine::compile(source, "availability.ind")
1715 .expect("source should compile");
1716 let registration = registration(compiled, origin).expect("source should register");
1717 for (basis, expected) in [
1718 (Basis::Time(exchange::Timeframe::M1), None),
1719 (Basis::Tick(crate::aggr::TickCount(100)), tick_reason),
1720 (Basis::Time(exchange::Timeframe::M1), None),
1721 ] {
1722 assert_eq!(
1723 registration.unsupported_reason(AvailabilityContext {
1724 basis,
1725 base_ticker: ticker
1726 }),
1727 expected
1728 );
1729 }
1730 }
1731 }
1732
1733 #[test]
1734 fn indicator_metadata_is_exposed_to_host_definition() {
1735 let compiled = indicator_engine::compile(
1736 r#"
1737 language(1)
1738 indicator("Metadata", id = "catalog.metadata",
1739 author = "Ada", author_url = "https://example.com/ada",
1740 description = "A sample indicator",
1741 source = "https://example.com/source")
1742 format(precision = "base_ticker_min_tick")
1743 plot(chart.close, id = "value")
1744 "#,
1745 "metadata.ind",
1746 )
1747 .expect("metadata source should compile");
1748 let registration = registration(compiled, PackageOrigin::Installed)
1749 .expect("metadata source should register");
1750
1751 assert_eq!(
1752 registration.definition.author,
1753 Some(IndicatorAuthor {
1754 name: "Ada".to_owned(),
1755 url: "https://example.com/ada".to_owned(),
1756 })
1757 );
1758 assert_eq!(
1759 registration.definition.description.as_deref(),
1760 Some("A sample indicator")
1761 );
1762 assert_eq!(
1763 registration.definition.source.as_deref(),
1764 Some("https://example.com/source")
1765 );
1766 }
1767
1768 #[test]
1769 fn visible_inputs_are_filtered_without_dropping_persisted_values() {
1770 let compiled = indicator_engine::compile(
1771 r#"
1772 language(1)
1773 indicator("Visibility", id = "catalog.visibility")
1774 format(precision = "base_ticker_min_tick")
1775 show = input.bool(false, "Show", group = "divergence")
1776 length = input.int(5, "Length", min = 1, max = 100,
1777 group = "divergence", visible_when = "show",
1778 tooltip = "Explains the setting")
1779 mode = input.bool(true, "Mode", group = "divergence",
1780 visible_when = "show")
1781 plot(chart.close, id = "value")
1782 "#,
1783 "visibility.ind",
1784 )
1785 .expect("visibility source should compile");
1786 let registration = registration(compiled, PackageOrigin::Installed)
1787 .expect("visibility source should register");
1788
1789 let hidden = registration
1790 .settings(registration.default_config(), settings_context())
1791 .expect("default settings should be available");
1792 assert_eq!(
1793 hidden
1794 .iter()
1795 .map(|setting| setting.id.as_str())
1796 .collect::<Vec<_>>(),
1797 vec!["show"]
1798 );
1799 assert_eq!(hidden[0].tooltip, None);
1800
1801 let enabled = registration
1802 .with_setting(
1803 registration.default_config(),
1804 "show",
1805 IndicatorSettingValue::Bool(true),
1806 )
1807 .expect("show setting should be accepted");
1808 let customized = registration
1809 .with_setting(enabled, "length", IndicatorSettingValue::I64(42))
1810 .expect("hidden setting value should be accepted");
1811 let shown = registration
1812 .settings(customized.clone(), settings_context())
1813 .expect("enabled settings should be available");
1814 assert_eq!(shown.len(), 3);
1815 assert_eq!(
1816 shown
1817 .iter()
1818 .find(|setting| setting.id == "length")
1819 .and_then(|setting| setting.tooltip.as_deref()),
1820 Some("Explains the setting")
1821 );
1822 assert!(
1823 shown
1824 .iter()
1825 .all(|setting| setting.group.as_deref() == Some("divergence"))
1826 );
1827 assert_eq!(
1828 shown
1829 .iter()
1830 .find(|setting| setting.id == "length")
1831 .map(|setting| &setting.value),
1832 Some(&IndicatorSettingValue::I64(42))
1833 );
1834
1835 let disabled = registration
1836 .with_setting(customized, "show", IndicatorSettingValue::Bool(false))
1837 .expect("show setting should be accepted");
1838 let hidden_again = registration
1839 .settings(disabled.clone(), settings_context())
1840 .expect("disabled settings should be available");
1841 assert_eq!(hidden_again.len(), 1);
1842 let reenabled = registration
1843 .with_setting(disabled, "show", IndicatorSettingValue::Bool(true))
1844 .expect("show setting should be accepted");
1845 let restored = registration
1846 .settings(reenabled, settings_context())
1847 .expect("re-enabled settings should be available");
1848 assert_eq!(
1849 restored
1850 .iter()
1851 .find(|setting| setting.id == "length")
1852 .map(|setting| &setting.value),
1853 Some(&IndicatorSettingValue::I64(42))
1854 );
1855 }
1856
1857 #[test]
1858 fn host_input_dependencies_hide_settings_and_styles_without_coverage() {
1859 let compiled = indicator_engine::compile(
1860 r#"
1861 language(1)
1862 indicator("Dependencies", id = "catalog.dependencies")
1863 format(precision = "base_ticker_min_tick")
1864 highlight = input.bool(false, "Highlight", requires = "directional_volume")
1865 plot(chart.close, id = "value", color_slot = "main")
1866 style("main", title = "Main")
1867 style("warning", title = "Warning", role = "warning",
1868 requires = "directional_volume")
1869 "#,
1870 "dependencies.ind",
1871 )
1872 .expect("dependency source should compile");
1873 let registration = registration(compiled, PackageOrigin::Installed)
1874 .expect("dependency source should register");
1875
1876 assert_eq!(
1877 registration
1878 .definition
1879 .style_slots
1880 .iter()
1881 .find(|style| style.id == "warning")
1882 .and_then(|style| style.required_input),
1883 Some(IndicatorInputSource::DirectionalVolume)
1884 );
1885
1886 let unavailable = registration
1887 .settings(
1888 registration.default_config(),
1889 IndicatorSettingsContext {
1890 basis: Basis::Time(exchange::Timeframe::M1),
1891 input_coverage: input_coverage(false),
1892 },
1893 )
1894 .expect("unavailable settings should resolve");
1895 assert!(unavailable.is_empty());
1896
1897 let available = registration
1898 .settings(
1899 registration.default_config(),
1900 IndicatorSettingsContext {
1901 basis: Basis::Time(exchange::Timeframe::M1),
1902 input_coverage: input_coverage(true),
1903 },
1904 )
1905 .expect("available settings should resolve");
1906 assert_eq!(available.len(), 1);
1907 assert_eq!(available[0].id, "highlight");
1908 }
1909
1910 fn region(key: i64, upper: f64) -> RegionOutput {
1911 RegionOutput {
1912 id: "zones".to_owned(),
1913 key,
1914 left_index: 1,
1915 right_index: 3,
1916 upper,
1917 lower: upper - 1.0,
1918 border_color: None,
1919 fill_color: None,
1920 border_color_slot: None,
1921 fill_color_slot: None,
1922 extend_right: true,
1923 width: 1.0,
1924 }
1925 }
1926
1927 #[test]
1928 fn region_store_replaces_the_active_snapshot_and_duplicate_keys() {
1929 let mut store = IndicatorOutputStore::default();
1930 let first = FrameOutput {
1931 regions: vec![region(1, 10.0), region(2, 20.0)].into(),
1932 ..Default::default()
1933 };
1934 store.apply_output(first, false).unwrap();
1935 assert_eq!(store.region_values().len(), 2);
1936
1937 let second = FrameOutput {
1938 regions: vec![region(1, 30.0), region(1, 40.0)].into(),
1939 ..Default::default()
1940 };
1941 store.apply_output(second, false).unwrap();
1942 let regions = store.region_values();
1943 assert_eq!(regions.len(), 1);
1944 assert_eq!(regions[0].upper, 40.0);
1945 }
1946
1947 #[test]
1948 fn persistent_region_store_commits_and_previews_transactionally() {
1949 let mut store = IndicatorOutputStore::default();
1950 let closed = FrameOutput {
1951 region_updates: vec![RegionUpdate::Upsert(region(1, 10.0))].into(),
1952 ..Default::default()
1953 };
1954 store.apply_output(closed, false).unwrap();
1955 assert_eq!(store.region_values()[0].upper, 10.0);
1956
1957 let preview = FrameOutput {
1958 region_updates: vec![
1959 RegionUpdate::Upsert(region(1, 20.0)),
1960 RegionUpdate::Upsert(region(2, 30.0)),
1961 ]
1962 .into(),
1963 ..Default::default()
1964 };
1965 store.apply_output(preview, true).unwrap();
1966 let preview_regions = store.region_values();
1967 assert_eq!(preview_regions.len(), 2);
1968 assert_eq!(
1969 preview_regions
1970 .iter()
1971 .find(|region| region.key == 1)
1972 .map(|region| region.upper),
1973 Some(20.0)
1974 );
1975
1976 store.apply_output(FrameOutput::default(), true).unwrap();
1977 let reverted = store.region_values();
1978 assert_eq!(reverted.len(), 1);
1979 assert_eq!(reverted[0].upper, 10.0);
1980
1981 let preview_removed = FrameOutput {
1982 region_updates: vec![RegionUpdate::Remove {
1983 id: "zones".to_owned(),
1984 key: 1,
1985 }]
1986 .into(),
1987 ..Default::default()
1988 };
1989 store.apply_output(preview_removed, true).unwrap();
1990 assert!(store.region_values().is_empty());
1991 store.apply_output(FrameOutput::default(), true).unwrap();
1992 assert_eq!(store.region_values()[0].upper, 10.0);
1993
1994 let removed = FrameOutput {
1995 region_updates: vec![RegionUpdate::Remove {
1996 id: "zones".to_owned(),
1997 key: 1,
1998 }]
1999 .into(),
2000 ..Default::default()
2001 };
2002 store.apply_output(removed, false).unwrap();
2003 assert!(store.region_values().is_empty());
2004 }
2005
2006 #[test]
2007 fn persistent_region_store_evicts_oldest_without_reordering_updates() {
2008 let mut store = IndicatorOutputStore::default();
2009 let limit = RuntimeLimits::untrusted().max_output_items;
2010 let updates = (0..limit as i64)
2011 .map(|key| RegionUpdate::Upsert(region(key, key as f64)))
2012 .collect::<Vec<_>>();
2013 store
2014 .apply_output(
2015 FrameOutput {
2016 region_updates: updates.into(),
2017 ..Default::default()
2018 },
2019 false,
2020 )
2021 .unwrap();
2022
2023 store
2024 .apply_output(
2025 FrameOutput {
2026 region_updates: vec![
2027 RegionUpdate::Upsert(region(1, 100.0)),
2028 RegionUpdate::Upsert(region(limit as i64, 200.0)),
2029 ]
2030 .into(),
2031 ..Default::default()
2032 },
2033 false,
2034 )
2035 .unwrap();
2036
2037 let regions = store.region_values();
2038 assert_eq!(regions.len(), limit);
2039 assert!(!regions.iter().any(|region| region.key == 0));
2040 assert!(
2041 regions
2042 .iter()
2043 .any(|region| region.key == 1 && region.upper == 100.0)
2044 );
2045 assert!(regions.iter().any(|region| region.key == limit as i64));
2046 }
2047
2048 #[test]
2049 fn preview_region_retention_matches_commit_without_mutating_base() {
2050 let mut store = IndicatorOutputStore::default();
2051 let limit = RuntimeLimits::untrusted().max_output_items;
2052 let committed_updates = (0..limit as i64)
2053 .map(|key| RegionUpdate::Upsert(region(key, key as f64)))
2054 .collect::<Vec<_>>();
2055 store
2056 .apply_output(
2057 FrameOutput {
2058 region_updates: committed_updates.into(),
2059 ..Default::default()
2060 },
2061 false,
2062 )
2063 .unwrap();
2064
2065 let preview_update = RegionUpdate::Upsert(region(limit as i64, 100.0));
2066 store
2067 .apply_output(
2068 FrameOutput {
2069 region_updates: vec![preview_update.clone()].into(),
2070 ..Default::default()
2071 },
2072 true,
2073 )
2074 .unwrap();
2075 let preview_regions = store.region_values();
2076 assert_eq!(preview_regions.len(), limit);
2077 assert!(!preview_regions.iter().any(|region| region.key == 0));
2078 assert!(
2079 preview_regions
2080 .iter()
2081 .any(|region| region.key == limit as i64)
2082 );
2083
2084 store.apply_output(FrameOutput::default(), true).unwrap();
2085 let reverted = store.region_values();
2086 assert_eq!(reverted.len(), limit);
2087 assert!(reverted.iter().any(|region| region.key == 0));
2088 assert!(!reverted.iter().any(|region| region.key == limit as i64));
2089
2090 store
2091 .apply_output(
2092 FrameOutput {
2093 region_updates: vec![preview_update].into(),
2094 ..Default::default()
2095 },
2096 false,
2097 )
2098 .unwrap();
2099 let committed = store.region_values();
2100 assert_eq!(committed.len(), limit);
2101 assert!(!committed.iter().any(|region| region.key == 0));
2102 assert!(committed.iter().any(|region| region.key == limit as i64));
2103 }
2104}