wickra_core/indicators/
rocr.rs1use std::collections::VecDeque;
4
5use crate::error::{Error, Result};
6use crate::traits::Indicator;
7
8#[derive(Debug, Clone)]
30pub struct Rocr {
31 period: usize,
32 window: VecDeque<f64>,
33 last: Option<f64>,
34}
35
36impl Rocr {
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 Rocr {
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 { 0.0 } else { input / prev };
79 self.last = Some(rocr);
80 Some(rocr)
81 }
82
83 fn reset(&mut self) {
84 self.window.clear();
85 self.last = None;
86 }
87
88 #[inline]
89 fn warmup_period(&self) -> usize {
90 self.period + 1
91 }
92
93 #[inline]
94 fn is_ready(&self) -> bool {
95 self.window.len() == self.period + 1
96 }
97
98 #[inline]
99 fn name(&self) -> &'static str {
100 "ROCR"
101 }
102}
103
104#[cfg(test)]
105mod tests {
106 use super::*;
107 use crate::traits::BatchExt;
108 use approx::assert_relative_eq;
109
110 #[test]
111 fn rejects_zero_period() {
112 assert!(matches!(Rocr::new(0), Err(Error::PeriodZero)));
113 }
114
115 #[test]
116 fn accessors_report_config() {
117 let r = Rocr::new(3).unwrap();
118 assert_eq!(r.period(), 3);
119 assert_eq!(r.name(), "ROCR");
120 assert_eq!(r.warmup_period(), 4);
121 assert!(!r.is_ready());
122 }
123
124 #[test]
125 fn known_value_is_a_ratio() {
126 let mut r = Rocr::new(1).unwrap();
128 let out: Vec<Option<f64>> = r.batch(&[10.0, 11.0]);
129 assert_eq!(out[0], None);
130 assert_relative_eq!(out[1].unwrap(), 1.1, epsilon = 1e-12);
131 assert!(r.is_ready());
132 }
133
134 #[test]
135 fn constant_series_yields_one() {
136 let mut r = Rocr::new(3).unwrap();
137 for v in r.batch(&[10.0_f64; 12]).iter().skip(4).flatten() {
138 assert_relative_eq!(*v, 1.0, epsilon = 1e-12);
139 }
140 }
141
142 #[test]
143 fn zero_reference_price_reports_zero() {
144 let mut r = Rocr::new(1).unwrap();
145 let out: Vec<Option<f64>> = r.batch(&[0.0, 5.0]);
146 assert_relative_eq!(out[1].unwrap(), 0.0, epsilon = 1e-12);
147 }
148
149 #[test]
150 fn non_finite_input_holds_last() {
151 let mut r = Rocr::new(1).unwrap();
152 assert_eq!(r.update(10.0), None);
153 r.update(11.0).unwrap();
154 assert_eq!(r.update(f64::INFINITY), None);
155 }
156
157 #[test]
158 fn reset_clears_state() {
159 let mut r = Rocr::new(1).unwrap();
160 let _ = r.batch(&[10.0, 11.0]);
161 assert!(r.is_ready());
162 r.reset();
163 assert!(!r.is_ready());
164 assert_eq!(r.update(10.0), None);
165 }
166}