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}