Skip to main content

egui_charts/studies/builtin/
pivot_points_high_low.rs

1//! Pivot Points High Low.
2//!
3//! Marks swing highs and lows: a pivot high is a bar whose high is the highest of a window of
4//! `left` bars before it and `right` bars after it, a pivot low mirrors it with lows. Each
5//! pivot is labelled with its price.
6//!
7//! # Rule
8//!
9//! ```text
10//! Pivot high at bar p: high[p] >= high[p - left ..= p - 1] and high[p] > high[p + 1 ..= p + right]
11//! Pivot low at bar p:  low[p]  <= low[p - left ..= p - 1]  and low[p]  < low[p + 1 ..= p + right]
12//! ```
13//!
14//! Of equal extremes in one window the newest is the pivot. A pivot needs its full window, so
15//! it is known `right` bars after its bar, and the first `left` bars have none. The defaults
16//! are 10 bars on each side for both highs and lows.
17//!
18//! # Example
19//!
20//! ```rust
21//! use chrono::{TimeZone, Utc};
22//! use egui_charts::model::Bar;
23//! use egui_charts::studies::{Indicator, PivotPointsHighLow};
24//!
25//! let bars: Vec<Bar> = [1.0, 3.0, 3.0, 2.0, 1.0]
26//!     .into_iter()
27//!     .enumerate()
28//!     .map(|(i, p)| Bar::new(Utc.timestamp_opt(i as i64 * 60, 0).unwrap(), p, p, p, p, 1.0))
29//!     .collect();
30//! let mut pivots = PivotPointsHighLow::new(2, 2, 2, 2);
31//! pivots.calculate(&bars);
32//! // The newer of the two equal highs is the pivot, labelled with its price.
33//! assert_eq!(pivots.values().value(0, 2), Some(3.0));
34//! assert_eq!(pivots.plot_text(0, 2).as_deref(), Some("3.00"));
35//! ```
36
37use super::over_finite_bars;
38use crate::model::Bar;
39use crate::studies::palette;
40use crate::studies::stepper::{rolling_max, update_window, usable_behind};
41use crate::studies::{Indicator, IndicatorValues};
42use egui::Color32;
43
44/// Whether each value is a pivot: at least each of the `left` values before it and above each
45/// of the `right` values after it, every value of that window finite. `high` false finds
46/// lows. Lengths must be at least 1.
47pub(super) fn pivots(values: &[f64], left: usize, right: usize, high: bool) -> Vec<bool> {
48    let x: Vec<f64> = values.iter().map(|&v| if high { v } else { -v }).collect();
49    let before = rolling_max(x.iter().copied(), left);
50    let after = rolling_max(x.iter().copied(), right);
51    let mut finite = 0;
52    // Finite values ending at each index.
53    let runs: Vec<usize> = x
54        .iter()
55        .map(|v| {
56            finite = if v.is_finite() { finite + 1 } else { 0 };
57            finite
58        })
59        .collect();
60    (0..x.len())
61        .map(|p| {
62            let (Some(start), Some(end)) = (p.checked_sub(left), p.checked_add(right)) else {
63                return false;
64            };
65            end < x.len() && runs[end] > end - start && before[p - 1] <= x[p] && after[end] < x[p]
66        })
67        .collect()
68}
69
70/// [`pivots`] over a stream of bars: the window of the last `left + right + 1` bars with finite
71/// highs and lows, from which each new bar confirms the pivot `right` bars before it.
72#[derive(Clone, Default)]
73pub(super) struct Swings {
74    window: std::collections::VecDeque<(usize, f64, f64)>,
75}
76
77impl Swings {
78    /// Adds the bar at `index` and returns the pivot high and the pivot low it confirms, each as
79    /// its bar and price. A bar without a finite high and low is skipped, as if it were absent.
80    pub(super) fn push(
81        &mut self,
82        index: usize,
83        bar: &Bar,
84        left: usize,
85        right: usize,
86    ) -> [Option<(usize, f64)>; 2] {
87        if !(bar.high.is_finite() && bar.low.is_finite()) {
88            return [None, None];
89        }
90        let size = left.saturating_add(right).saturating_add(1);
91        self.window.push_back((index, bar.high, bar.low));
92        if self.window.len() > size {
93            self.window.pop_front();
94        }
95        if self.window.len() < size {
96            return [None, None];
97        }
98        let (at, high, low) = self.window[left];
99        let before = || self.window.range(..left);
100        let after = || self.window.range(left + 1..);
101        [
102            (before().all(|w| w.1 <= high) && after().all(|w| w.1 < high)).then_some((at, high)),
103            (before().all(|w| w.2 >= low) && after().all(|w| w.2 > low)).then_some((at, low)),
104        ]
105    }
106}
107
108/// Pivot Points High Low: swing highs and lows, each labelled with its price.
109#[derive(Clone)]
110pub struct PivotPointsHighLow {
111    high_left: usize,
112    high_right: usize,
113    low_left: usize,
114    low_right: usize,
115    values: IndicatorValues,
116    high_color: Color32,
117    low_color: Color32,
118    visible: bool,
119}
120
121impl PivotPointsHighLow {
122    /// Pivot highs with `high_left` and `high_right` bars on each side and pivot lows with
123    /// `low_left` and `low_right`, each raised to at least 1.
124    pub fn new(high_left: usize, high_right: usize, low_left: usize, low_right: usize) -> Self {
125        Self {
126            high_left: high_left.max(1),
127            high_right: high_right.max(1),
128            low_left: low_left.max(1),
129            low_right: low_right.max(1),
130            values: IndicatorValues::new(),
131            high_color: palette::WHITE,
132            low_color: palette::WHITE,
133            visible: true,
134        }
135    }
136
137    /// The values over `data`, every bar of which has a finite high and low.
138    fn compute(&self, data: &[Bar]) -> IndicatorValues {
139        let mut values = IndicatorValues::missing(data.len());
140        if !data.is_empty() {
141            // Both columns exist, so a bar with one pivot still has a value per line.
142            values.set_value(1, 0, f64::NAN);
143        }
144        let highs: Vec<f64> = data.iter().map(|b| b.high).collect();
145        let lows: Vec<f64> = data.iter().map(|b| b.low).collect();
146        for (line, prices, left, right) in [
147            (0, highs, self.high_left, self.high_right),
148            (1, lows, self.low_left, self.low_right),
149        ] {
150            for (bar, pivot) in pivots(&prices, left, right, line == 0)
151                .into_iter()
152                .enumerate()
153            {
154                if pivot {
155                    values.set_value(line, bar, prices[bar]);
156                }
157            }
158        }
159        values
160    }
161}
162
163/// Construct with the conventional default parameters.
164impl Default for PivotPointsHighLow {
165    fn default() -> Self {
166        Self::new(10, 10, 10, 10)
167    }
168}
169
170impl Indicator for PivotPointsHighLow {
171    crate::studies::inputs::study_inputs!(self;
172        high_left: length("Pivot high left bars", 1, usize::MAX),
173        high_right: length("Pivot high right bars", 1, usize::MAX),
174        low_left: length("Pivot low left bars", 1, usize::MAX),
175        low_right: length("Pivot low right bars", 1, usize::MAX)
176    ; {});
177
178    fn name(&self) -> &str {
179        "Pivot Points High Low"
180    }
181
182    fn desc(&self) -> &str {
183        "Pivot Points High Low - Swing highs and lows labelled with their price"
184    }
185
186    fn calculate(&mut self, data: &[Bar]) {
187        self.update(data, 0);
188    }
189
190    fn update(&mut self, data: &[Bar], from: usize) {
191        // Confirmation of a right-hand window can change earlier pivot bars.
192        let from = usable_behind(self.high_right.max(self.low_right), |b| {
193            b.high.is_finite() && b.low.is_finite()
194        })(data, from.min(self.values.len()).min(data.len()));
195        let mut values = std::mem::take(&mut self.values);
196        update_window(
197            &mut values,
198            data,
199            from,
200            usable_behind(self.high_left.max(self.low_left), |b| {
201                b.high.is_finite() && b.low.is_finite()
202            }),
203            |data| {
204                over_finite_bars(
205                    data,
206                    |b| b.high.is_finite() && b.low.is_finite(),
207                    |data| self.compute(data),
208                )
209            },
210        );
211        self.values = values;
212    }
213
214    fn is_incremental(&self) -> bool {
215        true
216    }
217
218    fn plot_style(&self, line: usize) -> crate::studies::PlotStyle {
219        crate::studies::PlotStyle {
220            color: if line == 0 {
221                self.high_color
222            } else {
223                self.low_color
224            },
225            width: 1.0,
226            pattern: crate::model::LineStyle::Solid,
227            // The label sits over a downward triangle and under an upward one.
228            plot_type: if line == 0 {
229                crate::studies::PlotType::TriangleDown
230            } else {
231                crate::studies::PlotType::TriangleUp
232            },
233            visible: true,
234        }
235    }
236
237    fn plot_label_colors(&self, line: usize) -> Option<(Color32, Color32)> {
238        Some((
239            if line == 0 {
240                self.high_color
241            } else {
242                self.low_color
243            },
244            Color32::from_rgb(54, 58, 69),
245        ))
246    }
247
248    fn plot_text(&self, line: usize, bar: usize) -> Option<String> {
249        Some(self.format_value(self.values.value(line, bar)?))
250    }
251
252    fn values(&self) -> &IndicatorValues {
253        &self.values
254    }
255
256    fn colors(&self) -> Vec<Color32> {
257        vec![self.high_color, self.low_color]
258    }
259
260    fn set_colors(&mut self, colors: Vec<Color32>) {
261        if let Some(&color) = colors.first() {
262            self.high_color = color;
263        }
264        if let Some(&color) = colors.get(1) {
265            self.low_color = color;
266        }
267    }
268
269    fn is_visible(&self) -> bool {
270        self.visible
271    }
272
273    fn set_visible(&mut self, visible: bool) {
274        self.visible = visible;
275    }
276
277    fn clone_box(&self) -> Box<dyn Indicator> {
278        Box::new(self.clone())
279    }
280
281    fn line_names(&self) -> Vec<String> {
282        vec!["Pivot High".to_string(), "Pivot Low".to_string()]
283    }
284}
285
286#[cfg(test)]
287mod tests {
288    use super::*;
289    use chrono::{TimeZone, Utc};
290
291    /// A stream of bars confirms exactly the pivots of the batch rule over its finite bars, each
292    /// `right` finite bars later, skipping a bar without a price.
293    #[test]
294    fn swings_confirm_the_pivots_of_the_batch_rule() {
295        let highs: Vec<f64> = (0..200u32)
296            .map(|i| {
297                if i == 90 {
298                    f64::NAN
299                } else {
300                    f64::from(i * 7919 % 31) + 1.0
301                }
302            })
303            .collect();
304        let bar = |i: usize, p: f64| {
305            Bar::new(
306                Utc.timestamp_opt(i as i64 * 60, 0).unwrap(),
307                p,
308                p,
309                p,
310                p,
311                1.0,
312            )
313        };
314        let finite: Vec<usize> = (0..highs.len()).filter(|&i| highs[i].is_finite()).collect();
315        let prices: Vec<f64> = finite.iter().map(|&i| highs[i]).collect();
316        for (left, right) in [(1, 1), (3, 2), (2, 5)] {
317            let mut swings = Swings::default();
318            let mut streamed = [vec![false; prices.len()], vec![false; prices.len()]];
319            for (i, &p) in highs.iter().enumerate() {
320                for (line, pivot) in swings
321                    .push(i, &bar(i, p), left, right)
322                    .into_iter()
323                    .enumerate()
324                {
325                    if let Some((at, price)) = pivot {
326                        streamed[line][finite.binary_search(&at).unwrap()] = true;
327                        assert_eq!(price, highs[at]);
328                    }
329                }
330            }
331            assert_eq!(
332                streamed[0],
333                pivots(&prices, left, right, true),
334                "{left} {right}"
335            );
336            assert_eq!(
337                streamed[1],
338                pivots(&prices, left, right, false),
339                "{left} {right}"
340            );
341        }
342    }
343}