Skip to main content

egui_charts/studies/builtin/
atr.rs

1//! Avg True Range (ATR) Indicator
2//!
3//! ATR measures market volatility by decomposing the entire range of an asset price.
4//! It's commonly used for position sizing and stop-loss placement.
5//!
6//! # Calculation
7//! True Range = max(high - low, |high - prev_close|, |low - prev_close|)
8//! ATR = Wilder's average of True Range over N periods, or a simple, exponential or weighted one
9//!
10//! # Example
11//! ```
12//! # 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();
13//! use egui_charts::studies::{ATR, Indicator};
14//!
15//! let mut atr = ATR::new(14);
16//! atr.calculate(&bars);
17//! ```
18
19use crate::model::Bar;
20use crate::studies::palette;
21use crate::studies::stepper::Stepper;
22use crate::studies::{Indicator, IndicatorValue, IndicatorValues};
23use egui::Color32;
24use std::collections::VecDeque;
25
26/// Average of the true range that [`ATR`] draws. Every smoothing has its first value on the
27/// `period`-th true range.
28#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
29#[non_exhaustive]
30pub enum AtrSmoothing {
31    /// Wilder's average, alpha 1/period, starting at the mean of the first `period` ranges.
32    #[default]
33    Wilder,
34    /// Mean of the last `period` ranges.
35    Simple,
36    /// Alpha 2/(period+1), starting at the mean of the first `period` ranges.
37    Exponential,
38    /// Linear weights 1 to `period` over the last `period` ranges, newest heaviest.
39    Weighted,
40}
41
42/// Avg True Range indicator
43#[derive(Clone)]
44pub struct ATR {
45    period: usize,
46    smoothing: AtrSmoothing,
47    values: IndicatorValues,
48    stepper: Stepper<State>,
49    color: Color32,
50    visible: bool,
51}
52
53#[derive(Clone)]
54struct State {
55    previous: Option<f64>,
56    usable: usize,
57    atr: f64,
58    /// The last `period` true ranges of a simple or weighted average.
59    window: VecDeque<f64>,
60}
61
62impl Default for State {
63    fn default() -> Self {
64        Self {
65            previous: None,
66            usable: 0,
67            atr: -0.0,
68            window: VecDeque::new(),
69        }
70    }
71}
72
73impl ATR {
74    /// Create a new ATR indicator with the specified period
75    ///
76    /// # Arguments
77    /// * `period` - The lookback period (typically 14)
78    pub fn new(period: usize) -> Self {
79        Self {
80            period: period.max(1),
81            smoothing: AtrSmoothing::Wilder,
82            values: IndicatorValues::new(),
83            stepper: Stepper::default(),
84            color: palette::DARK_RED,
85            visible: true,
86        }
87    }
88
89    /// Choose the average of the true range; Wilder's by default.
90    pub fn with_smoothing(mut self, smoothing: AtrSmoothing) -> Self {
91        self.smoothing = smoothing;
92        self
93    }
94
95    /// Set the indicator color
96    pub fn with_color(mut self, color: Color32) -> Self {
97        self.color = color;
98        self
99    }
100
101    /// Calculate True Range for a single bar
102    #[inline]
103    pub fn true_range(bar: &Bar, prev_close: f64) -> f64 {
104        let hl = bar.high - bar.low;
105        let hc = (bar.high - prev_close).abs();
106        let lc = (bar.low - prev_close).abs();
107        hl.max(hc).max(lc)
108    }
109}
110
111/// Construct with the conventional default parameters.
112impl Default for ATR {
113    fn default() -> Self {
114        Self::new(14)
115    }
116}
117
118impl Indicator for ATR {
119    crate::studies::inputs::study_inputs!(self;
120        period: length("Period", 1, usize::MAX),
121        smoothing: choice("Smoothing", [AtrSmoothing::Wilder, AtrSmoothing::Simple, AtrSmoothing::Exponential, AtrSmoothing::Weighted])
122    ; {});
123
124    fn value_format(&self) -> crate::scales::StudyFormat {
125        crate::scales::StudyFormat::Price
126    }
127
128    fn name(&self) -> &str {
129        "ATR"
130    }
131
132    fn desc(&self) -> &str {
133        "Avg True Range - Volatility indicator"
134    }
135
136    fn calculate(&mut self, data: &[Bar]) {
137        self.update(data, 0);
138    }
139
140    fn update(&mut self, data: &[Bar], from: usize) {
141        let period = self.period;
142        let smoothing = self.smoothing;
143        let multiplier = if smoothing == AtrSmoothing::Exponential {
144            2.0 / (period as f64 + 1.0)
145        } else {
146            1.0 / period as f64
147        };
148        self.stepper
149            .run(data, from, &mut self.values, |state, bar| {
150                if ![bar.high, bar.low, bar.close].iter().all(|p| p.is_finite()) {
151                    return IndicatorValue::None;
152                }
153                let tr = state
154                    .previous
155                    .map_or(bar.high - bar.low, |close| Self::true_range(bar, close));
156                state.previous = Some(bar.close);
157                if matches!(smoothing, AtrSmoothing::Simple | AtrSmoothing::Weighted) {
158                    state.window.push_back(tr);
159                    if state.window.len() > period {
160                        state.window.pop_front();
161                    }
162                    if state.window.len() < period {
163                        return IndicatorValue::None;
164                    }
165                    return IndicatorValue::Single(if smoothing == AtrSmoothing::Simple {
166                        state.window.iter().sum::<f64>() / period as f64
167                    } else {
168                        let weighted: f64 =
169                            (1..).zip(&state.window).map(|(w, tr)| w as f64 * tr).sum();
170                        weighted / (period as f64 * (period as f64 + 1.0) / 2.0)
171                    });
172                }
173                state.usable += 1;
174                if state.usable <= period {
175                    state.atr += tr;
176                    if state.usable < period {
177                        return IndicatorValue::None;
178                    }
179                    state.atr /= period as f64;
180                } else {
181                    state.atr += multiplier * (tr - state.atr);
182                }
183                IndicatorValue::Single(state.atr)
184            });
185    }
186
187    fn is_incremental(&self) -> bool {
188        true
189    }
190
191    fn values(&self) -> &IndicatorValues {
192        &self.values
193    }
194
195    fn colors(&self) -> Vec<Color32> {
196        vec![self.color]
197    }
198
199    fn set_colors(&mut self, colors: Vec<Color32>) {
200        if !colors.is_empty() {
201            self.color = colors[0];
202        }
203    }
204
205    fn is_overlay(&self) -> bool {
206        false // ATR is drawn in separate pane
207    }
208
209    fn is_visible(&self) -> bool {
210        self.visible
211    }
212
213    fn set_visible(&mut self, visible: bool) {
214        self.visible = visible;
215    }
216
217    fn clone_box(&self) -> Box<dyn Indicator> {
218        Box::new(self.clone())
219    }
220
221    fn line_names(&self) -> Vec<String> {
222        vec![match self.smoothing {
223            AtrSmoothing::Wilder => format!("ATR({})", self.period),
224            AtrSmoothing::Simple => format!("ATR({}, SMA)", self.period),
225            AtrSmoothing::Exponential => format!("ATR({}, EMA)", self.period),
226            AtrSmoothing::Weighted => format!("ATR({}, WMA)", self.period),
227        }]
228    }
229}
230
231#[cfg(test)]
232mod tests {
233    use super::*;
234    use chrono::Utc;
235
236    #[test]
237    fn test_true_range_calculation() {
238        let bar = Bar {
239            time: Utc::now(),
240            open: 100.0,
241            high: 105.0,
242            low: 98.0,
243            close: 103.0,
244            volume: 1000.0,
245            colors: None,
246            payload: None,
247        };
248
249        // Case 1: Normal range (high - low is largest)
250        let tr = ATR::true_range(&bar, 100.0);
251        assert!((tr - 7.0).abs() < 0.001); // high - low = 105 - 98 = 7
252
253        // Case 2: Gap up (high - prev_close is largest)
254        let tr = ATR::true_range(&bar, 95.0);
255        assert!((tr - 10.0).abs() < 0.001); // high - prev = 105 - 95 = 10
256
257        // Case 3: Gap down (|low - prev_close| is largest)
258        let tr = ATR::true_range(&bar, 110.0);
259        assert!((tr - 12.0).abs() < 0.001); // |low - prev| = |98 - 110| = 12
260    }
261
262    /// Wilder's ATR worked by hand. A bar's ATR does not depend on whether a later bar exists:
263    /// with length 1 the first bar has its range. A bar with a non-finite price has no value,
264    /// and the next true range uses the close before it.
265    #[test]
266    fn matches_wilder_by_hand() {
267        let bar = |high, low, close| Bar {
268            time: Utc::now(),
269            open: close,
270            high,
271            low,
272            close,
273            volume: 1000.0,
274            colors: None,
275            payload: None,
276        };
277        let first = bar(102.0, 98.0, 100.0);
278        // Length 2 around a bad bar: ranges 4, max(2, 3, 1) = 3, then from the close of 102
279        // max(4, 6, 2) = 6 and max(2, 1, 1) = 2; ATR (4 + 3) / 2 = 3.5, (3.5 + 6) / 2 = 4.75,
280        // (4.75 + 2) / 2 = 3.375.
281        let around = |bad: Bar| {
282            vec![
283                first.clone(),
284                bar(103.0, 101.0, 102.0),
285                bad,
286                bar(108.0, 104.0, 106.0),
287                bar(107.0, 105.0, 106.0),
288            ]
289        };
290        let skipped = vec![None, Some(3.5), None, Some(4.75), Some(3.375)];
291        // Ranges by hand: 102 - 98 = 4; for the second bar max(103 - 99, |103 - 100|, |99 - 100|) = 4.
292        let cases: [(usize, Vec<Bar>, Vec<Option<f64>>); 4] = [
293            (1, vec![first.clone()], vec![Some(4.0)]),
294            (
295                1,
296                vec![first.clone(), bar(103.0, 99.0, 101.0)],
297                vec![Some(4.0), Some(4.0)],
298            ),
299            (
300                2,
301                around(bar(f64::NAN, f64::NAN, f64::NAN)),
302                skipped.clone(),
303            ),
304            (2, around(bar(f64::INFINITY, 103.0, 104.0)), skipped),
305        ];
306        for (period, data, expected) in cases {
307            let mut atr = ATR::new(period);
308            atr.calculate(&data);
309            assert_eq!(atr.values().len(), expected.len());
310            for (i, (got, want)) in atr.values().to_rows().iter().zip(expected).enumerate() {
311                let ok = match (got, want) {
312                    (IndicatorValue::None, None) => true,
313                    (IndicatorValue::Single(v), Some(w)) => (v - w).abs() < 1e-12,
314                    _ => false,
315                };
316                assert!(ok, "ATR({period}) bar {i}: {got:?} != {want:?}");
317            }
318        }
319    }
320}