wickra_core/indicators/
rocp.rs1use std::collections::VecDeque;
4
5use crate::error::{Error, Result};
6use crate::traits::Indicator;
7
8#[derive(Debug, Clone)]
30pub struct Rocp {
31 period: usize,
32 window: VecDeque<f64>,
33 last: Option<f64>,
34}
35
36impl Rocp {
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 Rocp {
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 rocp = if prev == 0.0 {
79 0.0
80 } else {
81 (input - prev) / prev
82 };
83 self.last = Some(rocp);
84 Some(rocp)
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 "ROCP"
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!(Rocp::new(0), Err(Error::PeriodZero)));
117 }
118
119 #[test]
120 fn accessors_report_config() {
121 let r = Rocp::new(3).unwrap();
122 assert_eq!(r.period(), 3);
123 assert_eq!(r.name(), "ROCP");
124 assert_eq!(r.warmup_period(), 4);
125 assert!(!r.is_ready());
126 }
127
128 #[test]
129 fn known_value_is_a_fraction() {
130 let mut r = Rocp::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(), 0.1, epsilon = 1e-12);
135 assert!(r.is_ready());
136 }
137
138 #[test]
139 fn constant_series_yields_zero() {
140 let mut r = Rocp::new(3).unwrap();
141 for v in r.batch(&[10.0_f64; 12]).iter().skip(4).flatten() {
142 assert_relative_eq!(*v, 0.0, epsilon = 1e-12);
143 }
144 }
145
146 #[test]
147 fn zero_reference_price_reports_zero() {
148 let mut r = Rocp::new(1).unwrap();
150 let out: Vec<Option<f64>> = r.batch(&[0.0, 5.0]);
151 assert_relative_eq!(out[1].unwrap(), 0.0, epsilon = 1e-12);
152 }
153
154 #[test]
155 fn non_finite_input_holds_last() {
156 let mut r = Rocp::new(1).unwrap();
157 assert_eq!(r.update(10.0), None);
158 r.update(11.0).unwrap();
159 assert_eq!(r.update(f64::NAN), None);
160 }
161
162 #[test]
163 fn reset_clears_state() {
164 let mut r = Rocp::new(1).unwrap();
165 let _ = r.batch(&[10.0, 11.0]);
166 assert!(r.is_ready());
167 r.reset();
168 assert!(!r.is_ready());
169 assert_eq!(r.update(10.0), None);
170 }
171}