Skip to main content

wickra_core/indicators/
rocr100.rs

1//! Rate of Change Ratio scaled by 100 (ROCR100).
2
3use std::collections::VecDeque;
4
5use crate::error::{Error, Result};
6use crate::traits::Indicator;
7
8/// Rate of Change Ratio × 100 (`ROCR100`): `close / close[period] · 100`.
9///
10/// The same ratio as [`Rocr`](crate::Rocr) rescaled so that an unchanged price
11/// reads `100` rather than `1`: `> 100` is an advance, `< 100` a decline. Where
12/// 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, Rocr100};
21///
22/// let mut indicator = Rocr100::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 Rocr100 {
31    period: usize,
32    window: VecDeque<f64>,
33    last: Option<f64>,
34}
35
36impl Rocr100 {
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        if period > crate::error::MAX_PERIOD {
44            return Err(Error::InvalidPeriod {
45                message: crate::error::PERIOD_ABOVE_MAX,
46            });
47        }
48        Ok(Self {
49            period,
50            window: VecDeque::with_capacity(period + 1),
51            last: None,
52        })
53    }
54
55    /// Configured period.
56    pub const fn period(&self) -> usize {
57        self.period
58    }
59}
60
61impl Indicator for Rocr100 {
62    type Input = f64;
63    type Output = f64;
64
65    #[inline]
66    fn update(&mut self, input: f64) -> Option<f64> {
67        if !input.is_finite() {
68            return None;
69        }
70        if self.window.len() == self.period + 1 {
71            self.window.pop_front();
72        }
73        self.window.push_back(input);
74        if self.window.len() < self.period + 1 {
75            return None;
76        }
77        let prev = *self.window.front().expect("non-empty");
78        let rocr = if prev == 0.0 {
79            0.0
80        } else {
81            input / prev * 100.0
82        };
83        self.last = Some(rocr);
84        Some(rocr)
85    }
86
87    fn reset(&mut self) {
88        self.window.clear();
89        self.last = None;
90    }
91
92    #[inline]
93    fn warmup_period(&self) -> usize {
94        self.period + 1
95    }
96
97    #[inline]
98    fn is_ready(&self) -> bool {
99        self.window.len() == self.period + 1
100    }
101
102    #[inline]
103    fn name(&self) -> &'static str {
104        "ROCR100"
105    }
106}
107
108#[cfg(test)]
109mod tests {
110    use super::*;
111    use crate::traits::BatchExt;
112    use approx::assert_relative_eq;
113
114    #[test]
115    fn rejects_zero_period() {
116        assert!(matches!(Rocr100::new(0), Err(Error::PeriodZero)));
117    }
118
119    #[test]
120    fn accessors_report_config() {
121        let r = Rocr100::new(3).unwrap();
122        assert_eq!(r.period(), 3);
123        assert_eq!(r.name(), "ROCR100");
124        assert_eq!(r.warmup_period(), 4);
125        assert!(!r.is_ready());
126    }
127
128    #[test]
129    fn known_value_is_a_scaled_ratio() {
130        // period 1 over [10, 11]: 11 / 10 * 100 = 110.
131        let mut r = Rocr100::new(1).unwrap();
132        let out: Vec<Option<f64>> = r.batch(&[10.0, 11.0]);
133        assert_eq!(out[0], None);
134        assert_relative_eq!(out[1].unwrap(), 110.0, epsilon = 1e-12);
135        assert!(r.is_ready());
136    }
137
138    #[test]
139    fn constant_series_yields_hundred() {
140        let mut r = Rocr100::new(3).unwrap();
141        for v in r.batch(&[10.0_f64; 12]).iter().skip(4).flatten() {
142            assert_relative_eq!(*v, 100.0, epsilon = 1e-12);
143        }
144    }
145
146    #[test]
147    fn zero_reference_price_reports_zero() {
148        let mut r = Rocr100::new(1).unwrap();
149        let out: Vec<Option<f64>> = r.batch(&[0.0, 5.0]);
150        assert_relative_eq!(out[1].unwrap(), 0.0, epsilon = 1e-12);
151    }
152
153    #[test]
154    fn non_finite_input_holds_last() {
155        let mut r = Rocr100::new(1).unwrap();
156        assert_eq!(r.update(10.0), None);
157        r.update(11.0).unwrap();
158        assert_eq!(r.update(f64::NEG_INFINITY), None);
159    }
160
161    #[test]
162    fn reset_clears_state() {
163        let mut r = Rocr100::new(1).unwrap();
164        let _ = r.batch(&[10.0, 11.0]);
165        assert!(r.is_ready());
166        r.reset();
167        assert!(!r.is_ready());
168        assert_eq!(r.update(10.0), None);
169    }
170}