egui_charts/studies/builtin/
missed_pivots.rs1use super::clamp_param;
40use super::pivot_points_high_low::Swings;
41use crate::model::Bar;
42use crate::studies::palette;
43use crate::studies::stepper::Stepper;
44use crate::studies::{Indicator, IndicatorValue, IndicatorValues, StudyZone};
45use egui::Color32;
46use std::collections::VecDeque;
47
48const MAX_LEVELS: usize = 500;
50
51#[derive(Clone)]
53pub struct MissedPivots {
54 left: usize,
55 right: usize,
56 tolerance: f64,
57 max_levels: usize,
58 values: IndicatorValues,
59 stepper: Stepper<State>,
60 zones: Vec<StudyZone>,
61 colors: [Color32; 2],
63 visible: bool,
64}
65
66#[derive(Clone)]
67struct Level {
68 bar: usize,
69 price: f64,
70 high: bool,
71 revisited: Option<usize>,
72}
73
74#[derive(Clone, Default)]
75struct State {
76 source: usize,
77 swings: Swings,
78 levels: VecDeque<Level>,
79}
80
81impl MissedPivots {
82 pub fn new(left: usize, right: usize, tolerance: f64, max_levels: usize) -> Self {
86 Self {
87 left: left.max(1),
88 right: right.max(1),
89 tolerance: clamp_param(tolerance, 0.0, 100.0),
90 max_levels: max_levels.clamp(1, MAX_LEVELS),
91 values: IndicatorValues::new(),
92 stepper: Stepper::default(),
93 zones: Vec::new(),
94 colors: [palette::RED, palette::GREEN],
95 visible: true,
96 }
97 }
98}
99
100impl Default for MissedPivots {
102 fn default() -> Self {
103 Self::new(10, 10, 0.1, 20)
104 }
105}
106
107impl Indicator for MissedPivots {
108 crate::studies::inputs::study_inputs!(self;
109 left: length("Pivot left bars", 1, usize::MAX),
110 right: length("Pivot right bars", 1, usize::MAX),
111 tolerance: number("Revisit tolerance (%)", 0.0, 100.0),
112 max_levels: length("Max number of levels", 1, MAX_LEVELS)
113 ; {});
114
115 fn name(&self) -> &str {
116 "Missed Pivot Levels"
117 }
118
119 fn desc(&self) -> &str {
120 "Missed Pivot Levels - Swing highs and lows that price has not returned to"
121 }
122
123 fn calculate(&mut self, data: &[Bar]) {
124 self.update(data, 0);
125 }
126
127 fn update(&mut self, data: &[Bar], from: usize) {
128 let (left, right, max_levels) = (self.left, self.right, self.max_levels);
129 let tolerance = self.tolerance / 100.0;
130 self.stepper
131 .run(data, from, &mut self.values, |state, bar| {
132 let index = state.source;
133 state.source += 1;
134 if !(bar.high.is_finite() && bar.low.is_finite()) {
136 return IndicatorValue::None;
137 }
138 for level in state.levels.iter_mut().filter(|l| l.revisited.is_none()) {
139 let reach = level.price.abs() * tolerance;
140 let reached = if level.high {
141 bar.high >= level.price - reach
142 } else {
143 bar.low <= level.price + reach
144 };
145 if reached {
146 level.revisited = Some(index);
147 }
148 }
149 let confirmed = state.swings.push(index, bar, left, right);
150 for (high, pivot) in [(true, confirmed[0]), (false, confirmed[1])] {
151 if let Some((bar, price)) = pivot {
152 state.levels.push_back(Level {
153 bar,
154 price,
155 high,
156 revisited: None,
157 });
158 }
159 }
160 while state.levels.len() > max_levels {
161 state.levels.pop_front();
162 }
163 IndicatorValue::None
164 });
165 let state = self.stepper.state();
166 self.zones = state
167 .levels
168 .iter()
169 .map(|level| StudyZone {
170 bars: level.bar..level.revisited.map_or(state.source, |at| at + 1),
171 top: level.price,
172 bottom: level.price,
173 plot: usize::from(!level.high),
174 })
175 .collect();
176 }
177
178 fn is_incremental(&self) -> bool {
179 true
180 }
181
182 fn zones(&self) -> &[StudyZone] {
183 &self.zones
184 }
185
186 fn values(&self) -> &IndicatorValues {
187 &self.values
188 }
189
190 fn colors(&self) -> Vec<Color32> {
191 self.colors.to_vec()
192 }
193
194 fn set_colors(&mut self, colors: Vec<Color32>) {
195 for (line, color) in self.colors.iter_mut().zip(colors) {
196 *line = color;
197 }
198 }
199
200 fn is_overlay(&self) -> bool {
201 true
202 }
203
204 fn is_visible(&self) -> bool {
205 self.visible
206 }
207
208 fn set_visible(&mut self, visible: bool) {
209 self.visible = visible;
210 }
211
212 fn clone_box(&self) -> Box<dyn Indicator> {
213 Box::new(self.clone())
214 }
215
216 fn line_names(&self) -> Vec<String> {
217 vec!["Missed high".to_string(), "Missed low".to_string()]
218 }
219}
220
221#[cfg(test)]
222mod tests {
223 use super::*;
224 use chrono::{TimeZone, Utc};
225
226 fn bars(highs_lows: &[(f64, f64)]) -> Vec<Bar> {
227 highs_lows
228 .iter()
229 .enumerate()
230 .map(|(i, &(high, low))| {
231 let time = Utc.timestamp_opt(i as i64 * 60, 0).unwrap();
232 Bar::new(time, low, high, low, low, 1.0)
233 })
234 .collect()
235 }
236
237 #[test]
243 fn levels_are_confirmed_missed_and_revisited() {
244 let data = bars(&[
245 (6.0, 5.0),
246 (10.0, 8.0),
247 (9.0, 7.0),
248 (6.0, 4.0),
249 (7.0, 5.0),
250 (9.96, 8.0),
251 (8.0, 6.0),
252 ]);
253 type Zone = (std::ops::Range<usize>, f64, usize);
255 let want: [(usize, Vec<Zone>); 4] = [
256 (2, vec![]),
257 (3, vec![(1..3, 10.0, 0)]),
258 (5, vec![(1..5, 10.0, 0), (3..5, 4.0, 1)]),
259 (7, vec![(1..6, 10.0, 0), (3..7, 4.0, 1), (5..7, 9.96, 0)]),
260 ];
261 let mut missed = MissedPivots::new(1, 1, 0.5, 20);
262 for (seen, zones) in want {
263 missed.calculate(&data[..seen]);
264 let got: Vec<_> = missed
265 .zones()
266 .iter()
267 .map(|z| (z.bars.clone(), z.top, z.plot))
268 .collect();
269 assert_eq!(got, zones, "after {seen} bars");
270 }
271 }
272}