Skip to main content

wickra_core/indicators/
rocr.rs

1//! Rate of Change Ratio (ROCR).
2
3use std::collections::VecDeque;
4
5use crate::error::{Error, Result};
6use crate::traits::Indicator;
7
8/// Rate of Change Ratio (`ROCR`): `close / close[period]`.
9///
10/// The momentum ratio relative to the price `period` bars ago: `1.0` means no
11/// change, `> 1` an advance, `< 1` a decline. It is [`Rocp`](crate::Rocp) plus
12/// one. Where the reference price is zero the result is reported as `0`.
13///
14/// Non-finite inputs are ignored and leave the window untouched; the last
15/// computed value is returned instead, matching the SMA / EMA convention.
16///
17/// # Example
18///
19/// ```
20/// use wickra_core::{Indicator, Rocr};
21///
22/// let mut indicator = Rocr::new(3).unwrap();
23/// let mut last = None;
24/// for i in 0..80 {
25///     last = indicator.update(100.0 + f64::from(i));
26/// }
27/// assert!(last.is_some());
28/// ```
29#[derive(Debug, Clone)]
30pub struct Rocr {
31    period: usize,
32    window: VecDeque<f64>,
33    last: Option<f64>,
34}
35
36impl Rocr {
37    /// # Errors
38    /// Returns [`Error::PeriodZero`] if `period == 0`.
39    pub fn new(period: usize) -> Result<Self> {
40        if period == 0 {
41            return Err(Error::PeriodZero);
42        }
43        Ok(Self {
44            period,
45            window: VecDeque::with_capacity(period + 1),
46            last: None,
47        })
48    }
49
50    /// Configured period.
51    pub const fn period(&self) -> usize {
52        self.period
53    }
54}
55
56impl Indicator for Rocr {
57    type Input = f64;
58    type Output = f64;
59
60    fn update(&mut self, input: f64) -> Option<f64> {
61        if !input.is_finite() {
62            return self.last;
63        }
64        if self.window.len() == self.period + 1 {
65            self.window.pop_front();
66        }
67        self.window.push_back(input);
68        if self.window.len() < self.period + 1 {
69            return None;
70        }
71        let prev = *self.window.front().expect("non-empty");
72        let rocr = if prev == 0.0 { 0.0 } else { input / prev };
73        self.last = Some(rocr);
74        Some(rocr)
75    }
76
77    fn reset(&mut self) {
78        self.window.clear();
79        self.last = None;
80    }
81
82    fn warmup_period(&self) -> usize {
83        self.period + 1
84    }
85
86    fn is_ready(&self) -> bool {
87        self.window.len() == self.period + 1
88    }
89
90    fn name(&self) -> &'static str {
91        "ROCR"
92    }
93}
94
95#[cfg(test)]
96mod tests {
97    use super::*;
98    use crate::traits::BatchExt;
99    use approx::assert_relative_eq;
100
101    #[test]
102    fn rejects_zero_period() {
103        assert!(matches!(Rocr::new(0), Err(Error::PeriodZero)));
104    }
105
106    #[test]
107    fn accessors_report_config() {
108        let r = Rocr::new(3).unwrap();
109        assert_eq!(r.period(), 3);
110        assert_eq!(r.name(), "ROCR");
111        assert_eq!(r.warmup_period(), 4);
112        assert!(!r.is_ready());
113    }
114
115    #[test]
116    fn known_value_is_a_ratio() {
117        // period 1 over [10, 11]: 11 / 10 = 1.1.
118        let mut r = Rocr::new(1).unwrap();
119        let out: Vec<Option<f64>> = r.batch(&[10.0, 11.0]);
120        assert_eq!(out[0], None);
121        assert_relative_eq!(out[1].unwrap(), 1.1, epsilon = 1e-12);
122        assert!(r.is_ready());
123    }
124
125    #[test]
126    fn constant_series_yields_one() {
127        let mut r = Rocr::new(3).unwrap();
128        for v in r.batch(&[10.0_f64; 12]).iter().skip(4).flatten() {
129            assert_relative_eq!(*v, 1.0, epsilon = 1e-12);
130        }
131    }
132
133    #[test]
134    fn zero_reference_price_reports_zero() {
135        let mut r = Rocr::new(1).unwrap();
136        let out: Vec<Option<f64>> = r.batch(&[0.0, 5.0]);
137        assert_relative_eq!(out[1].unwrap(), 0.0, epsilon = 1e-12);
138    }
139
140    #[test]
141    fn non_finite_input_holds_last() {
142        let mut r = Rocr::new(1).unwrap();
143        assert_eq!(r.update(10.0), None);
144        let v = r.update(11.0).unwrap();
145        assert_eq!(r.update(f64::INFINITY), Some(v));
146    }
147
148    #[test]
149    fn reset_clears_state() {
150        let mut r = Rocr::new(1).unwrap();
151        let _ = r.batch(&[10.0, 11.0]);
152        assert!(r.is_ready());
153        r.reset();
154        assert!(!r.is_ready());
155        assert_eq!(r.update(10.0), None);
156    }
157}