Skip to main content

egui_charts/studies/builtin/
adx.rs

1//! Avg Directional Index (ADX) Indicator
2//!
3//! ADX measures trend strength regardless of direction. It's derived from the
4//! Directional Movement Index (DMI) system which includes +DI and -DI.
5//!
6//! # Components
7//! - ADX: Trend strength (0-100)
8//! - +DI: Positive directional indicator
9//! - -DI: Negative directional indicator
10//!
11//! # Interpretation
12//! - ADX > 25: Strong trend
13//! - ADX < 20: Weak or no trend
14//! - +DI > -DI: Bullish trend
15//! - -DI > +DI: Bearish trend
16//!
17//! # Example
18//! ```
19//! # let bars: Vec<egui_charts::model::Bar> = (0..60).map(|i| { let p = 100.0 + (i as f64 * 0.3).sin() * 5.0; egui_charts::model::Bar::new(chrono::Utc::now(), p, p + 1.0, p - 1.0, p + 0.5, 1_000.0) }).collect();
20//! use egui_charts::studies::{ADX, Indicator};
21//!
22//! let mut adx = ADX::new(14);
23//! adx.calculate(&bars);
24//! ```
25
26use crate::model::Bar;
27use crate::studies::palette;
28use crate::studies::stepper::Stepper;
29use crate::studies::{ATR, Indicator, IndicatorValue, IndicatorValues};
30use egui::Color32;
31
32/// Avg Directional Index indicator. Draws its ADX line; +DI and -DI are computed and can be shown
33/// from its style settings.
34///
35/// The `period` input sets both lengths, as studies saved with a single length expect;
36/// `adx_smoothing`, which follows it in `inputs()`, then sets the smoothing alone.
37#[derive(Clone)]
38pub struct ADX {
39    period: usize,
40    /// Length of Wilder's average of DX that forms the ADX line.
41    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    /// Create a new ADX indicator
63    ///
64    /// # Arguments
65    /// * `period` - Lookback period (typically 14)
66    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    /// Set the length of Wilder's average of DX that forms the ADX line, clamped to at least one.
80    /// `new` sets it to the DI period.
81    pub fn with_smoothing(mut self, smoothing: usize) -> Self {
82        self.adx_smoothing = smoothing.max(1);
83        self
84    }
85
86    /// Set colors for all three lines
87    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    /// Wilder's smoothing
95    #[inline]
96    fn wilders_smooth(prev: f64, current: f64, period: usize) -> f64 {
97        prev - (prev / period as f64) + current
98    }
99}
100
101/// Construct with the conventional default parameters.
102impl 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        // Before the smoothing input existed, `period` set both lengths; saved studies still do.
114        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            // +DI and -DI are available in the style settings.
126            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 // ADX is drawn in separate pane
224    }
225
226    fn line_count(&self) -> usize {
227        3 // ADX, +DI, -DI
228    }
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}