Skip to main content

egui_charts/widget/
studies.rs

1//! Study registration, settings and calculation order, including dependent studies.
2
3use super::{Chart, ChartElementId, SeriesId};
4use crate::model::Bar;
5use crate::studies::{Indicator, InputValue, RefusedStudy, StudyError, StudySource};
6use web_time::Instant;
7
8impl Chart {
9    /// Caps the studies at `limit`, or lifts the cap with `None`. The cap applies to
10    /// [`try_add_indicator`](Self::try_add_indicator) and to pasting a copied study, which the
11    /// terminal's dialogs and shortcuts use, and to applying a study template set; studies
12    /// already on the chart stay.
13    pub fn set_study_limit(&mut self, limit: Option<usize>) {
14        self.study_limit = limit;
15    }
16
17    /// The cap set with [`set_study_limit`](Self::set_study_limit); `None` when uncapped.
18    pub fn study_limit(&self) -> Option<usize> {
19        self.study_limit
20    }
21
22    /// Whether the chart holds as many studies as its cap allows.
23    pub(crate) fn study_limit_reached(&self) -> bool {
24        self.study_limit
25            .is_some_and(|limit| self.study_ids.len() >= limit)
26    }
27
28    /// Adds a study like [`add_indicator`](Self::add_indicator), unless the chart already holds
29    /// as many as [`study_limit`](Self::study_limit) allows. A refused study leaves the chart
30    /// unchanged and comes back in [`StudyError::StudyLimitReached`].
31    pub fn try_add_indicator(
32        &mut self,
33        indicator: Box<dyn Indicator>,
34    ) -> Result<SeriesId, StudyError> {
35        if self.study_limit_reached() {
36            return Err(StudyError::StudyLimitReached(RefusedStudy::new(indicator)));
37        }
38        Ok(self.add_indicator(indicator))
39    }
40
41    /// Adds a study, computed at once from the bars, which [`show`](Self::show) draws: on the
42    /// main pane if [`Indicator::is_overlay`], otherwise in a new pane at the bottom. Returns the
43    /// study's id, which stays valid when other studies are removed. Host code adds past the
44    /// cap; the interface goes through [`try_add_indicator`](Self::try_add_indicator).
45    pub fn add_indicator(&mut self, mut indicator: Box<dyn Indicator>) -> SeriesId {
46        if !indicator.has_settings() {
47            indicator = Box::new(crate::studies::ConfiguredStudy::new(indicator));
48        }
49        if let Some(timeframe) = self.timeframe() {
50            indicator.set_timeframe(timeframe);
51        }
52        if indicator.value_format() == crate::scales::StudyFormat::Volume {
53            indicator.set_value_formatter(self.volume_formatter.clone());
54        }
55        indicator.set_instrument(self.instrument());
56        indicator.set_time_scale_events(held_events(
57            &self.time_scale_events,
58            self.frozen_source_events,
59        ));
60        indicator.set_bar_clock(&self.bar_clock());
61        if let Some(crate::studies::StudySource::Study { id, plot }) = indicator.source() {
62            let source = self
63                .study_ids
64                .iter()
65                .position(|candidate| candidate.0 == id)
66                .and_then(|at| self.studies.indicators()[at].values().columns().get(plot));
67            indicator.update_source(
68                &self.state.data().bars,
69                0,
70                source.map_or(&[], Vec::as_slice),
71            );
72        } else {
73            let sources = self.study_data.data(
74                indicator.data_requests(),
75                &self.symbol,
76                self.timeframe,
77                self.replay_time(),
78            );
79            indicator.update_data(&self.state.data().bars, 0, &sources);
80        }
81        let id = SeriesId(self.next_series);
82        self.next_series += 1;
83        self.record_study(id);
84        let overlay = indicator.is_overlay();
85        self.studies.add(indicator);
86        self.study_ids.push(id);
87        self.study_autoscale.push(true);
88        self.study_inputs.push(String::new());
89        self.pending_from.push(None);
90        self.next_refresh.push(f64::NEG_INFINITY);
91        if !overlay {
92            self.add_study_pane(id);
93        }
94        id
95    }
96
97    /// Set the input caption displayed beside a study in the legend.
98    pub fn set_indicator_inputs(&mut self, id: SeriesId, inputs: impl Into<String>) -> bool {
99        let Some(index) = self.study_ids.iter().position(|candidate| *candidate == id) else {
100            return false;
101        };
102        self.record_study(id);
103        self.study_inputs[index] = inputs.into();
104        true
105    }
106
107    /// The host-supplied input caption of a study, if its id is still present.
108    pub fn indicator_inputs(&self, id: SeriesId) -> Option<&str> {
109        self.study_ids
110            .iter()
111            .position(|candidate| *candidate == id)
112            .map(|index| self.study_inputs[index].as_str())
113    }
114
115    pub(super) fn take_study(&mut self, id: SeriesId) -> Option<Box<dyn Indicator>> {
116        let at = self.study_ids.iter().position(|study| *study == id)?;
117        self.record_study(id);
118        #[cfg(feature = "studies")]
119        if self.volume_study == Some(id) {
120            self.volume_study = None;
121            self.config.show_volume = false;
122            self.volume_setting = false;
123        }
124        let indicator = self.studies.remove_indicator(at);
125        self.study_ids.remove(at);
126        self.study_autoscale.remove(at);
127        self.study_inputs.remove(at);
128        self.pending_from.remove(at);
129        self.next_refresh.remove(at);
130        if self.selection.selected_id() == Some(ChartElementId::Indicator(id)) {
131            self.selection.deselect();
132        }
133        indicator
134    }
135
136    /// Shows or hides the study `id`; a hidden study keeps its values. Returns `false` for an id
137    /// that is not a study of the chart.
138    pub fn set_indicator_visible(&mut self, id: SeriesId, visible: bool) -> bool {
139        let Some(at) = self.study_ids.iter().position(|study| *study == id) else {
140            return false;
141        };
142        self.record_study(id);
143        self.studies.indicators_mut()[at].set_visible(visible);
144        true
145    }
146
147    /// Includes or excludes study `id` from the main price auto-scale, without hiding it.
148    /// Defaults to `true`; only visible overlay studies contribute. Returns `false` for an id
149    /// that is not a study of the chart.
150    pub fn set_indicator_autoscale(&mut self, id: SeriesId, autoscale: bool) -> bool {
151        let Some(at) = self.study_ids.iter().position(|study| *study == id) else {
152            return false;
153        };
154        self.record_study(id);
155        self.study_autoscale[at] = autoscale;
156        true
157    }
158
159    /// Pins study `id` to the pane's price scale, a scale of its own or no scale. Returns `false`
160    /// for an id that is not a study of the chart, or a custom presentation that keeps its scale.
161    pub fn set_indicator_scale(&mut self, id: SeriesId, scale: crate::studies::StudyScale) -> bool {
162        let Some(at) = self.study_ids.iter().position(|study| *study == id) else {
163            return false;
164        };
165        self.record_study(id);
166        if let crate::studies::StudyScale::Shared(key) = scale {
167            self.ensure_series_scale(key);
168        }
169        self.studies.indicators_mut()[at].set_scale(scale)
170    }
171
172    /// The chart's studies with their ids, in the order they were added. Their values reflect the
173    /// bars as of the last [`show`](Self::show), or of the call to
174    /// [`add_indicator`](Self::add_indicator). A study that is not updated incrementally can lag
175    /// the bars by its wait between full recomputes, at least 0.25 s; after the bars changed from
176    /// the first one on, as when `set_data` loads other bars or older bars are loaded in front, it
177    /// has no values until it is recomputed. On price-based types these remain the source-bar
178    /// results; [`Self::study_values_at`] returns the displayed element results.
179    pub fn indicators(&self) -> impl Iterator<Item = (SeriesId, &dyn Indicator)> {
180        let studies = self.studies.indicators().iter().map(|study| study.as_ref());
181        self.study_ids.iter().copied().zip(studies)
182    }
183
184    /// The study `id`, to change its settings. Its values reflect the bars as of the last
185    /// [`show`](Self::show); the next one recomputes it in full.
186    pub fn indicator_mut(&mut self, id: SeriesId) -> Option<&mut (dyn Indicator + 'static)> {
187        let at = self.study_ids.iter().position(|study| *study == id)?;
188        self.record_study(id);
189        self.pending_from[at] = Some(0);
190        self.next_refresh[at] = f64::NEG_INFINITY;
191        Some(self.studies.indicators_mut()[at].as_mut())
192    }
193
194    /// Updates every study with a pending change, as of `now` in seconds. An incremental study
195    /// whose change starts at its last bar or later is updated at once. Any other change
196    /// recomputes the study in full, for at most one study per call: of the studies whose change
197    /// starts at the first bar or whose `next_refresh` has passed, the one whose `next_refresh`
198    /// is earliest. Its next full recompute then waits, from the end of this one,
199    /// the longer of a quarter second and four times what this one took. Until its turn, a
200    /// study whose change starts at the first bar has no values, since its values belong to
201    /// other bars. Returns how long until the next waiting study is due, zero if one is due now.
202    pub(super) fn refresh_studies(&mut self, now: f64) -> Option<f64> {
203        let instrument = self.instrument().cloned();
204        let clock = self.bar_clock();
205        let bars = &self.state.data().bars;
206        let events = held_events(&self.time_scale_events, self.frozen_source_events);
207        for (at, study) in self.studies.indicators_mut().iter_mut().enumerate() {
208            let instrument_changed = study.set_instrument(instrument.as_ref());
209            let clock_changed = study.set_bar_clock(&clock);
210            let events_changed = study.set_time_scale_events(events);
211            if instrument_changed || clock_changed || events_changed {
212                self.pending_from[at] = Some(0);
213                self.element_studies.dirty = true;
214            }
215        }
216        let requests = self
217            .studies
218            .indicators()
219            .iter()
220            .flat_map(|study| study.data_requests())
221            .collect();
222        let sources =
223            self.study_data
224                .data(requests, &self.symbol, self.timeframe, self.replay_time());
225        refresh_values(
226            self.studies.indicators_mut(),
227            &self.study_ids,
228            bars,
229            &mut self.pending_from,
230            &mut self.next_refresh,
231            now,
232            &sources,
233        )
234    }
235
236    pub(super) fn refresh_element_studies(&mut self, now: f64) -> Option<f64> {
237        let cutoff = self.replay_time();
238        let cache = &mut self.element_studies;
239        let requests = cache
240            .studies
241            .indicators()
242            .iter()
243            .flat_map(|study| study.data_requests())
244            .collect();
245        let sources = self
246            .study_data
247            .data(requests, &self.symbol, self.timeframe, cutoff);
248        refresh_values(
249            cache.studies.indicators_mut(),
250            &self.study_ids,
251            &cache.bars,
252            &mut cache.pending_from,
253            &mut cache.next_refresh,
254            now,
255            &sources,
256        )
257    }
258
259    pub(super) fn rendered_studies(&self) -> &crate::studies::IndicatorRegistry {
260        if self.chart_type.is_time_independent() {
261            &self.element_studies.studies
262        } else {
263            &self.studies
264        }
265    }
266
267    pub(super) fn recalculate_studies(&mut self) {
268        let requests = self
269            .studies
270            .indicators()
271            .iter()
272            .flat_map(|study| study.data_requests())
273            .collect();
274        let sources =
275            self.study_data
276                .data(requests, &self.symbol, self.timeframe, self.replay_time());
277        calculate(
278            self.studies.indicators_mut(),
279            &self.study_ids,
280            &self.state.data().bars,
281            &sources,
282        );
283    }
284
285    /// Select a price source or another study's undisplaced output. Rejects missing plots and
286    /// dependency cycles without changing either study. Hidden sources continue calculating.
287    pub fn set_indicator_source(
288        &mut self,
289        id: SeriesId,
290        source: StudySource,
291    ) -> Result<(), StudyError> {
292        let at = self
293            .study_ids
294            .iter()
295            .position(|candidate| *candidate == id)
296            .ok_or(StudyError::UnknownStudyId(id.0))?;
297        if let StudySource::Study { id: parent, plot } = source {
298            let parent_at = self
299                .study_ids
300                .iter()
301                .position(|candidate| candidate.0 == parent)
302                .ok_or(StudyError::UnknownSourceStudy(parent))?;
303            if plot >= self.studies.indicators()[parent_at].line_count() {
304                return Err(StudyError::UnknownSourcePlot { id: parent, plot });
305            }
306            let mut next = Some(parent);
307            for _ in 0..=self.study_ids.len() {
308                let Some(parent) = next else {
309                    break;
310                };
311                if parent == id.0 {
312                    return Err(StudyError::CyclicSource);
313                }
314                let index = self
315                    .study_ids
316                    .iter()
317                    .position(|candidate| candidate.0 == parent)
318                    .ok_or(StudyError::UnknownSourceStudy(parent))?;
319                next = match self.studies.indicators()[index].source() {
320                    Some(StudySource::Study { id, .. }) => Some(id),
321                    _ => None,
322                };
323            }
324            if next.is_some() {
325                return Err(StudyError::CyclicSource);
326            }
327        }
328        let mut edited = self.studies.indicators()[at].clone_box();
329        edited.set_input("source", InputValue::Source(source))?;
330        self.record_study(id);
331        self.studies.indicators_mut()[at] = edited;
332        self.pending_from[at] = Some(0);
333        self.next_refresh[at] = f64::NEG_INFINITY;
334        self.element_studies.dirty = true;
335        Ok(())
336    }
337}
338
339/// The typed events studies read, without the mark source page an export froze at their end: a page
340/// covers only the visible range, so restarts on it would move while scrolling.
341fn held_events(
342    events: &[crate::model::TimeScaleEvent],
343    frozen: usize,
344) -> &[crate::model::TimeScaleEvent] {
345    &events[..events.len().saturating_sub(frozen)]
346}
347
348/// Updates one study with its source column, keeping values indexed by the input bars.
349pub(super) fn update(
350    studies: &mut [Box<dyn Indicator>],
351    ids: &[SeriesId],
352    at: usize,
353    bars: &[Bar],
354    from: usize,
355    sources: &[crate::studies::StudyData<'_>],
356) {
357    if let Some(StudySource::Study { id, plot }) = studies[at].source() {
358        let values = ids
359            .iter()
360            .position(|candidate| candidate.0 == id)
361            .and_then(|source| studies[source].values().columns().get(plot))
362            .cloned()
363            .unwrap_or_default();
364        studies[at].update_source(bars, from, &values);
365    } else {
366        studies[at].update_data(bars, from, sources);
367    }
368}
369
370pub(super) fn calculate(
371    studies: &mut [Box<dyn Indicator>],
372    ids: &[SeriesId],
373    bars: &[Bar],
374    sources: &[crate::studies::StudyData<'_>],
375) {
376    let order = order(studies, ids);
377    for at in 0..studies.len() {
378        if !order.contains(&at) {
379            studies[at].update_source(bars, 0, &[]);
380        }
381    }
382    for at in order {
383        update(studies, ids, at, bars, 0, sources);
384    }
385}
386
387/// Orders acyclic dependencies before consumers. Missing and cyclic inputs are left out.
388pub(super) fn order(studies: &[Box<dyn Indicator>], ids: &[SeriesId]) -> Vec<usize> {
389    let mut ordered = Vec::with_capacity(studies.len());
390    loop {
391        let before = ordered.len();
392        for (at, study) in studies.iter().enumerate() {
393            if ordered.contains(&at) {
394                continue;
395            }
396            let ready = match study.source() {
397                Some(StudySource::Study { id, plot }) => ids
398                    .iter()
399                    .position(|candidate| candidate.0 == id)
400                    .is_some_and(|parent| {
401                        ordered.contains(&parent) && plot < studies[parent].line_count()
402                    }),
403                _ => true,
404            };
405            if ready {
406                ordered.push(at);
407            }
408        }
409        if ordered.len() == before {
410            break;
411        }
412    }
413    ordered
414}
415
416pub(super) fn refresh_values(
417    studies: &mut [Box<dyn Indicator>],
418    ids: &[SeriesId],
419    bars: &[Bar],
420    pending_from: &mut [Option<usize>],
421    next_refresh: &mut [f64],
422    now: f64,
423    sources: &[crate::studies::StudyData<'_>],
424) -> Option<f64> {
425    let order = super::studies::order(studies, ids);
426    for at in 0..studies.len() {
427        if !order.contains(&at) {
428            studies[at].update_source(bars, 0, &[]);
429            pending_from[at] = None;
430        }
431    }
432    let mut recomputed = false;
433    for &at in &order {
434        let parent = match studies[at].source() {
435            Some(crate::studies::StudySource::Study { id, .. }) => {
436                ids.iter().position(|candidate| candidate.0 == id)
437            }
438            _ => None,
439        };
440        // Retained parent values can feed a waiting consumer while another tail update is pending.
441        // Replaced histories and edited sources must finish their full invalidation first.
442        if parent.is_some_and(|parent| pending_from[parent] == Some(0)) {
443            continue;
444        }
445        let Some(from) = pending_from[at] else {
446            continue;
447        };
448        let len = studies[at].values().len();
449        let incremental =
450            studies[at].is_incremental() && len > 0 && from + 1 >= len && bars.len() >= len;
451        if !incremental {
452            if recomputed {
453                continue;
454            }
455            let due = order
456                .iter()
457                .copied()
458                .filter(|&at| {
459                    let parent_ready = match studies[at].source() {
460                        Some(crate::studies::StudySource::Study { id, .. }) => ids
461                            .iter()
462                            .position(|candidate| candidate.0 == id)
463                            .is_some_and(|parent| pending_from[parent] != Some(0)),
464                        _ => true,
465                    };
466                    parent_ready
467                        && pending_from[at].is_some_and(|from| {
468                            let len = studies[at].values().len();
469                            !(studies[at].is_incremental()
470                                && len > 0
471                                && from + 1 >= len
472                                && bars.len() >= len)
473                                && (from == 0 || now >= next_refresh[at])
474                        })
475                })
476                .min_by(|&a, &b| next_refresh[a].total_cmp(&next_refresh[b]));
477            if due != Some(at) {
478                continue;
479            }
480        }
481        let started = Instant::now();
482        // A parent can repaint earlier bars even when only its input tail changed.
483        let mut previous: Vec<(usize, Vec<f64>)> = Vec::new();
484        for study in studies.iter() {
485            if let Some(StudySource::Study { id, plot }) = study.source()
486                && id == ids[at].0
487                && !previous.iter().any(|(line, _)| *line == plot)
488            {
489                previous.push((plot, studies[at].values().line(plot).to_vec()));
490            }
491        }
492        super::studies::update(studies, ids, at, bars, from, sources);
493        pending_from[at] = None;
494        if !incremental {
495            recomputed = true;
496            let cost = started.elapsed().as_secs_f64();
497            next_refresh[at] = now + cost + REFRESH_INTERVAL.max(4.0 * cost);
498        }
499        for (plot, previous) in previous {
500            let values = studies[at].values().line(plot);
501            let from = previous
502                .iter()
503                .zip(values)
504                .position(|(old, new)| old.to_bits() != new.to_bits())
505                .or_else(|| {
506                    (previous.len() != values.len()).then_some(previous.len().min(values.len()))
507                })
508                .map_or(from, |changed| from.min(changed));
509            for (child, study) in studies.iter().enumerate() {
510                if matches!(study.source(), Some(StudySource::Study { id, plot: source_plot }) if id == ids[at].0 && source_plot == plot)
511                {
512                    pending_from[child] =
513                        Some(pending_from[child].map_or(from, |old| old.min(from)));
514                }
515            }
516        }
517    }
518    let mut wait = None::<f64>;
519    for (at, study) in studies.iter_mut().enumerate() {
520        let Some(from) = pending_from[at] else {
521            continue;
522        };
523        let left = if from == 0 {
524            if !study.values().is_empty() {
525                study.update(&[], 0);
526            }
527            0.0
528        } else {
529            (next_refresh[at] - now).max(0.0)
530        };
531        wait = Some(wait.map_or(left, |wait| wait.min(left)));
532    }
533    wait
534}
535
536/// Least time in seconds between two full recomputes of a study while bars stream; a study
537/// whose recompute took longer than a quarter of it waits four times that instead.
538const REFRESH_INTERVAL: f64 = 0.25;
539
540#[cfg(all(test, feature = "studies"))]
541mod tests {
542    use crate::model::{Bar, BarData, PriceSource};
543    use crate::studies::Indicator;
544    use crate::studies::{InputValue, RSI, SMA, StudyError, StudySource};
545    use crate::widget::test_frames::frame;
546    use crate::widget::{Chart, SeriesId, test_frames::shapes};
547    use chrono::{DateTime, Duration, TimeDelta, Utc};
548    use std::sync::Arc;
549    use std::sync::atomic::{AtomicUsize, Ordering};
550    use std::time::Instant;
551
552    /// SMA(1)(x) = x, including gaps and past values repainted as new swings arrive.
553    #[test]
554    fn study_sources_follow_repainting_output_during_appends() {
555        let bars: Vec<_> = (0..800)
556            .map(|i| {
557                let close = 100.0 + 10.0 * (i as f64 / 12.0).sin();
558                Bar::new(
559                    chrono::DateTime::<Utc>::UNIX_EPOCH + Duration::minutes(i),
560                    close,
561                    close + 1.0,
562                    close - 1.0,
563                    close,
564                    100.0,
565                )
566            })
567            .collect();
568        let ctx = egui::Context::default();
569        let mut chart = Chart::new(BarData::from_bars(bars[..500].to_vec()));
570        let parent = chart.add_indicator(Box::new(crate::studies::ZigZag::default()));
571        let child = chart.add_indicator(Box::new(SMA::new(1)));
572        chart
573            .set_indicator_source(
574                child,
575                StudySource::Study {
576                    id: parent.0,
577                    plot: 0,
578                },
579            )
580            .unwrap();
581        for frame in 0..6 {
582            shapes(&ctx, &mut chart, frame as f64, Vec::new());
583        }
584        for (at, bar) in bars[500..].iter().enumerate() {
585            chart.update(SeriesId::MAIN, bar.clone());
586            shapes(&ctx, &mut chart, 6.0 + at as f64, Vec::new());
587        }
588        for frame in 306..312 {
589            shapes(&ctx, &mut chart, frame as f64, Vec::new());
590        }
591        let output = |id| {
592            chart
593                .indicators()
594                .find(|(candidate, _)| *candidate == id)
595                .unwrap()
596                .1
597                .values()
598        };
599        assert_eq!(output(parent).len(), bars.len());
600        assert_eq!(output(child).len(), bars.len());
601        for at in 0..bars.len() {
602            assert_eq!(
603                output(child).value(0, at).map(f64::to_bits),
604                output(parent).value(0, at).map(f64::to_bits),
605                "bar {at}: SMA(1) must equal its source"
606            );
607        }
608    }
609
610    /// One-bar means follow a continuous stream in dependency order, within one full refresh per pass.
611    #[test]
612    fn study_dependencies_refresh_during_continuous_ticks() {
613        for (reverse, count, incremental_source) in [
614            (false, 2, true),
615            (true, 2, true),
616            (false, 3, true),
617            (true, 3, false),
618        ] {
619            let ctx = egui::Context::default();
620            let bars: Vec<_> = (0..1_000)
621                .map(|i| {
622                    Bar::new(
623                        chrono::DateTime::<Utc>::UNIX_EPOCH + Duration::minutes(i),
624                        10.0,
625                        10.0,
626                        10.0,
627                        10.0,
628                        100.0,
629                    )
630                })
631                .collect();
632            let mut chart = Chart::new(BarData::from_bars(bars.clone()));
633            let mut ids: Vec<_> = (0..count)
634                .map(|_| chart.add_indicator(Box::new(SMA::new(1))))
635                .collect();
636            if reverse {
637                ids.reverse();
638            }
639            if !incremental_source {
640                chart
641                    .set_indicator_source(ids[0], StudySource::Price(PriceSource::High))
642                    .unwrap();
643            }
644            for pair in ids.windows(2) {
645                chart
646                    .set_indicator_source(
647                        pair[1],
648                        StudySource::Study {
649                            id: pair[0].0,
650                            plot: 0,
651                        },
652                    )
653                    .unwrap();
654            }
655            for time in 0..6 {
656                shapes(&ctx, &mut chart, time as f64, Vec::new());
657            }
658            chart.next_refresh.fill(f64::NEG_INFINITY);
659            for tick in 1..=60 {
660                let close = 10.0 + tick as f64 * 0.01;
661                let mut bar = bars.last().unwrap().clone();
662                bar.close = close;
663                bar.high = close;
664                chart.update(SeriesId::MAIN, bar);
665                let refresh = chart.next_refresh.clone();
666                let pass = ctx.cumulative_pass_nr();
667                let now = 6.0 + tick as f64;
668                shapes(&ctx, &mut chart, now, Vec::new());
669                assert!(
670                    chart
671                        .next_refresh
672                        .iter()
673                        .zip(&refresh)
674                        .filter(|(a, b)| **a != **b)
675                        .count()
676                        <= (ctx.cumulative_pass_nr() - pass) as usize,
677                    "at most one full recompute per pass"
678                );
679                // Model equal costs so refresh ordering does not depend on runner preemption.
680                for (next, before) in chart.next_refresh.iter_mut().zip(refresh) {
681                    if *next != before {
682                        *next = now + super::REFRESH_INTERVAL;
683                    }
684                }
685                for (id, study) in chart.indicators().filter(|(id, _)| ids.contains(id)) {
686                    let value = study.values().value(0, 999).unwrap();
687                    // SMA(1) equals its input. Each full stage can wait for the other full
688                    // studies before its latest input reaches the next stage.
689                    let full = count - usize::from(incremental_source);
690                    let lag = full * (full - 1);
691                    assert!(
692                        value <= close + 1e-12 && value >= close - lag as f64 * 0.01 - 1e-12,
693                        "{id:?}, tick {tick}: {value}, expected {close} within {lag} ticks"
694                    );
695                }
696            }
697        }
698    }
699
700    #[test]
701    fn price_sources_and_study_dependencies_follow_edits_and_live_bars() {
702        let start = chrono::DateTime::<Utc>::UNIX_EPOCH;
703        let bars: Vec<_> = (0..3)
704            .map(|i| {
705                Bar::new(
706                    start + Duration::minutes(i),
707                    1.0 + 2.0 * i as f64,
708                    5.0 + 4.0 * i as f64,
709                    1.0 + 2.0 * i as f64,
710                    3.0 + 2.0 * i as f64,
711                    1.0,
712                )
713            })
714            .collect();
715        let ctx = egui::Context::default();
716        let settle = |chart: &mut Chart| {
717            for frame in 0..6 {
718                shapes(&ctx, chart, frame as f64, Vec::new());
719            }
720        };
721        for (source, expected, shifted) in [
722            (PriceSource::Open, 4.0, 2.0),
723            (PriceSource::High, 11.0, 7.0),
724            (PriceSource::Low, 4.0, 2.0),
725            (PriceSource::Close, 6.0, 4.0),
726            (PriceSource::HL2, 7.5, 4.5),
727            (PriceSource::HLC3, 7.0, 13.0 / 3.0),
728            (PriceSource::OHLC4, 6.25, 3.75),
729        ] {
730            let mut chart = Chart::new(BarData::from_bars(bars.clone()));
731            let id = chart.add_indicator(Box::new(SMA::new(2)));
732            let value = InputValue::Source(StudySource::Price(source));
733            chart
734                .indicator_mut(id)
735                .unwrap()
736                .set_input("source", value)
737                .unwrap();
738            settle(&mut chart);
739            assert_eq!(
740                chart.indicators().next().unwrap().1.values().value(0, 2),
741                Some(expected),
742                "{source:?}"
743            );
744            chart
745                .indicator_mut(id)
746                .unwrap()
747                .set_input("plot_offset", InputValue::Integer(1))
748                .unwrap();
749            settle(&mut chart);
750            let study = chart.indicators().next().unwrap().1;
751            assert!(
752                (study.plot_value(0, 2).unwrap() - shifted).abs() < 1e-12,
753                "{source:?}"
754            );
755            assert_eq!(study.values().value(0, 2), Some(expected));
756        }
757        // RSI(1) of 1, 2, 1, 2 is missing, 100, 0, 100. Its two-bar mean ends at 50.
758        let bars: Vec<_> = [1.0, 2.0, 1.0, 2.0]
759            .into_iter()
760            .enumerate()
761            .map(|(i, p)| Bar::new(start + Duration::minutes(i as i64), p, p, p, p, 1.0))
762            .collect();
763        let mut chart = Chart::new(BarData::from_bars(bars.clone()));
764        let mean = chart.add_indicator(Box::new(SMA::new(2)));
765        let rsi = chart.add_indicator(Box::new(RSI::new(1)));
766        let value = InputValue::Source(StudySource::Study { id: rsi.0, plot: 0 });
767        chart
768            .indicator_mut(mean)
769            .unwrap()
770            .set_input("source", value)
771            .unwrap();
772        chart.set_indicator_visible(rsi, false);
773        settle(&mut chart);
774        let output = |chart: &Chart, bar| {
775            chart
776                .indicators()
777                .find(|(id, _)| *id == mean)
778                .unwrap()
779                .1
780                .values()
781                .value(0, bar)
782        };
783        assert_eq!(output(&chart, 1), None);
784        assert!((output(&chart, 3).unwrap() - 50.0).abs() < 1e-8);
785        chart.update(
786            SeriesId::MAIN,
787            Bar::new(bars[3].time, 0.5, 0.5, 0.5, 0.5, 1.0),
788        );
789        settle(&mut chart);
790        assert_eq!(output(&chart, 3), Some(0.0));
791        chart
792            .indicator_mut(rsi)
793            .unwrap()
794            .set_input("period", InputValue::Length(2))
795            .unwrap();
796        settle(&mut chart);
797        // Wilder averages after +1, -1, -0.5 are gain=0.25, loss=0.5: RSI=100/3.
798        assert!((output(&chart, 3).unwrap() - (50.0 + 100.0 / 3.0) / 2.0).abs() < 1e-8);
799        assert!(matches!(
800            chart.set_indicator_source(
801                rsi,
802                StudySource::Study {
803                    id: mean.0,
804                    plot: 0
805                }
806            ),
807            Err(StudyError::CyclicSource)
808        ));
809        #[cfg(feature = "csv")]
810        {
811            let csv = chart.export_csv().unwrap();
812            assert!(csv.contains(",41.67"), "{csv}");
813        }
814        chart.user_action(|chart| chart.remove_series(rsi));
815        settle(&mut chart);
816        assert_eq!(output(&chart, 3), None);
817        assert!(chart.undo());
818        settle(&mut chart);
819        assert!((output(&chart, 3).unwrap() - 125.0 / 3.0).abs() < 1e-8);
820    }
821    /// A flat bar at `close`, stamped `seconds` after 10:00.
822    fn bar(seconds: i64, close: f64) -> Bar {
823        let t =
824            "2024-03-15T10:00:00Z".parse::<DateTime<Utc>>().unwrap() + TimeDelta::seconds(seconds);
825        Bar::new(t, close, close, close, close, 1.0)
826    }
827
828    /// RSI, counting the calls that recompute it.
829    struct Counted(RSI, Arc<AtomicUsize>, std::time::Duration);
830
831    impl Indicator for Counted {
832        fn name(&self) -> &str {
833            self.0.name()
834        }
835        fn calculate(&mut self, data: &[Bar]) {
836            if !data.is_empty() {
837                self.1.fetch_add(1, Ordering::Relaxed);
838                std::thread::sleep(self.2);
839            }
840            self.0.calculate(data);
841        }
842        fn update(&mut self, data: &[Bar], from: usize) {
843            if !data.is_empty() {
844                self.1.fetch_add(1, Ordering::Relaxed);
845                std::thread::sleep(self.2);
846            }
847            self.0.update(data, from);
848        }
849        fn values(&self) -> &crate::studies::IndicatorValues {
850            self.0.values()
851        }
852        fn colors(&self) -> Vec<egui::Color32> {
853            self.0.colors()
854        }
855        fn set_colors(&mut self, colors: Vec<egui::Color32>) {
856            self.0.set_colors(colors);
857        }
858        fn is_overlay(&self) -> bool {
859            self.0.is_overlay()
860        }
861        fn set_visible(&mut self, visible: bool) {
862            self.0.set_visible(visible);
863        }
864        fn clone_box(&self) -> Box<dyn Indicator> {
865            Box::new(Counted(self.0.clone(), Arc::clone(&self.1), self.2))
866        }
867    }
868
869    /// A replaced series gets one full recompute per frame, even while every wait is still
870    /// running; waiting studies have no stale values and request another frame until the last
871    /// study is current.
872    #[test]
873    fn full_recomputes_run_one_per_frame() {
874        let mut chart = Chart::new(BarData::from_bars(
875            (0..100).map(|i| bar(i * 60, i as f64)).collect(),
876        ));
877        let calls: Vec<_> = (0..3).map(|_| Arc::new(AtomicUsize::new(0))).collect();
878        for counter in &calls {
879            chart.add_indicator(Box::new(Counted(
880                RSI::new(14),
881                counter.clone(),
882                std::time::Duration::ZERO,
883            )));
884            counter.store(0, Ordering::Relaxed);
885        }
886        // The second round replaces the bars while the waits of the first are still running.
887        for (round, close) in [(0, 200.0), (1, 300.0)] {
888            if round == 1 {
889                // Model unequal measured costs without depending on the runner clock.
890                chart.next_refresh.copy_from_slice(&[3.0, 2.0, 1.0]);
891            }
892            for counter in &calls {
893                counter.store(0, Ordering::Relaxed);
894            }
895            chart.set_data(
896                SeriesId::MAIN,
897                (0..100)
898                    .map(|i| bar(i * 60, close + (i * 7 % 13) as f64))
899                    .collect(),
900            );
901            for k in 1..=3 {
902                let now = if round == 0 { k as f64 } else { 3.0 } / 60.0;
903                let wait = chart.refresh_studies(now);
904                let counts: Vec<_> = calls
905                    .iter()
906                    .map(|counter| counter.load(Ordering::Relaxed))
907                    .collect();
908                assert_eq!(counts.iter().sum::<usize>(), k, "round {round}, call {k}");
909                for (j, (_, study)) in chart.indicators().enumerate() {
910                    assert!(counts[j] <= 1, "round {round}, call {k}, study {j}");
911                    assert_eq!(
912                        study.values().is_empty(),
913                        counts[j] == 0,
914                        "round {round}, call {k}, study {j}"
915                    );
916                }
917                assert_eq!(wait, if k < 3 { Some(0.0) } else { None });
918            }
919        }
920        let mut fresh = RSI::new(14);
921        fresh.calculate(chart.bars(SeriesId::MAIN));
922        for (_, study) in chart.indicators() {
923            assert_eq!(
924                format!("{:?}", study.values()),
925                format!("{:?}", fresh.values())
926            );
927        }
928    }
929
930    /// The real cost advances the frame clock: re-arming from the frame start would make
931    /// every frame due again when a recompute takes longer than the minimum wait.
932    #[test]
933    fn a_slow_full_recompute_does_not_take_every_frame() {
934        let mut chart = Chart::new(BarData::from_bars(
935            (0..100).map(|i| bar(i * 60, i as f64)).collect(),
936        ));
937        let calls = Arc::new(AtomicUsize::new(0));
938        chart.add_indicator(Box::new(Counted(
939            RSI::new(14),
940            calls.clone(),
941            std::time::Duration::from_millis(300),
942        )));
943        calls.store(0, Ordering::Relaxed);
944        let mut time = 0.0;
945        for i in 0..10 {
946            chart.update(SeriesId::MAIN, bar(99 * 60, 50.0 + i as f64));
947            let started = Instant::now();
948            chart.refresh_studies(time);
949            time += 1.0 / 60.0 + started.elapsed().as_secs_f64();
950        }
951        assert_eq!(calls.load(Ordering::Relaxed), 1);
952    }
953
954    /// Bars set every frame, as a replay does: each study gets a turn despite recompute costs.
955    /// Resetting the waits on `set_data` gave the first study every frame and the others none.
956    #[test]
957    fn set_data_every_frame_recomputes_each_study_in_turn() {
958        for cost in [
959            std::time::Duration::ZERO,
960            std::time::Duration::from_millis(20),
961        ] {
962            let mut bars: Vec<Bar> = (0..100).map(|i| bar(i * 60, i as f64)).collect();
963            let mut chart = Chart::new(BarData::from_bars(bars.clone()));
964            let calls: Vec<_> = (0..3).map(|_| Arc::new(AtomicUsize::new(0))).collect();
965            for counter in &calls {
966                chart.add_indicator(Box::new(Counted(RSI::new(14), counter.clone(), cost)));
967                counter.store(0, Ordering::Relaxed);
968            }
969            for k in 0..120 {
970                bars.push(bar((100 + k) * 60, 50.0 + k as f64));
971                chart.set_data(SeriesId::MAIN, bars.clone());
972                chart.refresh_studies(k as f64 / 60.0);
973            }
974            // Measured costs determine how many turns fit; no study may starve.
975            let calls: Vec<_> = calls.iter().map(|c| c.load(Ordering::Relaxed)).collect();
976            assert!(
977                calls.iter().all(|&count| count > 0),
978                "cost {cost:?}: {calls:?}"
979            );
980        }
981    }
982
983    /// Guards the cost and the freshness of studies while bars stream: a study that does not
984    /// update incrementally is refreshed at most four times a second and once more after the
985    /// stream stops, an incremental one on every frame, and both end equal to a fresh
986    /// calculation. A refresh on every frame, a pending change lost after the last update, or
987    /// the wait applied to an incremental study fails it.
988    #[test]
989    fn slow_studies_refresh_four_times_a_second_and_end_current() {
990        let ctx = egui::Context::default();
991        let mut chart = Chart::new(BarData::from_bars(
992            (0..100).map(|i| bar(i * 60, i as f64)).collect(),
993        ));
994        let sma = chart.add_indicator(Box::new(SMA::new(5)));
995        let calls = Arc::new(AtomicUsize::new(0));
996        let rsi = chart.add_indicator(Box::new(Counted(
997            RSI::new(14),
998            Arc::clone(&calls),
999            std::time::Duration::ZERO,
1000        )));
1001        let values = |chart: &Chart, id| {
1002            let (_, study) = chart.indicators().find(|(study, _)| *study == id).unwrap();
1003            format!("{:?}", study.values())
1004        };
1005        let fresh = |chart: &Chart, mut study: Box<dyn Indicator>| {
1006            study.calculate(chart.bars(SeriesId::MAIN));
1007            format!("{:?}", study.values())
1008        };
1009        frame(&ctx, &mut chart, 0.0, Vec::new());
1010        calls.store(0, Ordering::Relaxed);
1011        let mut time = 0.0;
1012        for i in 0..60 {
1013            time += 1.0 / 60.0;
1014            // Alternately a new close of the last bar and a new bar.
1015            chart.update(
1016                SeriesId::MAIN,
1017                bar((100 + i / 2) * 60, 50.0 + (i * 7 % 13) as f64),
1018            );
1019            frame(&ctx, &mut chart, time, Vec::new());
1020            assert_eq!(
1021                values(&chart, sma),
1022                fresh(&chart, Box::new(SMA::new(5))),
1023                "SMA at {i}"
1024            );
1025        }
1026        assert!(
1027            calls.load(Ordering::Relaxed) <= 4,
1028            "{calls:?} refreshes while streaming"
1029        );
1030        let streamed = calls.load(Ordering::Relaxed);
1031        let stop = time;
1032        while time < stop + 0.25 {
1033            time += 1.0 / 60.0;
1034            frame(&ctx, &mut chart, time, Vec::new());
1035        }
1036        assert_eq!(
1037            calls.load(Ordering::Relaxed),
1038            streamed + 1,
1039            "one refresh after the stream"
1040        );
1041        assert_eq!(
1042            values(&chart, rsi),
1043            fresh(&chart, Box::new(RSI::new(14))),
1044            "RSI"
1045        );
1046    }
1047
1048    /// A chart capped at two studies refuses a third from the interface and pastes, and stays
1049    /// as it was; host code and lifting the cap still add.
1050    #[test]
1051    fn the_study_limit_refuses_additions_beyond_it() {
1052        let mut chart = super::super::test_frames::chart(&[100.0], &[110.0]);
1053        let copied = {
1054            let id = chart.add_indicator(Box::new(SMA::new(9)));
1055            let text = chart.copy_indicator(id).unwrap();
1056            chart.remove_series(id);
1057            text
1058        };
1059        chart.set_study_limit(Some(2));
1060        for period in [3, 5] {
1061            assert!(chart.try_add_indicator(Box::new(SMA::new(period))).is_ok());
1062        }
1063        let undoable = chart.can_undo();
1064        let period = |study: Box<dyn crate::studies::Indicator>| {
1065            study
1066                .inputs()
1067                .into_iter()
1068                .find(|input| input.id == "period")
1069                .map(|input| input.value)
1070        };
1071        let Err(StudyError::StudyLimitReached(refused)) =
1072            chart.try_add_indicator(Box::new(SMA::new(7)))
1073        else {
1074            panic!("the cap refuses a third study");
1075        };
1076        assert_eq!(period(refused.into_inner()), Some(InputValue::Length(7)));
1077        let Err(StudyError::StudyLimitReached(refused)) =
1078            chart.paste_indicator(&copied, &crate::studies::IndicatorFactory::new())
1079        else {
1080            panic!("the cap refuses a pasted study");
1081        };
1082        assert_eq!(period(refused.into_inner()), Some(InputValue::Length(9)));
1083        assert_eq!(chart.indicators().count(), 2);
1084        assert_eq!(chart.can_undo(), undoable);
1085        chart.add_indicator(Box::new(SMA::new(7)));
1086        assert_eq!(chart.indicators().count(), 3);
1087        chart.set_study_limit(None);
1088        assert!(chart.try_add_indicator(Box::new(SMA::new(8))).is_ok());
1089    }
1090    #[test]
1091    fn api_errors_are_std_errors() {
1092        fn needs_error<E: std::error::Error + Send + Sync + 'static>() {}
1093        needs_error::<crate::model::ParseError>();
1094        needs_error::<crate::model::PnfBoxesError>();
1095        needs_error::<crate::widget::ExportError>();
1096        needs_error::<crate::studies::StudyError>();
1097        needs_error::<crate::widget::settings::ChartSettingsError>();
1098        #[cfg(feature = "ui")]
1099        needs_error::<crate::widget::RestoreError>();
1100        #[cfg(feature = "drawings")]
1101        needs_error::<crate::widget::DrawingError>();
1102    }
1103}