Skip to main content

egui_charts/studies/
stepper.rs

1//! Shared driver for studies computed one bar at a time.
2
3#[cfg(feature = "studies")]
4pub(crate) use super::helpers::{
5    highest as rolling_max, lowest as rolling_min, sma as rolling_mean,
6};
7use crate::model::Bar;
8use crate::studies::IndicatorValue;
9use crate::studies::IndicatorValues;
10
11/// Runs a causal study one `step` per bar and keeps the state before the last bar, so a
12/// last-bar update or an append resumes instead of recomputing the whole series.
13/// `calculate` is `run(data, 0, ..)`, so batch and streaming share one code path.
14///
15/// A custom study with a three-bar mean of squared closes, available without
16/// the `studies` feature:
17///
18/// ```
19/// use egui_charts::model::Bar;
20/// use egui_charts::studies::{helpers, Indicator, IndicatorValue, IndicatorValues, Stepper};
21/// use std::collections::VecDeque;
22///
23/// #[derive(Clone, Default)]
24/// struct MeanSquare {
25///     driver: Stepper<VecDeque<f64>>,
26///     values: IndicatorValues,
27/// }
28/// impl Indicator for MeanSquare {
29///     fn name(&self) -> &str { "Mean square" }
30///     fn calculate(&mut self, bars: &[Bar]) { self.update(bars, 0); }
31///     fn update(&mut self, bars: &[Bar], from: usize) {
32///         self.driver.run(bars, from, &mut self.values, |window, bar| {
33///             window.push_back(bar.close * bar.close);
34///             if window.len() > 3 { window.pop_front(); }
35///             let mean = helpers::sma(window.make_contiguous(), 3);
36///             IndicatorValue::Single(*mean.last().unwrap())
37///         });
38///     }
39///     fn is_incremental(&self) -> bool { true }
40///     fn values(&self) -> &IndicatorValues { &self.values }
41///     fn colors(&self) -> Vec<ecolor::Color32> { vec![ecolor::Color32::YELLOW] }
42///     fn set_colors(&mut self, _: Vec<ecolor::Color32>) {}
43///     fn set_visible(&mut self, _: bool) {}
44///     fn clone_box(&self) -> Box<dyn Indicator> { Box::new(self.clone()) }
45/// }
46/// let mut bars: Vec<_> = [1.0, 2.0, 4.0].into_iter().enumerate().map(|(i, p)| {
47///     Bar::new(chrono::DateTime::from_timestamp(i as i64, 0).unwrap(), p, p, p, p, 1.0)
48/// }).collect();
49/// let mut study = MeanSquare::default();
50/// study.calculate(&bars);
51/// assert_eq!(study.values().value(0, 2), Some(7.0)); // (1 + 4 + 16) / 3
52/// bars[2].close = 5.0;
53/// study.update(&bars, 2);
54/// let mut fresh = MeanSquare::default();
55/// fresh.calculate(&bars);
56/// assert_eq!(format!("{:?}", study.values()), format!("{:?}", fresh.values()));
57/// bars.push(Bar::new(chrono::DateTime::from_timestamp(3, 0).unwrap(), 6.0, 6.0, 6.0, 6.0, 1.0));
58/// study.update(&bars, 3);
59/// fresh.calculate(&bars);
60/// assert_eq!(format!("{:?}", study.values()), format!("{:?}", fresh.values()));
61/// ```
62#[derive(Clone, Default)]
63pub struct Stepper<S> {
64    state: S,
65    before_last: S,
66    len: usize,
67}
68
69impl<S: Clone + Default> Stepper<S> {
70    /// The state after the previous run's last bar.
71    pub fn state(&self) -> &S {
72        &self.state
73    }
74
75    /// Recomputes `out[from..]`, given that `data[..from]` is unchanged since the previous run.
76    /// Resumes when `from >= len - 1` (last bar changed or bars appended), restarts from bar 0
77    /// otherwise (correction deep in history, prepended page, removed bars).
78    pub fn run(
79        &mut self,
80        data: &[Bar],
81        from: usize,
82        out: &mut IndicatorValues,
83        mut step: impl FnMut(&mut S, &Bar) -> IndicatorValue,
84    ) {
85        let start = if self.len > 0 && from >= self.len - 1 && data.len() >= self.len {
86            if from == self.len - 1 {
87                self.state.clone_from(&self.before_last);
88            }
89            from.min(self.len)
90        } else {
91            self.state = S::default();
92            0
93        };
94        out.truncate(start);
95        for (i, bar) in data.iter().enumerate().skip(start) {
96            if i + 1 == data.len() {
97                self.before_last.clone_from(&self.state);
98            }
99            out.push(step(&mut self.state, bar));
100        }
101        self.len = data.len();
102    }
103}
104
105/// Recomputes `values[from..]` of a window study, given that `data[..from]` is unchanged, from
106/// `compute(&data[read..])`, where `read = read_from(data, from)` is the first bar the values
107/// from `from` on depend on. Equals a full `compute` bit for bit when `compute` evaluates every
108/// window afresh and no value from `from` on depends on a bar before `read`.
109#[cfg(feature = "studies")]
110pub(crate) fn update_window(
111    values: &mut IndicatorValues,
112    data: &[Bar],
113    from: usize,
114    read_from: impl FnOnce(&[Bar], usize) -> usize,
115    compute: impl FnOnce(&[Bar]) -> IndicatorValues,
116) {
117    let from = from.min(values.len()).min(data.len());
118    values.truncate(from);
119    if from < data.len() {
120        let read = read_from(data, from).min(from);
121        values.append_from(&compute(&data[read..]), from - read);
122    }
123}
124
125/// `read_from` for a study whose value at a bar depends on that bar and the `count` usable bars
126/// before it.
127#[cfg(feature = "studies")]
128pub(crate) fn usable_behind(
129    count: usize,
130    usable: impl Fn(&Bar) -> bool,
131) -> impl FnOnce(&[Bar], usize) -> usize {
132    move |data, from| {
133        let (mut read, mut left) = (from, count);
134        while left > 0 && read > 0 {
135            read -= 1;
136            left -= usize::from(usable(&data[read]));
137        }
138        read
139    }
140}
141
142#[cfg(test)]
143mod tests {
144    use crate::studies::helpers;
145    use crate::studies::helpers::{highest as rolling_max, lowest as rolling_min};
146
147    /// Scalar helpers match their definitions through warm-up, gaps and expiring values.
148    #[test]
149    fn rolling_extremes_match_a_window_scan() {
150        let (nan, inf) = (f64::NAN, f64::INFINITY);
151        let cases: &[(&[f64], usize)] = &[
152            (&[], 3),
153            (&[5.0, 1.0, 2.0, 3.0, 0.0, 4.0], 2),
154            (&[1.0, 5.0, 2.0, 5.0, 3.0, 1.0, 1.0, 0.0], 3),
155            (&[2.0, 7.0, 1.0, 8.0], 1),
156            (&[3.0, 1.0, 2.0], 10),
157            (&[1e16, 1.0, 1.0], 2),
158            (&[4.0, nan, 1.0, nan, nan, nan, 2.0], 3),
159            (&[nan, nan, nan], 2),
160            (&[1.0, inf, 2.0, -inf, 3.0, 0.5, 0.25], 2),
161        ];
162        for &(values, period) in cases {
163            let scan = |fold: fn(f64, f64) -> f64, init: f64| -> Vec<f64> {
164                (0..values.len())
165                    .map(|i| {
166                        values[(i + 1).saturating_sub(period)..=i]
167                            .iter()
168                            .copied()
169                            .fold(init, fold)
170                    })
171                    .collect()
172            };
173            let case = format!("{values:?} period {period}");
174            assert_eq!(
175                rolling_max(values.iter().copied(), period),
176                scan(f64::max, -inf),
177                "max {case}"
178            );
179            assert_eq!(
180                rolling_min(values.iter().copied(), period),
181                scan(f64::min, inf),
182                "min {case}"
183            );
184        }
185        // Every complete-window helper is compared with its definition, not another helper.
186        for &(values, period) in cases {
187            let actual = [
188                helpers::sma(values, period),
189                helpers::sum(values, period),
190                helpers::wma(values, period),
191                helpers::stdev(values, period),
192            ];
193            for i in 0..values.len() {
194                let window = &values[(i + 1).saturating_sub(period)..=i];
195                let expected = if window.len() == period && window.iter().all(|v| v.is_finite()) {
196                    let sum = window.iter().sum::<f64>();
197                    let mean = sum / period as f64;
198                    [
199                        mean,
200                        sum,
201                        window
202                            .iter()
203                            .enumerate()
204                            .map(|(j, v)| (j + 1) as f64 * v)
205                            .sum::<f64>()
206                            / (period as f64 * (period as f64 + 1.0) / 2.0),
207                        (window.iter().map(|v| (v - mean).powi(2)).sum::<f64>() / period as f64)
208                            .sqrt(),
209                    ]
210                } else {
211                    [nan; 4]
212                };
213                for (got, want) in actual.iter().map(|line| line[i]).zip(expected) {
214                    assert!(
215                        got == want
216                            || (got.is_nan() && want.is_nan())
217                            || (got - want).abs() < 1e-10,
218                        "{values:?}, period {period}, bar {i}: {got} != {want}"
219                    );
220                }
221            }
222            for (line, alpha, seed_len) in [
223                (helpers::ema(values, period), 2.0 / (period as f64 + 1.0), 1),
224                (
225                    helpers::ema_sma(values, period),
226                    2.0 / (period as f64 + 1.0),
227                    period,
228                ),
229                (helpers::rma(values, period), 1.0 / period as f64, period),
230            ] {
231                for (i, &got) in line.iter().enumerate() {
232                    let inputs: Vec<_> = values[..=i]
233                        .iter()
234                        .copied()
235                        .filter(|v| v.is_finite())
236                        .collect();
237                    // Expand the recurrence as a geometrically weighted sum from the seed.
238                    let want = if !values[i].is_finite() || inputs.len() < seed_len {
239                        nan
240                    } else {
241                        let seed = inputs[..seed_len].iter().sum::<f64>() / seed_len as f64;
242                        let tail = inputs.len() - seed_len;
243                        seed * (1.0 - alpha).powf(tail as f64)
244                            + inputs[seed_len..]
245                                .iter()
246                                .enumerate()
247                                .map(|(j, v)| alpha * v * (1.0 - alpha).powf((tail - 1 - j) as f64))
248                                .sum::<f64>()
249                    };
250                    assert!(
251                        got == want
252                            || (got.is_nan() && want.is_nan())
253                            || (got - want).abs() <= 1e-10 * want.abs().max(1.0),
254                        "average {values:?}, period {period}, bar {i}: {got} != {want}"
255                    );
256                }
257            }
258            for (i, got) in helpers::change(values, period).into_iter().enumerate() {
259                let want = i.checked_sub(period).map_or(nan, |j| values[i] - values[j]);
260                assert!(got == want || (got.is_nan() && !want.is_finite()));
261            }
262        }
263        // A touch, a return to the same side and a gap produce no crossing.
264        let a = [-1.0, 0.0, -1.0, 1.0, 0.0, 1.0, -1.0, nan, 1.0];
265        let b = [0.0; 9];
266        assert_eq!(
267            helpers::crossover(&a, &b),
268            [false, false, false, true, false, false, false, false, false]
269        );
270        assert_eq!(
271            helpers::crossunder(&a, &b),
272            [false, false, false, false, false, false, true, false, false]
273        );
274        assert_eq!(helpers::sma(&[2.0, 4.0], 0), [2.0, 4.0]);
275    }
276}