Skip to main content

wickra_embed_core/indicators/
atr.rs

1//! Average True Range (Wilder) — const-generic, no-alloc, byte-parity with
2//! `wickra_core::Atr`.
3
4use crate::ohlcv::Candle;
5use crate::traits::Indicator;
6
7/// Average True Range over a Wilder period `N`.
8///
9/// Seeds with the mean of the first `N` true ranges, then applies Wilder
10/// smoothing `avg.mul_add(N - 1, tr) / N`, identical to `wickra_core::Atr`. The
11/// first value appears after `N` candles. Input is a [`Candle`]; only high, low
12/// and the previous close are read.
13///
14/// # Example
15///
16/// ```
17/// use wickra_embed_core::{Atr, Candle, Indicator};
18///
19/// let mut atr = Atr::<5>::new();
20/// let mut last = None;
21/// for i in 0..40 {
22///     let base = 100.0 + f64::from(i);
23///     let c = Candle::new(base, base + 2.0, base - 2.0, base + 1.0, 10.0, i64::from(i));
24///     last = atr.update(c);
25/// }
26/// assert!(last.is_some());
27/// ```
28#[derive(Debug, Clone)]
29pub struct Atr<const N: usize> {
30    n_minus_1: f64,
31    inv_period: f64,
32    prev_close: Option<f64>,
33    /// Running sum and count of the seed true ranges, replacing wickra-core's
34    /// `Vec<f64>` seed buffer. Incremental summation in arrival order is the
35    /// identical fold to `seed_buf.iter().sum()`, so the seed average matches.
36    seed_count: usize,
37    seed_sum: f64,
38    avg: f64,
39    seeded: bool,
40}
41
42impl<const N: usize> Atr<N> {
43    /// Construct a new ATR over a Wilder period of `N`.
44    #[must_use]
45    pub fn new() -> Self {
46        Self {
47            n_minus_1: (N - 1) as f64,
48            inv_period: 1.0 / N as f64,
49            prev_close: None,
50            seed_count: 0,
51            seed_sum: 0.0,
52            avg: 0.0,
53            seeded: false,
54        }
55    }
56
57    /// Current value if seeded.
58    #[must_use]
59    pub const fn value(&self) -> Option<f64> {
60        if self.seeded {
61            Some(self.avg)
62        } else {
63            None
64        }
65    }
66}
67
68impl<const N: usize> Default for Atr<N> {
69    fn default() -> Self {
70        Self::new()
71    }
72}
73
74impl<const N: usize> Indicator for Atr<N> {
75    type Input = Candle;
76
77    fn update(&mut self, candle: Candle) -> Option<f64> {
78        let tr = candle.true_range(self.prev_close);
79        self.prev_close = Some(candle.close);
80
81        if self.seeded {
82            let new_avg = crate::math::mul_add(self.avg, self.n_minus_1, tr) * self.inv_period;
83            self.avg = new_avg;
84            return Some(new_avg);
85        }
86
87        self.seed_sum += tr;
88        self.seed_count += 1;
89        if self.seed_count == N {
90            let seed = self.seed_sum / N as f64;
91            self.avg = seed;
92            self.seeded = true;
93            return Some(seed);
94        }
95        None
96    }
97
98    fn reset(&mut self) {
99        *self = Self::new();
100    }
101
102    fn warmup_period(&self) -> usize {
103        N
104    }
105
106    fn is_ready(&self) -> bool {
107        self.seeded
108    }
109
110    fn name(&self) -> &'static str {
111        "atr"
112    }
113}
114
115#[cfg(test)]
116mod tests {
117    use super::Atr;
118    use crate::ohlcv::Candle;
119    use crate::traits::Indicator;
120
121    fn candle(i: i64) -> Candle {
122        let base = 100.0 + i as f64;
123        Candle::new(base, base + 2.0, base - 2.0, base + 1.0, 10.0, i)
124    }
125
126    #[test]
127    fn warmup_then_value() {
128        let mut atr = Atr::<5>::new();
129        for i in 0..4 {
130            assert_eq!(atr.update(candle(i)), None);
131        }
132        let v = atr.update(candle(4));
133        assert!(v.is_some());
134        assert!(atr.is_ready());
135        assert_eq!(atr.warmup_period(), 5);
136    }
137
138    #[test]
139    fn reset_clears_state() {
140        let mut atr = Atr::<5>::new();
141        for i in 0..20 {
142            atr.update(candle(i));
143        }
144        atr.reset();
145        assert!(!atr.is_ready());
146        assert_eq!(atr.value(), None);
147    }
148
149    #[test]
150    fn byte_parity_with_wickra_core() {
151        let mut ours = Atr::<14>::new();
152        let mut theirs = wickra_core::Atr::new(14).unwrap();
153        for i in 0..1000 {
154            let base = 100.0 + 20.0 * (f64::from(i) * 0.08).sin();
155            let (open, high, low, close) = (base, base + 3.0, base - 3.0, base + 1.5);
156            let ours_c = Candle::new(open, high, low, close, 10.0, i64::from(i));
157            let theirs_c =
158                wickra_core::Candle::new(open, high, low, close, 10.0, i64::from(i)).unwrap();
159            let a = ours.update(ours_c);
160            let b = <wickra_core::Atr as wickra_core::Indicator>::update(&mut theirs, theirs_c);
161            assert_eq!(a.map(f64::to_bits), b.map(f64::to_bits), "index {i}");
162        }
163    }
164}