Skip to main content

egui_charts/studies/
settings.rs

1//! Applied study presentation and transactional input edits.
2
3use super::{Indicator, IndicatorLineStyle, IndicatorValues, InputValue};
4use crate::model::LineStyle;
5use crate::model::{Bar, Instrument, Timeframe};
6use egui::Color32;
7
8/// Geometry for one output line.
9#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
10#[non_exhaustive]
11pub enum PlotType {
12    /// Connected values, broken at missing bars.
13    #[default]
14    Line,
15    /// Bars from zero to the output value.
16    Histogram,
17    /// A circle at each defined value.
18    Circles,
19    /// Horizontal segments followed by a vertical change at the next bar.
20    StepLine,
21    /// Columns from zero, at 80% of the bar pitch.
22    Columns,
23    /// A plus at each defined value.
24    Crosses,
25    /// Filled area between the output and zero.
26    Area,
27    /// An upward triangle centred on each defined value.
28    TriangleUp,
29    /// A downward triangle centred on each defined value.
30    TriangleDown,
31    /// Repaint the source price bars using this plot's per-bar colour.
32    BarColor,
33    /// Colour the full pane height at each defined bar.
34    BackgroundColor,
35}
36
37impl PlotType {
38    /// Every plot type, in the order a style editor lists them.
39    pub const ALL: [Self; 11] = [
40        Self::Line,
41        Self::StepLine,
42        Self::Histogram,
43        Self::Columns,
44        Self::Area,
45        Self::Circles,
46        Self::Crosses,
47        Self::TriangleUp,
48        Self::TriangleDown,
49        Self::BarColor,
50        Self::BackgroundColor,
51    ];
52
53    /// User-facing display name.
54    pub fn name(self) -> &'static str {
55        match self {
56            Self::Line => "Line",
57            Self::StepLine => "Step line",
58            Self::Histogram => "Histogram",
59            Self::Columns => "Columns",
60            Self::Area => "Area",
61            Self::Circles => "Circles",
62            Self::Crosses => "Crosses",
63            Self::TriangleUp => "Triangle up",
64            Self::TriangleDown => "Triangle down",
65            Self::BarColor => "Bar colour",
66            Self::BackgroundColor => "Background colour",
67        }
68    }
69
70    /// Why a plot of this type cannot be drawn for a study, or `None` when it can.
71    /// `overlay` is whether the study is drawn on the price chart, and `price_scale` whether it
72    /// shares the price scale there, which a volume study does not.
73    pub fn incompatibility(self, overlay: bool, price_scale: bool) -> Option<&'static str> {
74        match self {
75            Self::BarColor if !overlay => {
76                Some("Bar colour repaints the price bars, so the study must be on the price chart.")
77            }
78            Self::Histogram | Self::Columns | Self::Area if overlay && price_scale => Some(
79                "This type draws from zero, which is far from the prices on the price scale. \
80                 Give the study its own scale first.",
81            ),
82            _ => None,
83        }
84    }
85}
86
87/// Editable presentation of one output line.
88#[derive(Clone, Copy, Debug, serde::Serialize, serde::Deserialize, PartialEq)]
89pub struct PlotStyle {
90    /// Plot colour.
91    pub color: Color32,
92    /// Stroke width in logical pixels, from 0.5 to 10.
93    pub width: f32,
94    /// Stroke pattern for line plots.
95    pub pattern: LineStyle,
96    /// Plot geometry.
97    pub plot_type: PlotType,
98    /// Whether the plot participates in rendering, hit testing and scaling.
99    pub visible: bool,
100}
101
102/// Editable presentation of one reference level in a study's pane, such as 70 for RSI.
103#[derive(Clone, Copy, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
104#[non_exhaustive]
105pub struct LevelStyle {
106    /// Value the level is drawn at, or `None` for the value the study declares, which can
107    /// follow its inputs.
108    pub value: Option<f64>,
109    /// Line colour.
110    pub color: Color32,
111    /// Stroke pattern.
112    pub pattern: LineStyle,
113    /// Whether the line is drawn. A hidden level still bounds the fill between the outer levels.
114    pub visible: bool,
115}
116
117/// The levels and fill a study declares, as the defaults of its level settings.
118pub(super) fn declared_levels<S: Indicator + ?Sized>(
119    study: &S,
120) -> (Vec<LevelStyle>, bool, Color32) {
121    let pane = study.pane();
122    let levels = pane
123        .levels
124        .iter()
125        .map(|&(value, color, _)| LevelStyle {
126            value: None,
127            color,
128            pattern: study.level_style(value),
129            visible: true,
130        })
131        .collect();
132    (levels, pane.fill_between_levels, study.level_fill_color())
133}
134
135/// Settings without one style per declared level take the study's own levels and fill.
136fn match_declared_levels(study: &dyn Indicator, settings: &mut StudySettings) {
137    let (levels, fill, color) = declared_levels(study);
138    if settings.levels.len() != levels.len() {
139        settings.levels = levels;
140        settings.fill_between_levels = fill;
141        settings.level_fill_color = color;
142    }
143}
144
145/// Interval visibility evaluated for every interval, including custom and trade-based ones.
146/// Ordered ranges compare nominal duration for time bars and thresholds within the same
147/// trade-based family. Other families are outside the range. `Only` matches the exact interval.
148#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
149pub enum StudyIntervalRange {
150    /// Every interval.
151    #[default]
152    All,
153    /// The chosen interval and longer durations or larger thresholds in its family.
154    AndAbove(Timeframe),
155    /// The chosen interval and shorter durations or smaller thresholds in its family.
156    AndBelow(Timeframe),
157    /// The exact chosen interval.
158    Only(Timeframe),
159}
160
161impl StudyIntervalRange {
162    /// Whether `interval` belongs to this visibility range.
163    pub fn contains(self, interval: Timeframe) -> bool {
164        let current = match self {
165            Self::All => return true,
166            Self::Only(current) => return interval == current,
167            Self::AndAbove(current) | Self::AndBelow(current) => current,
168        };
169        let order = match (interval, current) {
170            (Timeframe::Ticks(a), Timeframe::Ticks(b))
171            | (Timeframe::Volume(a), Timeframe::Volume(b))
172            | (Timeframe::Range(a), Timeframe::Range(b))
173            | (Timeframe::Delta(a), Timeframe::Delta(b)) => Some(a.cmp(&b)),
174            (a, b) if !a.is_trade_based() && !b.is_trade_based() => {
175                Some(a.duration_ms().cmp(&b.duration_ms()))
176            }
177            _ => None,
178        };
179        order.is_some_and(|order| match self {
180            Self::AndAbove(_) => !order.is_lt(),
181            Self::AndBelow(_) => !order.is_gt(),
182            _ => false,
183        })
184    }
185}
186
187/// Host-serializable presentation settings of an applied study.
188#[derive(Clone, Debug, serde::Serialize, serde::Deserialize, PartialEq)]
189pub struct StudySettings {
190    /// One style per declared output line.
191    pub plots: Vec<PlotStyle>,
192    /// Decimal places, or the chart's default precision.
193    pub precision: Option<usize>,
194    /// Exact intervals on which the study is hidden, including custom intervals.
195    pub hidden_intervals: Vec<Timeframe>,
196    /// A range applied in addition to the exact hidden intervals. Older saved settings show
197    /// every interval except those in `hidden_intervals`.
198    #[serde(default)]
199    pub interval_range: StudyIntervalRange,
200    /// One style per level of the study's pane, in the order of
201    /// [`StudyPane::levels`](super::StudyPane::levels). Settings saved before levels were
202    /// editable, and settings with another number of levels, take the study's own levels and fill.
203    #[serde(default)]
204    pub levels: Vec<LevelStyle>,
205    /// Whether the band between the lowest and the highest level is filled.
206    #[serde(default)]
207    pub fill_between_levels: bool,
208    /// Colour of the band between the lowest and the highest level.
209    #[serde(default)]
210    pub level_fill_color: Color32,
211    /// Plots, as indices into [`plots`](Self::plots), whose last value has no tag on the price
212    /// scale while the chart shows study values; the legend still shows it. A study starts
213    /// with the plots its [`Indicator::plot_tagged`] leaves out, every plot of a study with more
214    /// than four. An empty list, as in settings saved before this field, tags every plot.
215    /// Applying settings drops indices without a plot and sorts the rest.
216    #[serde(default)]
217    pub untagged_plots: Vec<usize>,
218}
219
220/// An owned settings and input snapshot without calculated study data.
221/// Used by chart history; restoring it requires recalculation on the current bars.
222#[derive(Clone)]
223pub struct StudySettingsSnapshot {
224    inputs: Vec<(String, InputValue)>,
225    settings: StudySettings,
226    timeframe: Option<Timeframe>,
227    colors: Vec<Color32>,
228    visible: bool,
229    overlay: Option<bool>,
230    scale: Option<super::StudyScale>,
231    source: Option<super::StudySource>,
232    inherited_overlay: bool,
233    inherited_scale: super::StudyScale,
234    inherited_source: Option<super::StudySource>,
235    calculation_symbol: String,
236    calculation_timeframe: Option<Timeframe>,
237    wait_for_close: bool,
238    offset: i32,
239}
240
241impl PartialEq for StudySettingsSnapshot {
242    fn eq(&self, other: &Self) -> bool {
243        self.inputs == other.inputs
244            && self.settings == other.settings
245            && self.colors == other.colors
246            && self.visible == other.visible
247            && self.overlay.unwrap_or(self.inherited_overlay)
248                == other.overlay.unwrap_or(other.inherited_overlay)
249            && self.scale.unwrap_or(self.inherited_scale)
250                == other.scale.unwrap_or(other.inherited_scale)
251            && self.source.or(self.inherited_source) == other.source.or(other.inherited_source)
252            && self.calculation_symbol == other.calculation_symbol
253            && self.calculation_timeframe == other.calculation_timeframe
254            && self.wait_for_close == other.wait_for_close
255            && self.offset == other.offset
256    }
257}
258
259impl StudySettingsSnapshot {
260    /// Capture declared inputs and presentation with the global visibility toggle.
261    /// `visible` precedes any interval filtering performed by [`Indicator::is_visible`].
262    pub fn capture(study: &(impl Indicator + ?Sized), visible: bool) -> Self {
263        Self {
264            inputs: study
265                .inputs()
266                .into_iter()
267                .map(|input| (input.id, input.value))
268                .collect(),
269            settings: study.settings(),
270            timeframe: None,
271            colors: study.colors(),
272            visible,
273            overlay: Some(study.is_overlay()),
274            scale: Some(study.scale()),
275            source: None,
276            inherited_overlay: study.is_overlay(),
277            inherited_scale: study.scale(),
278            inherited_source: study.source(),
279            calculation_symbol: String::new(),
280            calculation_timeframe: None,
281            wait_for_close: true,
282            offset: 0,
283        }
284    }
285
286    pub(super) fn restore(
287        &self,
288        study: &mut (impl Indicator + ?Sized),
289    ) -> Result<(), crate::studies::StudyError> {
290        let current = study.inputs();
291        let inputs: Vec<_> = self
292            .inputs
293            .iter()
294            .filter(|(id, value)| {
295                !current
296                    .iter()
297                    .any(|input| input.id == *id && input.value == *value)
298            })
299            .cloned()
300            .collect();
301        if !inputs.is_empty() || study.settings() != self.settings {
302            study.apply_settings(&inputs, self.settings.clone())?;
303        }
304        study.set_colors(self.colors.clone());
305        study.set_visible(self.visible);
306        if let Some(overlay) = self.overlay {
307            study.set_overlay(overlay);
308        }
309        if let Some(scale) = self.scale {
310            study.set_scale(scale);
311        }
312        Ok(())
313    }
314}
315
316/// A study with editable presentation, used automatically by the chart.
317/// Hosts composing their own chart can wrap built-in or custom studies the same way.
318///
319/// Calculated on another interval or symbol, a study's volume profiles and zones span the chart
320/// bars that share time with their bars, its markers sit on the last such chart bar once the
321/// values on the chart reach their bar, and its labels are hidden.
322#[derive(Clone)]
323pub struct ConfiguredStudy {
324    study: Box<dyn Indicator>,
325    settings: StudySettings,
326    timeframe: Option<Timeframe>,
327    instrument_precision: Option<u8>,
328    formatter: Option<std::sync::Arc<dyn crate::scales::PriceFormatter>>,
329    overlay: Option<bool>,
330    scale: Option<super::StudyScale>,
331    scale_mapping: Option<crate::scales::PriceMap>,
332    source: Option<super::StudySource>,
333    calculation_symbol: String,
334    calculation_timeframe: Option<Timeframe>,
335    wait_for_close: bool,
336    offset: i32,
337    clock: crate::model::BarClock,
338    aligned: Option<IndicatorValues>,
339    aligned_bars: Vec<Option<usize>>,
340    /// For each chart bar, the bars of another interval or symbol that share its time, by which
341    /// the study's profiles, zones and markers are placed on chart bars.
342    spans: Vec<std::ops::Range<usize>>,
343    /// The study's markers and zones on chart bars while it runs on another interval or symbol.
344    aligned_markers: Vec<super::IndicatorMarker>,
345    aligned_zones: Vec<super::StudyZone>,
346}
347
348impl ConfiguredStudy {
349    /// Retain the study's current colours, widths and calculation parameters.
350    pub fn new(study: Box<dyn Indicator>) -> Self {
351        let mut settings = study.settings();
352        // The optional average input must not become a permanent style override.
353        if study.is_volume()
354            && let Some(plot) = settings.plots.get_mut(2)
355        {
356            plot.visible = true;
357        }
358        match_declared_levels(study.as_ref(), &mut settings);
359        Self {
360            study,
361            settings,
362            timeframe: None,
363            instrument_precision: None,
364            formatter: None,
365            overlay: None,
366            scale: None,
367            scale_mapping: None,
368            source: None,
369            calculation_symbol: String::new(),
370            calculation_timeframe: None,
371            wait_for_close: true,
372            offset: 0,
373            clock: crate::model::BarClock::new(Timeframe::Min1),
374            aligned: None,
375            aligned_bars: Vec::new(),
376            spans: Vec::new(),
377            aligned_markers: Vec::new(),
378            aligned_zones: Vec::new(),
379        }
380    }
381    /// Whether the study can be calculated on another interval: it reads one source, or the bars
382    /// alone, without data of its own to request or order flow it cannot do without.
383    fn runs_on_other_bars(&self) -> bool {
384        self.study.source().is_some()
385            || (self.study.data_requests().is_empty() && !self.study.needs_order_flow())
386    }
387    fn update_price(&mut self, data: &[Bar], from: usize) {
388        self.aligned = None;
389        match self.source() {
390            Some(super::StudySource::Price(source)) if self.source() != self.study.source() => {
391                let values: Vec<_> = data
392                    .iter()
393                    .map(|bar| source.compute(bar.open, bar.high, bar.low, bar.close))
394                    .collect();
395                self.study.update_source(data, from, &values);
396            }
397            Some(super::StudySource::Study { .. }) => self.study.update_source(data, from, &[]),
398            _ => self.study.update(data, from),
399        }
400    }
401}
402
403impl Indicator for ConfiguredStudy {
404    fn projections(&self) -> &[super::StudyProjection] {
405        self.study.projections()
406    }
407
408    fn table(&self) -> Option<&super::StudyTable> {
409        self.study.table()
410    }
411
412    fn has_settings(&self) -> bool {
413        true
414    }
415
416    fn name(&self) -> &str {
417        self.study.name()
418    }
419    fn desc(&self) -> &str {
420        self.study.desc()
421    }
422    fn source(&self) -> Option<super::StudySource> {
423        self.source.or_else(|| self.study.source())
424    }
425    fn inputs(&self) -> Vec<super::StudyInput> {
426        let mut inputs = self.study.inputs();
427        if let Some(default) = self.study.source() {
428            let initial = inputs
429                .iter()
430                .find(|input| input.id == "source")
431                .and_then(|input| match input.default {
432                    InputValue::Choice(index) => crate::model::PriceSource::all()
433                        .get(index)
434                        .copied()
435                        .map(super::StudySource::Price),
436                    InputValue::Source(value) => Some(value),
437                    _ => None,
438                })
439                .unwrap_or(default);
440            inputs.retain(|input| input.id != "source");
441            inputs.push(
442                super::StudyInput::new(
443                    "source",
444                    "Source",
445                    InputValue::Source(self.source.unwrap_or(default)),
446                    super::InputKind::Source,
447                )
448                .with_default(InputValue::Source(initial)),
449            );
450        }
451        use super::{InputKind, StudyInput};
452        if self.study.source().is_some() {
453            inputs.extend([
454                StudyInput::new(
455                    "plot_offset",
456                    "Offset",
457                    InputValue::Integer(self.offset),
458                    InputKind::Integer(i32::MIN, i32::MAX),
459                )
460                .with_default(InputValue::Integer(0)),
461                StudyInput::new(
462                    "calculation_symbol",
463                    "Symbol",
464                    InputValue::Symbol(self.calculation_symbol.clone()),
465                    InputKind::Symbol,
466                )
467                .with_default(InputValue::Symbol(String::new())),
468            ]);
469        }
470        if self.runs_on_other_bars() {
471            inputs.extend([
472                StudyInput::new(
473                    "calculation_timeframe",
474                    "Interval",
475                    InputValue::Timeframe(self.calculation_timeframe),
476                    InputKind::Timeframe,
477                )
478                .with_default(InputValue::Timeframe(None)),
479                StudyInput::new(
480                    "wait_for_close",
481                    "Wait for interval close",
482                    InputValue::Toggle(self.wait_for_close),
483                    InputKind::Toggle,
484                )
485                .with_default(InputValue::Toggle(true)),
486            ]);
487        }
488        inputs
489    }
490    fn set_input(&mut self, id: &str, value: InputValue) -> Result<(), crate::studies::StudyError> {
491        if id == "plot_offset" && self.study.source().is_some() {
492            let InputValue::Integer(offset) = value else {
493                return Err(crate::studies::StudyError::invalid_input(id));
494            };
495            self.offset = offset;
496            self.aligned = None;
497            self.study.calculate(&[]);
498            return Ok(());
499        }
500        if match id {
501            "calculation_symbol" => self.study.source().is_some(),
502            "calculation_timeframe" | "wait_for_close" => self.runs_on_other_bars(),
503            _ => false,
504        } {
505            let schema = self
506                .inputs()
507                .into_iter()
508                .find(|input| input.id == id)
509                .ok_or_else(|| crate::studies::StudyError::UnknownInput(id.into()))?;
510            if !schema.accepts(&value) {
511                return Err(crate::studies::StudyError::invalid_input(id));
512            }
513            if matches!(self.source(), Some(super::StudySource::Study { .. }))
514                && !matches!(&value, InputValue::Symbol(s) if s.is_empty())
515                && value != InputValue::Timeframe(None)
516                && id != "wait_for_close"
517            {
518                return Err(super::StudyError::InvalidInput {
519                    id: id.into(),
520                    reason: "must follow the chart while the source is another study's output"
521                        .into(),
522                });
523            }
524            match value {
525                InputValue::Symbol(value) => self.calculation_symbol = value,
526                InputValue::Timeframe(value) => self.calculation_timeframe = value,
527                InputValue::Toggle(value) => self.wait_for_close = value,
528                _ => unreachable!(),
529            }
530            self.aligned = None;
531            self.study.calculate(&[]);
532            return Ok(());
533        }
534        if id == "source" && self.study.source().is_some() {
535            if matches!(value, InputValue::Choice(_)) {
536                self.study.set_input(id, value)?;
537                self.source = None;
538                self.aligned = None;
539                return Ok(());
540            }
541            let InputValue::Source(source) = value else {
542                return Err(crate::studies::StudyError::invalid_input(id));
543            };
544            if matches!(source, super::StudySource::Study { .. })
545                && (!self.calculation_symbol.is_empty() || self.calculation_timeframe.is_some())
546            {
547                return Err(super::StudyError::InvalidInput {
548                    id: id.into(),
549                    reason: "cannot be another study's output while the study runs on another symbol or interval".into(),
550                });
551            }
552            self.source = Some(source);
553            self.aligned = None;
554            self.study.calculate(&[]);
555            Ok(())
556        } else {
557            let format = self.study.value_format();
558            self.study.set_input(id, value)?;
559            if self.study.value_format() != format {
560                self.formatter = None;
561            }
562            self.aligned = None;
563            Ok(())
564        }
565    }
566    fn data_requests(&self) -> Vec<super::StudyDataRequest> {
567        if !self.calculation_symbol.is_empty() || self.calculation_timeframe.is_some() {
568            vec![super::StudyDataRequest {
569                metric: None,
570                symbol: self.calculation_symbol.clone(),
571                timeframe: self.calculation_timeframe,
572            }]
573        } else {
574            self.study.data_requests()
575        }
576    }
577    fn data_start(
578        &self,
579        request: &super::StudyDataRequest,
580    ) -> Option<chrono::DateTime<chrono::Utc>> {
581        if self.calculation_symbol.is_empty() && self.calculation_timeframe.is_none() {
582            self.study.data_start(request)
583        } else {
584            None
585        }
586    }
587    fn update_data(&mut self, data: &[Bar], from: usize, sources: &[super::StudyData<'_>]) {
588        if self.calculation_symbol.is_empty() && self.calculation_timeframe.is_none() {
589            if self.study.data_requests().is_empty() {
590                self.update(data, from);
591            } else {
592                self.study.update_data(data, from, sources);
593            }
594            return;
595        }
596        let request = &self.data_requests()[0];
597        let Some(source) = sources.iter().find(|source| &source.request == request) else {
598            self.aligned = Some(IndicatorValues::missing(data.len()));
599            self.aligned_bars.clear();
600            self.spans.clear();
601            self.aligned_markers.clear();
602            self.aligned_zones.clear();
603            return;
604        };
605        self.instrument_precision = source
606            .instrument
607            .and_then(crate::scales::instrument_price_precision);
608        self.study.set_instrument(source.instrument);
609        self.study.set_timeframe(source.timeframe);
610        self.study.set_bar_clock(&source.clock);
611        self.update_price(source.bars, 0);
612        let mut aligned = IndicatorValues::new();
613        self.aligned_bars.clear();
614        for (at, bar) in data.iter().enumerate() {
615            let forming_higher = at + 1 == data.len()
616                && self.wait_for_close
617                && self
618                    .clock
619                    .timeframe()
620                    .is_some_and(|tf| source.timeframe.duration_ms() > tf.duration_ms());
621            let close = if forming_higher {
622                bar.time
623            } else {
624                self.clock
625                    .next_open(self.clock.bar_open(bar.time))
626                    .unwrap_or(bar.time)
627            };
628            let output = source.bar_at(bar.time).and_then(|_| {
629                source.output_at(close, !self.wait_for_close && at + 1 == data.len())
630            });
631            self.aligned_bars.push(output);
632            let values: Vec<_> = (0..self.study.line_count())
633                .map(|line| {
634                    output
635                        .and_then(|at| self.study.values().value(line, at))
636                        .unwrap_or(f64::NAN)
637                })
638                .collect();
639            aligned.push(super::IndicatorValue::Multiple(values));
640        }
641        self.aligned = Some(aligned);
642        self.spans = spans_of(data, &self.clock, source);
643        // A marker appears once the values on the chart reach its bar (Wait for interval close).
644        let shown = self.aligned_bars.iter().flatten().max().copied();
645        self.aligned_markers = self
646            .study
647            .markers()
648            .iter()
649            .filter(|marker| shown >= Some(marker.bar))
650            .filter_map(|marker| {
651                let bars = chart_bars(&self.spans, &(marker.bar..marker.bar.saturating_add(1)))?;
652                Some(super::IndicatorMarker {
653                    bar: bars.end - 1,
654                    ..*marker
655                })
656            })
657            .collect();
658        self.aligned_zones = self
659            .study
660            .zones()
661            .iter()
662            .filter_map(|zone| {
663                Some(super::StudyZone {
664                    bars: chart_bars(&self.spans, &zone.bars)?,
665                    ..zone.clone()
666                })
667            })
668            .collect();
669    }
670    fn order_flow_inputs(&self) -> &[crate::data::OrderFlowInput] {
671        self.study.order_flow_inputs()
672    }
673    fn needs_order_flow(&self) -> bool {
674        self.study.needs_order_flow()
675    }
676
677    fn calculate(&mut self, data: &[Bar]) {
678        self.update(data, 0);
679    }
680    fn update(&mut self, data: &[Bar], from: usize) {
681        if !self.data_requests().is_empty() {
682            self.update_data(data, from, &[]);
683        } else {
684            self.update_price(data, from);
685        }
686    }
687    fn update_source(&mut self, data: &[Bar], from: usize, source: &[f64]) {
688        self.study.update_source(data, from, source);
689    }
690    fn is_incremental(&self) -> bool {
691        self.data_requests().is_empty()
692            && self.source() == self.study.source()
693            && self.study.is_incremental()
694    }
695    fn set_instrument(&mut self, instrument: Option<&Instrument>) -> bool {
696        if self.calculation_symbol.is_empty() && self.calculation_timeframe.is_none() {
697            self.instrument_precision =
698                instrument.and_then(crate::scales::instrument_price_precision);
699            self.study.set_instrument(instrument)
700        } else {
701            false
702        }
703    }
704    fn set_time_scale_events(&mut self, events: &[crate::model::TimeScaleEvent]) -> bool {
705        // A study calculated on another symbol has none of the chart's events.
706        self.study
707            .set_time_scale_events(if self.calculation_symbol.is_empty() {
708                events
709            } else {
710                &[]
711            })
712    }
713    fn set_timeframe(&mut self, timeframe: Timeframe) {
714        self.timeframe = Some(timeframe);
715        self.clock = crate::model::BarClock::new(timeframe);
716        if self.calculation_symbol.is_empty() && self.calculation_timeframe.is_none() {
717            self.study.set_timeframe(timeframe);
718        }
719    }
720    fn set_visible_range(&mut self, range: std::ops::Range<usize>) -> bool {
721        self.study.set_visible_range(range)
722    }
723    fn set_bar_clock(&mut self, clock: &crate::model::BarClock) -> bool {
724        self.clock = clock.clone();
725        if self.calculation_symbol.is_empty() && self.calculation_timeframe.is_none() {
726            self.study.set_bar_clock(clock)
727        } else {
728            false
729        }
730    }
731    #[cfg(feature = "drawings")]
732    fn drawings(&self) -> &[crate::drawings::Drawing] {
733        self.study.drawings()
734    }
735    fn candles(&self) -> &[super::StudyCandle] {
736        self.study.candles()
737    }
738    fn tpo(&self) -> Option<&crate::model::TPOConfig> {
739        self.study.tpo()
740    }
741    fn values(&self) -> &IndicatorValues {
742        self.aligned.as_ref().unwrap_or_else(|| self.study.values())
743    }
744    fn colors(&self) -> Vec<Color32> {
745        self.settings.plots.iter().map(|plot| plot.color).collect()
746    }
747    fn set_colors(&mut self, colors: Vec<Color32>) {
748        self.study.set_colors(colors);
749        for (plot, color) in self.settings.plots.iter_mut().zip(self.study.colors()) {
750            plot.color = color;
751        }
752    }
753    fn set_color(&mut self, line: usize, color: Color32) {
754        if let Some(plot) = self.settings.plots.get_mut(line) {
755            self.study.set_color(line, color);
756            plot.color = self.study.plot_style(line).color;
757        }
758    }
759    fn line_style(&self, line: usize) -> IndicatorLineStyle {
760        let mut style = self.study.line_style(line);
761        if let Some(plot) = self.settings.plots.get(line) {
762            style.width = plot.width;
763        }
764        style
765    }
766    fn plot_style(&self, line: usize) -> PlotStyle {
767        let mut plot = self.settings.plots.get(line).copied().unwrap_or_else(|| {
768            let mut plot = self.study.plot_style(line);
769            let output = self.settings.plots.get(self.study.plot_output(line));
770            plot.visible &= output.is_none_or(|output| output.visible);
771            plot
772        });
773        if self.study.is_volume() {
774            plot.visible &= self.study.plot_style(line).visible;
775        }
776        plot
777    }
778    fn plot_tagged(&self, line: usize) -> bool {
779        if line < self.settings.plots.len() {
780            !self.settings.untagged_plots.contains(&line)
781        } else {
782            self.study.plot_tagged(line)
783        }
784    }
785    fn plot_offset(&self, line: usize) -> isize {
786        self.study
787            .plot_offset(line)
788            .saturating_add(self.offset as isize)
789    }
790    fn plot_value(&self, line: usize, bar: usize) -> Option<f64> {
791        if let Some(values) = &self.aligned {
792            values.value(
793                line,
794                bar.checked_add_signed(self.plot_offset(line).checked_neg()?)?,
795            )
796        } else {
797            self.study.plot_value(
798                line,
799                bar.checked_add_signed((self.offset as isize).checked_neg()?)?,
800            )
801        }
802    }
803    fn plot_color(&self, line: usize, bar: usize) -> Option<Color32> {
804        let bar = if self.aligned.is_some() {
805            self.aligned_bars.get(bar).copied().flatten()?
806        } else {
807            bar
808        };
809        self.study.plot_color(line, bar)
810    }
811    fn plot_text(&self, line: usize, bar: usize) -> Option<String> {
812        let bar = if self.aligned.is_some() {
813            self.aligned_bars.get(bar).copied().flatten()?
814        } else {
815            bar
816        };
817        self.study.plot_text(line, bar)
818    }
819    fn fills(&self) -> Vec<super::StudyFill> {
820        self.study.fills()
821    }
822    fn snapshot_settings(&self) -> StudySettingsSnapshot {
823        let mut snapshot =
824            StudySettingsSnapshot::capture(self.study.as_ref(), self.study.is_visible());
825        snapshot.settings = self.settings.clone();
826        snapshot.timeframe = self.timeframe;
827        snapshot.overlay = self.overlay;
828        snapshot.scale = self.scale;
829        snapshot.source = self.source;
830        snapshot.calculation_symbol = self.calculation_symbol.clone();
831        snapshot.calculation_timeframe = self.calculation_timeframe;
832        snapshot.wait_for_close = self.wait_for_close;
833        snapshot.offset = self.offset;
834        snapshot
835    }
836    fn restore_settings(
837        &mut self,
838        snapshot: &StudySettingsSnapshot,
839    ) -> Result<(), crate::studies::StudyError> {
840        let mut inputs: Vec<_> = snapshot.inputs.iter().collect();
841        while !inputs.is_empty() {
842            let remaining = inputs.len();
843            let mut error = None;
844            // An input can change another input's accepted range.
845            inputs.retain(|(id, value)| {
846                if self
847                    .study
848                    .inputs()
849                    .iter()
850                    .any(|input| input.id == *id && input.value == *value)
851                {
852                    return false;
853                }
854                if let Err(message) = self.study.set_input(id, value.clone()) {
855                    error = Some(message);
856                    true
857                } else {
858                    false
859                }
860            });
861            if inputs.len() == remaining {
862                return Err(error.expect("a retained input failed to apply"));
863            }
864        }
865        self.study.set_colors(snapshot.colors.clone());
866        self.study.set_visible(snapshot.visible);
867        self.study.set_label_precision(snapshot.settings.precision);
868        self.settings = snapshot.settings.clone();
869        self.timeframe = snapshot.timeframe;
870        self.overlay = snapshot.overlay;
871        self.scale = snapshot.scale;
872        self.scale_mapping = None;
873        self.source = snapshot.source;
874        self.calculation_symbol = snapshot.calculation_symbol.clone();
875        self.calculation_timeframe = snapshot.calculation_timeframe;
876        self.wait_for_close = snapshot.wait_for_close;
877        self.offset = snapshot.offset;
878        self.aligned = None;
879        self.aligned_bars.clear();
880        Ok(())
881    }
882    fn settings(&self) -> StudySettings {
883        self.settings.clone()
884    }
885    fn apply_settings(
886        &mut self,
887        inputs: &[(String, InputValue)],
888        mut settings: StudySettings,
889    ) -> Result<(), crate::studies::StudyError> {
890        if let Some(precision) = settings.precision.filter(|precision| *precision > 12) {
891            return Err(super::StudyError::InvalidPrecision(precision));
892        }
893        if let Some((plot, style)) = settings
894            .plots
895            .iter()
896            .enumerate()
897            .find(|(_, plot)| !plot.width.is_finite() || !(0.5..=10.0).contains(&plot.width))
898        {
899            return Err(super::StudyError::InvalidPlotWidth {
900                plot,
901                width: style.width,
902            });
903        }
904        if let Some((level, value)) =
905            settings
906                .levels
907                .iter()
908                .enumerate()
909                .find_map(|(level, style)| {
910                    style
911                        .value
912                        .filter(|value| !value.is_finite())
913                        .map(|value| (level, value))
914                })
915        {
916            return Err(super::StudyError::InvalidLevelValue { level, value });
917        }
918        let mut edited = self.clone();
919        let mut inputs: Vec<_> = inputs.iter().collect();
920        if inputs.iter().any(|(id, value)| {
921            id == "source" && matches!(value, InputValue::Source(super::StudySource::Study { .. }))
922        }) {
923            inputs.sort_by_key(|(id, _)| {
924                !matches!(id.as_str(), "calculation_symbol" | "calculation_timeframe")
925            });
926        }
927        for (id, value) in inputs {
928            edited.set_input(id, value.clone())?;
929        }
930        // Styles and tags of the plots before the inputs changed their number keep their place,
931        // and plots they add start with the study's own style and tag.
932        if settings.plots.len() == self.line_count() && edited.line_count() != self.line_count() {
933            let defaults = edited.study.settings();
934            let kept = settings.plots.len().min(defaults.plots.len());
935            settings.plots.truncate(kept);
936            settings.plots.extend_from_slice(&defaults.plots[kept..]);
937            settings.untagged_plots.retain(|&line| line < kept);
938            settings
939                .untagged_plots
940                .extend(defaults.untagged_plots.iter().filter(|&&line| line >= kept));
941        }
942        if settings.plots.len() != edited.line_count() {
943            return Err(super::StudyError::PlotCount {
944                expected: edited.line_count(),
945                found: settings.plots.len(),
946            });
947        }
948        settings
949            .untagged_plots
950            .retain(|&line| line < settings.plots.len());
951        settings.untagged_plots.sort_unstable();
952        settings.untagged_plots.dedup();
953        match_declared_levels(edited.study.as_ref(), &mut settings);
954        // Unchanged plot colours must retain any per-bar palette stored by the study.
955        for (line, plot) in settings.plots.iter().enumerate() {
956            if plot.color != edited.study.plot_style(line).color {
957                edited.study.set_color(line, plot.color);
958            }
959        }
960        if self.study.value_format() != edited.study.value_format() {
961            self.formatter = None;
962        }
963        self.study = edited.study;
964        self.study.set_label_precision(settings.precision);
965        self.source = edited.source;
966        self.calculation_symbol = edited.calculation_symbol;
967        self.calculation_timeframe = edited.calculation_timeframe;
968        self.wait_for_close = edited.wait_for_close;
969        self.offset = edited.offset;
970        self.aligned = None;
971        self.settings = settings;
972        Ok(())
973    }
974    fn scale_mapping(&self) -> Option<crate::scales::PriceMap> {
975        self.scale_mapping
976    }
977    fn set_scale_mapping(&mut self, mapping: Option<crate::scales::PriceMap>) {
978        self.scale_mapping = mapping;
979    }
980    fn set_value_formatter(
981        &mut self,
982        formatter: Option<std::sync::Arc<dyn crate::scales::PriceFormatter>>,
983    ) -> bool {
984        self.formatter = formatter;
985        true
986    }
987    fn format_value(&self, value: f64) -> String {
988        self.formatter
989            .as_ref()
990            .filter(|_| value.is_finite())
991            .map_or_else(
992                || {
993                    if self.precision() == self.study.precision() {
994                        self.study.format_value(value)
995                    } else {
996                        self.value_format().format(value, self.precision())
997                    }
998                },
999                |formatter| formatter.format(value),
1000            )
1001    }
1002    fn format_plot_value(&self, line: usize, value: f64) -> String {
1003        if self.formatter.is_some() || self.precision() != self.study.precision() {
1004            self.format_value(value)
1005        } else {
1006            self.study.format_plot_value(line, value)
1007        }
1008    }
1009    fn legend_visible(&self, line: usize) -> bool {
1010        self.plot_style(line).visible
1011            || (self.study.legend_visible(line) && !self.study.plot_style(line).visible)
1012    }
1013    fn value_format(&self) -> crate::scales::StudyFormat {
1014        self.study.value_format()
1015    }
1016    fn scale(&self) -> super::StudyScale {
1017        self.scale.unwrap_or_else(|| self.study.scale())
1018    }
1019    fn precision(&self) -> Option<usize> {
1020        self.settings
1021            .precision
1022            .or_else(|| self.study.precision())
1023            .or_else(|| {
1024                (self.value_format() == crate::scales::StudyFormat::Price)
1025                    .then_some(self.instrument_precision.map(usize::from))
1026                    .flatten()
1027            })
1028    }
1029    fn line_count(&self) -> usize {
1030        self.study.line_count()
1031    }
1032    fn line_names(&self) -> Vec<String> {
1033        self.study.line_names()
1034    }
1035    fn projection_fill_color(&self, projection: &super::StudyProjection) -> Color32 {
1036        self.study.projection_fill_color(projection)
1037    }
1038    fn plot_label_colors(&self, line: usize) -> Option<(Color32, Color32)> {
1039        self.study
1040            .plot_label_colors(line)
1041            .map(|(_, text)| (self.plot_style(line).color, text))
1042    }
1043    fn marker_style(&self) -> Option<PlotStyle> {
1044        self.study.marker_style()
1045    }
1046    fn markers(&self) -> &[super::IndicatorMarker] {
1047        if self.aligned.is_some() {
1048            &self.aligned_markers
1049        } else {
1050            self.study.markers()
1051        }
1052    }
1053    fn volume_profiles(&self, visible: std::ops::Range<usize>) -> Vec<crate::data::StudyProfile> {
1054        if self.aligned.is_none() {
1055            return self.study.volume_profiles(visible);
1056        }
1057        // Source bars of a study on another interval or symbol are not chart bars.
1058        self.study
1059            .volume_profiles(source_bars(&self.spans, &visible))
1060            .into_iter()
1061            .filter_map(|mut plot| {
1062                plot.bars = chart_bars(&self.spans, &plot.bars)?;
1063                Some(plot)
1064            })
1065            .collect()
1066    }
1067    fn zones(&self) -> &[super::StudyZone] {
1068        if self.aligned.is_some() {
1069            &self.aligned_zones
1070        } else {
1071            self.study.zones()
1072        }
1073    }
1074    fn zones_cover_whole_bars(&self) -> bool {
1075        self.study.zones_cover_whole_bars()
1076    }
1077    fn labels(&self) -> &[super::StudyLabel] {
1078        // Source bars of a study on another interval or symbol are not chart bars.
1079        if self.aligned.is_some() {
1080            &[]
1081        } else {
1082            self.study.labels()
1083        }
1084    }
1085    fn labels_on_price(&self) -> bool {
1086        self.study.labels_on_price()
1087    }
1088    fn set_label_precision(&mut self, precision: Option<usize>) {
1089        self.study.set_label_precision(precision);
1090    }
1091    fn provisional_after(&self, line: usize) -> Option<usize> {
1092        self.aligned
1093            .is_none()
1094            .then(|| self.study.provisional_after(line))
1095            .flatten()
1096    }
1097    fn extend_right(&self) -> bool {
1098        self.study.extend_right()
1099    }
1100    fn plot_label(&self) -> Option<(usize, String)> {
1101        self.study.plot_label()
1102    }
1103    fn is_volume(&self) -> bool {
1104        self.study.is_volume()
1105    }
1106    fn level_style(&self, value: f64) -> LineStyle {
1107        // Levels moved onto one value draw with the pattern of the first visible one.
1108        self.study
1109            .pane()
1110            .levels
1111            .iter()
1112            .zip(&self.settings.levels)
1113            .find(|((declared, ..), style)| {
1114                style.visible && style.value.unwrap_or(*declared) == value
1115            })
1116            .map_or_else(|| self.study.level_style(value), |(_, style)| style.pattern)
1117    }
1118    fn level_fill_color(&self) -> Color32 {
1119        self.settings.level_fill_color
1120    }
1121    fn pane(&self) -> super::StudyPane {
1122        let mut pane = self.study.pane();
1123        for ((value, color, _), style) in pane.levels.iter_mut().zip(&self.settings.levels) {
1124            *value = style.value.unwrap_or(*value);
1125            // A hidden level still bounds the fill; its transparent line is not drawn.
1126            *color = if style.visible {
1127                style.color
1128            } else {
1129                Color32::TRANSPARENT
1130            };
1131        }
1132        pane.fill_between_levels = self.settings.fill_between_levels;
1133        pane
1134    }
1135    fn is_overlay(&self) -> bool {
1136        self.overlay.unwrap_or_else(|| self.study.is_overlay())
1137    }
1138    fn set_overlay(&mut self, overlay: bool) -> bool {
1139        self.overlay = Some(overlay);
1140        true
1141    }
1142    fn set_scale(&mut self, scale: super::StudyScale) -> bool {
1143        self.scale_mapping = None;
1144        self.scale = Some(scale);
1145        true
1146    }
1147    fn is_visible(&self) -> bool {
1148        self.study.is_visible()
1149            && self.timeframe.is_none_or(|tf| {
1150                !self.settings.hidden_intervals.contains(&tf)
1151                    && self.settings.interval_range.contains(tf)
1152            })
1153    }
1154    fn set_visible(&mut self, visible: bool) {
1155        self.study.set_visible(visible);
1156    }
1157    fn clone_box(&self) -> Box<dyn Indicator> {
1158        Box::new(self.clone())
1159    }
1160}
1161
1162/// For each of the chart's bars `data`, opened by `clock`, the bars of `source` that share its
1163/// time: those that open before it closes and close after it opens. A bar of an interval not
1164/// measured in time lasts until the next one opens, and the last one without end.
1165fn spans_of(
1166    data: &[Bar],
1167    clock: &crate::model::BarClock,
1168    source: &super::StudyData<'_>,
1169) -> Vec<std::ops::Range<usize>> {
1170    let close = |by: &crate::model::BarClock, bars: &[Bar], at: usize| {
1171        by.next_open(by.bar_open(bars[at].time))
1172            .or_else(|| bars.get(at + 1).map(|bar| bar.time))
1173            .unwrap_or(chrono::DateTime::<chrono::Utc>::MAX_UTC)
1174    };
1175    let (mut first, mut end) = (0, 0);
1176    data.iter()
1177        .enumerate()
1178        .map(|(at, bar)| {
1179            let (open, until) = (clock.bar_open(bar.time), close(clock, data, at));
1180            while first < source.bars.len() && close(&source.clock, source.bars, first) <= open {
1181                first += 1;
1182            }
1183            end = end.max(first);
1184            while end < source.bars.len() && source.clock.bar_open(source.bars[end].time) < until {
1185                end += 1;
1186            }
1187            first..end
1188        })
1189        .collect()
1190}
1191
1192/// The chart bars that share time with the source bars `bars`, from the `spans` of each chart
1193/// bar; `None` when no chart bar does.
1194fn chart_bars(
1195    spans: &[std::ops::Range<usize>],
1196    bars: &std::ops::Range<usize>,
1197) -> Option<std::ops::Range<usize>> {
1198    let start = spans.partition_point(|span| span.end <= bars.start);
1199    let end = spans.partition_point(|span| span.start < bars.end);
1200    (bars.start < bars.end && start < end).then_some(start..end)
1201}
1202
1203/// The source bars that share time with the chart bars `bars`: from the first source bar of the
1204/// first chart bar to the last of the last, from the `spans` of each chart bar.
1205fn source_bars(
1206    spans: &[std::ops::Range<usize>],
1207    bars: &std::ops::Range<usize>,
1208) -> std::ops::Range<usize> {
1209    let end = bars.end.min(spans.len());
1210    if bars.start >= end {
1211        return 0..0;
1212    }
1213    spans[bars.start].start..spans[end - 1].end
1214}
1215
1216#[cfg(all(test, feature = "studies"))]
1217mod tests {
1218    use super::*;
1219    use crate::studies::BollingerBands;
1220
1221    #[test]
1222    fn value_tags_default_by_plot_count_and_persist() {
1223        use crate::studies::{
1224            CustomIndicator, IndicatorValue, MARibbon, StudyTemplate, VolumeWeightedAvgPrice,
1225        };
1226        for (lines, untagged) in [(4, vec![]), (5, vec![0, 1, 2, 3, 4])] {
1227            let study = ConfiguredStudy::new(Box::new(
1228                CustomIndicator::new("Lines", |bars| vec![IndicatorValue::None; bars.len()])
1229                    .with_line_count(lines),
1230            ));
1231            assert_eq!(study.settings().untagged_plots, untagged, "{lines} plots");
1232        }
1233
1234        let mut ribbon = ConfiguredStudy::new(Box::new(MARibbon::new()));
1235        assert_eq!(
1236            ribbon.settings().untagged_plots,
1237            (0..12).collect::<Vec<_>>()
1238        );
1239        let mut settings = ribbon.settings();
1240        settings.untagged_plots = (1..11).collect();
1241        ribbon.apply_settings(&[], settings).unwrap();
1242        let template = StudyTemplate::from_study(&ribbon);
1243        let json = serde_json::to_string(&template).unwrap();
1244        let restored = serde_json::from_str::<StudyTemplate>(&json)
1245            .unwrap()
1246            .create()
1247            .unwrap();
1248        assert_eq!(
1249            restored.settings().untagged_plots,
1250            (1..11).collect::<Vec<_>>()
1251        );
1252        assert!(restored.plot_tagged(0) && restored.plot_tagged(11) && !restored.plot_tagged(5));
1253        // Settings saved before the field existed tag every plot, as they did.
1254        let mut old = serde_json::to_value(&template).unwrap();
1255        old["settings"]
1256            .as_object_mut()
1257            .unwrap()
1258            .remove("untagged_plots");
1259        let restored = serde_json::from_value::<StudyTemplate>(old)
1260            .unwrap()
1261            .create()
1262            .unwrap();
1263        assert!((0..12).all(|line| restored.plot_tagged(line)));
1264
1265        // Two bands take VWAP from one plot to five: the kept VWAP line stays tagged.
1266        let mut vwap = ConfiguredStudy::new(Box::new(VolumeWeightedAvgPrice::new()));
1267        let settings = vwap.settings();
1268        vwap.apply_settings(&[("band_count".into(), InputValue::Length(2))], settings)
1269            .unwrap();
1270        assert_eq!(vwap.settings().untagged_plots, [1, 2, 3, 4]);
1271        let mut settings = vwap.settings();
1272        settings.untagged_plots = vec![3, 1, 3, 7];
1273        vwap.apply_settings(&[], settings).unwrap();
1274        assert_eq!(vwap.settings().untagged_plots, [1, 3]);
1275    }
1276
1277    #[test]
1278    fn automatic_precision_tracks_metadata_and_preserves_study_overrides() {
1279        let mut study = ConfiguredStudy::new(Box::new(super::super::MACD::default()));
1280        let instrument = Instrument::new("FX").with_tick_size(0.00001);
1281        study.set_instrument(Some(&instrument));
1282        assert_eq!(study.precision(), Some(5));
1283        assert_eq!(study.settings().precision, None);
1284        let mut settings = study.settings();
1285        settings.precision = Some(3);
1286        study.apply_settings(&[], settings).unwrap();
1287        study.set_instrument(Some(&Instrument::new("INT").with_tick_size(1.0)));
1288        assert_eq!(study.precision(), Some(3));
1289        let mut settings = study.settings();
1290        settings.precision = None;
1291        study.apply_settings(&[], settings).unwrap();
1292        assert_eq!(study.precision(), Some(0));
1293        study.set_instrument(None);
1294        assert_eq!(study.precision(), None);
1295        for (bounded, precision) in [
1296            (
1297                Box::new(super::super::RSI::default()) as Box<dyn Indicator>,
1298                Some(2),
1299            ),
1300            (Box::new(super::super::Stochastic::default()), Some(2)),
1301            (Box::new(super::super::OnBalanceVolume::default()), None),
1302            (
1303                Box::new(
1304                    super::super::CustomIndicator::new("Small", |_| Vec::new())
1305                        .with_value_format(crate::scales::StudyFormat::Fixed(4)),
1306                ),
1307                Some(4),
1308            ),
1309        ] {
1310            let mut bounded = ConfiguredStudy::new(bounded);
1311            bounded.set_instrument(Some(&instrument));
1312            let mut settings = bounded.settings();
1313            settings.precision = None;
1314            bounded.apply_settings(&[], settings).unwrap();
1315            assert_eq!(bounded.precision(), precision);
1316        }
1317    }
1318
1319    /// Net Volume starts as a line; PPO and PVO keep columns for their differences.
1320    /// A saved plot type replaces each default.
1321    #[test]
1322    fn histogram_defaults_yield_to_a_saved_plot_type() {
1323        use super::super::{NetVolume, PercentageVolumeOscillator, PriceOscillator};
1324        let (line, columns) = (PlotType::Line, PlotType::Columns);
1325        let studies: [(Box<dyn Indicator>, &[PlotType]); 3] = [
1326            (Box::new(NetVolume::default()), &[line]),
1327            (Box::new(PriceOscillator::default()), &[line, line, columns]),
1328            (
1329                Box::new(PercentageVolumeOscillator::default()),
1330                &[line, line, columns],
1331            ),
1332        ];
1333        for (study, expected) in studies {
1334            let name = study.name().to_owned();
1335            let mut study = ConfiguredStudy::new(study);
1336            let types = |study: &ConfiguredStudy| -> Vec<_> {
1337                study.settings().plots.iter().map(|p| p.plot_type).collect()
1338            };
1339            assert_eq!(types(&study), expected, "{name}");
1340            let mut settings = study.settings();
1341            settings.plots[0].plot_type = PlotType::Area;
1342            study.apply_settings(&[], settings).unwrap();
1343            assert_eq!(study.plot_style(0).plot_type, PlotType::Area, "{name}");
1344        }
1345    }
1346
1347    /// Studies that read the bars alone offer the interval and close-wait inputs as well as the
1348    /// single-source studies, keep their own inputs, and calculate on the interval they are given.
1349    /// A study that requests data of its own, or reads trades, does not.
1350    #[test]
1351    fn ohlc_studies_offer_a_calculation_interval() {
1352        use super::super::{ADX, ATR};
1353        let ids = |study: &ConfiguredStudy| -> Vec<String> {
1354            study.inputs().into_iter().map(|input| input.id).collect()
1355        };
1356        for inner in [
1357            Box::new(ATR::new(3)) as Box<dyn Indicator>,
1358            Box::new(ADX::new(3)),
1359        ] {
1360            let mut study = ConfiguredStudy::new(inner);
1361            let own = study.study.inputs().len();
1362            let ids = ids(&study);
1363            assert_eq!(
1364                &ids[own..],
1365                ["calculation_timeframe", "wait_for_close"],
1366                "{}",
1367                study.name()
1368            );
1369            assert!(study.data_requests().is_empty());
1370            study
1371                .set_input(
1372                    "calculation_timeframe",
1373                    InputValue::Timeframe(Some(Timeframe::Hour1)),
1374                )
1375                .unwrap();
1376            assert_eq!(study.data_requests().len(), 1);
1377            assert!(
1378                study
1379                    .set_input("calculation_symbol", InputValue::Symbol("X".into()))
1380                    .is_err()
1381            );
1382        }
1383        let breadth = ConfiguredStudy::new(Box::new(super::super::AdvanceDeclineRatio::default()));
1384        assert!(!ids(&breadth).contains(&"calculation_timeframe".to_owned()));
1385    }
1386
1387    /// Volume profiles, zones and markers of a study calculated on a finer or a coarser interval
1388    /// lie on the chart bars that share time with their bars, a marker once the values on the chart
1389    /// reach its bar, and none are left when the other history is missing.
1390    #[test]
1391    fn profiles_zones_and_markers_of_another_interval_lie_on_their_chart_bars() {
1392        use super::super::{
1393            CrossDirection, IndicatorMarker, StudyData, StudyZone, VolProfileSession,
1394            VolProfileVisible,
1395        };
1396        // Markers on the second and the last bar, and a zone from the one to the other.
1397        #[derive(Clone, Default)]
1398        struct Marks {
1399            values: IndicatorValues,
1400            markers: Vec<IndicatorMarker>,
1401            zones: Vec<StudyZone>,
1402        }
1403        impl Indicator for Marks {
1404            fn name(&self) -> &str {
1405                "Marks"
1406            }
1407            fn calculate(&mut self, data: &[Bar]) {
1408                self.values = IndicatorValues::missing(data.len());
1409                let last = data.len().saturating_sub(1);
1410                let marker = |bar| IndicatorMarker {
1411                    bar,
1412                    value: 100.0,
1413                    direction: CrossDirection::Up,
1414                };
1415                let zone = StudyZone {
1416                    bars: 1..last + 1,
1417                    top: 101.0,
1418                    bottom: 100.0,
1419                    plot: 0,
1420                };
1421                (self.markers, self.zones) = if last > 1 {
1422                    (vec![marker(1), marker(last)], vec![zone])
1423                } else {
1424                    (Vec::new(), Vec::new())
1425                };
1426            }
1427            fn values(&self) -> &IndicatorValues {
1428                &self.values
1429            }
1430            fn colors(&self) -> Vec<Color32> {
1431                vec![Color32::WHITE]
1432            }
1433            fn set_colors(&mut self, _: Vec<Color32>) {}
1434            fn set_visible(&mut self, _: bool) {}
1435            fn markers(&self) -> &[IndicatorMarker] {
1436                &self.markers
1437            }
1438            fn zones(&self) -> &[StudyZone] {
1439                &self.zones
1440            }
1441            fn clone_box(&self) -> Box<dyn Indicator> {
1442                Box::new(self.clone())
1443            }
1444        }
1445        // Bar i opens i intervals after 23:50 UTC on 6 March, so midnight starts a second
1446        // session, and trades a volume of 1 from 100 + i to 101 + i.
1447        let start: chrono::DateTime<chrono::Utc> = "2026-03-06T23:50:00Z".parse().unwrap();
1448        let bars = |count: i32, interval: Timeframe| -> Vec<Bar> {
1449            (0..count)
1450                .map(|i| {
1451                    let low = 100.0 + f64::from(i);
1452                    let time = start + interval.duration() * i;
1453                    Bar::new(time, low, low + 1.0, low, low + 1.0, 1.0)
1454                })
1455                .collect()
1456        };
1457        let profiles = |study: &ConfiguredStudy, visible| -> Vec<_> {
1458            study
1459                .volume_profiles(visible)
1460                .into_iter()
1461                .map(|plot| {
1462                    let rows = &plot.profile;
1463                    (plot.bars, rows.low(), rows.high(), rows.volume().to_vec())
1464                })
1465                .collect()
1466        };
1467        for (name, [chart, other], sessions, (view, visible), markers, zones) in [
1468            (
1469                "finer",
1470                [(4, Timeframe::Min5), (20, Timeframe::Min1)],
1471                [
1472                    (0..2, 100.0, 110.0, vec![10.0]),
1473                    (2..4, 110.0, 120.0, vec![10.0]),
1474                ],
1475                (1..3, (1..3, 105.0, 115.0, vec![10.0])),
1476                vec![0, 3],
1477                0..4,
1478            ),
1479            (
1480                "coarser",
1481                [(20, Timeframe::Min1), (4, Timeframe::Min5)],
1482                [
1483                    (0..10, 100.0, 102.0, vec![2.0]),
1484                    (10..20, 102.0, 104.0, vec![2.0]),
1485                ],
1486                (3..12, (0..15, 100.0, 103.0, vec![3.0])),
1487                vec![9],
1488                5..20,
1489            ),
1490        ] {
1491            let (interval, source_interval) = (chart.1, other.1);
1492            let (chart, source) = (bars(chart.0, interval), bars(other.0, source_interval));
1493            let calculated = |inner: Box<dyn Indicator>| {
1494                let mut study = ConfiguredStudy::new(inner);
1495                study.set_timeframe(interval);
1496                let other = InputValue::Timeframe(Some(source_interval));
1497                study.set_input("calculation_timeframe", other).unwrap();
1498                let data = StudyData {
1499                    request: study.data_requests().remove(0),
1500                    bars: &source,
1501                    clock: crate::model::BarClock::new(source_interval),
1502                    instrument: None,
1503                    timeframe: source_interval,
1504                };
1505                study.update_data(&chart, 0, &[data]);
1506                study
1507            };
1508            let mut studies = [
1509                calculated(Box::new(VolProfileSession::new(1))),
1510                calculated(Box::new(VolProfileVisible::new(1))),
1511                calculated(Box::<Marks>::default()),
1512            ];
1513            assert_eq!(profiles(&studies[0], 0..chart.len()), sessions, "{name}");
1514            assert_eq!(profiles(&studies[1], view), [visible], "{name}");
1515            let marked: Vec<_> = studies[2]
1516                .markers()
1517                .iter()
1518                .map(|marker| marker.bar)
1519                .collect();
1520            assert_eq!(marked, markers, "{name}");
1521            let zone = StudyZone {
1522                bars: zones,
1523                top: 101.0,
1524                bottom: 100.0,
1525                plot: 0,
1526            };
1527            assert_eq!(studies[2].zones(), [zone], "{name}");
1528            for study in &mut studies {
1529                study.update_data(&chart, 0, &[]);
1530            }
1531            for study in &studies {
1532                assert!(profiles(study, 0..chart.len()).is_empty(), "{name}");
1533                assert!(
1534                    study.markers().is_empty() && study.zones().is_empty(),
1535                    "{name}"
1536                );
1537            }
1538        }
1539    }
1540
1541    #[test]
1542    fn edits_are_atomic_and_interval_visibility_preserves_the_global_toggle() {
1543        let mut study = ConfiguredStudy::new(Box::new(BollingerBands::new(3, 2.0)));
1544        let mut settings = study.settings();
1545        settings.plots[0].visible = false;
1546        settings.plots[1].width = 3.0;
1547        settings.precision = Some(4);
1548        settings.hidden_intervals.push(Timeframe::Min1);
1549        assert!(
1550            study
1551                .apply_settings(
1552                    &[
1553                        ("period".into(), InputValue::Length(2)),
1554                        ("std_dev".into(), InputValue::Number(f64::NAN))
1555                    ],
1556                    settings.clone()
1557                )
1558                .is_err()
1559        );
1560        assert_eq!(study.inputs()[0].value, InputValue::Length(3));
1561        assert!(study.plot_style(0).visible);
1562        study
1563            .apply_settings(&[("period".into(), InputValue::Length(2))], settings)
1564            .unwrap();
1565        assert_eq!(study.inputs()[0].value, InputValue::Length(2));
1566        assert!(!study.plot_style(0).visible);
1567        assert_eq!(study.line_style(1).width, 3.0);
1568        assert_eq!(study.precision(), Some(4));
1569        study.set_timeframe(Timeframe::Min1);
1570        assert!(!study.is_visible());
1571        study.set_timeframe(Timeframe::Hour1);
1572        assert!(study.is_visible());
1573        study.set_visible(false);
1574        study.set_timeframe(Timeframe::Day1);
1575        assert!(!study.is_visible());
1576        for (range, interval, visible) in [
1577            (
1578                StudyIntervalRange::AndAbove(Timeframe::Min5),
1579                Timeframe::Custom(std::num::NonZeroU64::new(120).unwrap()),
1580                false,
1581            ),
1582            (
1583                StudyIntervalRange::AndAbove(Timeframe::Min5),
1584                Timeframe::Custom(std::num::NonZeroU64::new(420).unwrap()),
1585                true,
1586            ),
1587            (
1588                StudyIntervalRange::AndBelow(Timeframe::Ticks(
1589                    std::num::NonZeroU64::new(100).unwrap(),
1590                )),
1591                Timeframe::Ticks(std::num::NonZeroU64::new(50).unwrap()),
1592                true,
1593            ),
1594            (
1595                StudyIntervalRange::AndBelow(Timeframe::Ticks(
1596                    std::num::NonZeroU64::new(100).unwrap(),
1597                )),
1598                Timeframe::Volume(std::num::NonZeroU64::new(50).unwrap()),
1599                false,
1600            ),
1601        ] {
1602            let mut settings = study.settings();
1603            settings.interval_range = range;
1604            study.apply_settings(&[], settings).unwrap();
1605            study.set_visible(true);
1606            study.set_timeframe(interval);
1607            assert_eq!(study.is_visible(), visible);
1608            study.set_visible(false);
1609            assert!(!study.is_visible());
1610        }
1611        let saved = serde_json::to_string(&study.settings()).unwrap();
1612        let restored: StudySettings = serde_json::from_str(&saved).unwrap();
1613        assert!(!restored.plots[0].visible);
1614        assert_eq!(restored.hidden_intervals, [Timeframe::Min1]);
1615    }
1616
1617    #[test]
1618    fn rsi_levels_round_trip_and_older_settings_take_the_declared_levels() {
1619        use crate::studies::{AdxBreakout, RSI, palette};
1620        let values = |study: &ConfiguredStudy| -> Vec<f64> {
1621            study.pane().levels.iter().map(|level| level.0).collect()
1622        };
1623        let mut study = ConfiguredStudy::new(Box::new(RSI::default()));
1624        let declared = study.settings();
1625        assert_eq!(
1626            declared
1627                .levels
1628                .iter()
1629                .map(|l| (l.value, l.color, l.pattern, l.visible))
1630                .collect::<Vec<_>>(),
1631            [(None, palette::MUTED, LineStyle::Dashed, true); 2]
1632        );
1633        assert!(declared.fill_between_levels);
1634        assert_eq!(
1635            declared.level_fill_color,
1636            Color32::from_rgba_unmultiplied(126, 87, 194, 26)
1637        );
1638        let before = study.snapshot_settings();
1639        let mut edited = declared.clone();
1640        edited.levels[0].value = Some(80.0);
1641        edited.levels[1].visible = false;
1642        study.apply_settings(&[], edited.clone()).unwrap();
1643        assert_eq!(values(&study), [80.0, 30.0]);
1644        // Templates, copies and layouts rebuild a fresh study from the saved JSON.
1645        let saved = serde_json::to_value(&edited).unwrap();
1646        let mut loaded = ConfiguredStudy::new(Box::new(RSI::default()));
1647        loaded
1648            .apply_settings(&[], serde_json::from_value(saved.clone()).unwrap())
1649            .unwrap();
1650        assert_eq!(loaded.settings(), edited);
1651        let mut older = saved;
1652        for key in ["levels", "fill_between_levels", "level_fill_color"] {
1653            older.as_object_mut().unwrap().remove(key);
1654        }
1655        loaded
1656            .apply_settings(&[], serde_json::from_value(older).unwrap())
1657            .unwrap();
1658        assert_eq!(loaded.settings(), declared);
1659        study.restore_settings(&before).unwrap();
1660        assert_eq!(values(&study), [70.0, 30.0]);
1661        let mut invalid = declared.clone();
1662        invalid.levels[0].value = Some(f64::INFINITY);
1663        assert!(matches!(
1664            study.apply_settings(&[], invalid),
1665            Err(super::super::StudyError::InvalidLevelValue { level: 0, value }) if value == f64::INFINITY
1666        ));
1667        assert_eq!(study.settings(), declared);
1668        let mut adx = ConfiguredStudy::new(Box::new(AdxBreakout::default()));
1669        let settings = adx.settings();
1670        adx.apply_settings(&[("threshold".into(), InputValue::Length(25))], settings)
1671            .unwrap();
1672        assert_eq!(values(&adx), [25.0]);
1673    }
1674}