egui_charts/studies/builtin/
key_levels.rs1use 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
33const MAX_PIVOTS: usize = 1000;
35
36#[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 colors: [Color32; 2],
49 visible: bool,
50}
51
52#[derive(Clone, Default)]
53struct State {
54 source: usize,
55 swings: Swings,
56 pivots: VecDeque<(usize, f64)>,
58 close: Option<f64>,
59}
60
61impl KeyLevels {
62 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 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
104impl 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 #[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}