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