Skip to main content

egui_charts/studies/builtin/
key_levels.rs

1//! Key Levels.
2//!
3//! Prices at which swing highs and lows repeatedly formed, found by clustering the pivots of
4//! [`PivotPointsHighLow`](super::PivotPointsHighLow). Highs and lows share one pool, so a
5//! resistance that later held as a support is one level.
6//!
7//! # Rule
8//!
9//! Pivots are confirmed `right` bars after their bar, with `left` and `right` bars on each side.
10//! The latest 1000 are kept. Sorted by price (older first among equals), the lowest pivot not
11//! yet in a level opens one and every following pivot priced at most `tolerance` percent above it
12//! (price <= lowest + |lowest| * tolerance / 100) joins it, so a level never spans more than the
13//! tolerance. A cluster of at least `min_touches` pivots is a level at the mean of their prices;
14//! smaller clusters are not shown.
15//!
16//! Each level is a zone from its earliest pivot bar to the last bar, in the resistance colour
17//! when at or above the last close and the support colour below it. The table lists the levels
18//! from the highest down, with their number of touches and each contributing pivot as
19//! `bar@price`, bar being the zero-based index of the bar. The defaults are 10 bars on each side,
20//! 2 touches and a tolerance of 0.25%. Overlay indicator without per-bar values.
21
22use super::clamp_param;
23use super::pivot_points_high_low::Swings;
24use crate::model::Bar;
25use crate::studies::palette;
26use crate::studies::stepper::Stepper;
27use crate::studies::{
28    Indicator, IndicatorValue, IndicatorValues, StudyCell, StudyTable, StudyZone,
29};
30use egui::Color32;
31use std::collections::VecDeque;
32
33/// The most pivots that are clustered.
34const MAX_PIVOTS: usize = 1000;
35
36/// Key Levels: prices at which swing highs and lows repeatedly formed.
37#[derive(Clone)]
38pub struct KeyLevels {
39    left: usize,
40    right: usize,
41    min_touches: usize,
42    tolerance: f64,
43    values: IndicatorValues,
44    stepper: Stepper<State>,
45    zones: Vec<StudyZone>,
46    table: StudyTable,
47    /// Resistance, support.
48    colors: [Color32; 2],
49    visible: bool,
50}
51
52#[derive(Clone, Default)]
53struct State {
54    source: usize,
55    swings: Swings,
56    /// Bar and price of each pivot, oldest first.
57    pivots: VecDeque<(usize, f64)>,
58    close: Option<f64>,
59}
60
61impl KeyLevels {
62    /// Pivots with `left` and `right` bars on each side (at least 1), clustered within
63    /// `tolerance` percent (0 to 100, NaN to 0) into levels of at least `min_touches` pivots
64    /// (at least 2).
65    pub fn new(left: usize, right: usize, min_touches: usize, tolerance: f64) -> Self {
66        Self {
67            left: left.max(1),
68            right: right.max(1),
69            min_touches: min_touches.max(2),
70            tolerance: clamp_param(tolerance, 0.0, 100.0),
71            values: IndicatorValues::new(),
72            stepper: Stepper::default(),
73            zones: Vec::new(),
74            table: StudyTable::default(),
75            colors: [palette::RED, palette::GREEN],
76            visible: true,
77        }
78    }
79
80    /// The clusters of `pivots` of at least `min_touches` within `tolerance` percent, lowest
81    /// first, as the pivots (sorted by bar) of each.
82    fn clusters(&self, pivots: &VecDeque<(usize, f64)>) -> Vec<Vec<(usize, f64)>> {
83        let mut sorted: Vec<_> = pivots.iter().copied().collect();
84        sorted.sort_by(|a, b| a.1.total_cmp(&b.1).then(a.0.cmp(&b.0)));
85        let mut clusters: Vec<Vec<(usize, f64)>> = Vec::new();
86        let mut limit = f64::NEG_INFINITY;
87        for pivot in sorted {
88            match clusters.last_mut().filter(|_| pivot.1 <= limit) {
89                Some(cluster) => cluster.push(pivot),
90                None => {
91                    limit = pivot.1 + pivot.1.abs() * self.tolerance / 100.0;
92                    clusters.push(vec![pivot]);
93                }
94            }
95        }
96        clusters.retain(|cluster| cluster.len() >= self.min_touches);
97        for cluster in &mut clusters {
98            cluster.sort_by_key(|pivot| pivot.0);
99        }
100        clusters
101    }
102}
103
104/// Construct with the conventional default parameters.
105impl Default for KeyLevels {
106    fn default() -> Self {
107        Self::new(10, 10, 2, 0.25)
108    }
109}
110
111impl Indicator for KeyLevels {
112    crate::studies::inputs::study_inputs!(self;
113        left: length("Pivot left bars", 1, usize::MAX),
114        right: length("Pivot right bars", 1, usize::MAX),
115        min_touches: length("Minimum touches", 2, usize::MAX),
116        tolerance: number("Price tolerance (%)", 0.0, 100.0)
117    ; {});
118
119    fn name(&self) -> &str {
120        "Key Levels"
121    }
122
123    fn desc(&self) -> &str {
124        "Key Levels - Prices where swing highs and lows repeatedly formed"
125    }
126
127    fn calculate(&mut self, data: &[Bar]) {
128        self.update(data, 0);
129    }
130
131    fn update(&mut self, data: &[Bar], from: usize) {
132        let (left, right) = (self.left, self.right);
133        self.stepper
134            .run(data, from, &mut self.values, |state, bar| {
135                let index = state.source;
136                state.source += 1;
137                for (bar, price) in state
138                    .swings
139                    .push(index, bar, left, right)
140                    .into_iter()
141                    .flatten()
142                {
143                    state.pivots.push_back((bar, price));
144                    if state.pivots.len() > MAX_PIVOTS {
145                        state.pivots.pop_front();
146                    }
147                }
148                if bar.close.is_finite() {
149                    state.close = Some(bar.close);
150                }
151                IndicatorValue::None
152            });
153        let state = self.stepper.state();
154        let clusters = self.clusters(&state.pivots);
155        let mean = |cluster: &[(usize, f64)]| {
156            cluster.iter().map(|pivot| pivot.1).sum::<f64>() / cluster.len() as f64
157        };
158        self.zones = clusters
159            .iter()
160            .map(|cluster| {
161                let price = mean(cluster);
162                StudyZone {
163                    bars: cluster[0].0..state.source,
164                    top: price,
165                    bottom: price,
166                    plot: usize::from(state.close.is_some_and(|close| price < close)),
167                }
168            })
169            .collect();
170        let rows = clusters
171            .iter()
172            .rev()
173            .map(|cluster| {
174                let pivots: Vec<_> = cluster
175                    .iter()
176                    .map(|&(bar, price)| format!("{bar}@{}", self.format_value(price)))
177                    .collect();
178                vec![
179                    StudyCell::Text(self.format_value(mean(cluster))),
180                    StudyCell::Text(cluster.len().to_string()),
181                    StudyCell::Text(pivots.join(", ")),
182                ]
183            })
184            .collect();
185        self.table = StudyTable {
186            title: "Key levels".to_string(),
187            columns: ["Level", "Touches", "Pivots"].map(String::from).to_vec(),
188            rows,
189        };
190    }
191
192    fn is_incremental(&self) -> bool {
193        true
194    }
195
196    fn zones(&self) -> &[StudyZone] {
197        &self.zones
198    }
199
200    fn table(&self) -> Option<&StudyTable> {
201        Some(&self.table)
202    }
203
204    fn values(&self) -> &IndicatorValues {
205        &self.values
206    }
207
208    fn colors(&self) -> Vec<Color32> {
209        self.colors.to_vec()
210    }
211
212    fn set_colors(&mut self, colors: Vec<Color32>) {
213        for (line, color) in self.colors.iter_mut().zip(colors) {
214            *line = color;
215        }
216    }
217
218    fn is_overlay(&self) -> bool {
219        true
220    }
221
222    fn is_visible(&self) -> bool {
223        self.visible
224    }
225
226    fn set_visible(&mut self, visible: bool) {
227        self.visible = visible;
228    }
229
230    fn clone_box(&self) -> Box<dyn Indicator> {
231        Box::new(self.clone())
232    }
233
234    fn line_names(&self) -> Vec<String> {
235        vec!["Resistance".to_string(), "Support".to_string()]
236    }
237}
238
239#[cfg(test)]
240mod tests {
241    use super::*;
242    use chrono::{TimeZone, Utc};
243
244    /// Pivots of one bar on each side, hand-labelled: highs 100.0 (bar 1), 100.2 (bar 5) and
245    /// 106 (bar 8); lows 90 (bar 3), 96 (bar 6) and 100.1 (bar 9). With a tolerance of 0.25% the
246    /// pivots 100.0, 100.1 and 100.2 (limit 100.25) form one level of three touches at 100.1,
247    /// mixing highs and a low; 90, 96 and 106 stand alone and are dropped at two touches. The last
248    /// close 101 is above the level, so it is a support. A tolerance of 0.12% (limit 100.12)
249    /// leaves 100.0 and 100.1, and 100.2 alone.
250    #[test]
251    fn nearby_pivots_form_a_level() {
252        let data: Vec<Bar> = [
253            (95.0, 94.0, 94.5),
254            (100.0, 96.0, 98.0),
255            (95.0, 93.0, 94.0),
256            (97.0, 90.0, 95.0),
257            (98.0, 94.0, 96.0),
258            (100.2, 97.0, 99.0),
259            (99.0, 96.0, 97.0),
260            (104.0, 101.0, 103.0),
261            (106.0, 103.0, 105.0),
262            (104.0, 100.1, 101.0),
263            (105.0, 102.0, 101.0),
264        ]
265        .into_iter()
266        .enumerate()
267        .map(|(i, (high, low, close))| {
268            let time = Utc.timestamp_opt(i as i64 * 60, 0).unwrap();
269            Bar::new(time, close, high, low, close, 1.0)
270        })
271        .collect();
272        let mut levels = KeyLevels::new(1, 1, 2, 0.25);
273        levels.calculate(&data);
274        let zone = &levels.zones()[0];
275        assert_eq!(levels.zones().len(), 1);
276        assert_eq!(zone.bars, 1..11);
277        assert!((zone.top - 100.1).abs() < 1e-9);
278        assert_eq!(zone.plot, 1);
279        let table = levels.table().unwrap();
280        assert_eq!(table.rows.len(), 1);
281        assert_eq!(table.rows[0][1], StudyCell::Text("3".into()));
282        assert_eq!(
283            table.rows[0][2],
284            StudyCell::Text("1@100.00, 5@100.20, 9@100.10".into())
285        );
286        let mut tight = KeyLevels::new(1, 1, 2, 0.12);
287        tight.calculate(&data);
288        assert_eq!(
289            tight.table().unwrap().rows[0][1],
290            StudyCell::Text("2".into())
291        );
292    }
293}