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#[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(®ion, 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}