wickra_core/indicators/
reflex.rs1#![allow(clippy::doc_markdown)]
3
4use std::collections::VecDeque;
5
6use crate::error::{Error, Result};
7use crate::indicators::super_smoother::SuperSmoother;
8use crate::traits::Indicator;
9
10#[derive(Debug, Clone)]
50pub struct Reflex {
51 period: usize,
52 smoother: SuperSmoother,
53 filt: VecDeque<f64>,
54 ms: f64,
55 last: Option<f64>,
56}
57
58impl Reflex {
59 pub fn new(period: usize) -> Result<Self> {
65 if period == 0 {
66 return Err(Error::PeriodZero);
67 }
68 if period > crate::error::MAX_PERIOD {
69 return Err(Error::InvalidPeriod {
70 message: crate::error::PERIOD_ABOVE_MAX,
71 });
72 }
73 Ok(Self {
74 period,
75 smoother: SuperSmoother::with_critical_period(period, 0.5 * period as f64),
77 filt: VecDeque::with_capacity(period + 1),
78 ms: 0.0,
79 last: None,
80 })
81 }
82
83 pub const fn period(&self) -> usize {
85 self.period
86 }
87
88 pub const fn value(&self) -> Option<f64> {
90 self.last
91 }
92}
93
94impl Indicator for Reflex {
95 type Input = f64;
96 type Output = f64;
97
98 #[inline]
99 fn update(&mut self, price: f64) -> Option<f64> {
100 if !price.is_finite() {
101 return None;
102 }
103 let filt = self.smoother.update(price)?;
104 if self.filt.len() == self.period + 1 {
105 self.filt.pop_front();
106 }
107 self.filt.push_back(filt);
108 if self.filt.len() < self.period + 1 {
109 return None;
110 }
111 let newest = self.filt[self.period];
113 let oldest = self.filt[0];
114 let slope = (oldest - newest) / self.period as f64;
115 let mut sum = 0.0;
116 for i in 1..=self.period {
117 sum += (newest + i as f64 * slope) - self.filt[self.period - i];
118 }
119 sum /= self.period as f64;
120 self.ms = 0.04 * sum * sum + 0.96 * self.ms;
121 let reflex = if self.ms > 0.0 {
122 sum / self.ms.sqrt()
123 } else {
124 0.0
125 };
126 self.last = Some(reflex);
127 Some(reflex)
128 }
129
130 fn reset(&mut self) {
131 self.smoother.reset();
132 self.filt.clear();
133 self.ms = 0.0;
134 self.last = None;
135 }
136
137 #[inline]
138 fn warmup_period(&self) -> usize {
139 self.period + 1
140 }
141
142 #[inline]
143 fn is_ready(&self) -> bool {
144 self.last.is_some()
145 }
146
147 #[inline]
148 fn name(&self) -> &'static str {
149 "Reflex"
150 }
151}
152
153#[cfg(test)]
154mod tests {
155 use super::*;
156 use crate::traits::BatchExt;
157 use approx::assert_relative_eq;
158
159 #[test]
160 fn rejects_zero_period() {
161 assert!(matches!(Reflex::new(0), Err(Error::PeriodZero)));
162 }
163
164 #[test]
165 fn accessors_and_metadata() {
166 let r = Reflex::new(20).unwrap();
167 assert_eq!(r.period(), 20);
168 assert_eq!(r.warmup_period(), 21);
169 assert_eq!(r.name(), "Reflex");
170 assert!(!r.is_ready());
171 assert_eq!(r.value(), None);
172 }
173
174 #[test]
175 fn first_emission_at_warmup_period() {
176 let mut r = Reflex::new(5).unwrap();
177 let xs: Vec<f64> = (0..12)
178 .map(|i| 100.0 + (f64::from(i) * 0.4).sin() * 3.0)
179 .collect();
180 let out = r.batch(&xs);
181 for v in out.iter().take(5) {
182 assert!(v.is_none());
183 }
184 assert!(out[5].is_some());
185 }
186
187 #[test]
188 fn constant_input_is_zero() {
189 let mut r = Reflex::new(10).unwrap();
191 for v in r.batch(&[50.0; 100]).into_iter().flatten() {
192 assert_relative_eq!(v, 0.0, epsilon = 1e-9);
193 }
194 }
195
196 #[test]
197 fn cyclic_input_oscillates_around_zero() {
198 let mut r = Reflex::new(20).unwrap();
199 let xs: Vec<f64> = (0..400)
200 .map(|i| 100.0 + (std::f64::consts::TAU * f64::from(i) / 20.0).sin() * 5.0)
201 .collect();
202 let out: Vec<f64> = r.batch(&xs).into_iter().flatten().skip(100).collect();
203 assert!(out.iter().any(|&v| v > 0.5));
204 assert!(out.iter().any(|&v| v < -0.5));
205 }
206
207 #[test]
208 fn ignores_non_finite() {
209 let mut r = Reflex::new(10).unwrap();
210 r.batch(
211 &(0..40)
212 .map(|i| 100.0 + (f64::from(i) * 0.3).sin())
213 .collect::<Vec<_>>(),
214 );
215 let before = r.value();
216 assert_eq!(r.update(f64::NAN), None);
217 assert_eq!(r.value(), before);
219 }
220
221 #[test]
222 fn reset_clears_state() {
223 let mut r = Reflex::new(10).unwrap();
224 r.batch(
225 &(0..40)
226 .map(|i| 100.0 + (f64::from(i) * 0.3).sin())
227 .collect::<Vec<_>>(),
228 );
229 assert!(r.is_ready());
230 r.reset();
231 assert!(!r.is_ready());
232 assert_eq!(r.value(), None);
233 }
234
235 #[test]
236 fn batch_equals_streaming() {
237 let xs: Vec<f64> = (0..120)
238 .map(|i| 100.0 + (f64::from(i) * 0.25).sin() * 9.0)
239 .collect();
240 let batch = Reflex::new(20).unwrap().batch(&xs);
241 let mut b = Reflex::new(20).unwrap();
242 let streamed: Vec<_> = xs.iter().map(|x| b.update(*x)).collect();
243 assert_eq!(batch, streamed);
244 }
245
246 use crate::traits::BatchNanExt;
247
248 #[test]
249 fn rejects_period_above_max() {
250 assert!(matches!(
251 Reflex::new(crate::error::MAX_PERIOD + 1),
252 Err(Error::InvalidPeriod { .. })
253 ));
254 }
255
256 #[test]
257 fn first_value_lands_exactly_at_warmup_for_several_periods() {
258 for period in [1_usize, 2, 7] {
259 let mut r = Reflex::new(period).unwrap();
260 let xs: Vec<f64> = (0..20)
261 .map(|i| 100.0 + (f64::from(i) * 0.4).sin() * 3.0)
262 .collect();
263 let out = r.batch(&xs);
264 let warmup = r.warmup_period();
265 assert!(out[..warmup - 1].iter().all(Option::is_none));
266 assert!(out[warmup - 1].is_some());
267 }
268 }
269
270 #[test]
271 fn reset_replays_identically() {
272 let xs: Vec<f64> = (0..120)
273 .map(|i| 100.0 + (f64::from(i) * 0.25).sin() * 9.0)
274 .collect();
275 let fresh = Reflex::new(13).unwrap().batch(&xs);
276 let mut r = Reflex::new(13).unwrap();
277 let first = r.batch(&xs);
278 r.reset();
279 let second = r.batch(&xs);
280 assert_eq!(first, fresh);
281 assert_eq!(second, fresh);
282 }
283
284 #[test]
285 fn batch_nan_paths_match_streaming_bitwise() {
286 let xs: Vec<f64> = (0..120)
287 .map(|i| 100.0 + (f64::from(i) * 0.25).sin() * 9.0)
288 .collect();
289 let mut out = vec![0.0; xs.len()];
290 Reflex::new(13).unwrap().batch_nan_into(&xs, &mut out);
291 let nan = Reflex::new(13).unwrap().batch_nan(&xs);
292 let fast = Reflex::new(13).unwrap().batch_fast(&xs);
293 let mut stream = Reflex::new(13).unwrap();
294 let expected: Vec<u64> = xs
295 .iter()
296 .map(|&p| stream.update(p).unwrap_or(f64::NAN).to_bits())
297 .collect();
298 assert!(out.iter().zip(&expected).all(|(v, e)| v.to_bits() == *e));
299 assert!(nan.iter().zip(&expected).all(|(v, e)| v.to_bits() == *e));
300 assert!(fast.iter().zip(&expected).all(|(v, e)| v.to_bits() == *e));
301 }
302
303 #[test]
304 fn smoother_uses_half_period_critical() {
305 let r = Reflex::new(7).unwrap();
308 let got = r.smoother.coefficients();
309 let want = SuperSmoother::with_critical_period(7, 3.5).coefficients();
310 assert_eq!(
311 (got.0.to_bits(), got.1.to_bits(), got.2.to_bits()),
312 (want.0.to_bits(), want.1.to_bits(), want.2.to_bits())
313 );
314 assert_eq!(r.smoother.period(), 7);
315 let full = SuperSmoother::new(7).unwrap().coefficients();
316 assert!((got.0 - full.0).abs() > 1e-3);
317 }
318
319 #[test]
320 fn first_value_hand_computed() {
321 let mut r = Reflex::new(2).unwrap();
327 let (c1, _, _) = r.smoother.coefficients();
328 assert!(c1 > 0.0);
329 let out = r.batch(&[0.0, 0.0, 6.0]);
330 assert_eq!(out[1], None);
331 assert_relative_eq!(out[2].unwrap(), 5.0, epsilon = 1e-12);
332 assert_relative_eq!(r.ms, 0.04 * (0.75 * c1) * (0.75 * c1), epsilon = 1e-12);
333 let mut r = Reflex::new(2).unwrap();
335 let out = r.batch(&[0.0, 0.0, -6.0]);
336 assert_relative_eq!(out[2].unwrap(), -5.0, epsilon = 1e-12);
337 }
338
339 #[test]
340 fn zero_series_takes_zero_normaliser_branch() {
341 let mut r = Reflex::new(4).unwrap();
344 let out = r.batch(&[0.0; 30]);
345 assert!(out
346 .iter()
347 .flatten()
348 .all(|v| v.to_bits() == 0.0_f64.to_bits()));
349 assert_eq!(out.iter().flatten().count(), 30 - 4);
350 }
351}