egui_charts/studies/builtin/
adx.rs1use crate::model::Bar;
27use crate::studies::palette;
28use crate::studies::stepper::Stepper;
29use crate::studies::{ATR, Indicator, IndicatorValue, IndicatorValues};
30use egui::Color32;
31
32#[derive(Clone)]
38pub struct ADX {
39 period: usize,
40 adx_smoothing: usize,
42 values: IndicatorValues,
43 stepper: Stepper<State>,
44 adx_color: Color32,
45 plus_di_color: Color32,
46 minus_di_color: Color32,
47 visible: bool,
48}
49
50#[derive(Clone, Default)]
51struct State {
52 previous: Option<[f64; 3]>,
53 count: usize,
54 plus: f64,
55 minus: f64,
56 tr: f64,
57 dx_count: usize,
58 adx: f64,
59}
60
61impl ADX {
62 pub fn new(period: usize) -> Self {
67 Self {
68 period: period.max(1),
69 adx_smoothing: period.max(1),
70 values: IndicatorValues::new(),
71 stepper: Stepper::default(),
72 adx_color: palette::RED,
73 plus_di_color: palette::BLUE,
74 minus_di_color: palette::ORANGE,
75 visible: true,
76 }
77 }
78
79 pub fn with_smoothing(mut self, smoothing: usize) -> Self {
82 self.adx_smoothing = smoothing.max(1);
83 self
84 }
85
86 pub fn with_colors(mut self, adx: Color32, plus_di: Color32, minus_di: Color32) -> Self {
88 self.adx_color = adx;
89 self.plus_di_color = plus_di;
90 self.minus_di_color = minus_di;
91 self
92 }
93
94 #[inline]
96 fn wilders_smooth(prev: f64, current: f64, period: usize) -> f64 {
97 prev - (prev / period as f64) + current
98 }
99}
100
101impl Default for ADX {
103 fn default() -> Self {
104 Self::new(14)
105 }
106}
107
108impl Indicator for ADX {
109 crate::studies::inputs::study_inputs!(self;
110 period: length("DI period", 1, usize::MAX),
111 adx_smoothing: length("ADX smoothing period", 1, usize::MAX)
112 ; changed => {
113 if changed == "period" {
115 self.adx_smoothing = self.period;
116 }
117 });
118
119 fn plot_style(&self, line: usize) -> crate::studies::PlotStyle {
120 crate::studies::PlotStyle {
121 color: self.colors().get(line).copied().unwrap_or(Color32::WHITE),
122 width: self.line_style(line).width,
123 pattern: crate::model::LineStyle::Solid,
124 plot_type: crate::studies::PlotType::Line,
125 visible: line == 0,
127 }
128 }
129
130 fn name(&self) -> &str {
131 "ADX"
132 }
133
134 fn desc(&self) -> &str {
135 "Avg Directional Index - Trend strength indicator"
136 }
137
138 fn calculate(&mut self, data: &[Bar]) {
139 self.update(data, 0);
140 }
141
142 fn update(&mut self, data: &[Bar], from: usize) {
143 self.stepper
144 .run(data, from, &mut self.values, |state, bar| {
145 if !bar.high.is_finite() || !bar.low.is_finite() || !bar.close.is_finite() {
146 return IndicatorValue::None;
147 }
148 let Some(previous) = state.previous.replace([bar.high, bar.low, bar.close]) else {
149 return IndicatorValue::None;
150 };
151 state.count += 1;
152 let up = bar.high - previous[0];
153 let down = previous[1] - bar.low;
154 let plus = if up > down && up > 0.0 { up } else { 0.0 };
155 let minus = if down > up && down > 0.0 { down } else { 0.0 };
156 let tr = ATR::true_range(bar, previous[2]);
157 if state.count <= self.period {
158 state.plus += plus;
159 state.minus += minus;
160 state.tr += tr;
161 if state.count < self.period {
162 return IndicatorValue::None;
163 }
164 } else {
165 state.plus = Self::wilders_smooth(state.plus, plus, self.period);
166 state.minus = Self::wilders_smooth(state.minus, minus, self.period);
167 state.tr = Self::wilders_smooth(state.tr, tr, self.period);
168 }
169 let (plus, minus) = if state.tr != 0.0 {
170 (
171 100.0 * state.plus / state.tr,
172 100.0 * state.minus / state.tr,
173 )
174 } else {
175 (0.0, 0.0)
176 };
177 let sum = plus + minus;
178 let dx = if sum.abs() > 1e-10 {
179 100.0 * (plus - minus).abs() / sum
180 } else {
181 0.0
182 };
183 if state.dx_count < self.adx_smoothing {
184 state.dx_count += 1;
185 state.adx += dx;
186 if state.dx_count < self.adx_smoothing {
187 return IndicatorValue::Multiple(vec![f64::NAN, plus, minus]);
188 }
189 state.adx /= self.adx_smoothing as f64;
190 } else {
191 state.adx = (state.adx * (self.adx_smoothing - 1) as f64 + dx)
192 / self.adx_smoothing as f64;
193 }
194 IndicatorValue::Multiple(vec![state.adx, plus, minus])
195 });
196 }
197
198 fn is_incremental(&self) -> bool {
199 true
200 }
201
202 fn values(&self) -> &IndicatorValues {
203 &self.values
204 }
205
206 fn colors(&self) -> Vec<Color32> {
207 vec![self.adx_color, self.plus_di_color, self.minus_di_color]
208 }
209
210 fn set_colors(&mut self, colors: Vec<Color32>) {
211 if !colors.is_empty() {
212 self.adx_color = colors[0];
213 }
214 if colors.len() > 1 {
215 self.plus_di_color = colors[1];
216 }
217 if colors.len() > 2 {
218 self.minus_di_color = colors[2];
219 }
220 }
221
222 fn is_overlay(&self) -> bool {
223 false }
225
226 fn line_count(&self) -> usize {
227 3 }
229
230 fn is_visible(&self) -> bool {
231 self.visible
232 }
233
234 fn set_visible(&mut self, visible: bool) {
235 self.visible = visible;
236 }
237
238 fn clone_box(&self) -> Box<dyn Indicator> {
239 Box::new(self.clone())
240 }
241
242 fn line_names(&self) -> Vec<String> {
243 vec![
244 if self.adx_smoothing == self.period {
245 format!("ADX({})", self.period)
246 } else {
247 format!("ADX({},{})", self.period, self.adx_smoothing)
248 },
249 "+DI".to_string(),
250 "-DI".to_string(),
251 ]
252 }
253}
254
255#[cfg(test)]
256mod tests {
257 use crate::studies::{ConfiguredStudy, Indicator, IndicatorFactory};
258
259 #[test]
260 fn a_new_adx_draws_only_its_adx_line() {
261 for (name, visible) in [("ADX", [true, false, false]), ("DMI", [true; 3])] {
262 let study = ConfiguredStudy::new(IndicatorFactory::new().create(name).unwrap());
263 assert_eq!(
264 visible,
265 [0, 1, 2].map(|line| study.plot_style(line).visible)
266 );
267 }
268 }
269}