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