//! Rate of Change (ROC). use std::collections::VecDeque; use crate::error::{Error, Result}; use crate::traits::Indicator; /// Rate of Change as a percentage: `(close - close[period]) / close[period] * 100`. /// /// Non-finite inputs are ignored and leave the window untouched; the last /// computed value is returned instead, matching the SMA / EMA convention. /// /// # Example /// /// ``` /// use wickra_core::{Indicator, Roc}; /// /// let mut indicator = Roc::new(3).unwrap(); /// let mut last = None; /// for i in 0..80 { /// last = indicator.update(100.0 + f64::from(i)); /// } /// assert!(last.is_some()); /// ``` #[derive(Debug, Clone)] pub struct Roc { period: usize, window: VecDeque, last: Option, } impl Roc { /// # Errors /// Returns [`Error::PeriodZero`] if `period == 0`. pub fn new(period: usize) -> Result { if period == 0 { return Err(Error::PeriodZero); } Ok(Self { period, window: VecDeque::with_capacity(period + 1), last: None, }) } /// Configured period. pub const fn period(&self) -> usize { self.period } } impl Indicator for Roc { type Input = f64; type Output = f64; fn update(&mut self, input: f64) -> Option { // Non-finite inputs are ignored: return the last value, leave state as is. if !input.is_finite() { return self.last; } if self.window.len() == self.period + 1 { self.window.pop_front(); } self.window.push_back(input); if self.window.len() < self.period + 1 { return None; } let prev = *self.window.front().expect("non-empty"); let roc = if prev == 0.0 { 0.0 } else { (input - prev) / prev * 100.0 }; self.last = Some(roc); Some(roc) } fn reset(&mut self) { self.window.clear(); self.last = None; } fn warmup_period(&self) -> usize { self.period + 1 } fn is_ready(&self) -> bool { self.window.len() == self.period + 1 } fn name(&self) -> &'static str { "ROC" } } #[cfg(test)] mod tests { use super::*; use crate::traits::BatchExt; use approx::assert_relative_eq; #[test] fn constant_series_yields_zero() { let mut roc = Roc::new(5).unwrap(); let out = roc.batch(&[10.0_f64; 20]); for v in out.iter().skip(5).flatten() { assert_relative_eq!(*v, 0.0, epsilon = 1e-12); } } #[test] fn known_value() { // ROC(3) where prev = 100, now = 110 -> 10% let mut roc = Roc::new(3).unwrap(); let out = roc.batch(&[100.0, 105.0, 108.0, 110.0]); assert_relative_eq!(out[3].unwrap(), 10.0, epsilon = 1e-12); } #[test] fn batch_equals_streaming() { let prices: Vec = (1..=30).map(|i| f64::from(i) * 2.0).collect(); let mut a = Roc::new(5).unwrap(); let mut b = Roc::new(5).unwrap(); assert_eq!( a.batch(&prices), prices.iter().map(|p| b.update(*p)).collect::>() ); } #[test] fn reset_clears_state() { let mut roc = Roc::new(5).unwrap(); roc.batch(&[1.0, 2.0, 3.0, 4.0, 5.0, 6.0]); assert!(roc.is_ready()); roc.reset(); assert!(!roc.is_ready()); } #[test] fn rejects_zero_period() { assert!(Roc::new(0).is_err()); } /// Cover the const accessor `period` (47-49) and the Indicator-impl /// `warmup_period` (83-85) + `name` (91-93). Existing tests never /// inspect these metadata methods. #[test] fn accessors_and_metadata() { let roc = Roc::new(5).unwrap(); assert_eq!(roc.period(), 5); assert_eq!(roc.warmup_period(), 6); assert_eq!(roc.name(), "ROC"); } /// Cover the `prev == 0.0` defensive branch (line 70). All existing /// tests use prices ≥ 1.0, so the divide-by-zero guard was never /// triggered. Feed a leading zero followed by `period` more values /// so the front of the window is exactly 0.0, then assert the next /// emission is the flat-momentum fallback 0.0 (not NaN). #[test] fn zero_previous_price_yields_zero_roc() { let mut roc = Roc::new(3).unwrap(); let out = roc.batch(&[0.0, 5.0, 7.0, 9.0]); let v = out[3].expect("ready after period + 1 inputs"); assert_eq!(v, 0.0); } #[test] fn ignores_non_finite_input() { let mut roc = Roc::new(3).unwrap(); let out = roc.batch(&[100.0, 105.0, 108.0, 110.0]); let ready = out[3].expect("ROC(3) ready after four inputs"); // Non-finite inputs return the last value without sliding the window. assert_eq!(roc.update(f64::NAN), Some(ready)); assert_eq!(roc.update(f64::INFINITY), Some(ready)); // Window untouched: the next finite input still references prev = 105. assert_relative_eq!( roc.update(115.0).unwrap(), (115.0 - 105.0) / 105.0 * 100.0, epsilon = 1e-12 ); } }