Skip to main content

flowsurface_data/chart/kline/indicators/core/
runtime.rs

1use super::super::region_intersects_source_range;
2use super::super::scripting::ScriptIndicatorRuntime;
3use super::input::IndicatorInputSource;
4use super::{
5    BaseSeriesSource, IndicatorConfig, IndicatorConfigError, IndicatorId, IndicatorOutputRef,
6    OutputValue,
7};
8use crate::chart::kline::{
9    IndicatorInstance, IndicatorInstanceId, IndicatorInstanceStore, SourceBindingProblem,
10};
11use indicator_engine::{IndicatorFrame, SeriesInputValue};
12use rustc_hash::{FxHashMap, FxHashSet};
13use std::collections::{BTreeMap, BTreeSet};
14use std::ops::{ControlFlow, Range};
15
16struct IndicatorRuntimeEntry {
17    id: IndicatorInstanceId,
18    kind: IndicatorId,
19    config: IndicatorConfig,
20    source_inputs: Vec<String>,
21    source_bindings: BTreeMap<String, IndicatorOutputRef>,
22    base_source_selections: BTreeMap<String, BaseSeriesSource>,
23    runtime: ScriptIndicatorRuntime,
24    output_indices: Option<OutputIndexMap>,
25}
26
27enum OutputIndexMap {
28    Prefix(usize),
29    Explicit(Vec<Option<usize>>),
30}
31
32impl OutputIndexMap {
33    fn output_index(&self, index: usize) -> Option<usize> {
34        match self {
35            Self::Prefix(prefix) => index.checked_sub(*prefix),
36            Self::Explicit(indices) => indices.get(index).copied().flatten(),
37        }
38    }
39
40    fn current_index(&self, index: usize) -> Option<usize> {
41        match self {
42            Self::Prefix(prefix) => index.checked_add(*prefix),
43            Self::Explicit(indices) => indices.iter().position(|previous| *previous == Some(index)),
44        }
45    }
46}
47
48impl IndicatorRuntimeEntry {
49    fn new(instance: &IndicatorInstance) -> Result<Option<Self>, IndicatorConfigError> {
50        let Some(registration) = super::catalog().get(&instance.kind) else {
51            return Ok(None);
52        };
53        let runtime = registration.create_runtime(instance.config.clone())?;
54        Ok(Some(Self {
55            id: instance.id,
56            kind: instance.kind.clone(),
57            config: instance.config.clone(),
58            source_inputs: registration
59                .definition
60                .source_inputs
61                .iter()
62                .map(|input| input.id.clone())
63                .collect(),
64            source_bindings: instance.source_bindings.clone(),
65            base_source_selections: instance.base_source_selections.clone(),
66            runtime,
67            output_indices: None,
68        }))
69    }
70
71    fn output_index(&self, index: usize) -> Option<usize> {
72        self.output_indices
73            .as_ref()
74            .map_or(Some(index), |indices| indices.output_index(index))
75    }
76
77    fn current_index(&self, index: usize) -> Option<usize> {
78        self.output_indices
79            .as_ref()
80            .map_or(Some(index), |indices| indices.current_index(index))
81    }
82
83    fn runtime_range_for_source_range(&self, source_range: &Range<usize>) -> Option<Range<usize>> {
84        match &self.output_indices {
85            None => Some(source_range.clone()),
86            Some(OutputIndexMap::Prefix(prefix)) => Some(
87                source_range.start.saturating_sub(*prefix)
88                    ..source_range.end.saturating_sub(*prefix),
89            ),
90            Some(OutputIndexMap::Explicit(_)) => None,
91        }
92    }
93}
94
95enum IndicatorGraphDiagnostic {
96    AttachedHasNoNumericPlotLayer,
97    InvalidSourceBinding(SourceBindingProblem),
98    DependencyCycle,
99}
100
101impl std::fmt::Display for IndicatorGraphDiagnostic {
102    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
103        match self {
104            Self::AttachedHasNoNumericPlotLayer => {
105                formatter.write_str("attached indicator has no numeric plot layer")
106            }
107            Self::InvalidSourceBinding(problem) => write!(formatter, "{problem}"),
108            Self::DependencyCycle => formatter.write_str("indicator dependency cycle"),
109        }
110    }
111}
112
113/// Owns active indicator runtimes, output stores, and the selected dependency graph.
114#[derive(Default)]
115pub struct IndicatorHost {
116    entries: Vec<IndicatorRuntimeEntry>,
117    entry_indices: FxHashMap<IndicatorInstanceId, usize>,
118    execution_order: Vec<IndicatorInstanceId>,
119    dependencies: FxHashMap<IndicatorInstanceId, Vec<IndicatorInstanceId>>,
120    dependents: FxHashMap<IndicatorInstanceId, Vec<IndicatorInstanceId>>,
121    graph_diagnostics: FxHashMap<IndicatorInstanceId, IndicatorGraphDiagnostic>,
122    runtime_diagnostics: FxHashMap<IndicatorInstanceId, IndicatorConfigError>,
123    blocked_nodes: FxHashSet<IndicatorInstanceId>,
124}
125
126impl IndicatorHost {
127    fn record_runtime_diagnostic(
128        &mut self,
129        instance: &IndicatorInstance,
130        error: IndicatorConfigError,
131    ) {
132        log::error!(
133            "indicator {} (instance {:?}) runtime rebuild failure: {error}",
134            instance.kind.id(),
135            instance.id
136        );
137        self.runtime_diagnostics.insert(instance.id, error);
138    }
139
140    pub fn remap_outputs(&mut self, previous_indices: &[Option<usize>]) {
141        for entry in &mut self.entries {
142            entry.output_indices = Some(OutputIndexMap::Explicit(
143                previous_indices
144                    .iter()
145                    .map(|index| index.and_then(|index| entry.output_index(index)))
146                    .collect(),
147            ));
148        }
149    }
150
151    pub fn remap_outputs_prefix(&mut self, prefix: usize) {
152        if prefix == 0 {
153            return;
154        }
155
156        for entry in &mut self.entries {
157            let output_indices = match entry.output_indices.take() {
158                None => OutputIndexMap::Prefix(prefix),
159                Some(OutputIndexMap::Prefix(previous_prefix)) => {
160                    OutputIndexMap::Prefix(previous_prefix.saturating_add(prefix))
161                }
162                Some(OutputIndexMap::Explicit(previous_indices)) => {
163                    let mut indices =
164                        Vec::with_capacity(prefix.saturating_add(previous_indices.len()));
165                    indices.resize(prefix, None);
166                    indices.extend(previous_indices);
167                    OutputIndexMap::Explicit(indices)
168                }
169            };
170            entry.output_indices = Some(output_indices);
171        }
172    }
173
174    pub fn has_entries(&self, ids: &[IndicatorInstanceId]) -> bool {
175        ids.iter().all(|id| self.entry_indices.contains_key(id))
176    }
177
178    pub fn can_append_from(&self, ids: &[IndicatorInstanceId], start: usize) -> bool {
179        ids.iter().all(|id| {
180            self.entry_indices.get(id).is_some_and(|index| {
181                let entry = &self.entries[*index];
182                entry.output_indices.is_none()
183                    && entry
184                        .runtime
185                        .committed_index()
186                        .map_or(start == 0, |index| index.checked_add(1) == Some(start))
187            })
188        })
189    }
190
191    pub fn dependent_ids(&self, roots: &[IndicatorInstanceId]) -> Vec<IndicatorInstanceId> {
192        let mut affected = roots.iter().copied().collect::<FxHashSet<_>>();
193        let mut pending = roots.to_vec();
194        while let Some(id) = pending.pop() {
195            for dependent in self.dependents.get(&id).into_iter().flatten().copied() {
196                if affected.insert(dependent) {
197                    pending.push(dependent);
198                }
199            }
200        }
201
202        let mut result = Vec::new();
203        let mut returned = FxHashSet::default();
204        for id in roots
205            .iter()
206            .copied()
207            .chain(self.execution_order.iter().copied())
208        {
209            if affected.contains(&id) && returned.insert(id) {
210                result.push(id);
211            }
212        }
213        result
214    }
215
216    pub fn calculation_ids(&self, roots: &[IndicatorInstanceId]) -> Vec<IndicatorInstanceId> {
217        let mut included = roots.iter().copied().collect::<FxHashSet<_>>();
218        let mut discovered = roots.to_vec();
219        let mut pending = roots.to_vec();
220        while let Some(id) = pending.pop() {
221            for dependency in self.dependencies.get(&id).into_iter().flatten().copied() {
222                if included.insert(dependency) {
223                    discovered.push(dependency);
224                    pending.push(dependency);
225                }
226            }
227        }
228
229        let mut result = self
230            .execution_order
231            .iter()
232            .copied()
233            .filter(|id| included.contains(id))
234            .collect::<Vec<_>>();
235        for id in discovered {
236            if !result.contains(&id) {
237                result.push(id);
238            }
239        }
240        result
241    }
242
243    pub fn merge_rebuilt(
244        &mut self,
245        mut rebuilt: Self,
246        replaced_ids: &[IndicatorInstanceId],
247        active: &[IndicatorInstanceId],
248        instances: &IndicatorInstanceStore,
249    ) {
250        let replaced = replaced_ids.iter().copied().collect::<FxHashSet<_>>();
251        self.entries.retain(|entry| !replaced.contains(&entry.id));
252        self.entries.append(&mut rebuilt.entries);
253
254        self.runtime_diagnostics
255            .retain(|id, _| !replaced.contains(id));
256        self.runtime_diagnostics.extend(rebuilt.runtime_diagnostics);
257
258        self.entries.sort_by_key(|entry| {
259            active
260                .iter()
261                .position(|id| *id == entry.id)
262                .unwrap_or(usize::MAX)
263        });
264        self.rebuild_entry_indices();
265        self.rebuild_graph(active, instances);
266    }
267
268    pub fn recalculate_interruptible(
269        &mut self,
270        active: &[IndicatorInstanceId],
271        instances: &IndicatorInstanceStore,
272        frames: &[IndicatorFrame],
273        mut control: impl FnMut() -> ControlFlow<()>,
274    ) -> ControlFlow<()> {
275        let mut rebuilt = Self::default();
276        for instance in active.iter().filter_map(|id| instances.get(*id)) {
277            control()?;
278            match IndicatorRuntimeEntry::new(instance) {
279                Ok(Some(entry)) => rebuilt.entries.push(entry),
280                Ok(None) => {}
281                Err(error) => rebuilt.record_runtime_diagnostic(instance, error),
282            };
283        }
284        rebuilt.rebuild_entry_indices();
285        rebuilt.rebuild_graph(active, instances);
286        rebuilt.replay_closed_interruptible(frames, control)?;
287        *self = rebuilt;
288        ControlFlow::Continue(())
289    }
290
291    pub fn replay_closed_interruptible(
292        &mut self,
293        frames: &[IndicatorFrame],
294        mut control: impl FnMut() -> ControlFlow<()>,
295    ) -> ControlFlow<()> {
296        for chunk in frames.chunks(64) {
297            for frame in chunk {
298                control()?;
299                for id in self.execution_order.clone() {
300                    control()?;
301                    self.evaluate_entry(id, frame, false);
302                }
303            }
304            std::thread::yield_now();
305        }
306        ControlFlow::Continue(())
307    }
308
309    pub fn set_active(
310        &mut self,
311        active: &[IndicatorInstanceId],
312        instances: &IndicatorInstanceStore,
313    ) -> Vec<IndicatorInstanceId> {
314        let changed = active
315            .iter()
316            .copied()
317            .filter(|id| {
318                instances.get(*id).is_some_and(|instance| {
319                    instance.kind.is_registered()
320                        && self.entry_indices.get(id).is_none_or(|index| {
321                            let entry = &self.entries[*index];
322                            entry.kind != instance.kind
323                                || entry.config != instance.config
324                                || entry.source_bindings != instance.source_bindings
325                                || entry.base_source_selections != instance.base_source_selections
326                        })
327                })
328            })
329            .collect::<Vec<_>>();
330        self.entries.retain(|entry| {
331            active.contains(&entry.id)
332                && !changed.contains(&entry.id)
333                && instances
334                    .get(entry.id)
335                    .is_some_and(|instance| instance.kind.is_registered())
336        });
337        self.runtime_diagnostics.retain(|id, _| {
338            active.contains(id)
339                && instances
340                    .get(*id)
341                    .is_some_and(|instance| instance.kind.is_registered())
342        });
343        self.entries.sort_by_key(|entry| {
344            active
345                .iter()
346                .position(|id| *id == entry.id)
347                .unwrap_or(usize::MAX)
348        });
349        self.rebuild_entry_indices();
350        self.rebuild_graph(active, instances);
351        changed
352    }
353
354    pub fn evaluate_closed(&mut self, frame: &IndicatorFrame) {
355        let order = self.execution_order.clone();
356        for id in order {
357            self.evaluate_entry(id, frame, false);
358        }
359    }
360
361    pub fn evaluate_closed_selected(
362        &mut self,
363        ids: &[IndicatorInstanceId],
364        frame: &IndicatorFrame,
365    ) {
366        let selected = ids.iter().copied().collect::<FxHashSet<_>>();
367        self.evaluate_nodes(&selected, frame, false);
368    }
369
370    pub fn preview_open_selected(&mut self, ids: &[IndicatorInstanceId], frame: &IndicatorFrame) {
371        let selected = ids.iter().copied().collect::<FxHashSet<_>>();
372        self.evaluate_nodes(&selected, frame, true);
373    }
374
375    fn evaluate_nodes(
376        &mut self,
377        selected: &FxHashSet<IndicatorInstanceId>,
378        frame: &IndicatorFrame,
379        preview: bool,
380    ) {
381        let order = self.execution_order.clone();
382        for id in order {
383            if selected.contains(&id) {
384                self.evaluate_entry(id, frame, preview);
385            }
386        }
387    }
388
389    fn evaluate_entry(
390        &mut self,
391        id: IndicatorInstanceId,
392        base_frame: &IndicatorFrame,
393        preview: bool,
394    ) {
395        if self.blocked_nodes.contains(&id) {
396            return;
397        }
398        let Some(entry_index) = self.entry_indices.get(&id).copied() else {
399            return;
400        };
401        let frame = self.frame_for(entry_index, base_frame);
402        if let Some(entry) = self.entries.get_mut(entry_index) {
403            if preview {
404                entry.runtime.preview_open(&frame);
405            } else {
406                entry.runtime.evaluate_closed(&frame);
407            }
408        }
409    }
410
411    fn frame_for(&self, entry_index: usize, base_frame: &IndicatorFrame) -> IndicatorFrame {
412        let Some(entry) = self.entries.get(entry_index) else {
413            return base_frame.clone();
414        };
415        let values = entry
416            .source_inputs
417            .iter()
418            .map(|input_id| match entry.source_bindings.get(input_id) {
419                None => entry
420                    .base_source_selections
421                    .get(input_id)
422                    .copied()
423                    .map_or(SeriesInputValue::Default, SeriesInputValue::Base),
424                Some(_)
425                    if !entry
426                        .kind
427                        .definition()
428                        .source_inputs
429                        .iter()
430                        .any(|input| input.id == *input_id && input.allow_indicator) =>
431                {
432                    SeriesInputValue::Value(None)
433                }
434                Some(reference) if reference.instance_id == entry.id => {
435                    SeriesInputValue::Value(None)
436                }
437                Some(reference) => self
438                    .entry_indices
439                    .get(&reference.instance_id)
440                    .and_then(|target_index| self.entries.get(*target_index))
441                    .filter(|target| {
442                        !self.blocked_nodes.contains(&target.id)
443                            && target
444                                .kind
445                                .definition()
446                                .sourceable_numeric_output(&reference.channel_id)
447                                .is_some()
448                    })
449                    .and_then(|_| {
450                        self.output_value_for(
451                            reference.instance_id,
452                            &reference.channel_id,
453                            base_frame.index,
454                        )
455                    })
456                    .map_or(SeriesInputValue::Value(None), |value| {
457                        SeriesInputValue::Value(value.as_number())
458                    }),
459            })
460            .collect::<Vec<_>>();
461        let mut frame = base_frame.clone();
462        frame.series_inputs = values.into();
463        frame
464    }
465
466    fn rebuild_graph(
467        &mut self,
468        active_ids: &[IndicatorInstanceId],
469        instances: &IndicatorInstanceStore,
470    ) {
471        self.execution_order.clear();
472        self.dependencies.clear();
473        self.dependents.clear();
474        self.graph_diagnostics.clear();
475        self.blocked_nodes.clear();
476        let ids = active_ids
477            .iter()
478            .copied()
479            .filter(|id| {
480                instances
481                    .get(*id)
482                    .is_some_and(|instance| instance.kind.is_registered())
483            })
484            .collect::<Vec<_>>();
485        let active = ids.iter().copied().collect::<BTreeSet<_>>();
486        for id in &ids {
487            let Some(instance) = instances.get(*id) else {
488                continue;
489            };
490            if !instance.source_bindings.is_empty()
491                && !instance.kind.definition().has_numeric_plot_layers()
492            {
493                self.graph_diagnostics
494                    .insert(*id, IndicatorGraphDiagnostic::AttachedHasNoNumericPlotLayer);
495                self.blocked_nodes.insert(*id);
496                continue;
497            }
498            for source_id in instance.source_bindings.keys() {
499                let target = match instances.dependency_target(instance, source_id, &active) {
500                    Ok(Some(target)) => target,
501                    Ok(None) => continue,
502                    Err(problem) => {
503                        self.graph_diagnostics
504                            .entry(*id)
505                            .or_insert(IndicatorGraphDiagnostic::InvalidSourceBinding(problem));
506                        continue;
507                    }
508                };
509                let dependencies = self.dependencies.entry(*id).or_default();
510                if !dependencies.contains(&target.id) {
511                    dependencies.push(target.id);
512                    self.dependents.entry(target.id).or_default().push(*id);
513                }
514            }
515        }
516        let mut indegree = ids
517            .iter()
518            .map(|id| (*id, self.dependencies.get(id).map_or(0, Vec::len)))
519            .collect::<FxHashMap<_, _>>();
520        let mut emitted = FxHashSet::default();
521        while let Some(id) = ids
522            .iter()
523            .copied()
524            .find(|id| !emitted.contains(id) && indegree.get(id).copied() == Some(0))
525        {
526            emitted.insert(id);
527            self.execution_order.push(id);
528            for dependent in self.dependents.get(&id).into_iter().flatten() {
529                if let Some(degree) = indegree.get_mut(dependent) {
530                    *degree = degree.saturating_sub(1);
531                }
532            }
533        }
534        for id in ids {
535            if emitted.contains(&id) {
536                continue;
537            }
538            self.blocked_nodes.insert(id);
539            self.graph_diagnostics
540                .entry(id)
541                .or_insert(IndicatorGraphDiagnostic::DependencyCycle);
542            self.execution_order.push(id);
543        }
544    }
545
546    pub fn has_input_consumer(
547        &self,
548        source: IndicatorInputSource,
549        instances: &IndicatorInstanceStore,
550    ) -> bool {
551        self.execution_order.iter().any(|id| {
552            instances
553                .get(*id)
554                .is_some_and(|instance| instance.kind.definition().uses(source))
555        })
556    }
557
558    pub fn output_value_for(
559        &self,
560        id: IndicatorInstanceId,
561        channel_id: &str,
562        index: usize,
563    ) -> Option<OutputValue> {
564        self.entry_indices
565            .get(&id)
566            .and_then(|entry_index| self.entries.get(*entry_index))
567            .and_then(|entry| {
568                entry
569                    .runtime
570                    .output_value(channel_id, entry.output_index(index)?)
571            })
572    }
573
574    pub fn output_color_for(
575        &self,
576        id: IndicatorInstanceId,
577        channel_id: &str,
578        index: usize,
579    ) -> Option<indicator_engine::Color> {
580        self.entry_indices
581            .get(&id)
582            .and_then(|entry_index| self.entries.get(*entry_index))
583            .and_then(|entry| {
584                entry
585                    .runtime
586                    .output_color(channel_id, entry.output_index(index)?)
587            })
588    }
589
590    pub fn marker_outputs_for(
591        &self,
592        id: IndicatorInstanceId,
593        index: usize,
594    ) -> Vec<indicator_engine::MarkerOutput> {
595        self.entry_indices
596            .get(&id)
597            .and_then(|entry_index| self.entries.get(*entry_index))
598            .and_then(|entry| Some(entry.runtime.marker_outputs_at(entry.output_index(index)?)))
599            .unwrap_or_default()
600    }
601
602    pub fn segment_outputs_for(
603        &self,
604        id: IndicatorInstanceId,
605        source_range: Range<usize>,
606        mut intersects_viewport: impl FnMut(usize, usize) -> bool,
607    ) -> Vec<indicator_engine::SegmentOutput> {
608        if source_range.is_empty() {
609            return Vec::new();
610        }
611        self.entry_indices
612            .get(&id)
613            .and_then(|entry_index| self.entries.get(*entry_index))
614            .map(|entry| {
615                let inverse = match &entry.output_indices {
616                    Some(OutputIndexMap::Explicit(indices)) => Some(
617                        indices
618                            .iter()
619                            .enumerate()
620                            .filter_map(|(current, previous)| {
621                                previous.map(|previous| (previous, current))
622                            })
623                            .collect::<FxHashMap<_, _>>(),
624                    ),
625                    _ => None,
626                };
627                let current_index = |index| match &inverse {
628                    Some(indices) => indices.get(&index).copied(),
629                    None => entry.current_index(index),
630                };
631
632                entry
633                    .runtime
634                    .segment_outputs()
635                    .filter_map(|segment| {
636                        let start = current_index(segment.start_index)?;
637                        let end = current_index(segment.end_index)?;
638                        if start.min(end) >= source_range.end
639                            || start.max(end) < source_range.start
640                            || !intersects_viewport(start, end)
641                        {
642                            return None;
643                        }
644                        let mut segment = segment.clone();
645                        segment.start_index = start;
646                        segment.end_index = end;
647                        Some(segment)
648                    })
649                    .collect()
650            })
651            .unwrap_or_default()
652    }
653
654    pub fn level_outputs_for(
655        &self,
656        id: IndicatorInstanceId,
657    ) -> Vec<indicator_engine::HorizontalLevelOutput> {
658        self.entry_indices
659            .get(&id)
660            .and_then(|entry_index| self.entries.get(*entry_index))
661            .map(|entry| entry.runtime.level_outputs())
662            .unwrap_or_default()
663    }
664
665    pub fn ohlc_output_for(
666        &self,
667        id: IndicatorInstanceId,
668        channel_id: &str,
669        index: usize,
670    ) -> Option<indicator_engine::OhlcOutput> {
671        self.entry_indices
672            .get(&id)
673            .and_then(|entry_index| self.entries.get(*entry_index))
674            .and_then(|entry| {
675                entry
676                    .runtime
677                    .ohlc_output(channel_id, entry.output_index(index)?)
678            })
679    }
680
681    pub fn range_output_for(
682        &self,
683        id: IndicatorInstanceId,
684        channel_id: &str,
685        index: usize,
686    ) -> Option<indicator_engine::RangeOutput> {
687        self.entry_indices
688            .get(&id)
689            .and_then(|entry_index| self.entries.get(*entry_index))
690            .and_then(|entry| {
691                entry
692                    .runtime
693                    .range_output(channel_id, entry.output_index(index)?)
694            })
695    }
696
697    pub fn region_outputs_for_range(
698        &self,
699        id: IndicatorInstanceId,
700        source_range: Option<Range<usize>>,
701    ) -> Vec<indicator_engine::RegionOutput> {
702        self.entry_indices
703            .get(&id)
704            .and_then(|entry_index| self.entries.get(*entry_index))
705            .map(|entry| {
706                let runtime_range = source_range
707                    .as_ref()
708                    .and_then(|range| entry.runtime_range_for_source_range(range));
709                entry
710                    .runtime
711                    .region_outputs_in_range(runtime_range)
712                    .into_iter()
713                    .filter_map(|mut region| {
714                        region.left_index = entry.current_index(region.left_index)?;
715                        if !region.extend_right {
716                            region.right_index = entry.current_index(region.right_index)?;
717                        } else if let Some(right_index) = entry.current_index(region.right_index) {
718                            region.right_index = right_index;
719                        }
720                        if let Some(source_range) = &source_range {
721                            region_intersects_source_range(&region, source_range).then_some(region)
722                        } else {
723                            Some(region)
724                        }
725                    })
726                    .collect()
727            })
728            .unwrap_or_default()
729    }
730
731    pub fn diagnostic_for(&self, id: IndicatorInstanceId) -> Option<String> {
732        if let Some(diagnostic) = self.graph_diagnostics.get(&id) {
733            return Some(diagnostic.to_string());
734        }
735        if let Some(diagnostic) = self.runtime_diagnostics.get(&id) {
736            return Some(format!("rebuild: {diagnostic}"));
737        }
738
739        self.entry_indices
740            .get(&id)
741            .and_then(|entry_index| self.entries.get(*entry_index))
742            .and_then(|entry| entry.runtime.diagnostic().map(str::to_owned))
743    }
744
745    fn rebuild_entry_indices(&mut self) {
746        self.entry_indices.clear();
747        self.entry_indices.extend(
748            self.entries
749                .iter()
750                .enumerate()
751                .map(|(index, entry)| (entry.id, index)),
752        );
753    }
754}
755
756#[cfg(test)]
757mod tests {
758    use super::*;
759
760    #[test]
761    fn source_index_remapping_supports_region_endpoints() {
762        let explicit = OutputIndexMap::Explicit(vec![Some(10), None, Some(12)]);
763        assert_eq!(explicit.current_index(10), Some(0));
764        assert_eq!(explicit.current_index(12), Some(2));
765        assert_eq!(explicit.current_index(11), None);
766
767        let prefixed = OutputIndexMap::Prefix(3);
768        assert_eq!(prefixed.current_index(7), Some(10));
769    }
770
771    #[test]
772    fn region_culling_keeps_intersecting_finite_and_right_extending_zones() {
773        let finite = |left, right| indicator_engine::RegionOutput {
774            id: "zones".to_owned(),
775            key: left as i64,
776            left_index: left,
777            right_index: right,
778            upper: 10.0,
779            lower: 5.0,
780            border_color: None,
781            fill_color: None,
782            border_color_slot: None,
783            fill_color_slot: None,
784            extend_right: false,
785            width: 1.0,
786        };
787        let extending = |left| indicator_engine::RegionOutput {
788            extend_right: true,
789            ..finite(left, left)
790        };
791        let visible = 5..10;
792
793        assert!(!region_intersects_source_range(&finite(0, 4), &visible));
794        assert!(region_intersects_source_range(&finite(0, 5), &visible));
795        assert!(region_intersects_source_range(&finite(8, 12), &visible));
796        assert!(!region_intersects_source_range(&finite(10, 12), &visible));
797        assert!(region_intersects_source_range(&extending(0), &visible));
798        assert!(region_intersects_source_range(&extending(9), &visible));
799        assert!(!region_intersects_source_range(&extending(10), &visible));
800    }
801
802    #[test]
803    fn interrupted_replay_preserves_the_previous_host() {
804        let id = IndicatorInstanceId::default_for(&IndicatorId::parse("core.volume").unwrap());
805        let mut host = IndicatorHost {
806            execution_order: vec![id],
807            ..IndicatorHost::default()
808        };
809        let frames = (0..130)
810            .map(|index| IndicatorFrame {
811                index,
812                time: index as i64,
813                basis: indicator_engine::TimeOrTick::Tick,
814                instrument: indicator_engine::InstrumentContext::default(),
815                chart: indicator_engine::ChartFrame {
816                    open: Some(1.0),
817                    high: Some(1.0),
818                    low: Some(1.0),
819                    close: Some(1.0),
820                    volume: Some(1.0),
821                    directional_volume_available: false,
822                    buy_qty: 0.0,
823                    sell_qty: 0.0,
824                    trade: None,
825                },
826                open_interest: None,
827                series_inputs: std::sync::Arc::from([]),
828            })
829            .collect::<Vec<_>>();
830        let mut checks = 0;
831        assert!(
832            host.replay_closed_interruptible(&frames, || {
833                checks += 1;
834                if checks == 5 {
835                    ControlFlow::Break(())
836                } else {
837                    ControlFlow::Continue(())
838                }
839            })
840            .is_break()
841        );
842        assert_eq!(checks, 5);
843        assert!(
844            host.recalculate_interruptible(
845                &[],
846                &IndicatorInstanceStore::default(),
847                &frames,
848                || ControlFlow::Break(())
849            )
850            .is_break()
851        );
852        assert_eq!(host.execution_order, vec![id]);
853        assert!(
854            host.recalculate_interruptible(
855                &[],
856                &IndicatorInstanceStore::default(),
857                &frames,
858                || ControlFlow::Continue(())
859            )
860            .is_continue()
861        );
862        assert!(host.execution_order.is_empty());
863    }
864}