Skip to main content

wickra_embed_core/indicators/
roc.rs

1//! Rate of Change — const-generic, no-alloc, byte-parity with `wickra_core::Roc`.
2
3use crate::traits::Indicator;
4
5/// Rate of Change as a percentage: `(close - close[N ago]) / close[N ago] · 100`,
6/// where `N` is the lookback period.
7///
8/// A ring of exactly `N` slots holds the last `N` values; the slot about to be
9/// overwritten is the value from `N` steps ago, so no `N + 1` buffer is needed.
10/// Non-finite inputs are ignored (the last value is returned, state untouched),
11/// matching `wickra_core::Roc`. The first output appears after `N + 1` inputs.
12///
13/// # Example
14///
15/// ```
16/// use wickra_embed_core::{Indicator, Roc};
17///
18/// // ROC(3): prev = 100 three steps back, now = 110 -> 10%.
19/// let mut roc = Roc::<3>::new();
20/// assert_eq!(roc.update(100.0), None);
21/// assert_eq!(roc.update(105.0), None);
22/// assert_eq!(roc.update(108.0), None);
23/// assert_eq!(roc.update(110.0), Some(10.0));
24/// ```
25#[derive(Debug, Clone)]
26pub struct Roc<const N: usize> {
27    buf: [f64; N],
28    head: usize,
29    count: usize,
30    last: Option<f64>,
31}
32
33impl<const N: usize> Roc<N> {
34    /// Construct a new ROC over a lookback of `N`.
35    #[must_use]
36    pub const fn new() -> Self {
37        Self {
38            buf: [0.0; N],
39            head: 0,
40            count: 0,
41            last: None,
42        }
43    }
44
45    /// Current value if available.
46    #[must_use]
47    pub const fn value(&self) -> Option<f64> {
48        self.last
49    }
50}
51
52impl<const N: usize> Default for Roc<N> {
53    fn default() -> Self {
54        Self::new()
55    }
56}
57
58impl<const N: usize> Indicator for Roc<N> {
59    type Input = f64;
60
61    fn update(&mut self, input: f64) -> Option<f64> {
62        if !input.is_finite() {
63            return self.last;
64        }
65        if self.count == N {
66            let prev = self.buf[self.head];
67            self.buf[self.head] = input;
68            self.head += 1;
69            if self.head == N {
70                self.head = 0;
71            }
72            let roc = if prev == 0.0 {
73                0.0
74            } else {
75                (input - prev) / prev * 100.0
76            };
77            self.last = Some(roc);
78            Some(roc)
79        } else {
80            self.buf[self.head] = input;
81            self.count += 1;
82            self.head += 1;
83            if self.head == N {
84                self.head = 0;
85            }
86            None
87        }
88    }
89
90    fn reset(&mut self) {
91        *self = Self::new();
92    }
93
94    fn warmup_period(&self) -> usize {
95        N + 1
96    }
97
98    fn is_ready(&self) -> bool {
99        self.last.is_some()
100    }
101
102    fn name(&self) -> &'static str {
103        "roc"
104    }
105}
106
107#[cfg(test)]
108mod tests {
109    use super::Roc;
110    use crate::traits::Indicator;
111
112    #[test]
113    fn known_value() {
114        let mut roc = Roc::<3>::new();
115        assert_eq!(roc.update(100.0), None);
116        assert_eq!(roc.update(105.0), None);
117        assert_eq!(roc.update(108.0), None);
118        assert_eq!(roc.update(110.0), Some(10.0));
119        assert_eq!(roc.warmup_period(), 4);
120    }
121
122    #[test]
123    fn constant_series_is_zero() {
124        let mut roc = Roc::<5>::new();
125        let mut last = None;
126        for _ in 0..20 {
127            last = roc.update(42.0);
128        }
129        assert_eq!(last, Some(0.0));
130    }
131
132    #[test]
133    fn non_finite_is_ignored() {
134        let mut roc = Roc::<2>::new();
135        roc.update(100.0);
136        roc.update(110.0);
137        let v = roc.update(120.0);
138        assert!(v.is_some());
139        assert_eq!(roc.update(f64::NAN), v);
140    }
141
142    #[test]
143    fn byte_parity_with_wickra_core() {
144        let mut ours = Roc::<12>::new();
145        let mut theirs = wickra_core::Roc::new(12).unwrap();
146        for i in 0..1000 {
147            let x = 100.0 + 30.0 * (f64::from(i) * 0.06).sin() + 0.03 * f64::from(i);
148            let a = ours.update(x);
149            let b = <wickra_core::Roc as wickra_core::Indicator>::update(&mut theirs, x);
150            assert_eq!(a.map(f64::to_bits), b.map(f64::to_bits), "index {i}");
151        }
152    }
153}