wickra_core/indicators/
rocr100.rs1use std::collections::VecDeque;
4
5use crate::error::{Error, Result};
6use crate::traits::Indicator;
7
8#[derive(Debug, Clone)]
30pub struct Rocr100 {
31 period: usize,
32 window: VecDeque<f64>,
33 last: Option<f64>,
34}
35
36impl Rocr100 {
37 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 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 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}