1mod bundled;
2mod core;
3pub mod language;
4mod loader;
5pub mod manifest;
6mod registry;
7pub mod scripting;
8
9use super::composition::{
10 AxisBinding, LayerDataKind, MarkKind, PanelScaleMode, PanelValueLabelPolicy,
11 PanelValuePrecision,
12};
13use crate::chart::Basis;
14use crate::config::theme::{ThemeColor, ThemeColorRole, ThemeColorShade};
15use exchange::{TickerInfo, UnixMs};
16use indicator_engine::IndicatorFrame;
17use indicator_engine::PaneTarget;
18pub use indicator_engine::{BaseSeriesSource, IndicatorAuthor};
19use std::fmt::{Display, Formatter};
20use std::ops::Range;
21use std::sync::Arc;
22
23pub use core::input::{IndicatorInputRequirement, IndicatorInputSource, OpenInterestSeries};
24pub use core::output::{IndicatorOutputRef, OutputChannelSpec, OutputValue, OutputValueType};
25pub use core::runtime::IndicatorHost;
26
27pub use loader::{
28 IndicatorCatalogPaths, initialize_indicator_catalog, initialize_indicator_catalog_with_paths,
29 remove_indicator_source, write_indicator_source,
30};
31pub(crate) use registry::install_catalog_with_bundled_diagnostics;
32pub use registry::{
33 CatalogInstallError, IndicatorCatalog, IndicatorConfig, IndicatorConfigError, IndicatorId,
34 IndicatorIdError, IndicatorRegistration, all_indicators, availability, catalog,
35 declared_input_requirements, display_name, indicator_definition, panel_recipe,
36 requires_trade_data, table_columns, unsupported_reason,
37};
38
39pub(crate) fn region_intersects_source_range(
40 region: &indicator_engine::RegionOutput,
41 source_range: &Range<usize>,
42) -> bool {
43 if source_range.start >= source_range.end {
44 return false;
45 }
46 if region.extend_right {
47 return region.left_index < source_range.end;
48 }
49
50 let left = region.left_index.min(region.right_index);
51 let right = region.left_index.max(region.right_index);
52 left < source_range.end && right >= source_range.start
53}
54
55#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
56pub struct IndicatorInputAvailability {
57 pub klines: bool,
58 pub directional_volume: bool,
59 pub trades: bool,
60 pub open_interest: bool,
61}
62
63impl IndicatorInputAvailability {
64 fn from_frame(frame: &IndicatorFrame) -> Self {
65 Self {
66 klines: frame.chart.close.is_some(),
67 directional_volume: frame.chart.directional_volume_available,
68 trades: frame.chart.trade.is_some(),
69 open_interest: frame.open_interest.is_some(),
70 }
71 }
72
73 fn contains(self, source: IndicatorInputSource) -> bool {
74 match source {
75 IndicatorInputSource::Klines => self.klines,
76 IndicatorInputSource::DirectionalVolume => self.directional_volume,
77 IndicatorInputSource::Trades => self.trades,
78 IndicatorInputSource::OpenInterest => self.open_interest,
79 }
80 }
81}
82
83#[derive(Debug, Clone, Copy, PartialEq, Eq)]
84pub enum IndicatorInputCoverageState {
85 Unknown,
86 Available,
87 Missing,
88}
89
90#[derive(Debug, Clone, Default, PartialEq, Eq)]
91pub struct IndicatorInputCoverage {
92 by_runtime_index: Arc<Vec<IndicatorInputAvailability>>,
93 prefix_offset: usize,
94}
95
96impl IndicatorInputCoverage {
97 pub fn remap(&mut self, previous_indices: &[Option<usize>]) {
98 let by_runtime_index = previous_indices
99 .iter()
100 .map(|index| {
101 index
102 .and_then(|index| self.at_index(index))
103 .unwrap_or_default()
104 })
105 .collect();
106 self.by_runtime_index = Arc::new(by_runtime_index);
107 self.prefix_offset = 0;
108 }
109
110 pub fn remap_prefix(&mut self, prefix: usize) {
111 self.prefix_offset = self.prefix_offset.saturating_add(prefix);
112 }
113
114 fn at_index(&self, runtime_index: usize) -> Option<IndicatorInputAvailability> {
115 runtime_index
116 .checked_sub(self.prefix_offset)
117 .and_then(|index| self.by_runtime_index.get(index))
118 .copied()
119 }
120
121 pub fn from_frames(frames: &[IndicatorFrame]) -> Self {
122 let mut by_runtime_index = vec![
123 IndicatorInputAvailability::default();
124 frames
125 .iter()
126 .map(|frame| frame.index)
127 .max()
128 .map_or(0, |index| index.saturating_add(1))
129 ];
130 for frame in frames {
131 by_runtime_index[frame.index] = IndicatorInputAvailability::from_frame(frame);
132 }
133
134 Self {
135 by_runtime_index: Arc::new(by_runtime_index),
136 prefix_offset: 0,
137 }
138 }
139
140 pub fn update_frames(&mut self, frames: &[IndicatorFrame]) {
141 let Some(max_index) = frames
142 .iter()
143 .filter_map(|frame| frame.index.checked_sub(self.prefix_offset))
144 .max()
145 else {
146 return;
147 };
148
149 let by_runtime_index = Arc::make_mut(&mut self.by_runtime_index);
150 by_runtime_index.resize(
151 by_runtime_index.len().max(max_index.saturating_add(1)),
152 Default::default(),
153 );
154 for frame in frames {
155 let Some(index) = frame.index.checked_sub(self.prefix_offset) else {
156 continue;
157 };
158 by_runtime_index[index] = IndicatorInputAvailability::from_frame(frame);
159 }
160 }
161
162 pub fn state(&self, source: IndicatorInputSource) -> IndicatorInputCoverageState {
163 if self.by_runtime_index.is_empty() {
164 return IndicatorInputCoverageState::Unknown;
165 }
166 if self
167 .by_runtime_index
168 .iter()
169 .copied()
170 .any(|availability| availability.contains(source))
171 {
172 IndicatorInputCoverageState::Available
173 } else {
174 IndicatorInputCoverageState::Missing
175 }
176 }
177
178 pub fn at(&self, source: IndicatorInputSource, runtime_index: usize) -> bool {
179 self.at_index(runtime_index)
180 .is_some_and(|availability| availability.contains(source))
181 }
182}
183
184#[derive(Debug, Clone, PartialEq, Eq)]
190pub struct IndicatorSourceInput {
191 pub id: String,
192 pub label: String,
193 pub default_label: String,
194 pub allow_indicator: bool,
195}
196
197#[derive(Debug, Clone, Copy, PartialEq, Eq)]
198pub enum IndicatorAttachmentMode {
199 Overlay,
200 Panel,
201}
202
203#[derive(Debug, Clone)]
204pub struct IndicatorDefinition {
205 pub display_name: String,
206 pub author: Option<IndicatorAuthor>,
207 pub description: Option<String>,
208 pub source: Option<String>,
209 pub sourceable: bool,
210 pub inputs: Vec<IndicatorInputRequirement>,
211 pub source_inputs: Vec<IndicatorSourceInput>,
212 pub attachment_mode: IndicatorAttachmentMode,
213 pub outputs: Vec<OutputChannelSpec>,
214 pub style_slots: Vec<IndicatorColorSpec>,
215 pub region_style_slots: Vec<String>,
216 pub mark_specs: Vec<IndicatorMarkSpec>,
217 pub segment_specs: Vec<IndicatorSegmentSpec>,
218 pub overlay_channels: Vec<IndicatorOverlayChannelSpec>,
219 pub fill_specs: Vec<IndicatorFillSpec>,
220 pub panel_recipe: IndicatorPanelRecipe,
221}
222
223#[derive(Debug, Clone, PartialEq)]
228pub struct IndicatorOutputLayerSpec {
229 pub channel_id: String,
230 pub layer_name: String,
231 pub pane: PaneTarget,
232 pub data_kind: LayerDataKind,
233 pub mark: MarkKind,
234 pub axis: AxisBinding,
235}
236
237#[derive(Debug, Clone, Copy, PartialEq, Eq)]
238pub enum IndicatorDefinitionError {
239 EmptyDisplayName,
240 EmptyInputId,
241 EmptyOutputId,
242 EmptyOutputLabel,
243 InvalidOutputPrecision,
244 InvalidOutputWidth,
245 DuplicateInput,
246 DuplicateOutput,
247 EmptyStyleSlot,
248 DuplicateStyleSlot,
249 UnknownOutputStyleSlot,
250 EmptyMarkSource,
251 UnknownMarkSource,
252 InvalidMarkFallback,
253 EmptySegmentId,
254 EmptySegmentLabel,
255 DuplicateSegment,
256 InvalidSegmentWidth,
257 UnknownSegmentStyleSlot,
258 EmptyLayerName,
259 IncompatibleLayerMark,
260 InvalidOverlayWidth,
261 InvalidFillOpacity,
262 UnknownStyleBinding,
263 EmptyReadoutLabel,
264 EmptyTableColumnLabel,
265 EmptyOutputLayers,
266 UnknownViewOutput,
267 DuplicateViewOutput,
268 BooleanPlotOutput,
269 IncompatibleTableOutput,
270}
271
272impl IndicatorDefinition {
273 pub fn new(
274 display_name: impl Into<String>,
275 inputs: impl Into<Vec<IndicatorInputRequirement>>,
276 outputs: impl Into<Vec<OutputChannelSpec>>,
277 panel_recipe: IndicatorPanelRecipe,
278 ) -> Self {
279 Self {
280 display_name: display_name.into(),
281 author: None,
282 description: None,
283 source: None,
284 sourceable: false,
285 inputs: inputs.into(),
286 source_inputs: Vec::new(),
287 attachment_mode: IndicatorAttachmentMode::Panel,
288 outputs: outputs.into(),
289 style_slots: Vec::new(),
290 region_style_slots: Vec::new(),
291 mark_specs: Vec::new(),
292 segment_specs: Vec::new(),
293 overlay_channels: Vec::new(),
294 fill_specs: Vec::new(),
295 panel_recipe,
296 }
297 }
298
299 pub fn with_style_slots(mut self, style_slots: impl Into<Vec<IndicatorColorSpec>>) -> Self {
300 self.style_slots = style_slots.into();
301 Self { ..self }
302 }
303
304 pub fn with_region_style_slots(mut self, slots: impl Into<Vec<String>>) -> Self {
305 self.region_style_slots = slots.into();
306 Self { ..self }
307 }
308
309 pub fn with_source_inputs(
310 mut self,
311 source_inputs: impl Into<Vec<IndicatorSourceInput>>,
312 ) -> Self {
313 self.source_inputs = source_inputs.into();
314 Self { ..self }
315 }
316
317 pub fn with_sourceable(mut self, sourceable: bool) -> Self {
318 self.sourceable = sourceable;
319 Self { ..self }
320 }
321
322 pub fn with_author(mut self, author: Option<IndicatorAuthor>) -> Self {
323 self.author = author;
324 Self { ..self }
325 }
326
327 pub fn with_description(mut self, description: Option<String>) -> Self {
328 self.description = description;
329 Self { ..self }
330 }
331
332 pub fn with_source(mut self, source: Option<String>) -> Self {
333 self.source = source;
334 Self { ..self }
335 }
336
337 pub fn with_attachment_mode(mut self, attachment_mode: IndicatorAttachmentMode) -> Self {
338 self.attachment_mode = attachment_mode;
339 Self { ..self }
340 }
341
342 pub fn with_display_specs(
343 mut self,
344 mark_specs: impl Into<Vec<IndicatorMarkSpec>>,
345 overlay_channels: impl Into<Vec<IndicatorOverlayChannelSpec>>,
346 fill_specs: impl Into<Vec<IndicatorFillSpec>>,
347 ) -> Self {
348 self.mark_specs = mark_specs.into();
349 self.overlay_channels = overlay_channels.into();
350 self.fill_specs = fill_specs.into();
351 Self { ..self }
352 }
353
354 pub fn with_segment_specs(
355 mut self,
356 segment_specs: impl Into<Vec<IndicatorSegmentSpec>>,
357 ) -> Self {
358 self.segment_specs = segment_specs.into();
359 Self { ..self }
360 }
361
362 pub fn output(&self, id: &str) -> Option<&OutputChannelSpec> {
363 self.outputs.iter().find(|output| output.id == id)
364 }
365
366 fn numeric_plot_output(&self, id: &str) -> Option<&OutputChannelSpec> {
367 let output = self.output(id)?;
368 (output.value_type == OutputValueType::Number
369 && self
370 .output_layers()
371 .iter()
372 .any(|layer| layer.channel_id == id))
373 .then_some(output)
374 }
375
376 pub fn sourceable_numeric_output(&self, id: &str) -> Option<&OutputChannelSpec> {
377 self.sourceable
378 .then(|| self.numeric_plot_output(id))
379 .flatten()
380 }
381
382 pub fn has_numeric_plot_layers(&self) -> bool {
383 self.output_layers()
384 .iter()
385 .any(|layer| self.numeric_plot_output(&layer.channel_id).is_some())
386 }
387
388 pub fn default_view(&self) -> &IndicatorPanelRecipe {
389 &self.panel_recipe
390 }
391
392 pub fn output_layers(&self) -> &[IndicatorOutputLayerSpec] {
393 match &self.panel_recipe {
394 IndicatorPanelRecipe::Plot { layers, .. }
395 | IndicatorPanelRecipe::PrimaryOverlay { layers, .. } => layers,
396 IndicatorPanelRecipe::Table { .. } => &[],
397 }
398 }
399
400 pub fn segment_specs(&self) -> &[IndicatorSegmentSpec] {
401 &self.segment_specs
402 }
403
404 pub fn requires(&self, source: IndicatorInputSource) -> bool {
405 self.inputs
406 .iter()
407 .any(|requirement| requirement.source == source && requirement.is_required())
408 }
409
410 pub fn uses(&self, source: IndicatorInputSource) -> bool {
411 self.inputs
412 .iter()
413 .any(|requirement| requirement.source == source)
414 }
415
416 pub fn validate(&self) -> Result<(), IndicatorDefinitionError> {
417 if self.display_name.trim().is_empty() {
418 return Err(IndicatorDefinitionError::EmptyDisplayName);
419 }
420
421 for (index, input) in self.inputs.iter().enumerate() {
422 if input.id().trim().is_empty() {
423 return Err(IndicatorDefinitionError::EmptyInputId);
424 }
425 if self.inputs[..index]
426 .iter()
427 .any(|previous| previous.source == input.source)
428 {
429 return Err(IndicatorDefinitionError::DuplicateInput);
430 }
431 }
432
433 for (index, output) in self.outputs.iter().enumerate() {
434 if output.id.trim().is_empty() {
435 return Err(IndicatorDefinitionError::EmptyOutputId);
436 }
437 if output.label.trim().is_empty() {
438 return Err(IndicatorDefinitionError::EmptyOutputLabel);
439 }
440 if !output.precision.is_valid() {
441 return Err(IndicatorDefinitionError::InvalidOutputPrecision);
442 }
443 if !output.line_width.is_finite() || output.line_width <= 0.0 {
444 return Err(IndicatorDefinitionError::InvalidOutputWidth);
445 }
446 if self.outputs[..index]
447 .iter()
448 .any(|previous| previous.id == output.id)
449 {
450 return Err(IndicatorDefinitionError::DuplicateOutput);
451 }
452 if let Some(style_slot) = output.style_slot.as_deref() {
453 if style_slot.trim().is_empty() {
454 return Err(IndicatorDefinitionError::EmptyStyleSlot);
455 }
456 if !self.style_slots.iter().any(|slot| slot.id == style_slot) {
457 return Err(IndicatorDefinitionError::UnknownOutputStyleSlot);
458 }
459 }
460 if let Some(style_by) = output.style_by.as_deref()
461 && self
462 .output(style_by)
463 .is_none_or(|binding| binding.value_type != OutputValueType::Boolean)
464 {
465 return Err(IndicatorDefinitionError::UnknownStyleBinding);
466 }
467 }
468
469 for (index, style_slot) in self.style_slots.iter().enumerate() {
470 if style_slot.id.trim().is_empty() {
471 return Err(IndicatorDefinitionError::EmptyStyleSlot);
472 }
473 if self.style_slots[..index]
474 .iter()
475 .any(|previous| previous.id == style_slot.id)
476 {
477 return Err(IndicatorDefinitionError::DuplicateStyleSlot);
478 }
479 }
480
481 let validate_style_slot = |style_slot: &str| {
482 if style_slot.trim().is_empty() {
483 return Err(IndicatorDefinitionError::EmptyStyleSlot);
484 }
485 if !self.style_slots.iter().any(|slot| slot.id == style_slot) {
486 return Err(IndicatorDefinitionError::UnknownOutputStyleSlot);
487 }
488 Ok(())
489 };
490 for style_slot in &self.region_style_slots {
491 validate_style_slot(style_slot)?;
492 }
493 let validate_source = |channel: &str| {
494 self.output(channel)
495 .map(|_| ())
496 .ok_or(IndicatorDefinitionError::UnknownViewOutput)
497 };
498 let validate_numeric_source = |channel: &str| {
499 validate_source(channel)?;
500 if self
501 .output(channel)
502 .is_some_and(|output| output.value_type == OutputValueType::Number)
503 {
504 Ok(())
505 } else {
506 Err(IndicatorDefinitionError::BooleanPlotOutput)
507 }
508 };
509
510 for mark in &self.mark_specs {
511 if mark.label.trim().is_empty() {
512 return Err(IndicatorDefinitionError::EmptyLayerName);
513 }
514 if mark.source_channel.trim().is_empty() {
515 return Err(IndicatorDefinitionError::EmptyMarkSource);
516 }
517 let Some(output) = self.output(&mark.source_channel) else {
518 return Err(IndicatorDefinitionError::UnknownMarkSource);
519 };
520 if output.value_type != OutputValueType::Number {
521 return Err(IndicatorDefinitionError::UnknownMarkSource);
522 }
523 if mark.color_slot.trim().is_empty() {
524 return Err(IndicatorDefinitionError::EmptyStyleSlot);
525 }
526 validate_style_slot(&mark.color_slot)?;
527 if mark.true_color_slot.is_some() != mark.false_color_slot.is_some() {
528 return Err(IndicatorDefinitionError::UnknownStyleBinding);
529 }
530 if let Some(slot) = mark.true_color_slot.as_deref() {
531 validate_style_slot(slot)?;
532 }
533 if let Some(slot) = mark.false_color_slot.as_deref() {
534 validate_style_slot(slot)?;
535 }
536 if mark.highlight_by.is_some() != mark.highlight_color_slot.is_some() {
537 return Err(IndicatorDefinitionError::UnknownStyleBinding);
538 }
539 if let Some(highlight_by) = mark.highlight_by.as_deref()
540 && (self
541 .output(highlight_by)
542 .is_none_or(|binding| binding.value_type != OutputValueType::Boolean)
543 || !matches!(
544 mark.mark,
545 MarkKind::Bar(
546 super::composition::BarMode::Regular
547 | super::composition::BarMode::Histogram(_)
548 )
549 ))
550 {
551 return Err(IndicatorDefinitionError::UnknownStyleBinding);
552 }
553 if let Some(slot) = mark.highlight_color_slot.as_deref() {
554 validate_style_slot(slot)?;
555 }
556 }
557
558 for mark in &self.mark_specs {
559 let Some(fallback_mark) = mark.fallback_mark else {
560 continue;
561 };
562 if fallback_mark == mark.mark {
563 return Err(IndicatorDefinitionError::InvalidMarkFallback);
564 }
565 let fallback_marks = self
566 .mark_specs
567 .iter()
568 .filter(|candidate| candidate.mark == fallback_mark)
569 .collect::<Vec<_>>();
570 if fallback_marks.len() != 1 || fallback_marks[0].required_input.is_some() {
571 return Err(IndicatorDefinitionError::InvalidMarkFallback);
572 }
573 }
574
575 for (index, segment) in self.segment_specs.iter().enumerate() {
576 if segment.id.trim().is_empty() {
577 return Err(IndicatorDefinitionError::EmptySegmentId);
578 }
579 if segment.label.trim().is_empty() {
580 return Err(IndicatorDefinitionError::EmptySegmentLabel);
581 }
582 if self.segment_specs[..index]
583 .iter()
584 .any(|previous| previous.id == segment.id)
585 {
586 return Err(IndicatorDefinitionError::DuplicateSegment);
587 }
588 if !segment.line_width.is_finite() || segment.line_width <= 0.0 {
589 return Err(IndicatorDefinitionError::InvalidSegmentWidth);
590 }
591 if segment.color_slot.trim().is_empty()
592 || !self
593 .style_slots
594 .iter()
595 .any(|style| style.id == segment.color_slot)
596 {
597 return Err(IndicatorDefinitionError::UnknownSegmentStyleSlot);
598 }
599 }
600
601 for channel in &self.overlay_channels {
602 validate_numeric_source(&channel.source)?;
603 validate_style_slot(&channel.color_slot)?;
604 if !channel.line_width.is_finite() || channel.line_width <= 0.0 {
605 return Err(IndicatorDefinitionError::InvalidOverlayWidth);
606 }
607 if let Some(style_by) = channel.style_by.as_deref()
608 && self
609 .output(style_by)
610 .is_none_or(|binding| binding.value_type != OutputValueType::Boolean)
611 {
612 return Err(IndicatorDefinitionError::UnknownStyleBinding);
613 }
614 }
615
616 for fill in &self.fill_specs {
617 validate_numeric_source(&fill.upper)?;
618 validate_numeric_source(&fill.lower)?;
619 validate_style_slot(&fill.color_slot)?;
620 if fill.true_color_slot.is_some() != fill.false_color_slot.is_some() {
621 return Err(IndicatorDefinitionError::UnknownStyleBinding);
622 }
623 if let Some(style_slot) = fill.true_color_slot.as_deref() {
624 validate_style_slot(style_slot)?;
625 }
626 if let Some(style_slot) = fill.false_color_slot.as_deref() {
627 validate_style_slot(style_slot)?;
628 }
629 if let Some(style_by) = fill.style_by.as_deref()
630 && self
631 .output(style_by)
632 .is_none_or(|binding| binding.value_type != OutputValueType::Boolean)
633 {
634 return Err(IndicatorDefinitionError::UnknownStyleBinding);
635 }
636 if !fill.opacity.is_finite() || !(0.0..=1.0).contains(&fill.opacity) {
637 return Err(IndicatorDefinitionError::InvalidFillOpacity);
638 }
639 }
640
641 let validate_layers = |layers: &[IndicatorOutputLayerSpec]| {
642 for (index, layer) in layers.iter().enumerate() {
643 if layer.layer_name.trim().is_empty() {
644 return Err(IndicatorDefinitionError::EmptyLayerName);
645 }
646 let Some(output) = self.output(&layer.channel_id) else {
647 return Err(IndicatorDefinitionError::UnknownViewOutput);
648 };
649 if output.value_type == OutputValueType::Boolean {
650 return Err(IndicatorDefinitionError::BooleanPlotOutput);
651 }
652 if super::composition::resolve_mark_for_data_kind(layer.mark, layer.data_kind)
653 != layer.mark
654 {
655 return Err(IndicatorDefinitionError::IncompatibleLayerMark);
656 }
657 if layers[..index]
658 .iter()
659 .any(|previous| previous.channel_id == layer.channel_id)
660 {
661 return Err(IndicatorDefinitionError::DuplicateViewOutput);
662 }
663 }
664 Ok(())
665 };
666
667 match &self.panel_recipe {
668 IndicatorPanelRecipe::Plot {
669 layers, readouts, ..
670 } => {
671 if layers.is_empty() {
672 return Err(IndicatorDefinitionError::EmptyOutputLayers);
673 }
674 validate_layers(layers)?;
675 for readout in readouts {
676 if readout.label.trim().is_empty() {
677 return Err(IndicatorDefinitionError::EmptyReadoutLabel);
678 }
679 validate_numeric_source(&readout.source)?;
680 validate_style_slot(&readout.color_slot)?;
681 }
682 }
683 IndicatorPanelRecipe::Table { columns, .. } => {
684 for column in columns {
685 if column.label.trim().is_empty() {
686 return Err(IndicatorDefinitionError::EmptyTableColumnLabel);
687 }
688 match &column.color {
689 TableColumnColor::Static { slot } => {
690 validate_style_slot(slot)?;
691 }
692 TableColumnColor::Signed {
693 positive_slot,
694 negative_slot,
695 } => {
696 validate_style_slot(positive_slot)?;
697 validate_style_slot(negative_slot)?;
698 }
699 }
700 let output = self
701 .output(&column.value)
702 .ok_or(IndicatorDefinitionError::UnknownViewOutput)?;
703 let expected_type = match column.format {
704 TableColumnFormat::Boolean => OutputValueType::Boolean,
705 TableColumnFormat::Quantity
706 | TableColumnFormat::Integer
707 | TableColumnFormat::Percent => OutputValueType::Number,
708 };
709 if output.value_type != expected_type {
710 return Err(IndicatorDefinitionError::IncompatibleTableOutput);
711 }
712 if matches!(&column.color, TableColumnColor::Signed { .. })
713 && output.value_type != OutputValueType::Number
714 {
715 return Err(IndicatorDefinitionError::IncompatibleTableOutput);
716 }
717 }
718 }
719 IndicatorPanelRecipe::PrimaryOverlay { layers, .. } => {
720 if layers.is_empty() {
721 return Err(IndicatorDefinitionError::EmptyOutputLayers);
722 }
723 validate_layers(layers)?;
724 }
725 }
726
727 Ok(())
728 }
729}
730
731#[derive(Debug, Clone, Copy, Default)]
732pub struct IndicatorHistoryRequirement {
733 history_start: Option<UnixMs>,
734 history_bars: u64,
735 warmup_bars: u64,
736}
737
738impl IndicatorHistoryRequirement {
739 pub fn new(history_start: Option<UnixMs>, history_bars: u64, warmup_bars: u64) -> Self {
740 Self {
741 history_start,
742 history_bars,
743 warmup_bars,
744 }
745 }
746
747 pub fn history_start(self) -> Option<UnixMs> {
748 self.history_start
749 }
750
751 pub fn history_bars(self) -> u64 {
752 self.history_bars
753 }
754
755 pub fn warmup_bars(self) -> u64 {
756 self.warmup_bars
757 }
758
759 pub(crate) fn merge(self, other: Self) -> Self {
760 Self {
761 history_start: match (self.history_start, other.history_start) {
762 (Some(left), Some(right)) => Some(left.min(right)),
763 (Some(time), None) | (None, Some(time)) => Some(time),
764 (None, None) => None,
765 },
766 history_bars: self.history_bars.max(other.history_bars),
767 warmup_bars: self.warmup_bars.max(other.warmup_bars),
768 }
769 }
770}
771
772#[derive(Debug, Clone, PartialEq, Eq)]
773pub struct IndicatorColorSpec {
774 pub id: String,
775 pub label: String,
776 pub default_role: ThemeColorRole,
777 pub default_shade: ThemeColorShade,
778 pub required_input: Option<IndicatorInputSource>,
779}
780
781#[derive(Debug, Clone, PartialEq, Eq)]
782pub struct IndicatorMarkSpec {
783 pub label: String,
784 pub mark: MarkKind,
785 pub source_channel: String,
786 pub required_input: Option<IndicatorInputSource>,
787 pub fallback_mark: Option<MarkKind>,
788 pub color_slot: String,
790 pub true_color_slot: Option<String>,
792 pub false_color_slot: Option<String>,
794 pub highlight_by: Option<String>,
796 pub highlight_color_slot: Option<String>,
798}
799
800#[derive(Debug, Clone, PartialEq)]
801pub struct IndicatorSegmentSpec {
802 pub id: String,
803 pub label: String,
804 pub color_slot: String,
805 pub line_width: f32,
806}
807
808#[derive(Debug, Clone, PartialEq)]
809pub struct IndicatorOverlayChannelSpec {
810 pub label: String,
811 pub source: String,
812 pub line_width: f32,
813 pub color_role: ThemeColorRole,
814 pub color_slot: String,
815 pub style_by: Option<String>,
816}
817
818#[derive(Debug, Clone, Copy, PartialEq, Eq)]
819pub enum IndicatorReadoutScope {
820 Always,
821 Mark(MarkKind),
822}
823
824impl IndicatorReadoutScope {
825 pub fn matches(self, mark: MarkKind) -> bool {
826 match self {
827 Self::Always => true,
828 Self::Mark(expected) => expected == mark,
829 }
830 }
831}
832
833#[derive(Debug, Clone, PartialEq)]
834pub struct IndicatorReadoutSpec {
835 pub label: String,
836 pub source: String,
837 pub color_role: ThemeColorRole,
838 pub color_slot: String,
839 pub scope: IndicatorReadoutScope,
840}
841
842#[derive(Debug, Clone, Copy, PartialEq, Eq)]
843pub enum IndicatorFillGeometry {
844 PanelBand,
845 SeriesBand,
846}
847
848#[derive(Debug, Clone, PartialEq)]
849pub struct IndicatorFillSpec {
850 pub upper: String,
851 pub lower: String,
852 pub upper_offset_bars: i32,
853 pub lower_offset_bars: i32,
854 pub style_by: Option<String>,
855 pub geometry: IndicatorFillGeometry,
856 pub color_slot: String,
857 pub true_color_slot: Option<String>,
858 pub false_color_slot: Option<String>,
859 pub opacity: f32,
860}
861
862#[derive(Debug, Clone, Copy, PartialEq, Eq)]
863pub enum TableColumnFormat {
864 Quantity,
865 Integer,
866 Percent,
867 Boolean,
868}
869
870#[derive(Debug, Clone, PartialEq, Eq)]
871pub enum TableColumnColor {
872 Static {
873 slot: String,
874 },
875 Signed {
876 positive_slot: String,
877 negative_slot: String,
878 },
879}
880
881#[derive(Debug, Clone, PartialEq, Eq)]
882pub struct TableColumnSpec {
883 pub label: String,
884 pub value: String,
885 pub format: TableColumnFormat,
886 pub color: TableColumnColor,
887}
888
889impl IndicatorColorSpec {
890 pub fn new(
891 id: impl Into<String>,
892 label: impl Into<String>,
893 default_role: ThemeColorRole,
894 default_shade: ThemeColorShade,
895 ) -> Self {
896 Self {
897 id: id.into(),
898 label: label.into(),
899 default_role,
900 default_shade,
901 required_input: None,
902 }
903 }
904
905 pub fn default_color(&self) -> ThemeColor {
906 ThemeColor::new(self.default_role, self.default_shade)
907 }
908
909 pub fn with_required_input(mut self, required_input: Option<IndicatorInputSource>) -> Self {
910 self.required_input = required_input;
911 self
912 }
913}
914
915#[derive(Debug, Clone, Copy, PartialEq, Eq)]
916pub enum IndicatorUnsupportedReason {
917 NotInstalled,
918 BasisNotSupported,
919 IncompleteHistory,
920 SourceNotSupported,
921 ResolutionNotSupported,
922 InsufficientData { available: usize, required: usize },
923 MissingRequiredInput { source: IndicatorInputSource },
924}
925
926impl IndicatorUnsupportedReason {
927 pub fn indicator_unavailable_tooltip(&self) -> &'static str {
928 match self {
929 IndicatorUnsupportedReason::NotInstalled => "Not included in this build",
930 IndicatorUnsupportedReason::BasisNotSupported => {
931 "Not available for the selected chart basis"
932 }
933 IndicatorUnsupportedReason::SourceNotSupported => {
934 "Not available for the selected market"
935 }
936 IndicatorUnsupportedReason::ResolutionNotSupported => {
937 "Not available at the selected timeframe"
938 }
939 IndicatorUnsupportedReason::IncompleteHistory => "Requires complete historical data",
940 IndicatorUnsupportedReason::InsufficientData { .. } => {
941 "Not enough data to use this indicator"
942 }
943 IndicatorUnsupportedReason::MissingRequiredInput { source } => match source {
944 IndicatorInputSource::Klines => "Requires candle data",
945 IndicatorInputSource::DirectionalVolume => {
946 "Requires directional volume from candles or trade data"
947 }
948 IndicatorInputSource::Trades => "Enable trade data to use this indicator",
949 IndicatorInputSource::OpenInterest => "Requires open-interest data",
950 },
951 }
952 }
953}
954
955#[derive(Debug, Clone, Copy, PartialEq, Eq)]
956pub enum IndicatorAvailability {
957 Available,
958 PendingProbe,
959 Unsupported(IndicatorUnsupportedReason),
960}
961
962#[derive(Debug, Clone, PartialEq)]
963pub enum IndicatorPanelRecipe {
964 Plot {
965 panel_title: String,
966 layer_name: String,
967 value_precision: PanelValuePrecision,
968 value_label_policy: PanelValueLabelPolicy,
969 preferred_scale: PanelScaleMode,
970 layers: Vec<IndicatorOutputLayerSpec>,
971 readouts: Vec<IndicatorReadoutSpec>,
972 },
973 Table {
974 panel_title: String,
975 columns: Vec<TableColumnSpec>,
976 },
977 PrimaryOverlay {
978 layer_name: String,
979 layers: Vec<IndicatorOutputLayerSpec>,
980 },
981}
982
983#[derive(Debug, Clone, Copy)]
984pub struct AvailabilityContext {
985 pub basis: Basis,
986 pub base_ticker: TickerInfo,
987}
988
989#[derive(Debug, Clone)]
990pub struct IndicatorSettingsContext {
991 pub basis: Basis,
992 pub input_coverage: IndicatorInputCoverage,
993}
994
995#[derive(Debug, Clone, PartialEq)]
1001pub enum IndicatorSettingValue {
1002 I64(i64),
1003 F64(f64),
1004 Bool(bool),
1005 Color(u32),
1006 Choice(String),
1007 Text(String),
1008 TextList(Vec<String>),
1009}
1010
1011impl IndicatorSettingValue {
1012 pub fn text(value: impl Into<String>) -> Self {
1013 Self::Text(value.into())
1014 }
1015
1016 pub fn choice(value: impl Into<String>) -> Self {
1017 Self::Choice(value.into())
1018 }
1019
1020 pub fn as_text(&self) -> Option<&str> {
1021 match self {
1022 Self::Choice(value) | Self::Text(value) => Some(value),
1023 Self::I64(_) | Self::F64(_) | Self::Bool(_) | Self::Color(_) | Self::TextList(_) => {
1024 None
1025 }
1026 }
1027 }
1028
1029 pub fn into_text(self) -> Option<String> {
1030 match self {
1031 Self::Choice(value) | Self::Text(value) => Some(value),
1032 Self::I64(_) | Self::F64(_) | Self::Bool(_) | Self::Color(_) | Self::TextList(_) => {
1033 None
1034 }
1035 }
1036 }
1037}
1038
1039#[derive(Debug, Clone, PartialEq, Eq)]
1040pub struct IndicatorSettingChoice {
1041 pub id: String,
1042 pub label: String,
1043}
1044
1045impl Display for IndicatorSettingChoice {
1046 fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
1047 formatter.write_str(&self.label)
1048 }
1049}
1050
1051#[derive(Debug, Clone, PartialEq)]
1052pub enum IndicatorSettingControl {
1053 SliderI64 {
1054 min: Option<i64>,
1055 max: Option<i64>,
1056 },
1057 SliderF64 {
1058 min: Option<f64>,
1059 max: Option<f64>,
1060 step: f64,
1061 },
1062 Toggle,
1063 Color,
1064 Text,
1065 Choices(Vec<IndicatorSettingChoice>),
1066}
1067
1068#[derive(Debug, Clone, PartialEq)]
1069pub struct IndicatorSetting {
1070 pub id: String,
1071 pub label: String,
1072 pub tooltip: Option<String>,
1073 pub group: Option<String>,
1074 pub value: IndicatorSettingValue,
1075 pub control: IndicatorSettingControl,
1076}