Skip to main content

wickra_core/indicators/
rocp.rs

1//! Rate of Change Percentage (ROCP).
2
3use std::collections::VecDeque;
4
5use crate::error::{Error, Result};
6use crate::traits::Indicator;
7
8/// Rate of Change Percentage (`ROCP`): `(close - close[period]) / close[period]`.
9///
10/// The same momentum measure as [`Roc`](crate::Roc) but expressed as a raw
11/// fraction rather than a percentage — `Roc` is exactly `100 · ROCP`. Where the
12/// 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, Rocp};
21///
22/// let mut indicator = Rocp::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 Rocp {
31    period: usize,
32    window: VecDeque<f64>,
33    last: Option<f64>,
34}
35
36impl Rocp {
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 Rocp {
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 rocp = if prev == 0.0 {
73            0.0
74        } else {
75            (input - prev) / prev
76        };
77        self.last = Some(rocp);
78        Some(rocp)
79    }
80
81    fn reset(&mut self) {
82        self.window.clear();
83        self.last = None;
84    }
85
86    fn warmup_period(&self) -> usize {
87        self.period + 1
88    }
89
90    fn is_ready(&self) -> bool {
91        self.window.len() == self.period + 1
92    }
93
94    fn name(&self) -> &'static str {
95        "ROCP"
96    }
97}
98
99#[cfg(test)]
100mod tests {
101    use super::*;
102    use crate::traits::BatchExt;
103    use approx::assert_relative_eq;
104
105    #[test]
106    fn rejects_zero_period() {
107        assert!(matches!(Rocp::new(0), Err(Error::PeriodZero)));
108    }
109
110    #[test]
111    fn accessors_report_config() {
112        let r = Rocp::new(3).unwrap();
113        assert_eq!(r.period(), 3);
114        assert_eq!(r.name(), "ROCP");
115        assert_eq!(r.warmup_period(), 4);
116        assert!(!r.is_ready());
117    }
118
119    #[test]
120    fn known_value_is_a_fraction() {
121        // period 1 over [10, 11]: (11 - 10) / 10 = 0.1.
122        let mut r = Rocp::new(1).unwrap();
123        let out: Vec<Option<f64>> = r.batch(&[10.0, 11.0]);
124        assert_eq!(out[0], None);
125        assert_relative_eq!(out[1].unwrap(), 0.1, epsilon = 1e-12);
126        assert!(r.is_ready());
127    }
128
129    #[test]
130    fn constant_series_yields_zero() {
131        let mut r = Rocp::new(3).unwrap();
132        for v in r.batch(&[10.0_f64; 12]).iter().skip(4).flatten() {
133            assert_relative_eq!(*v, 0.0, epsilon = 1e-12);
134        }
135    }
136
137    #[test]
138    fn zero_reference_price_reports_zero() {
139        // period 1 over [0, 5]: reference price is zero -> guarded to 0.
140        let mut r = Rocp::new(1).unwrap();
141        let out: Vec<Option<f64>> = r.batch(&[0.0, 5.0]);
142        assert_relative_eq!(out[1].unwrap(), 0.0, epsilon = 1e-12);
143    }
144
145    #[test]
146    fn non_finite_input_holds_last() {
147        let mut r = Rocp::new(1).unwrap();
148        assert_eq!(r.update(10.0), None);
149        let v = r.update(11.0).unwrap();
150        assert_eq!(r.update(f64::NAN), Some(v));
151    }
152
153    #[test]
154    fn reset_clears_state() {
155        let mut r = Rocp::new(1).unwrap();
156        let _ = r.batch(&[10.0, 11.0]);
157        assert!(r.is_ready());
158        r.reset();
159        assert!(!r.is_ready());
160        assert_eq!(r.update(10.0), None);
161    }
162}