wickra_core/indicators/
roc.rs1use std::collections::VecDeque;
4
5use crate::error::{Error, Result};
6use crate::traits::Indicator;
7
8#[derive(Debug, Clone)]
26pub struct Roc {
27 period: usize,
28 window: VecDeque<f64>,
29 last: Option<f64>,
30}
31
32impl Roc {
33 pub fn new(period: usize) -> Result<Self> {
36 if period == 0 {
37 return Err(Error::PeriodZero);
38 }
39 if period > crate::error::MAX_PERIOD {
40 return Err(Error::InvalidPeriod {
41 message: crate::error::PERIOD_ABOVE_MAX,
42 });
43 }
44 Ok(Self {
45 period,
46 window: VecDeque::with_capacity(period + 1),
47 last: None,
48 })
49 }
50
51 pub const fn period(&self) -> usize {
53 self.period
54 }
55}
56
57impl Indicator for Roc {
58 type Input = f64;
59 type Output = f64;
60
61 #[inline]
62 fn update(&mut self, input: f64) -> Option<f64> {
63 if !input.is_finite() {
65 return None;
66 }
67 if self.window.len() == self.period + 1 {
68 self.window.pop_front();
69 }
70 self.window.push_back(input);
71 if self.window.len() < self.period + 1 {
72 return None;
73 }
74 let prev = *self.window.front().expect("non-empty");
75 let roc = if prev == 0.0 {
76 0.0
77 } else {
78 (input - prev) / prev * 100.0
79 };
80 self.last = Some(roc);
81 Some(roc)
82 }
83
84 fn reset(&mut self) {
85 self.window.clear();
86 self.last = None;
87 }
88
89 #[inline]
90 fn warmup_period(&self) -> usize {
91 self.period + 1
92 }
93
94 #[inline]
95 fn is_ready(&self) -> bool {
96 self.window.len() == self.period + 1
97 }
98
99 #[inline]
100 fn name(&self) -> &'static str {
101 "ROC"
102 }
103}
104
105#[cfg(test)]
106mod tests {
107 use super::*;
108 use crate::traits::BatchExt;
109 use approx::assert_relative_eq;
110
111 #[test]
112 fn constant_series_yields_zero() {
113 let mut roc = Roc::new(5).unwrap();
114 let out = roc.batch(&[10.0_f64; 20]);
115 for v in out.iter().skip(5).flatten() {
116 assert_relative_eq!(*v, 0.0, epsilon = 1e-12);
117 }
118 }
119
120 #[test]
121 fn known_value() {
122 let mut roc = Roc::new(3).unwrap();
124 let out = roc.batch(&[100.0, 105.0, 108.0, 110.0]);
125 assert_relative_eq!(out[3].unwrap(), 10.0, epsilon = 1e-12);
126 }
127
128 #[test]
129 fn batch_equals_streaming() {
130 let prices: Vec<f64> = (1..=30).map(|i| f64::from(i) * 2.0).collect();
131 let mut a = Roc::new(5).unwrap();
132 let mut b = Roc::new(5).unwrap();
133 assert_eq!(
134 a.batch(&prices),
135 prices.iter().map(|p| b.update(*p)).collect::<Vec<_>>()
136 );
137 }
138
139 #[test]
140 fn reset_clears_state() {
141 let mut roc = Roc::new(5).unwrap();
142 roc.batch(&[1.0, 2.0, 3.0, 4.0, 5.0, 6.0]);
143 assert!(roc.is_ready());
144 roc.reset();
145 assert!(!roc.is_ready());
146 }
147
148 #[test]
149 fn rejects_zero_period() {
150 assert!(Roc::new(0).is_err());
151 }
152
153 #[test]
157 fn accessors_and_metadata() {
158 let roc = Roc::new(5).unwrap();
159 assert_eq!(roc.period(), 5);
160 assert_eq!(roc.warmup_period(), 6);
161 assert_eq!(roc.name(), "ROC");
162 }
163
164 #[test]
170 fn zero_previous_price_yields_zero_roc() {
171 let mut roc = Roc::new(3).unwrap();
172 let out = roc.batch(&[0.0, 5.0, 7.0, 9.0]);
173 let v = out[3].expect("ready after period + 1 inputs");
174 assert_eq!(v, 0.0);
175 }
176
177 #[test]
178 fn ignores_non_finite_input() {
179 let mut roc = Roc::new(3).unwrap();
180 let out = roc.batch(&[100.0, 105.0, 108.0, 110.0]);
181 out[3].expect("ROC(3) ready after four inputs");
182 assert_eq!(roc.update(f64::NAN), None);
184 assert_eq!(roc.update(f64::INFINITY), None);
185 assert_relative_eq!(
187 roc.update(115.0).unwrap(),
188 (115.0 - 105.0) / 105.0 * 100.0,
189 epsilon = 1e-12
190 );
191 }
192}