egui_charts/studies/builtin/
atr.rs1use 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#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
29#[non_exhaustive]
30pub enum AtrSmoothing {
31 #[default]
33 Wilder,
34 Simple,
36 Exponential,
38 Weighted,
40}
41
42#[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 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 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 pub fn with_smoothing(mut self, smoothing: AtrSmoothing) -> Self {
91 self.smoothing = smoothing;
92 self
93 }
94
95 pub fn with_color(mut self, color: Color32) -> Self {
97 self.color = color;
98 self
99 }
100
101 #[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
111impl 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 }
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 let tr = ATR::true_range(&bar, 100.0);
251 assert!((tr - 7.0).abs() < 0.001); let tr = ATR::true_range(&bar, 95.0);
255 assert!((tr - 10.0).abs() < 0.001); let tr = ATR::true_range(&bar, 110.0);
259 assert!((tr - 12.0).abs() < 0.001); }
261
262 #[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 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 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}