egui_charts/studies/builtin/
adx_breakout.rs1use super::ADX;
21use crate::model::Bar;
22use crate::studies::palette;
23use crate::studies::stepper::Stepper;
24use crate::studies::{Indicator, IndicatorValue, IndicatorValues};
25use egui::Color32;
26use std::collections::VecDeque;
27
28#[derive(Clone)]
30pub struct AdxBreakout {
31 period: usize,
32 threshold: usize,
33 range: usize,
34 adx: ADX,
35 values: IndicatorValues,
36 stepper: Stepper<State>,
37 colors: [Color32; 3],
38 visible: bool,
39}
40
41#[derive(Clone, Default)]
42struct State {
43 source: usize,
44 previous: Option<f64>,
45 window: VecDeque<(f64, f64)>,
46}
47
48impl AdxBreakout {
49 pub fn new(period: usize, threshold: usize, range: usize) -> Self {
52 Self {
53 period: period.max(1),
54 threshold: threshold.min(100),
55 range: range.max(1),
56 adx: ADX::new(period),
57 values: IndicatorValues::new(),
58 stepper: Stepper::default(),
59 colors: [palette::PURPLE, palette::GREEN, palette::RED],
60 visible: true,
61 }
62 }
63}
64
65impl Default for AdxBreakout {
67 fn default() -> Self {
68 Self::new(14, 20, 20)
69 }
70}
71
72impl Indicator for AdxBreakout {
73 crate::studies::inputs::study_inputs!(self;
74 period: length("ADX period", 1, usize::MAX),
75 threshold: length("ADX threshold", 0, 100),
76 range: length("Range", 1, usize::MAX)
77 ; {
78 self.adx = ADX::new(self.period);
79 });
80
81 fn name(&self) -> &str {
82 "ADX Breakout"
83 }
84
85 fn desc(&self) -> &str {
86 "ADX Breakout - Closes beyond the range of the last bars after a weak trend"
87 }
88
89 fn calculate(&mut self, data: &[Bar]) {
90 self.update(data, 0);
91 }
92
93 fn update(&mut self, data: &[Bar], from: usize) {
94 self.adx.update(data, from);
95 let (adx, range, threshold) = (self.adx.values(), self.range, self.threshold as f64);
96 self.stepper
97 .run(data, from, &mut self.values, |state, bar| {
98 let source = state.source;
99 state.source += 1;
100 if ![bar.high, bar.low, bar.close].iter().all(|v| v.is_finite()) {
101 return IndicatorValue::None;
102 }
103 let current = adx.value(0, source);
104 let mut row = [current.unwrap_or(f64::NAN), f64::NAN, f64::NAN];
105 if state.window.len() == range && state.previous.is_some_and(|adx| adx <= threshold)
106 {
107 let highest = state
110 .window
111 .iter()
112 .map(|b| b.0)
113 .fold(f64::NEG_INFINITY, f64::max);
114 let lowest = state
115 .window
116 .iter()
117 .map(|b| b.1)
118 .fold(f64::INFINITY, f64::min);
119 if bar.close > highest {
120 row[1] = row[0];
121 } else if bar.close < lowest {
122 row[2] = row[0];
123 }
124 }
125 state.previous = current;
126 state.window.push_back((bar.high, bar.low));
127 if state.window.len() > range {
128 state.window.pop_front();
129 }
130 IndicatorValue::Multiple(row.to_vec())
131 });
132 }
133
134 fn is_incremental(&self) -> bool {
135 true
136 }
137
138 fn plot_style(&self, line: usize) -> crate::studies::PlotStyle {
139 crate::studies::PlotStyle {
140 color: self.colors.get(line).copied().unwrap_or(Color32::WHITE),
141 width: 1.0,
142 pattern: crate::model::LineStyle::Solid,
143 plot_type: match line {
144 0 => crate::studies::PlotType::Line,
145 1 => crate::studies::PlotType::TriangleUp,
146 _ => crate::studies::PlotType::TriangleDown,
147 },
148 visible: true,
149 }
150 }
151
152 fn values(&self) -> &IndicatorValues {
153 &self.values
154 }
155
156 fn colors(&self) -> Vec<Color32> {
157 self.colors.to_vec()
158 }
159
160 fn set_colors(&mut self, colors: Vec<Color32>) {
161 for (old, new) in self.colors.iter_mut().zip(colors) {
162 *old = new;
163 }
164 }
165
166 fn pane(&self) -> crate::studies::StudyPane {
167 crate::studies::StudyPane {
168 range: Some((0.0, 100.0)),
169 levels: vec![(self.threshold as f64, palette::GRAY, "Threshold")],
170 ..Default::default()
171 }
172 }
173
174 fn is_overlay(&self) -> bool {
175 false
176 }
177
178 fn is_visible(&self) -> bool {
179 self.visible
180 }
181
182 fn set_visible(&mut self, visible: bool) {
183 self.visible = visible;
184 }
185
186 fn clone_box(&self) -> Box<dyn Indicator> {
187 Box::new(self.clone())
188 }
189
190 fn line_names(&self) -> Vec<String> {
191 ["ADX", "Upward Breakout", "Downward Breakout"]
192 .map(String::from)
193 .to_vec()
194 }
195}
196
197#[cfg(test)]
198mod tests {
199 use super::*;
200
201 #[test]
207 fn breakouts_need_a_weak_trend_before_a_close_beyond_the_range() {
208 let up = [
209 (10.0, 9.0, 9.5),
210 (10.0, 9.0, 9.5),
211 (10.0, 9.0, 9.5),
212 (10.0, 9.0, 9.5),
213 (10.0, 9.0, 9.5),
214 (11.5, 9.5, 11.0),
215 (12.5, 10.5, 12.0),
216 ];
217 let down = up.map(|(high, low, close)| (-low, -high, -close));
218 let none = None;
219 for (threshold, prices, line, expected) in [
220 (25, up, 1, [none, none, none, none, none, Some(50.0), none]),
221 (
222 100,
223 up,
224 1,
225 [none, none, none, none, none, Some(50.0), Some(75.0)],
226 ),
227 (
228 25,
229 down,
230 2,
231 [none, none, none, none, none, Some(50.0), none],
232 ),
233 ] {
234 let study = AdxBreakout::new(2, threshold, 3);
235 let bars = super::super::hlc_bars(&prices);
236 super::super::assert_replay_matches_batch(&study, &bars);
237 let mut study = study;
238 study.calculate(&bars);
239 let actual: Vec<_> = (0..bars.len())
240 .map(|at| study.values().value(line, at))
241 .collect();
242 assert_eq!(actual, expected, "threshold {threshold} line {line}");
243 assert_eq!(study.values().value(0, 4), Some(0.0));
244 }
245 }
246}