wickra_core/indicators/
universal_oscillator.rs1#![allow(clippy::doc_markdown)]
3
4use crate::error::{Error, Result};
5use crate::indicators::super_smoother::SuperSmoother;
6use crate::traits::Indicator;
7
8#[derive(Debug, Clone)]
45pub struct UniversalOscillator {
46 period: usize,
47 smoother: SuperSmoother,
48 prev_price_1: Option<f64>,
49 prev_price_2: Option<f64>,
50 peak: f64,
51 last: Option<f64>,
52}
53
54impl UniversalOscillator {
55 pub fn new(period: usize) -> Result<Self> {
61 if period == 0 {
62 return Err(Error::PeriodZero);
63 }
64 if period > crate::error::MAX_PERIOD {
65 return Err(Error::InvalidPeriod {
66 message: crate::error::PERIOD_ABOVE_MAX,
67 });
68 }
69 Ok(Self {
70 period,
71 smoother: SuperSmoother::new(period)?,
72 prev_price_1: None,
73 prev_price_2: None,
74 peak: 0.0,
75 last: None,
76 })
77 }
78
79 pub const fn period(&self) -> usize {
81 self.period
82 }
83
84 pub const fn value(&self) -> Option<f64> {
86 self.last
87 }
88}
89
90impl Indicator for UniversalOscillator {
91 type Input = f64;
92 type Output = f64;
93
94 #[inline]
95 fn update(&mut self, price: f64) -> Option<f64> {
96 if !price.is_finite() {
97 return None;
98 }
99 let Some(p2) = self.prev_price_2 else {
100 self.prev_price_2 = self.prev_price_1;
101 self.prev_price_1 = Some(price);
102 return None;
103 };
104 let white_noise = (price - p2) / 2.0;
105 if !white_noise.is_finite() {
106 self.prev_price_2 = self.prev_price_1;
109 self.prev_price_1 = Some(price);
110 return self.last;
111 }
112 let filt = self
113 .smoother
114 .update(white_noise)
115 .expect("supersmoother emits");
116 self.peak = filt.abs().max(0.991 * self.peak);
117 let universal = if self.peak > 0.0 {
118 (filt / self.peak).clamp(-1.0, 1.0)
119 } else {
120 0.0
121 };
122 self.prev_price_2 = self.prev_price_1;
123 self.prev_price_1 = Some(price);
124 self.last = Some(universal);
125 Some(universal)
126 }
127
128 fn reset(&mut self) {
129 self.smoother.reset();
130 self.prev_price_1 = None;
131 self.prev_price_2 = None;
132 self.peak = 0.0;
133 self.last = None;
134 }
135
136 #[inline]
137 fn warmup_period(&self) -> usize {
138 3
139 }
140
141 #[inline]
142 fn is_ready(&self) -> bool {
143 self.last.is_some()
144 }
145
146 #[inline]
147 fn name(&self) -> &'static str {
148 "UniversalOscillator"
149 }
150}
151
152#[cfg(test)]
153mod tests {
154 use super::*;
155 use crate::traits::BatchExt;
156
157 #[test]
158 fn rejects_zero_period() {
159 assert!(matches!(
160 UniversalOscillator::new(0),
161 Err(Error::PeriodZero)
162 ));
163 }
164
165 #[test]
166 fn accessors_and_metadata() {
167 let u = UniversalOscillator::new(20).unwrap();
168 assert_eq!(u.period(), 20);
169 assert_eq!(u.warmup_period(), 3);
170 assert_eq!(u.name(), "UniversalOscillator");
171 assert!(!u.is_ready());
172 assert_eq!(u.value(), None);
173 }
174
175 #[test]
176 fn first_emission_at_warmup_period() {
177 let mut u = UniversalOscillator::new(20).unwrap();
178 let out = u.batch(&[1.0, 2.0, 3.0, 4.0, 5.0]);
179 assert!(out[0].is_none());
180 assert!(out[1].is_none());
181 assert!(out[2].is_some());
182 }
183
184 #[test]
185 fn constant_input_is_zero() {
186 let mut u = UniversalOscillator::new(20).unwrap();
188 for v in u.batch(&[50.0; 200]).into_iter().flatten() {
189 assert!(v.abs() < 1e-9);
190 }
191 }
192
193 #[test]
194 fn output_in_range() {
195 let mut u = UniversalOscillator::new(20).unwrap();
196 let xs: Vec<f64> = (0..400)
197 .map(|i| 100.0 + (std::f64::consts::TAU * f64::from(i) / 20.0).sin() * 5.0)
198 .collect();
199 for v in u.batch(&xs).into_iter().flatten() {
200 assert!((-1.0..=1.0).contains(&v), "out of range: {v}");
201 }
202 }
203
204 #[test]
205 fn cyclic_input_swings_both_signs() {
206 let mut u = UniversalOscillator::new(20).unwrap();
207 let xs: Vec<f64> = (0..400)
208 .map(|i| 100.0 + (std::f64::consts::TAU * f64::from(i) / 20.0).sin() * 5.0)
209 .collect();
210 let out: Vec<f64> = u.batch(&xs).into_iter().flatten().skip(100).collect();
211 assert!(out.iter().any(|&v| v > 0.5));
212 assert!(out.iter().any(|&v| v < -0.5));
213 }
214
215 #[test]
216 fn ignores_non_finite() {
217 let mut u = UniversalOscillator::new(20).unwrap();
218 u.batch(
219 &(0..40)
220 .map(|i| 100.0 + (f64::from(i) * 0.3).sin())
221 .collect::<Vec<_>>(),
222 );
223 let before = u.value();
224 assert_eq!(u.update(f64::NAN), None);
225 assert_eq!(u.value(), before);
227 }
228
229 #[test]
230 fn reset_clears_state() {
231 let mut u = UniversalOscillator::new(20).unwrap();
232 u.batch(
233 &(0..40)
234 .map(|i| 100.0 + (f64::from(i) * 0.3).sin())
235 .collect::<Vec<_>>(),
236 );
237 assert!(u.is_ready());
238 u.reset();
239 assert!(!u.is_ready());
240 assert_eq!(u.value(), None);
241 }
242
243 #[test]
244 fn batch_equals_streaming() {
245 let xs: Vec<f64> = (0..120)
246 .map(|i| 100.0 + (f64::from(i) * 0.25).sin() * 9.0)
247 .collect();
248 let batch = UniversalOscillator::new(20).unwrap().batch(&xs);
249 let mut b = UniversalOscillator::new(20).unwrap();
250 let streamed: Vec<_> = xs.iter().map(|x| b.update(*x)).collect();
251 assert_eq!(batch, streamed);
252 }
253
254 #[test]
255 fn non_finite_white_noise_is_skipped() {
256 let mut u = UniversalOscillator::new(20).unwrap();
260 assert_eq!(u.update(-1e308), None);
261 assert_eq!(u.update(0.0), None);
262 assert_eq!(u.update(1e308), None);
264 }
265}