1use crate::error::{Error, Result};
4use crate::ohlcv::Candle;
5use crate::traits::Indicator;
6
7#[derive(Debug, Clone)]
29pub struct Atr {
30 period: usize,
31 n_minus_1: f64,
33 inv_period: f64,
36 prev_close: Option<f64>,
37 seed_buf: Vec<f64>,
38 avg: f64,
41 seeded: bool,
42}
43
44impl Atr {
45 pub fn new(period: usize) -> Result<Self> {
51 if period == 0 {
52 return Err(Error::PeriodZero);
53 }
54 if period > crate::error::MAX_PERIOD {
55 return Err(Error::InvalidPeriod {
56 message: crate::error::PERIOD_ABOVE_MAX,
57 });
58 }
59 Ok(Self {
60 period,
61 n_minus_1: (period - 1) as f64,
62 inv_period: 1.0 / period as f64,
63 prev_close: None,
64 seed_buf: Vec::with_capacity(period),
65 avg: 0.0,
66 seeded: false,
67 })
68 }
69
70 pub const fn period(&self) -> usize {
72 self.period
73 }
74
75 pub const fn value(&self) -> Option<f64> {
77 if self.seeded {
78 Some(self.avg)
79 } else {
80 None
81 }
82 }
83
84 pub fn batch_atr(&mut self, high: &[f64], low: &[f64], close: &[f64]) -> Vec<f64> {
96 let p = self.period;
97 let n = high.len();
98 if self.seeded || !self.seed_buf.is_empty() || self.prev_close.is_some() || n < p {
99 let mut out = vec![f64::NAN; n];
100 for i in 0..n {
101 let candle = Candle::new_unchecked(close[i], high[i], low[i], close[i], 0.0, 0);
102 if let Some(v) = self.update(candle) {
103 out[i] = v;
104 }
105 }
106 return out;
107 }
108
109 let mut out = vec![f64::NAN; p - 1];
111 out.reserve(n - (p - 1));
112 let mut prev_close = close[0];
114 let mut sum_tr = high[0] - low[0];
115 self.seed_buf.push(sum_tr);
116 for i in 1..p {
117 let (h, l) = (high[i], low[i]);
118 let tr = (h - l)
119 .max((h - prev_close).abs())
120 .max((l - prev_close).abs());
121 prev_close = close[i];
122 self.seed_buf.push(tr);
123 sum_tr += tr;
124 }
125 let mut avg = sum_tr / p as f64;
126 out.push(avg);
127 for i in p..n {
129 let (h, l) = (high[i], low[i]);
130 let tr = (h - l)
131 .max((h - prev_close).abs())
132 .max((l - prev_close).abs());
133 prev_close = close[i];
134 avg = avg.mul_add(self.n_minus_1, tr) * self.inv_period;
135 out.push(avg);
136 }
137
138 self.prev_close = Some(prev_close);
140 self.avg = avg;
141 self.seeded = true;
142 out
143 }
144}
145
146impl Indicator for Atr {
147 type Input = Candle;
148 type Output = f64;
149
150 #[inline]
151 fn update(&mut self, candle: Candle) -> Option<f64> {
152 let tr = candle.true_range(self.prev_close);
153 self.prev_close = Some(candle.close);
154
155 if self.seeded {
156 let new_avg = self.avg.mul_add(self.n_minus_1, tr) * self.inv_period;
158 self.avg = new_avg;
159 return Some(new_avg);
160 }
161
162 self.seed_buf.push(tr);
163 if self.seed_buf.len() == self.period {
164 let seed = self.seed_buf.iter().copied().sum::<f64>() / self.period as f64;
165 self.avg = seed;
166 self.seeded = true;
167 return Some(seed);
168 }
169 None
170 }
171
172 fn reset(&mut self) {
173 self.prev_close = None;
174 self.seed_buf.clear();
175 self.avg = 0.0;
176 self.seeded = false;
177 }
178
179 #[inline]
180 fn warmup_period(&self) -> usize {
181 self.period
182 }
183
184 #[inline]
185 fn is_ready(&self) -> bool {
186 self.seeded
187 }
188
189 #[inline]
190 fn name(&self) -> &'static str {
191 "ATR"
192 }
193}
194
195#[cfg(test)]
196mod tests {
197 use super::*;
198 use crate::traits::BatchExt;
199 use approx::assert_relative_eq;
200
201 fn c(h: f64, l: f64, cl: f64) -> Candle {
202 Candle::new(cl, h, l, cl, 1.0, 0).unwrap()
204 }
205
206 fn atr_naive(hlc: &[(f64, f64, f64)], period: usize) -> Vec<Option<f64>> {
208 let n = period as f64;
209 let mut out = Vec::with_capacity(hlc.len());
210 let mut trs: Vec<f64> = Vec::new();
211 let mut avg: Option<f64> = None;
212 let mut prev_close: Option<f64> = None;
213 for &(h, l, cl) in hlc {
214 let tr = match prev_close {
215 None => h - l,
216 Some(pc) => (h - l).max((h - pc).abs()).max((l - pc).abs()),
217 };
218 prev_close = Some(cl);
219 if let Some(a) = avg {
220 let na = (a * (n - 1.0) + tr) / n;
221 avg = Some(na);
222 out.push(Some(na));
223 } else {
224 trs.push(tr);
225 if trs.len() == period {
226 avg = Some(trs.iter().sum::<f64>() / n);
227 out.push(avg);
228 } else {
229 out.push(None);
230 }
231 }
232 }
233 out
234 }
235
236 #[test]
237 fn rejects_zero_period() {
238 assert!(matches!(Atr::new(0), Err(Error::PeriodZero)));
239 }
240
241 #[test]
245 fn accessors_and_metadata() {
246 let mut atr = Atr::new(14).unwrap();
247 assert_eq!(atr.period(), 14);
248 assert_eq!(atr.name(), "ATR");
249 assert_eq!(atr.value(), None);
250 for _ in 0..14 {
251 atr.update(c(11.0, 9.0, 10.0));
252 }
253 assert!(atr.value().is_some());
254 }
255
256 #[test]
257 fn warmup_emits_on_period_th_candle() {
258 let candles = vec![
259 c(2.0, 1.0, 1.5),
260 c(3.0, 2.0, 2.5),
261 c(4.0, 3.0, 3.5),
262 c(5.0, 4.0, 4.5),
263 c(6.0, 5.0, 5.5),
264 ];
265 let mut atr = Atr::new(3).unwrap();
266 let out = atr.batch(&candles);
267 assert!(out[0].is_none());
268 assert!(out[1].is_none());
269 assert!(out[2].is_some());
270 assert!(out[3].is_some());
271 }
272
273 #[test]
274 fn constant_range_yields_constant_atr() {
275 let candles: Vec<Candle> = (0..30).map(|_| c(11.0, 9.0, 10.0)).collect();
277 let mut atr = Atr::new(14).unwrap();
278 let out = atr.batch(&candles);
279 for v in out.iter().skip(13).flatten() {
280 assert_relative_eq!(*v, 2.0, epsilon = 1e-12);
281 }
282 }
283
284 #[test]
285 fn gap_up_uses_high_minus_prev_close() {
286 let candles = vec![
288 c(6.0, 4.0, 5.0), c(10.0, 9.0, 9.5), ];
291 let mut atr = Atr::new(2).unwrap();
292 let out = atr.batch(&candles);
293 assert_relative_eq!(out[1].unwrap(), 3.5, epsilon = 1e-12);
296 }
297
298 #[test]
299 fn batch_equals_streaming() {
300 let candles: Vec<Candle> = (0..40)
301 .map(|i| {
302 let mid = f64::from(i) + 10.0;
303 c(mid + 0.5, mid - 0.5, mid)
304 })
305 .collect();
306 let mut a = Atr::new(14).unwrap();
307 let mut b = Atr::new(14).unwrap();
308 assert_eq!(
309 a.batch(&candles),
310 candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
311 );
312 }
313
314 #[test]
315 fn reset_clears_state() {
316 let candles: Vec<Candle> = (0..20).map(|_| c(11.0, 9.0, 10.0)).collect();
317 let mut atr = Atr::new(5).unwrap();
318 atr.batch(&candles);
319 assert!(atr.is_ready());
320 atr.reset();
321 assert!(!atr.is_ready());
322 assert_eq!(atr.update(candles[0]), None);
323 }
324
325 #[test]
326 fn never_negative() {
327 let candles: Vec<Candle> = (0..200)
328 .map(|i| {
329 let base = 100.0 + (f64::from(i) * 0.3).sin() * 5.0;
330 c(base + 1.0, base - 1.0, base)
331 })
332 .collect();
333 let mut atr = Atr::new(14).unwrap();
334 for v in atr.batch(&candles).into_iter().flatten() {
335 assert!(v >= 0.0, "ATR must be non-negative: {v}");
336 }
337 }
338
339 fn bits_eq(a: &[f64], b: &[f64]) -> bool {
340 a.len() == b.len()
341 && a.iter()
342 .zip(b)
343 .all(|(x, y)| x == y || (x.is_nan() && y.is_nan()))
344 }
345
346 fn atr_replay(period: usize, high: &[f64], low: &[f64], close: &[f64]) -> Vec<f64> {
347 let mut a = Atr::new(period).unwrap();
348 (0..high.len())
349 .map(|i| {
350 let candle = Candle::new_unchecked(close[i], high[i], low[i], close[i], 0.0, 0);
351 a.update(candle).unwrap_or(f64::NAN)
352 })
353 .collect()
354 }
355
356 fn columns(n: usize) -> (Vec<f64>, Vec<f64>, Vec<f64>) {
358 let base: Vec<f64> = (0..n)
359 .map(|i| (f64::from(u32::try_from(i).unwrap()) * 0.3).sin() * 5.0 + 100.0)
360 .collect();
361 let high = base.iter().map(|b| b + 1.0).collect();
362 let low = base.iter().map(|b| b - 1.0).collect();
363 (high, low, base)
364 }
365
366 #[test]
367 fn batch_atr_fast_path_is_bit_identical() {
368 let (high, low, close) = columns(300);
369 let mut atr = Atr::new(14).unwrap();
370 let got = atr.batch_atr(&high, &low, &close);
371 assert!(bits_eq(&got, &atr_replay(14, &high, &low, &close)));
372 let mut ref_atr = Atr::new(14).unwrap();
373 for i in 0..high.len() {
374 ref_atr.update(Candle::new_unchecked(
375 close[i], high[i], low[i], close[i], 0.0, 0,
376 ));
377 }
378 let next = Candle::new_unchecked(101.0, 102.0, 100.0, 101.0, 0.0, 0);
379 assert_eq!(atr.update(next), ref_atr.update(next));
380 }
381
382 #[test]
383 fn batch_atr_falls_back_when_not_fresh() {
384 let (high, low, close) = columns(40);
385 let mut atr = Atr::new(14).unwrap();
386 atr.update(Candle::new_unchecked(
387 close[0], high[0], low[0], close[0], 0.0, 0,
388 ));
389 let mut ref_atr = Atr::new(14).unwrap();
390 ref_atr.update(Candle::new_unchecked(
391 close[0], high[0], low[0], close[0], 0.0, 0,
392 ));
393 let want: Vec<f64> = (0..high.len())
394 .map(|i| {
395 ref_atr
396 .update(Candle::new_unchecked(
397 close[i], high[i], low[i], close[i], 0.0, 0,
398 ))
399 .unwrap_or(f64::NAN)
400 })
401 .collect();
402 assert!(bits_eq(&atr.batch_atr(&high, &low, &close), &want));
403 }
404
405 #[test]
406 fn batch_atr_sub_period_slice_falls_back() {
407 let (high, low, close) = columns(5);
408 let mut atr = Atr::new(14).unwrap();
409 let got = atr.batch_atr(&high, &low, &close);
410 assert!(bits_eq(&got, &atr_replay(14, &high, &low, &close)));
411 assert!(got.iter().all(|x| x.is_nan()));
412 }
413
414 proptest::proptest! {
415 #![proptest_config(proptest::test_runner::Config::with_cases(48))]
416 #[test]
417 fn atr_matches_naive(
418 period in 1usize..15,
419 bars in proptest::collection::vec(
420 (10.0_f64..1000.0, 0.0_f64..50.0, 0.0_f64..1.0),
421 0..120,
422 ),
423 ) {
424 let hlc: Vec<(f64, f64, f64)> = bars
426 .iter()
427 .map(|&(low, range, frac)| (low + range, low, low + range * frac))
428 .collect();
429 let candles: Vec<Candle> = hlc.iter().map(|&(h, l, cl)| c(h, l, cl)).collect();
430 let mut atr = Atr::new(period).unwrap();
431 let got = atr.batch(&candles);
432 let want = atr_naive(&hlc, period);
433 proptest::prop_assert_eq!(got.len(), want.len());
434 for (g, w) in got.iter().zip(want.iter()) {
435 match (g, w) {
436 (None, None) => {}
437 (Some(a), Some(b)) => proptest::prop_assert!(
438 (a - b).abs() <= 1e-9 * a.abs().max(1.0),
439 "got={a} want={b}"
440 ),
441 _ => proptest::prop_assert!(false, "warmup mismatch"),
442 }
443 }
444 }
445 }
446}