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 Ok(Self {
69 period,
70 smoother: SuperSmoother::new(period)?,
71 filt: VecDeque::with_capacity(period + 1),
72 ms: 0.0,
73 last: None,
74 })
75 }
76
77 pub const fn period(&self) -> usize {
79 self.period
80 }
81
82 pub const fn value(&self) -> Option<f64> {
84 self.last
85 }
86}
87
88impl Indicator for Reflex {
89 type Input = f64;
90 type Output = f64;
91
92 fn update(&mut self, price: f64) -> Option<f64> {
93 if !price.is_finite() {
94 return self.last;
95 }
96 let filt = self.smoother.update(price)?;
97 if self.filt.len() == self.period + 1 {
98 self.filt.pop_front();
99 }
100 self.filt.push_back(filt);
101 if self.filt.len() < self.period + 1 {
102 return None;
103 }
104 let newest = self.filt[self.period];
106 let oldest = self.filt[0];
107 let slope = (oldest - newest) / self.period as f64;
108 let mut sum = 0.0;
109 for i in 1..=self.period {
110 sum += (newest + i as f64 * slope) - self.filt[self.period - i];
111 }
112 sum /= self.period as f64;
113 self.ms = 0.04 * sum * sum + 0.96 * self.ms;
114 let reflex = if self.ms > 0.0 {
115 sum / self.ms.sqrt()
116 } else {
117 0.0
118 };
119 self.last = Some(reflex);
120 Some(reflex)
121 }
122
123 fn reset(&mut self) {
124 self.smoother.reset();
125 self.filt.clear();
126 self.ms = 0.0;
127 self.last = None;
128 }
129
130 fn warmup_period(&self) -> usize {
131 self.period + 1
132 }
133
134 fn is_ready(&self) -> bool {
135 self.last.is_some()
136 }
137
138 fn name(&self) -> &'static str {
139 "Reflex"
140 }
141}
142
143#[cfg(test)]
144mod tests {
145 use super::*;
146 use crate::traits::BatchExt;
147 use approx::assert_relative_eq;
148
149 #[test]
150 fn rejects_zero_period() {
151 assert!(matches!(Reflex::new(0), Err(Error::PeriodZero)));
152 }
153
154 #[test]
155 fn accessors_and_metadata() {
156 let r = Reflex::new(20).unwrap();
157 assert_eq!(r.period(), 20);
158 assert_eq!(r.warmup_period(), 21);
159 assert_eq!(r.name(), "Reflex");
160 assert!(!r.is_ready());
161 assert_eq!(r.value(), None);
162 }
163
164 #[test]
165 fn first_emission_at_warmup_period() {
166 let mut r = Reflex::new(5).unwrap();
167 let xs: Vec<f64> = (0..12)
168 .map(|i| 100.0 + (f64::from(i) * 0.4).sin() * 3.0)
169 .collect();
170 let out = r.batch(&xs);
171 for v in out.iter().take(5) {
172 assert!(v.is_none());
173 }
174 assert!(out[5].is_some());
175 }
176
177 #[test]
178 fn constant_input_is_zero() {
179 let mut r = Reflex::new(10).unwrap();
181 for v in r.batch(&[50.0; 100]).into_iter().flatten() {
182 assert_relative_eq!(v, 0.0, epsilon = 1e-9);
183 }
184 }
185
186 #[test]
187 fn cyclic_input_oscillates_around_zero() {
188 let mut r = Reflex::new(20).unwrap();
189 let xs: Vec<f64> = (0..400)
190 .map(|i| 100.0 + (std::f64::consts::TAU * f64::from(i) / 20.0).sin() * 5.0)
191 .collect();
192 let out: Vec<f64> = r.batch(&xs).into_iter().flatten().skip(100).collect();
193 assert!(out.iter().any(|&v| v > 0.5));
194 assert!(out.iter().any(|&v| v < -0.5));
195 }
196
197 #[test]
198 fn ignores_non_finite() {
199 let mut r = Reflex::new(10).unwrap();
200 r.batch(
201 &(0..40)
202 .map(|i| 100.0 + (f64::from(i) * 0.3).sin())
203 .collect::<Vec<_>>(),
204 );
205 let before = r.value();
206 assert_eq!(r.update(f64::NAN), before);
207 }
208
209 #[test]
210 fn reset_clears_state() {
211 let mut r = Reflex::new(10).unwrap();
212 r.batch(
213 &(0..40)
214 .map(|i| 100.0 + (f64::from(i) * 0.3).sin())
215 .collect::<Vec<_>>(),
216 );
217 assert!(r.is_ready());
218 r.reset();
219 assert!(!r.is_ready());
220 assert_eq!(r.value(), None);
221 }
222
223 #[test]
224 fn batch_equals_streaming() {
225 let xs: Vec<f64> = (0..120)
226 .map(|i| 100.0 + (f64::from(i) * 0.25).sin() * 9.0)
227 .collect();
228 let batch = Reflex::new(20).unwrap().batch(&xs);
229 let mut b = Reflex::new(20).unwrap();
230 let streamed: Vec<_> = xs.iter().map(|x| b.update(*x)).collect();
231 assert_eq!(batch, streamed);
232 }
233}