egui_charts/studies/builtin/
gaps.rs1use super::clamp_param;
51use crate::model::Bar;
52use crate::studies::palette;
53use crate::studies::stepper::Stepper;
54use crate::studies::{Indicator, IndicatorValue, IndicatorValues, StudyZone};
55use egui::Color32;
56
57const MAX_GAPS: usize = 500;
59
60const RANGE_LENGTH: usize = 14;
62
63#[derive(Clone)]
65pub struct Gaps {
66 max_gaps: usize,
67 deviation: f64,
68 close_partially: bool,
69 limit_trail: bool,
70 trail_length: usize,
71 values: IndicatorValues,
72 stepper: Stepper<State>,
73 closed: Vec<(usize, StudyZone)>,
74 before_last_closed: usize,
75 expired: Vec<(usize, (usize, StudyZone))>,
76 zones: Vec<StudyZone>,
77 up_color: Color32,
78 down_color: Color32,
79 visible: bool,
80}
81
82#[derive(Clone)]
84struct Gap {
85 index: usize,
87 up: bool,
88 near: f64,
89 far: f64,
90 left: usize,
91 source_left: usize,
92}
93
94impl Gap {
95 fn zone(&self, last: usize) -> (usize, StudyZone) {
97 let zone = StudyZone {
98 bars: self.source_left..last + 1,
99 top: self.near.max(self.far),
100 bottom: self.near.min(self.far),
101 plot: usize::from(!self.up),
102 };
103 (self.index, zone)
104 }
105}
106
107#[derive(Clone, Default)]
108struct State {
109 source: usize,
110 usable: usize,
111 previous: Option<(usize, [f64; 2])>,
112 last_source: Option<usize>,
113 ranges: std::collections::VecDeque<f64>,
114 open: Vec<Gap>,
115 found: usize,
116}
117
118impl Gaps {
119 pub fn new(max_gaps: usize, deviation: f64) -> Self {
122 Self {
123 max_gaps: max_gaps.clamp(1, MAX_GAPS),
124 deviation: clamp_param(deviation, 1.0, 100.0),
125 close_partially: false,
126 limit_trail: false,
127 trail_length: 300,
128 values: IndicatorValues::new(),
129 stepper: Stepper::default(),
130 closed: Vec::new(),
131 before_last_closed: 0,
132 expired: Vec::new(),
133 zones: Vec::new(),
134 up_color: palette::GREEN,
135 down_color: palette::RED,
136 visible: true,
137 }
138 }
139
140 pub fn with_partial_closing(mut self, close_partially: bool) -> Self {
142 self.close_partially = close_partially;
143 self
144 }
145
146 pub fn with_trail_limit(mut self, length: Option<usize>) -> Self {
148 self.limit_trail = length.is_some();
149 if let Some(length) = length {
150 self.trail_length = length.max(1);
151 }
152 self
153 }
154}
155
156impl Default for Gaps {
158 fn default() -> Self {
159 Self::new(15, 30.0)
160 }
161}
162
163impl Indicator for Gaps {
164 crate::studies::inputs::study_inputs!(self;
165 close_partially: toggle("Close gaps partially"),
166 max_gaps: length("Max number of gaps", 1, MAX_GAPS),
167 deviation: number("Minimal deviation (%)", 1.0, 100.0),
168 limit_trail: toggle("Limit max gap trail length"),
169 trail_length: length("Max gap trail length (bars)", 1, usize::MAX)
170 ; {});
171
172 fn name(&self) -> &str {
173 "Gaps"
174 }
175
176 fn desc(&self) -> &str {
177 "Gaps - Price gaps between bars, extended until price fills them"
178 }
179
180 fn calculate(&mut self, data: &[Bar]) {
181 self.update(data, 0);
182 }
183
184 fn update(&mut self, data: &[Bar], from: usize) {
185 let len = self.values.len();
186 if len > 0 && from == len && data.len() == len {
187 return;
188 }
189 if len == 0 || from < len - 1 || data.len() < len {
190 self.closed.clear();
191 self.zones.clear();
192 } else if from == len - 1 {
193 if self.expired.is_empty() {
194 self.closed.truncate(self.before_last_closed);
195 self.zones.truncate(self.before_last_closed);
196 } else {
197 let mut kept = std::mem::take(&mut self.closed).into_iter();
198 let mut expired = std::mem::take(&mut self.expired).into_iter().peekable();
199 for at in 0..self.before_last_closed {
200 self.closed
201 .push(if expired.peek().is_some_and(|(index, _)| *index == at) {
202 expired.next().unwrap().1
203 } else {
204 kept.next().unwrap()
205 });
206 }
207 self.zones.clear();
208 }
209 } else {
210 self.zones.truncate(self.closed.len());
211 }
212 let mut retained = self.zones.len();
213 self.stepper
214 .run(data, from, &mut self.values, |state, bar| {
215 let source = state.source;
216 state.source += 1;
217 let last = source + 1 == data.len();
218 if last {
219 self.before_last_closed = self.closed.len();
220 self.expired.clear();
221 }
222 if !bar.high.is_finite() || !bar.low.is_finite() {
223 return IndicatorValue::None;
224 }
225 let i = state.usable;
226 state.usable += 1;
227 state.last_source = Some(source);
228 state.ranges.push_back(bar.high - bar.low);
229 if state.ranges.len() > RANGE_LENGTH {
230 state.ranges.pop_front();
231 }
232 let average = if state.ranges.len() == RANGE_LENGTH
233 && state.ranges.iter().all(|v| v.is_finite())
234 {
235 state.ranges.iter().sum::<f64>() / RANGE_LENGTH as f64
236 } else {
237 f64::NAN
238 };
239 state.open.retain_mut(|gap| {
240 let (entered, covered) = if gap.up {
241 (bar.low < gap.near, bar.low <= gap.far)
242 } else {
243 (bar.high > gap.near, bar.high >= gap.far)
244 };
245 if entered {
246 self.closed.push(gap.zone(source));
247 if covered || !self.close_partially {
248 return false;
249 }
250 gap.near = if gap.up { bar.low } else { bar.high };
251 gap.left = i;
252 gap.source_left = source;
253 } else if self.limit_trail && i - gap.left >= self.trail_length {
254 self.closed.push(gap.zone(source));
255 return false;
256 }
257 true
258 });
259 let Some((previous_source, previous)) =
260 state.previous.replace((source, [bar.high, bar.low]))
261 else {
262 return IndicatorValue::None;
263 };
264 let threshold = average * self.deviation / 100.0;
265 let (up, near, far) = if bar.low > previous[0] && bar.low - previous[0] >= threshold
266 {
267 (true, bar.low, previous[0])
268 } else if bar.high < previous[1] && previous[1] - bar.high >= threshold {
269 (false, bar.high, previous[1])
270 } else {
271 return IndicatorValue::None;
272 };
273 let gap = Gap {
274 index: state.found,
275 up,
276 near,
277 far,
278 left: i - 1,
279 source_left: previous_source,
280 };
281 state.found += 1;
282 if self.limit_trail && self.trail_length == 1 {
283 self.closed.push(gap.zone(source));
284 } else {
285 state.open.push(gap);
286 }
287 let shown = state.found.saturating_sub(self.max_gaps);
288 state.open.retain(|gap| gap.index >= shown);
289 let mut at = 0;
290 self.closed.retain(|zone| {
291 let keep = zone.0 >= shown;
292 if !keep && last && at < self.before_last_closed {
293 self.expired.push((at, zone.clone()));
294 }
295 if !keep {
296 retained = 0;
297 }
298 at += 1;
299 keep
300 });
301 IndicatorValue::None
302 });
303 let state = self.stepper.state();
304 self.zones.truncate(retained);
306 self.zones
307 .extend(self.closed[retained..].iter().map(|(_, zone)| zone.clone()));
308 if let Some(last) = state.last_source {
309 self.zones
310 .extend(state.open.iter().map(|gap| gap.zone(last).1));
311 }
312 }
313
314 fn is_incremental(&self) -> bool {
315 true
316 }
317
318 fn zones(&self) -> &[StudyZone] {
319 &self.zones
320 }
321
322 fn values(&self) -> &IndicatorValues {
323 &self.values
324 }
325
326 fn colors(&self) -> Vec<Color32> {
327 vec![self.up_color, self.down_color]
328 }
329
330 fn set_colors(&mut self, colors: Vec<Color32>) {
331 if let Some(&color) = colors.first() {
332 self.up_color = color;
333 }
334 if let Some(&color) = colors.get(1) {
335 self.down_color = color;
336 }
337 }
338
339 fn is_visible(&self) -> bool {
340 self.visible
341 }
342
343 fn set_visible(&mut self, visible: bool) {
344 self.visible = visible;
345 }
346
347 fn clone_box(&self) -> Box<dyn Indicator> {
348 Box::new(self.clone())
349 }
350
351 fn line_names(&self) -> Vec<String> {
352 vec!["Up gaps".to_string(), "Down gaps".to_string()]
353 }
354}
355
356#[cfg(test)]
357mod tests {
358 use super::*;
359 use chrono::{TimeZone, Utc};
360
361 #[test]
363 fn memory_grows_with_the_gaps_shown_not_the_bars() {
364 let bars = |step: f64| -> Vec<Bar> {
366 (0..100_000)
367 .map(|i| {
368 let p = 100.0 + step * (i / 100) as f64;
369 let time = Utc.timestamp_opt(i as i64 * 60, 0).unwrap();
370 Bar::new(time, p, p + 1.0, p - 1.0, p, 1.0)
371 })
372 .collect()
373 };
374 let mut gaps = Gaps::new(500, 30.0);
375 gaps.calculate(&bars(0.0));
376 assert!(gaps.values().columns().is_empty() && gaps.zones().is_empty());
377 gaps.calculate(&bars(10.0));
378 assert!(gaps.values().columns().is_empty());
379 assert_eq!(gaps.zones().len(), 500);
380 assert!(gaps.zones().iter().all(|zone| zone.bars.end == 100_000));
381 assert_eq!(gaps.zones()[0].bars.start, 49_999);
382 }
383
384 #[test]
387 fn visible_study_zones_auto_scale() {
388 let bars: Vec<Bar> = [100.0; 14]
390 .into_iter()
391 .chain([103.0])
392 .chain([150.0; 100])
393 .enumerate()
394 .map(|(i, p)| {
395 let t = Utc.timestamp_opt(1_700_000_000 + i as i64 * 60, 0).unwrap();
396 Bar::new(t, p, p + 1.0, p - 1.0, p, 1.0)
397 })
398 .collect();
399 let mut gaps = Gaps::default();
400 gaps.calculate(&bars);
401 let mut state = crate::studies::ChartState::new(crate::model::BarData::from_bars(bars));
402 state.time_scale_mut().set_width(80.0);
403 state.time_scale_mut().set_bar_spacing(8.0);
404 let transform = state.fit_price_scale([&gaps as &dyn Indicator]);
405 let range = state.price_scale().range();
406 assert!(transform.to_price(range.min) <= 101.0, "{range:?}");
407 }
408}