Skip to main content

flowsurface_data/chart/
kline.rs

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/// Definition-local style overrides keyed by stable declaration IDs.
434#[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(&current).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/// Why a declared source binding cannot act as an indicator dependency.
974#[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    /// Resolves `source_id` on `instance` to the instance it draws from.
1016    ///
1017    /// `Ok(None)` means the binding is absent, so the input contributes nothing.
1018    /// `Err` means the binding is present but cannot act as a dependency.
1019    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    /// Dependency edges among `active` instances, optionally ignoring one binding.
1068    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}