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 Ok(Self {
44 period,
45 window: VecDeque::with_capacity(period + 1),
46 last: None,
47 })
48 }
49
50 pub const fn period(&self) -> usize {
52 self.period
53 }
54}
55
56impl Indicator for Rocp {
57 type Input = f64;
58 type Output = f64;
59
60 fn update(&mut self, input: f64) -> Option<f64> {
61 if !input.is_finite() {
62 return self.last;
63 }
64 if self.window.len() == self.period + 1 {
65 self.window.pop_front();
66 }
67 self.window.push_back(input);
68 if self.window.len() < self.period + 1 {
69 return None;
70 }
71 let prev = *self.window.front().expect("non-empty");
72 let rocp = if prev == 0.0 {
73 0.0
74 } else {
75 (input - prev) / prev
76 };
77 self.last = Some(rocp);
78 Some(rocp)
79 }
80
81 fn reset(&mut self) {
82 self.window.clear();
83 self.last = None;
84 }
85
86 fn warmup_period(&self) -> usize {
87 self.period + 1
88 }
89
90 fn is_ready(&self) -> bool {
91 self.window.len() == self.period + 1
92 }
93
94 fn name(&self) -> &'static str {
95 "ROCP"
96 }
97}
98
99#[cfg(test)]
100mod tests {
101 use super::*;
102 use crate::traits::BatchExt;
103 use approx::assert_relative_eq;
104
105 #[test]
106 fn rejects_zero_period() {
107 assert!(matches!(Rocp::new(0), Err(Error::PeriodZero)));
108 }
109
110 #[test]
111 fn accessors_report_config() {
112 let r = Rocp::new(3).unwrap();
113 assert_eq!(r.period(), 3);
114 assert_eq!(r.name(), "ROCP");
115 assert_eq!(r.warmup_period(), 4);
116 assert!(!r.is_ready());
117 }
118
119 #[test]
120 fn known_value_is_a_fraction() {
121 let mut r = Rocp::new(1).unwrap();
123 let out: Vec<Option<f64>> = r.batch(&[10.0, 11.0]);
124 assert_eq!(out[0], None);
125 assert_relative_eq!(out[1].unwrap(), 0.1, epsilon = 1e-12);
126 assert!(r.is_ready());
127 }
128
129 #[test]
130 fn constant_series_yields_zero() {
131 let mut r = Rocp::new(3).unwrap();
132 for v in r.batch(&[10.0_f64; 12]).iter().skip(4).flatten() {
133 assert_relative_eq!(*v, 0.0, epsilon = 1e-12);
134 }
135 }
136
137 #[test]
138 fn zero_reference_price_reports_zero() {
139 let mut r = Rocp::new(1).unwrap();
141 let out: Vec<Option<f64>> = r.batch(&[0.0, 5.0]);
142 assert_relative_eq!(out[1].unwrap(), 0.0, epsilon = 1e-12);
143 }
144
145 #[test]
146 fn non_finite_input_holds_last() {
147 let mut r = Rocp::new(1).unwrap();
148 assert_eq!(r.update(10.0), None);
149 let v = r.update(11.0).unwrap();
150 assert_eq!(r.update(f64::NAN), Some(v));
151 }
152
153 #[test]
154 fn reset_clears_state() {
155 let mut r = Rocp::new(1).unwrap();
156 let _ = r.batch(&[10.0, 11.0]);
157 assert!(r.is_ready());
158 r.reset();
159 assert!(!r.is_ready());
160 assert_eq!(r.update(10.0), None);
161 }
162}