1pub mod bar;
2pub mod chart_data;
3pub mod composition;
4pub mod drawing;
5pub mod fetch_plan;
6pub mod footprint;
7pub mod indicators;
8pub mod poc;
9pub mod series;
10pub mod studies;
11pub mod viewport;
12
13use self::footprint::{ClusterKind, ClusterScaling};
14use crate::chart::Basis;
15pub use crate::config::theme::ThemeAccent;
16use crate::config::theme::ThemeColor;
17use crate::config::theme::ThemeColorRole;
18use crate::config::theme::ThemeColorShade;
19
20pub use chart_data::{HistoricalInputUpdate, KlineData, LiveIndicatorUpdate};
21use composition::{
22 LayerSource as DomainLayerSource, LayerSpec, PanelSpec,
23 PanelValuePrecision as DomainPanelValuePrecision,
24};
25pub use fetch_plan::{FetchBatch, FetchPlanInput, FetchRange, visible_window};
26pub use indicators::{
27 BaseSeriesSource, CatalogInstallError, IndicatorAuthor, IndicatorCatalog, IndicatorColorSpec,
28 IndicatorConfigError, IndicatorDefinition, IndicatorDefinitionError, IndicatorFillGeometry,
29 IndicatorFillSpec, IndicatorHistoryRequirement, IndicatorHost, IndicatorInputAvailability,
30 IndicatorInputCoverage, IndicatorInputCoverageState, IndicatorInputRequirement,
31 IndicatorInputSource, IndicatorMarkSpec, IndicatorOutputLayerSpec, IndicatorOutputRef,
32 IndicatorOverlayChannelSpec, IndicatorPanelRecipe, IndicatorReadoutScope, IndicatorReadoutSpec,
33 IndicatorSegmentSpec, IndicatorSetting, IndicatorSettingChoice, IndicatorSettingControl,
34 IndicatorSettingValue, IndicatorSettingsContext, IndicatorSourceInput,
35 IndicatorUnsupportedReason, OpenInterestSeries, OutputChannelSpec, OutputValue,
36 OutputValueType, TableColumnColor, TableColumnFormat, TableColumnSpec, availability,
37 declared_input_requirements, indicator_definition, table_columns, unsupported_reason,
38};
39pub use indicators::{IndicatorConfig, IndicatorId};
40pub use series::{KlineIndexChange, KlineSeriesPoint, KlineSeriesPoints, KlineUpdate, SeriesColor};
41pub use studies::tpo::{
42 TPO_TRADE_COVERAGE_TIMEFRAME, TpoAnchor, TpoBoundaryEdits, TpoCalculationError, TpoProfile,
43 TpoProfileSettings, TpoProfileStatus, TpoRow, TradeTpoCache,
44 build_tpo_profile_with_source_timeframe, build_tpo_profiles_with_source_timeframe,
45};
46pub use studies::volume_profile::{
47 AnchoredProfileDistribution, ProfileDistribution, ProfileLevel, RecurringKlineProfileIndex,
48 kline_volume_profile, recurring_kline_volume_profiles,
49};
50pub use studies::{
51 BigTradesConfig, DEFAULT_BIG_TRADES_THRESHOLD, DEFAULT_RECURRING_VOLUME_PROFILE_INTERVAL,
52 DEFAULT_RECURRING_VOLUME_PROFILE_OPACITY_PERCENT, DEFAULT_TPO_LETTER_PERIOD,
53 DEFAULT_TPO_VALUE_AREA_PERCENT, DEFAULT_VISIBLE_VOLUME_PROFILE_OPACITY_PERCENT,
54 DEFAULT_VOLUME_PROFILE_WIDTH_PERCENT, KlineStudy, KlineStudyId, MAX_BIG_TRADES_THRESHOLD,
55 MAX_TPO_VALUE_AREA_PERCENT, MAX_VOLUME_PROFILE_OPACITY_PERCENT,
56 MAX_VOLUME_PROFILE_WIDTH_PERCENT, MIN_BIG_TRADES_THRESHOLD, MIN_TPO_VALUE_AREA_PERCENT,
57 MIN_VOLUME_PROFILE_OPACITY_PERCENT, MIN_VOLUME_PROFILE_WIDTH_PERCENT, SourceConstraint,
58 StudyDataSource, TpoColorConfig, VolumeProfileColorConfig, VolumeProfileColorSlot,
59 VolumeProfileColorSpec, VolumeProfileSchedule, big_trades_step_count,
60 big_trades_step_for_threshold, big_trades_threshold_at_step, big_trades_threshold_label, tpo,
61 volume_profile,
62};
63pub use viewport::{
64 ChartCoord, ChartStepMs, DEFAULT_HORIZONTAL_SPACING_PX, HorizontalWorld,
65 MAX_HORIZONTAL_SPACING_PX, MIN_HORIZONTAL_SPACING_PX, PanelYValueRange, WorldAxis, YUnit,
66 YUnitRange, ZOOM_SENSITIVITY,
67};
68
69use rustc_hash::FxHashSet;
70use serde::{Deserialize, Deserializer, Serialize, Serializer};
71use std::collections::{BTreeMap, BTreeSet};
72
73pub const MIN_CANDLESTICK_OPACITY_PERCENT: u8 = 5;
74pub const MAX_CANDLESTICK_OPACITY_PERCENT: u8 = 100;
75pub const MIN_LINE_WIDTH: u8 = 1;
76pub const MAX_LINE_WIDTH: u8 = 4;
77
78const DEFAULT_FOOTPRINT_IMBALANCE_THRESHOLD_PERCENT: usize = 200;
79const DEFAULT_FOOTPRINT_IMBALANCE_IGNORE_ZEROS: bool = true;
80const DEFAULT_FOOTPRINT_IMBALANCE_COLOR: ThemeColor =
81 ThemeColor::new(ThemeColorRole::Warning, ThemeColorShade::Strong);
82const DEFAULT_CANDLESTICK_OPACITY_PERCENT: u8 = 100;
83
84const DEFAULT_INDICATOR_INSTANCE_ID_BASE: u64 = 1 << 63;
85
86#[derive(Debug, Clone, Copy, PartialEq, Eq)]
87pub enum FootprintPocColorSlot {
88 Poc,
89 Npoc,
90}
91
92#[derive(Debug, Clone, Copy, PartialEq, Eq)]
93pub struct FootprintPocColorSpec {
94 pub label: &'static str,
95 pub slot: FootprintPocColorSlot,
96}
97
98const FOOTPRINT_POC_COLOR_SPECS: [FootprintPocColorSpec; 2] = [
99 FootprintPocColorSpec {
100 label: "POC",
101 slot: FootprintPocColorSlot::Poc,
102 },
103 FootprintPocColorSpec {
104 label: "NPOC",
105 slot: FootprintPocColorSlot::Npoc,
106 },
107];
108
109#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
110pub struct FootprintPocColorConfig {
111 pub poc: ThemeColor,
112 pub npoc: ThemeColor,
113}
114
115impl Default for FootprintPocColorConfig {
116 fn default() -> Self {
117 Self::defaults()
118 }
119}
120
121impl FootprintPocColorConfig {
122 pub const fn defaults() -> Self {
123 Self {
124 poc: ThemeColor::new(ThemeColorRole::Neutral, ThemeColorShade::Strong),
125 npoc: ThemeColor::new(ThemeColorRole::Warning, ThemeColorShade::Strong),
126 }
127 }
128
129 pub const fn color_specs() -> &'static [FootprintPocColorSpec] {
130 &FOOTPRINT_POC_COLOR_SPECS
131 }
132
133 pub const fn color_for(self, slot: FootprintPocColorSlot) -> ThemeColor {
134 match slot {
135 FootprintPocColorSlot::Poc => self.poc,
136 FootprintPocColorSlot::Npoc => self.npoc,
137 }
138 }
139
140 pub const fn with_color(self, slot: FootprintPocColorSlot, color: ThemeColor) -> Self {
141 match slot {
142 FootprintPocColorSlot::Poc => Self { poc: color, ..self },
143 FootprintPocColorSlot::Npoc => Self {
144 npoc: color,
145 ..self
146 },
147 }
148 }
149}
150
151#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
152pub struct FootprintPocConfig {
153 pub show_poc: bool,
154 pub show_npoc: bool,
155 #[serde(default)]
156 pub colors: FootprintPocColorConfig,
157}
158
159impl FootprintPocConfig {
160 pub const fn with_show_poc(self, show_poc: bool) -> Self {
161 Self { show_poc, ..self }
162 }
163
164 pub const fn with_show_npoc(self, show_npoc: bool) -> Self {
165 Self { show_npoc, ..self }
166 }
167
168 pub const fn with_colors(self, colors: FootprintPocColorConfig) -> Self {
169 Self { colors, ..self }
170 }
171}
172
173#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
174#[serde(default)]
175pub struct FootprintImbalanceConfig {
176 pub show: bool,
177 pub threshold_percent: usize,
178 pub ignore_zeros: bool,
179 pub color: ThemeColor,
180}
181
182impl Default for FootprintImbalanceConfig {
183 fn default() -> Self {
184 Self {
185 show: false,
186 threshold_percent: DEFAULT_FOOTPRINT_IMBALANCE_THRESHOLD_PERCENT,
187 ignore_zeros: DEFAULT_FOOTPRINT_IMBALANCE_IGNORE_ZEROS,
188 color: DEFAULT_FOOTPRINT_IMBALANCE_COLOR,
189 }
190 }
191}
192
193impl FootprintImbalanceConfig {
194 pub const fn with_show(self, show: bool) -> Self {
195 Self { show, ..self }
196 }
197
198 pub const fn with_threshold_percent(self, threshold_percent: usize) -> Self {
199 Self {
200 threshold_percent,
201 ..self
202 }
203 }
204
205 pub const fn with_ignore_zeros(self, ignore_zeros: bool) -> Self {
206 Self {
207 ignore_zeros,
208 ..self
209 }
210 }
211
212 pub const fn with_color(self, color: ThemeColor) -> Self {
213 Self { color, ..self }
214 }
215}
216
217#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
218#[serde(default)]
219pub struct FootprintConfig {
220 pub visible: bool,
221 pub cluster_kind: ClusterKind,
222 pub cluster_scaling: ClusterScaling,
223 pub imbalance: FootprintImbalanceConfig,
224 pub poc: FootprintPocConfig,
225}
226
227impl FootprintConfig {
228 pub const fn with_visible(self, visible: bool) -> Self {
229 Self { visible, ..self }
230 }
231
232 pub const fn with_cluster_kind(self, cluster_kind: ClusterKind) -> Self {
233 Self {
234 cluster_kind,
235 ..self
236 }
237 }
238
239 pub const fn with_cluster_scaling(self, cluster_scaling: ClusterScaling) -> Self {
240 Self {
241 cluster_scaling,
242 ..self
243 }
244 }
245
246 pub const fn with_imbalance(self, imbalance: FootprintImbalanceConfig) -> Self {
247 Self { imbalance, ..self }
248 }
249
250 pub const fn with_poc(self, poc: FootprintPocConfig) -> Self {
251 Self { poc, ..self }
252 }
253
254 pub fn with_default_options(self) -> Self {
255 Self {
256 visible: self.visible,
257 ..Self::default()
258 }
259 }
260}
261
262#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
263pub struct IndicatorInstanceId(u64);
264
265impl IndicatorInstanceId {
266 fn new(value: u64) -> Self {
267 Self(value)
268 }
269
270 pub fn default_for(kind: &IndicatorId) -> Self {
271 Self(DEFAULT_INDICATOR_INSTANCE_ID_BASE + kind.default_instance_slot())
272 }
273}
274
275impl Serialize for IndicatorInstanceId {
276 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
277 where
278 S: Serializer,
279 {
280 serializer.serialize_u64(self.0)
281 }
282}
283
284impl<'de> Deserialize<'de> for IndicatorInstanceId {
285 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
286 where
287 D: Deserializer<'de>,
288 {
289 u64::deserialize(deserializer).map(Self::new)
290 }
291}
292
293#[derive(Debug, Clone, Copy, PartialEq, Eq)]
294pub enum IndicatorUpdateScope {
295 Full,
296 From(usize),
297}
298
299impl IndicatorUpdateScope {
300 pub fn is_full(self) -> bool {
301 matches!(self, Self::Full)
302 }
303
304 pub fn merge(self, other: Self) -> Self {
305 match (self, other) {
306 (Self::Full, _) | (_, Self::Full) => Self::Full,
307 (Self::From(first), Self::From(second)) => Self::From(first.min(second)),
308 }
309 }
310}
311
312#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
313pub enum CandlestickColorSlot {
314 Bullish,
315 Bearish,
316}
317
318#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
319pub struct CandlestickColorConfig {
320 pub bullish: ThemeColor,
321 pub bearish: ThemeColor,
322}
323
324impl Default for CandlestickColorConfig {
325 fn default() -> Self {
326 Self::defaults()
327 }
328}
329
330impl CandlestickColorConfig {
331 pub const fn defaults() -> Self {
332 Self {
333 bullish: ThemeColor::new(ThemeColorRole::Success, ThemeColorShade::Base),
334 bearish: ThemeColor::new(ThemeColorRole::Danger, ThemeColorShade::Base),
335 }
336 }
337
338 pub const fn color_for(self, slot: CandlestickColorSlot) -> ThemeColor {
339 match slot {
340 CandlestickColorSlot::Bullish => self.bullish,
341 CandlestickColorSlot::Bearish => self.bearish,
342 }
343 }
344
345 pub const fn with_color(self, slot: CandlestickColorSlot, color: ThemeColor) -> Self {
346 match slot {
347 CandlestickColorSlot::Bullish => Self {
348 bullish: color,
349 ..self
350 },
351 CandlestickColorSlot::Bearish => Self {
352 bearish: color,
353 ..self
354 },
355 }
356 }
357}
358
359#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
360#[serde(default)]
361pub struct CandlestickConfig {
362 pub visible: bool,
363 pub colors: CandlestickColorConfig,
364 pub opacity_percent: u8,
365}
366
367impl Default for CandlestickConfig {
368 fn default() -> Self {
369 Self {
370 visible: true,
371 colors: CandlestickColorConfig::defaults(),
372 opacity_percent: DEFAULT_CANDLESTICK_OPACITY_PERCENT,
373 }
374 }
375}
376
377impl CandlestickConfig {
378 pub const fn with_visible(self, visible: bool) -> Self {
379 Self { visible, ..self }
380 }
381
382 pub const fn with_color(self, slot: CandlestickColorSlot, color: ThemeColor) -> Self {
383 Self {
384 colors: self.colors.with_color(slot, color),
385 ..self
386 }
387 }
388
389 pub const fn with_opacity_percent(self, opacity_percent: u8) -> Self {
390 Self {
391 opacity_percent,
392 ..self
393 }
394 }
395
396 pub fn with_default_style(self) -> Self {
397 Self {
398 visible: self.visible,
399 ..Self::default()
400 }
401 }
402
403 pub fn opacity_ratio(self) -> f32 {
404 f32::from(self.opacity_percent.clamp(
405 MIN_CANDLESTICK_OPACITY_PERCENT,
406 MAX_CANDLESTICK_OPACITY_PERCENT,
407 )) / f32::from(MAX_CANDLESTICK_OPACITY_PERCENT)
408 }
409}
410
411#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
412pub enum LineStrokeStyle {
413 #[default]
414 Solid,
415 Dashed,
416 Dotted,
417}
418
419impl LineStrokeStyle {
420 pub const ALL: [Self; 3] = [Self::Solid, Self::Dashed, Self::Dotted];
421}
422
423impl std::fmt::Display for LineStrokeStyle {
424 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
425 formatter.write_str(match self {
426 Self::Solid => "Solid",
427 Self::Dashed => "Dashed",
428 Self::Dotted => "Dotted",
429 })
430 }
431}
432
433#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
435#[serde(default)]
436pub struct IndicatorStyleOverrides {
437 pub fill_visible: bool,
438 #[serde(default)]
439 colors: BTreeMap<String, ThemeColor>,
440 #[serde(default)]
441 line_widths: BTreeMap<String, u8>,
442 #[serde(default)]
443 stroke_styles: BTreeMap<String, LineStrokeStyle>,
444}
445
446impl Default for IndicatorStyleOverrides {
447 fn default() -> Self {
448 Self {
449 fill_visible: true,
450 colors: BTreeMap::new(),
451 line_widths: BTreeMap::new(),
452 stroke_styles: BTreeMap::new(),
453 }
454 }
455}
456
457impl IndicatorStyleOverrides {
458 pub fn color(&self, id: &str) -> Option<ThemeColor> {
459 self.colors.get(id).copied()
460 }
461
462 pub fn set_color(&mut self, id: impl Into<String>, color: ThemeColor) -> bool {
463 let id = id.into();
464 if id.trim().is_empty() {
465 return false;
466 }
467 if self.color(&id) == Some(color) {
468 return false;
469 }
470 self.colors.insert(id, color);
471 true
472 }
473
474 pub fn line_width(&self, id: &str) -> Option<u8> {
475 self.line_widths.get(id).copied()
476 }
477
478 pub fn stroke_style(&self, id: &str) -> Option<LineStrokeStyle> {
479 self.stroke_styles.get(id).copied()
480 }
481
482 pub fn set_line_width(&mut self, id: impl Into<String>, width: u8) -> bool {
483 let id = id.into();
484 if id.trim().is_empty() {
485 return false;
486 }
487 let width = width.clamp(MIN_LINE_WIDTH, MAX_LINE_WIDTH);
488 if self.line_width(&id) == Some(width) {
489 return false;
490 }
491 self.line_widths.insert(id, width);
492 true
493 }
494
495 pub fn set_stroke_style(&mut self, id: impl Into<String>, style: LineStrokeStyle) -> bool {
496 let id = id.into();
497 if id.trim().is_empty() {
498 return false;
499 }
500 if self.stroke_style(&id) == Some(style) {
501 return false;
502 }
503 self.stroke_styles.insert(id, style);
504 true
505 }
506
507 pub fn normalized(mut self) -> Self {
508 for width in self.line_widths.values_mut() {
509 *width = (*width).clamp(MIN_LINE_WIDTH, MAX_LINE_WIDTH);
510 }
511 self.line_widths.retain(|id, _| !id.trim().is_empty());
512 self.stroke_styles.retain(|id, _| !id.trim().is_empty());
513 self.colors.retain(|id, _| !id.trim().is_empty());
514 self
515 }
516
517 fn normalized_for_slots(self, slots: &[indicators::IndicatorColorSpec]) -> Self {
518 let mut normalized = self.normalized();
519 normalized
520 .colors
521 .retain(|id, _| slots.iter().any(|slot| slot.id == *id));
522 normalized
523 .line_widths
524 .retain(|id, _| slots.iter().any(|slot| slot.id == *id));
525 normalized
526 .stroke_styles
527 .retain(|id, _| slots.iter().any(|slot| slot.id == *id));
528 normalized
529 }
530
531 pub fn clear(&mut self) -> bool {
532 let changed = !self.fill_visible
533 || !self.colors.is_empty()
534 || !self.line_widths.is_empty()
535 || !self.stroke_styles.is_empty();
536 self.colors.clear();
537 self.line_widths.clear();
538 self.stroke_styles.clear();
539 self.fill_visible = true;
540 changed
541 }
542}
543
544#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
545#[serde(deny_unknown_fields)]
546pub struct IndicatorInstance {
547 pub id: IndicatorInstanceId,
548 #[serde(rename = "indicator")]
549 pub kind: IndicatorId,
550 pub config: IndicatorConfig,
551 pub style_overrides: IndicatorStyleOverrides,
552 #[serde(default)]
553 pub source_bindings: BTreeMap<String, IndicatorOutputRef>,
554 #[serde(default)]
555 pub base_source_selections: BTreeMap<String, BaseSeriesSource>,
556}
557
558impl IndicatorInstance {
559 pub fn update_source_binding(
560 &mut self,
561 input_id: impl Into<String>,
562 output: Option<IndicatorOutputRef>,
563 ) -> bool {
564 let input_id = input_id.into();
565 if !self
566 .kind
567 .definition()
568 .source_inputs
569 .iter()
570 .any(|input| input.id == input_id)
571 {
572 return false;
573 }
574 match output {
575 Some(output) => {
576 let binding_changed = self.source_bindings.get(&input_id) != Some(&output);
577 let base_selection_removed = self.base_source_selections.remove(&input_id);
578 self.source_bindings.insert(input_id, output);
579 binding_changed || base_selection_removed.is_some()
580 }
581 None => self.source_bindings.remove(&input_id).is_some(),
582 }
583 }
584
585 pub fn update_base_source(
586 &mut self,
587 input_id: impl Into<String>,
588 source: Option<BaseSeriesSource>,
589 ) -> bool {
590 let input_id = input_id.into();
591 if !self
592 .kind
593 .definition()
594 .source_inputs
595 .iter()
596 .any(|input| input.id == input_id)
597 {
598 return false;
599 }
600 if self.source_bindings.contains_key(&input_id) {
601 return false;
602 }
603 match source {
604 Some(source) => {
605 if self.base_source_selections.get(&input_id) == Some(&source) {
606 return false;
607 }
608 self.base_source_selections.insert(input_id, source);
609 true
610 }
611 None => self.base_source_selections.remove(&input_id).is_some(),
612 }
613 }
614
615 pub fn configured_color(&self, slot: &str) -> Option<ThemeColor> {
616 self.style_overrides.color(slot).or_else(|| {
617 self.kind
618 .color_specs()
619 .iter()
620 .find(|spec| spec.id == slot)
621 .map(|spec| spec.default_color())
622 })
623 }
624
625 pub fn new(id: IndicatorInstanceId, kind: IndicatorId) -> Self {
626 let config = IndicatorConfig::default_for(&kind);
627 Self {
628 id,
629 kind,
630 config,
631 style_overrides: IndicatorStyleOverrides::default(),
632 source_bindings: BTreeMap::new(),
633 base_source_selections: BTreeMap::new(),
634 }
635 }
636
637 pub fn normalized(mut self) -> Self {
638 self.config = self.config.normalized_for(&self.kind);
639 self.style_overrides = if self.kind.is_registered() {
640 self.style_overrides
641 .normalized_for_slots(&self.kind.definition().style_slots)
642 } else {
643 self.style_overrides.normalized()
644 };
645 for input_id in self.source_bindings.keys().cloned().collect::<Vec<_>>() {
646 self.base_source_selections.remove(&input_id);
647 }
648 self
649 }
650}
651
652#[derive(Debug, Clone, Default)]
653pub struct IndicatorInstanceStore {
654 instances: Vec<IndicatorInstance>,
655 next_id: u64,
656 warmup_bars_by_id: BTreeMap<IndicatorInstanceId, Option<u64>>,
657}
658
659impl PartialEq for IndicatorInstanceStore {
660 fn eq(&self, other: &Self) -> bool {
661 self.instances == other.instances && self.next_id == other.next_id
662 }
663}
664
665impl IndicatorInstanceStore {
666 pub fn validate_source_binding(
667 &self,
668 active: &[IndicatorInstanceId],
669 id: IndicatorInstanceId,
670 input_id: &str,
671 ) -> Result<(), String> {
672 let active = active
673 .iter()
674 .copied()
675 .collect::<BTreeSet<IndicatorInstanceId>>();
676 let Some(instance) = self.get(id) else {
677 return Err(format!(
678 "source binding references missing indicator instance {id:?}"
679 ));
680 };
681 if !active.contains(&id) {
682 return Err("source binding target is not active".to_owned());
683 }
684 if !instance.kind.definition().has_numeric_plot_layers() {
685 return Err("attached indicator has no numeric plot layer".to_owned());
686 }
687 let Some(target) = self
688 .dependency_target(instance, input_id, &active)
689 .map_err(|problem| problem.to_string())?
690 else {
691 return Ok(());
692 };
693
694 let edges = self.dependency_edges(&active, Some((id, input_id)));
695 let mut pending = vec![target.id];
696 let mut visited = FxHashSet::default();
697 while let Some(current) = pending.pop() {
698 if current == id {
699 return Err("indicator dependency cycle".to_owned());
700 }
701 if visited.insert(current) {
702 pending.extend(edges.get(¤t).into_iter().flatten().copied());
703 }
704 }
705 Ok(())
706 }
707
708 pub fn source_offsets(&self) -> Vec<i64> {
709 let mut offsets = vec![0];
710 for instance in self.instances() {
711 let kind = &instance.kind;
712 offsets.extend(
713 kind.output_channels()
714 .iter()
715 .map(|output| i64::from(output.offset_bars)),
716 );
717
718 for fill in kind.fill_specs() {
719 let channel_offset = |channel: &str| {
720 kind.output_channels()
721 .iter()
722 .find(|output| output.id == channel)
723 .map(|output| i64::from(output.offset_bars))
724 .unwrap_or_default()
725 };
726 offsets.push(if fill.upper_offset_bars == 0 {
727 channel_offset(&fill.upper)
728 } else {
729 i64::from(fill.upper_offset_bars)
730 });
731 offsets.push(if fill.lower_offset_bars == 0 {
732 channel_offset(&fill.lower)
733 } else {
734 i64::from(fill.lower_offset_bars)
735 });
736 }
737 }
738 offsets
739 }
740
741 pub fn max_positive_output_offset(&self) -> i64 {
742 self.instances()
743 .iter()
744 .filter_map(|instance| {
745 instance
746 .kind
747 .output_channels()
748 .iter()
749 .map(|output| output.offset_bars)
750 .filter(|offset| *offset > 0)
751 .max()
752 })
753 .map(i64::from)
754 .max()
755 .unwrap_or_default()
756 }
757
758 pub fn from_instances(instances: Vec<IndicatorInstance>) -> Self {
759 let mut store = Self::default();
760 for instance in instances {
761 if !instance.kind.is_registered() {
762 continue;
763 }
764 let instance = instance.normalized();
765 if !instance.config.is_valid_for(&instance.kind) {
766 continue;
767 }
768 if store.get(instance.id).is_none() {
769 store.next_id = store.next_id.max(
770 instance
771 .id
772 .0
773 .checked_add(1)
774 .filter(|next| *next < DEFAULT_INDICATOR_INSTANCE_ID_BASE)
775 .unwrap_or(1),
776 );
777 store.instances.push(instance);
778 }
779 }
780 store.rebuild_warmup_cache();
781 store.next_id = store.next_id.max(1);
782 store
783 }
784
785 pub fn instances(&self) -> &[IndicatorInstance] {
786 &self.instances
787 }
788
789 pub fn get(&self, id: IndicatorInstanceId) -> Option<&IndicatorInstance> {
790 self.instances.iter().find(|instance| instance.id == id)
791 }
792
793 pub fn warmup_bars(&self, id: IndicatorInstanceId) -> Option<u64> {
794 self.warmup_bars_by_id.get(&id).copied().unwrap_or_else(|| {
795 self.get(id)
796 .and_then(|instance| instance.kind.warmup_bars(&instance.config))
797 })
798 }
799
800 pub fn create(&mut self, kind: IndicatorId) -> IndicatorInstanceId {
801 while self
802 .instances
803 .iter()
804 .any(|instance| instance.id == IndicatorInstanceId::new(self.next_id))
805 {
806 self.next_id += 1;
807 }
808 let id = IndicatorInstanceId::new(self.next_id);
809 self.next_id += 1;
810 let instance = IndicatorInstance::new(id, kind);
811 self.warmup_bars_by_id
812 .insert(id, instance.kind.warmup_bars(&instance.config));
813 self.instances.push(instance);
814 id
815 }
816
817 pub fn update_config(&mut self, id: IndicatorInstanceId, config: IndicatorConfig) -> bool {
818 let Some(instance) = self.instances.iter_mut().find(|instance| instance.id == id) else {
819 return false;
820 };
821 let config = config.normalized_for(&instance.kind);
822 if !config.is_valid_for(&instance.kind) {
823 return false;
824 }
825 if instance.config == config {
826 return false;
827 }
828 instance.config = config;
829 let warmup_bars = instance.kind.warmup_bars(&instance.config);
830 self.warmup_bars_by_id.insert(id, warmup_bars);
831 true
832 }
833
834 pub fn reset_config(&mut self, id: IndicatorInstanceId) -> bool {
835 let Some(kind) = self.get(id).map(|instance| instance.kind.clone()) else {
836 return false;
837 };
838 self.update_config(id, IndicatorConfig::default_for(&kind))
839 }
840
841 pub fn update_source_binding(
842 &mut self,
843 id: IndicatorInstanceId,
844 input_id: impl Into<String>,
845 output: Option<IndicatorOutputRef>,
846 ) -> bool {
847 self.instances
848 .iter_mut()
849 .find(|instance| instance.id == id)
850 .is_some_and(|instance| instance.update_source_binding(input_id, output))
851 }
852
853 pub fn update_base_source(
854 &mut self,
855 id: IndicatorInstanceId,
856 input_id: impl Into<String>,
857 source: Option<BaseSeriesSource>,
858 ) -> bool {
859 self.instances
860 .iter_mut()
861 .find(|instance| instance.id == id)
862 .is_some_and(|instance| instance.update_base_source(input_id, source))
863 }
864
865 pub fn update_color(
866 &mut self,
867 id: IndicatorInstanceId,
868 slot: impl Into<String>,
869 color: ThemeColor,
870 ) -> bool {
871 self.instances
872 .iter_mut()
873 .find(|instance| instance.id == id)
874 .is_some_and(|instance| instance.style_overrides.set_color(slot, color))
875 }
876
877 pub fn update_fill_visibility(&mut self, id: IndicatorInstanceId, visible: bool) -> bool {
878 self.instances
879 .iter_mut()
880 .find(|instance| instance.id == id)
881 .is_some_and(|instance| {
882 if instance.style_overrides.fill_visible == visible {
883 return false;
884 }
885 instance.style_overrides.fill_visible = visible;
886 true
887 })
888 }
889
890 pub fn update_line_width(
891 &mut self,
892 id: IndicatorInstanceId,
893 slot: impl Into<String>,
894 width: u8,
895 ) -> bool {
896 self.instances
897 .iter_mut()
898 .find(|instance| instance.id == id)
899 .is_some_and(|instance| instance.style_overrides.set_line_width(slot, width))
900 }
901
902 pub fn update_stroke_style(
903 &mut self,
904 id: IndicatorInstanceId,
905 slot: impl Into<String>,
906 stroke_style: LineStrokeStyle,
907 ) -> bool {
908 self.instances
909 .iter_mut()
910 .find(|instance| instance.id == id)
911 .is_some_and(|instance| {
912 instance
913 .style_overrides
914 .set_stroke_style(slot, stroke_style)
915 })
916 }
917
918 pub fn reset_style(&mut self, id: IndicatorInstanceId) -> bool {
919 let Some(instance) = self.instances.iter_mut().find(|instance| instance.id == id) else {
920 return false;
921 };
922 instance.style_overrides.clear()
923 }
924
925 pub fn remove(&mut self, id: IndicatorInstanceId) -> Option<IndicatorInstance> {
926 let index = self
927 .instances
928 .iter()
929 .position(|instance| instance.id == id)?;
930 self.warmup_bars_by_id.remove(&id);
931 Some(self.instances.remove(index))
932 }
933
934 pub fn history_requirement(
935 &self,
936 active: &[IndicatorInstanceId],
937 basis: Basis,
938 reference_time: exchange::UnixMs,
939 ) -> IndicatorHistoryRequirement {
940 let active_set = active
941 .iter()
942 .copied()
943 .filter(|id| self.get(*id).is_some())
944 .collect::<BTreeSet<_>>();
945 let mut memo = BTreeMap::new();
946 active
947 .iter()
948 .filter_map(|id| {
949 history_requirement_for_instance(
950 *id,
951 self,
952 &active_set,
953 basis,
954 reference_time,
955 &mut memo,
956 &mut BTreeSet::new(),
957 )
958 })
959 .fold(Default::default(), IndicatorHistoryRequirement::merge)
960 }
961
962 fn rebuild_warmup_cache(&mut self) {
963 self.warmup_bars_by_id = self
964 .instances
965 .iter()
966 .map(|instance| (instance.id, instance.kind.warmup_bars(&instance.config)))
967 .collect();
968 }
969}
970
971const MAX_INDICATOR_HISTORY_BARS: u64 = 50_000;
972
973#[derive(Debug, Clone, PartialEq, Eq)]
975pub enum SourceBindingProblem {
976 UnknownInput(String),
977 InputDisallowsIndicator(String),
978 SelfReference,
979 MissingInstance(IndicatorInstanceId),
980 MissingOutput(String),
981 NotSourceableOutput(String),
982}
983
984impl std::fmt::Display for SourceBindingProblem {
985 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
986 match self {
987 Self::UnknownInput(input) => {
988 write!(
989 formatter,
990 "source binding references unknown input '{input}'"
991 )
992 }
993 Self::InputDisallowsIndicator(input) => write!(
994 formatter,
995 "source input '{input}' does not allow indicator bindings"
996 ),
997 Self::SelfReference => formatter.write_str("indicator cannot depend on itself"),
998 Self::MissingInstance(id) => write!(
999 formatter,
1000 "source binding references missing indicator instance {id:?}"
1001 ),
1002 Self::MissingOutput(channel) => write!(
1003 formatter,
1004 "source binding references missing output '{channel}'"
1005 ),
1006 Self::NotSourceableOutput(channel) => write!(
1007 formatter,
1008 "source binding output '{channel}' is not a sourceable numeric plot output"
1009 ),
1010 }
1011 }
1012}
1013
1014impl IndicatorInstanceStore {
1015 pub fn dependency_target<'a>(
1020 &'a self,
1021 instance: &IndicatorInstance,
1022 source_id: &str,
1023 active: &BTreeSet<IndicatorInstanceId>,
1024 ) -> Result<Option<&'a IndicatorInstance>, SourceBindingProblem> {
1025 let definition = instance.kind.definition();
1026 let Some(input) = definition
1027 .source_inputs
1028 .iter()
1029 .find(|input| input.id == source_id)
1030 else {
1031 return Err(SourceBindingProblem::UnknownInput(source_id.to_owned()));
1032 };
1033 if !input.allow_indicator {
1034 return Err(SourceBindingProblem::InputDisallowsIndicator(
1035 source_id.to_owned(),
1036 ));
1037 }
1038 let Some(reference) = instance.source_bindings.get(source_id) else {
1039 return Ok(None);
1040 };
1041 if reference.instance_id == instance.id {
1042 return Err(SourceBindingProblem::SelfReference);
1043 }
1044 if !active.contains(&reference.instance_id) {
1045 return Err(SourceBindingProblem::MissingInstance(reference.instance_id));
1046 }
1047 let Some(target) = self.get(reference.instance_id) else {
1048 return Err(SourceBindingProblem::MissingInstance(reference.instance_id));
1049 };
1050 let target_definition = target.kind.definition();
1051 if target_definition.output(&reference.channel_id).is_none() {
1052 return Err(SourceBindingProblem::MissingOutput(
1053 reference.channel_id.clone(),
1054 ));
1055 }
1056 if target_definition
1057 .sourceable_numeric_output(&reference.channel_id)
1058 .is_none()
1059 {
1060 return Err(SourceBindingProblem::NotSourceableOutput(
1061 reference.channel_id.clone(),
1062 ));
1063 }
1064 Ok(Some(target))
1065 }
1066
1067 pub fn dependency_edges(
1069 &self,
1070 active: &BTreeSet<IndicatorInstanceId>,
1071 excluded: Option<(IndicatorInstanceId, &str)>,
1072 ) -> BTreeMap<IndicatorInstanceId, Vec<IndicatorInstanceId>> {
1073 let mut edges = BTreeMap::<IndicatorInstanceId, Vec<IndicatorInstanceId>>::new();
1074 for id in active {
1075 let Some(instance) = self.get(*id) else {
1076 continue;
1077 };
1078 let mut targets = Vec::new();
1079 for source_id in instance.source_bindings.keys() {
1080 if excluded == Some((*id, source_id.as_str())) {
1081 continue;
1082 }
1083 if let Ok(Some(target)) = self.dependency_target(instance, source_id, active)
1084 && !targets.contains(&target.id)
1085 {
1086 targets.push(target.id);
1087 }
1088 }
1089 if !targets.is_empty() {
1090 edges.insert(*id, targets);
1091 }
1092 }
1093 edges
1094 }
1095}
1096
1097fn history_requirement_for_instance(
1098 id: IndicatorInstanceId,
1099 store: &IndicatorInstanceStore,
1100 active: &BTreeSet<IndicatorInstanceId>,
1101 basis: Basis,
1102 reference_time: exchange::UnixMs,
1103 memo: &mut BTreeMap<IndicatorInstanceId, IndicatorHistoryRequirement>,
1104 visiting: &mut BTreeSet<IndicatorInstanceId>,
1105) -> Option<IndicatorHistoryRequirement> {
1106 if let Some(requirement) = memo.get(&id) {
1107 return Some(*requirement);
1108 }
1109 if !visiting.insert(id) {
1110 return Some(IndicatorHistoryRequirement::default());
1111 }
1112 let instance = store.get(id)?;
1113 let own_warmup = store.warmup_bars(id);
1114 let own = instance.kind.history_requirement_with_warmup(
1115 instance.config.clone(),
1116 basis,
1117 reference_time,
1118 own_warmup,
1119 );
1120 let dependency = instance
1121 .source_bindings
1122 .keys()
1123 .filter_map(|source_id| {
1124 store
1125 .dependency_target(instance, source_id, active)
1126 .ok()
1127 .flatten()
1128 .and_then(|target| {
1129 history_requirement_for_instance(
1130 target.id,
1131 store,
1132 active,
1133 basis,
1134 reference_time,
1135 memo,
1136 visiting,
1137 )
1138 })
1139 })
1140 .fold(Default::default(), IndicatorHistoryRequirement::merge);
1141 visiting.remove(&id);
1142 let history_start = match (own.history_start(), dependency.history_start()) {
1143 (Some(left), Some(right)) => Some(left.min(right)),
1144 (Some(time), None) | (None, Some(time)) => Some(time),
1145 (None, None) => None,
1146 };
1147 let requirement = IndicatorHistoryRequirement::new(
1148 history_start,
1149 own.history_bars()
1150 .saturating_add(dependency.history_bars())
1151 .min(MAX_INDICATOR_HISTORY_BARS),
1152 own_warmup
1153 .unwrap_or_default()
1154 .saturating_add(dependency.warmup_bars())
1155 .min(MAX_INDICATOR_HISTORY_BARS),
1156 );
1157 memo.insert(id, requirement);
1158 Some(requirement)
1159}
1160
1161#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
1162#[serde(default)]
1163pub struct FloatingToolbarPosition {
1164 pub x_ratio: f32,
1165 pub y_ratio: f32,
1166}
1167
1168impl Default for FloatingToolbarPosition {
1169 fn default() -> Self {
1170 Self {
1171 x_ratio: 0.0,
1172 y_ratio: 0.0,
1173 }
1174 }
1175}
1176
1177impl FloatingToolbarPosition {
1178 pub fn clamped(self) -> Self {
1179 let clamp_ratio = |ratio: f32| {
1180 if ratio.is_finite() {
1181 ratio.clamp(0.0, 1.0)
1182 } else {
1183 0.0
1184 }
1185 };
1186
1187 Self {
1188 x_ratio: clamp_ratio(self.x_ratio),
1189 y_ratio: clamp_ratio(self.y_ratio),
1190 }
1191 }
1192}
1193
1194#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
1195pub enum LegendVisibility {
1196 ShowOnHover,
1197 #[default]
1198 KeepLegendVisible,
1199 KeepLegendAndValuesVisible,
1200}
1201
1202impl LegendVisibility {
1203 pub const ALL: [Self; 3] = [
1204 Self::ShowOnHover,
1205 Self::KeepLegendVisible,
1206 Self::KeepLegendAndValuesVisible,
1207 ];
1208
1209 pub const fn legend_always_visible(self) -> bool {
1210 !matches!(self, Self::ShowOnHover)
1211 }
1212
1213 pub const fn legend_values_always_visible(self) -> bool {
1214 matches!(self, Self::KeepLegendAndValuesVisible)
1215 }
1216}
1217
1218impl std::fmt::Display for LegendVisibility {
1219 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1220 formatter.write_str(match self {
1221 Self::ShowOnHover => "Show on hover",
1222 Self::KeepLegendVisible => "Keep legend visible",
1223 Self::KeepLegendAndValuesVisible => "Keep legend and values visible",
1224 })
1225 }
1226}
1227
1228#[derive(Debug, Clone, Serialize, Deserialize)]
1229#[serde(deny_unknown_fields)]
1230pub struct Config {
1231 pub settings: SettingsSnapshot,
1232 #[serde(default)]
1233 pub selected_preset: Option<uuid::Uuid>,
1234 pub drawings: Vec<DrawingConfig>,
1235 pub active_drawing_tool: DrawingTool,
1236 pub floating_toolbar_position: FloatingToolbarPosition,
1237 pub drawing_sidebar_visible: bool,
1238 pub series: Vec<SeriesConfig>,
1239}
1240
1241impl Default for Config {
1242 fn default() -> Self {
1243 Self {
1244 settings: SettingsSnapshot::default(),
1245 selected_preset: None,
1246 drawings: Vec::new(),
1247 active_drawing_tool: DrawingTool::Cursor,
1248 floating_toolbar_position: FloatingToolbarPosition::default(),
1249 drawing_sidebar_visible: true,
1250 series: Vec::new(),
1251 }
1252 }
1253}
1254
1255#[derive(Debug, Default, Clone, Copy, PartialEq, Serialize, Deserialize)]
1256#[serde(default)]
1257pub struct ChartOptions {
1258 #[serde(rename = "trade_data_enabled")]
1259 pub trade_data_requested: bool,
1260 pub legend_visibility: LegendVisibility,
1261 pub candlestick: CandlestickConfig,
1262 pub footprint: FootprintConfig,
1263}
1264
1265impl ChartOptions {
1266 pub fn set_trade_data_requested(&mut self, enabled: bool, basis: Basis) -> bool {
1267 let mut changed = self.trade_data_requested != enabled;
1268 self.trade_data_requested = enabled;
1269 if !matches!(basis, Basis::Tick(_)) {
1270 changed |= self.footprint.visible != enabled;
1271 self.footprint = self.footprint.with_visible(enabled);
1272 }
1273 changed
1274 }
1275}
1276
1277#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)]
1278#[serde(default)]
1279pub struct SettingsSnapshot {
1280 pub primary_scale_inverted: bool,
1281 pub primary_series_style: Option<PrimarySeriesStyle>,
1282 #[serde(flatten)]
1283 pub options: ChartOptions,
1284 pub panels: Vec<PanelConfig>,
1285 pub splits: Vec<f32>,
1286 pub active_studies: Vec<KlineStudyId>,
1287 pub study_configs: Vec<KlineStudy>,
1288 #[serde(default)]
1289 pub tpo_profile_edits: TpoBoundaryEdits,
1290 pub indicator_instances: Vec<IndicatorInstance>,
1291}
1292
1293#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
1294pub struct PrimarySeriesStyle {
1295 pub color: SeriesColor,
1296 pub line_width: u8,
1297 pub stroke_style: LineStrokeStyle,
1298}
1299
1300#[derive(Debug, Clone, Copy, Default, PartialEq, Serialize, Deserialize)]
1301pub struct ChartSettings {
1302 pub drawing: drawing::DrawingSettings,
1303}
1304
1305impl SettingsSnapshot {
1306 pub fn normalized_preset(self) -> Self {
1307 let mut settings = self.preset_settings();
1308 settings.indicator_instances = settings
1309 .indicator_instances
1310 .into_iter()
1311 .map(IndicatorInstance::normalized)
1312 .collect();
1313 settings
1314 }
1315
1316 pub fn preset_settings(mut self) -> Self {
1317 self.tpo_profile_edits = TpoBoundaryEdits::default();
1318 for panel in &mut self.panels {
1319 panel
1320 .layers
1321 .retain(|layer| !matches!(layer.source, LayerSource::Comparison(_)));
1322 if panel
1323 .base_layer
1324 .is_some_and(|id| !panel.layers.iter().any(|layer| layer.id == id))
1325 {
1326 panel.base_layer = panel.layers.first().map(|layer| layer.id);
1327 }
1328 }
1329 self
1330 }
1331
1332 pub fn indicator_instance_store(&self) -> IndicatorInstanceStore {
1333 if self.panels.is_empty() && self.indicator_instances.is_empty() {
1334 let kind = IndicatorId::parse("core.volume").expect("static ID is valid");
1335 IndicatorInstanceStore::from_instances(vec![IndicatorInstance::new(
1336 IndicatorInstanceId::default_for(&kind),
1337 kind,
1338 )])
1339 } else {
1340 IndicatorInstanceStore::from_instances(self.indicator_instances.clone())
1341 }
1342 }
1343}
1344
1345#[derive(Debug, Clone, Serialize, Deserialize)]
1346pub struct ChartPreset {
1347 pub id: uuid::Uuid,
1348 pub name: String,
1349 pub settings: SettingsSnapshot,
1350}
1351
1352#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1353#[serde(default)]
1354pub struct PanelConfig {
1355 pub id: composition::PanelId,
1356 pub presentation: composition::PanelPresentation,
1357 #[serde(default)]
1358 pub named_pane: Option<u32>,
1359 pub title: Option<String>,
1360 pub value_precision: Option<ValuePrecision>,
1361 pub value_label_policy: ValueLabelPolicy,
1362 pub base_layer: Option<u32>,
1363 pub preferred_scale: PanelScaleMode,
1364 pub comparison_policy: ComparisonPolicy,
1365 pub layers: Vec<LayerConfig>,
1366}
1367
1368impl Default for PanelConfig {
1369 fn default() -> Self {
1370 Self {
1371 id: composition::PanelId::Main,
1372 presentation: composition::PanelPresentation::Plot,
1373 named_pane: None,
1374 title: None,
1375 value_precision: None,
1376 value_label_policy: ValueLabelPolicy::default(),
1377 base_layer: None,
1378 preferred_scale: PanelScaleMode::Absolute,
1379 comparison_policy: ComparisonPolicy::default(),
1380 layers: Vec::new(),
1381 }
1382 }
1383}
1384
1385#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1386#[serde(default)]
1387pub struct LayerConfig {
1388 pub id: u32,
1389 pub name: String,
1390 pub source: LayerSource,
1391 pub data_kind: LayerDataKind,
1392 #[serde(default)]
1393 pub purpose: LayerPurpose,
1394 pub mark: Option<MarkKind>,
1395 pub mark_override: Option<MarkKind>,
1396 pub axis: AxisBinding,
1397 pub visible: bool,
1398 pub style: LayerStyle,
1399}
1400
1401impl Default for LayerConfig {
1402 fn default() -> Self {
1403 Self {
1404 id: 0,
1405 name: String::new(),
1406 source: LayerSource::Primary,
1407 data_kind: LayerDataKind::Scalar,
1408 purpose: LayerPurpose::Plot,
1409 mark: None,
1410 mark_override: None,
1411 axis: AxisBinding::Primary,
1412 visible: true,
1413 style: LayerStyle::default(),
1414 }
1415 }
1416}
1417
1418pub use composition::{
1419 AxisBinding, BarMode, HistogramMode, LayerDataKind, LayerPurpose, LayerStyle, MarkKind,
1420 PanelPresentation, PanelScaleMode, PanelValueId, PanelValueLabelMode, PanelValueLabelPolicy,
1421};
1422pub type ComparisonPolicy = composition::PanelComparisonPolicy;
1423pub type ValueLabelPolicy = composition::PanelValueLabelPolicy;
1424
1425#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
1426pub enum LayerSource {
1427 #[default]
1428 Primary,
1429 Comparison(exchange::Ticker),
1430 IndicatorOutput(indicators::IndicatorOutputRef),
1431}
1432
1433#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
1434pub enum ValuePrecision {
1435 BaseTickerMinTick,
1436 BaseTickerMinQty,
1437 FixedPower10(i8),
1438 FixedStep(f64),
1439}
1440
1441pub use drawing::{
1442 DrawingAnchor, DrawingColor, DrawingEntity, DrawingObject, DrawingSettings, DrawingStyle,
1443 DrawingStyleDefaults, DrawingTool,
1444};
1445pub type DrawingConfig = DrawingEntity;
1446#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1447pub struct SeriesConfig {
1448 pub ticker: exchange::Ticker,
1449 #[serde(default)]
1450 pub color: Option<iced_core::Color>,
1451 #[serde(default)]
1452 pub accent: Option<ThemeAccent>,
1453 #[serde(default)]
1454 pub shade: ThemeColorShade,
1455 #[serde(default = "default_series_line_width")]
1456 pub line_width: u8,
1457 #[serde(default)]
1458 pub stroke_style: LineStrokeStyle,
1459}
1460
1461fn default_series_line_width() -> u8 {
1462 MIN_LINE_WIDTH
1463}
1464
1465pub fn panel_config_from_spec(panel: &PanelSpec) -> PanelConfig {
1466 PanelConfig {
1467 id: panel.id,
1468 presentation: panel.presentation,
1469 named_pane: panel.named_pane,
1470 title: panel.title.clone(),
1471 value_precision: panel.value_precision.map(value_precision_from_domain),
1472 value_label_policy: panel.value_label_policy,
1473 base_layer: panel.base_layer.map(|layer_id| layer_id.0),
1474 preferred_scale: panel.preferred_scale,
1475 comparison_policy: panel.comparison_policy,
1476 layers: panel.layers.iter().map(layer_config_from_spec).collect(),
1477 }
1478}
1479
1480fn layer_config_from_spec(layer: &LayerSpec) -> LayerConfig {
1481 LayerConfig {
1482 id: layer.id.0,
1483 name: layer.name.clone(),
1484 source: layer_source_from_domain(&layer.source),
1485 data_kind: layer.data_kind,
1486 purpose: layer.purpose,
1487 mark: Some(layer.mark),
1488 mark_override: layer.mark_override,
1489 axis: layer.axis,
1490 visible: layer.visible,
1491 style: layer.style.clone(),
1492 }
1493}
1494
1495pub fn panel_spec_from_config(panel: &PanelConfig) -> PanelSpec {
1496 PanelSpec {
1497 id: panel.id,
1498 presentation: panel.presentation,
1499 named_pane: panel.named_pane,
1500 title: panel.title.clone(),
1501 value_precision: panel.value_precision.map(value_precision_into_domain),
1502 value_label_policy: panel.value_label_policy,
1503 base_layer: panel.base_layer.map(composition::LayerId),
1504 preferred_scale: panel.preferred_scale,
1505 comparison_policy: panel.comparison_policy,
1506 layers: panel.layers.iter().map(layer_spec_from_config).collect(),
1507 }
1508}
1509
1510fn layer_spec_from_config(layer: &LayerConfig) -> LayerSpec {
1511 let source = layer_source_into_domain(layer.source.clone());
1512 LayerSpec {
1513 id: composition::LayerId(layer.id),
1514 name: layer.name.clone(),
1515 source: source.clone(),
1516 data_kind: layer.data_kind,
1517 purpose: layer.purpose,
1518 mark: layer
1519 .mark
1520 .unwrap_or_else(|| composition::default_mark_for_data_kind(layer.data_kind)),
1521 mark_override: layer.mark_override,
1522 axis: layer.axis,
1523 visible: layer.visible,
1524 style: layer.style.clone(),
1525 }
1526}
1527
1528fn layer_source_from_domain(source: &DomainLayerSource) -> LayerSource {
1529 match source {
1530 DomainLayerSource::RawKline {
1531 source: composition::DataSourceId::Comparison(ticker),
1532 } => LayerSource::Comparison(*ticker),
1533 DomainLayerSource::RawKline { .. } => LayerSource::Primary,
1534 DomainLayerSource::IndicatorOutput { output, .. } => {
1535 LayerSource::IndicatorOutput(output.clone())
1536 }
1537 }
1538}
1539
1540fn layer_source_into_domain(source: LayerSource) -> DomainLayerSource {
1541 match source {
1542 LayerSource::Primary => DomainLayerSource::RawKline {
1543 source: composition::DataSourceId::Primary,
1544 },
1545 LayerSource::Comparison(ticker) => DomainLayerSource::RawKline {
1546 source: composition::DataSourceId::Comparison(ticker),
1547 },
1548 LayerSource::IndicatorOutput(output) => DomainLayerSource::IndicatorOutput {
1549 source: composition::DataSourceId::Primary,
1550 output,
1551 },
1552 }
1553}
1554
1555fn value_precision_from_domain(precision: DomainPanelValuePrecision) -> ValuePrecision {
1556 match precision {
1557 DomainPanelValuePrecision::BaseTickerMinTick => ValuePrecision::BaseTickerMinTick,
1558 DomainPanelValuePrecision::BaseTickerMinQty => ValuePrecision::BaseTickerMinQty,
1559 DomainPanelValuePrecision::FixedPower10(step) => ValuePrecision::FixedPower10(step.power),
1560 DomainPanelValuePrecision::FixedStep(step) => ValuePrecision::FixedStep(step),
1561 }
1562}
1563
1564fn value_precision_into_domain(precision: ValuePrecision) -> DomainPanelValuePrecision {
1565 match precision {
1566 ValuePrecision::BaseTickerMinTick => DomainPanelValuePrecision::BaseTickerMinTick,
1567 ValuePrecision::BaseTickerMinQty => DomainPanelValuePrecision::BaseTickerMinQty,
1568 ValuePrecision::FixedPower10(power) => {
1569 DomainPanelValuePrecision::FixedPower10(exchange::unit::Power10::new(power))
1570 }
1571 ValuePrecision::FixedStep(step) => DomainPanelValuePrecision::FixedStep(step),
1572 }
1573}