quantwave_core/indicators/
universal_oscillator.rs1use crate::indicators::metadata::{IndicatorMetadata, ParamDef};
2use crate::indicators::math::AGC;
3use crate::traits::Next;
4use std::f64::consts::PI;
5
6#[derive(Debug, Clone)]
12pub struct UniversalOscillator {
13 c1: f64,
14 c2: f64,
15 c3: f64,
16
17 price_prev1: f64,
18 price_prev2: f64,
19
20 wn_prev1: f64,
21
22 filt_history: [f64; 2],
23 agc: AGC,
24
25 count: usize,
26}
27
28impl UniversalOscillator {
29 pub fn new(band_edge: usize) -> Self {
30 let band_edge_f = band_edge as f64;
31 let r2 = 2.0f64.sqrt();
32 let a1 = (-r2 * PI / band_edge_f).exp();
33 let b1 = 2.0 * a1 * (r2 * PI / band_edge_f).cos();
34 let c2 = b1;
35 let c3 = -a1 * a1;
36 let c1 = 1.0 - c2 - c3;
37
38 Self {
39 c1,
40 c2,
41 c3,
42 price_prev1: 0.0,
43 price_prev2: 0.0,
44 wn_prev1: 0.0,
45 filt_history: [0.0; 2],
46 agc: AGC::new(0.991),
47 count: 0,
48 }
49 }
50}
51
52impl Next<f64> for UniversalOscillator {
53 type Output = f64;
54
55 fn next(&mut self, input: f64) -> Self::Output {
56 self.count += 1;
57
58 if self.count < 3 {
59 self.price_prev2 = self.price_prev1;
60 self.price_prev1 = input;
61 return 0.0;
62 }
63
64 let wn = (input - self.price_prev2) / 2.0;
66
67 let white_noise_avg = (wn + self.wn_prev1) / 2.0;
69
70 let filt = self.c1 * white_noise_avg
72 + self.c2 * self.filt_history[0]
73 + self.c3 * self.filt_history[1];
74
75 let universal = self.agc.next(filt);
77
78 self.filt_history[1] = self.filt_history[0];
80 self.filt_history[0] = filt;
81 self.wn_prev1 = wn;
82 self.price_prev2 = self.price_prev1;
83 self.price_prev1 = input;
84
85 universal
86 }
87}
88
89pub const UNIVERSAL_OSCILLATOR_METADATA: IndicatorMetadata = IndicatorMetadata {
90 name: "Universal Oscillator",
91 description: "An adaptive oscillator that normalizes price momentum using a SuperSmoother filter and AGC.",
92 usage: "Use as a generic oscillator framework that works on any pre-filtered input. Feed it the output of any smoother or filter to produce a normalized zero-centered oscillator.",
93 keywords: &["oscillator", "ehlers", "dsp", "universal", "momentum"],
94 ehlers_summary: "Ehlers Universal Oscillator is a generic momentum computation that can be applied to any filtered price input. It computes the rate of change of the filtered series normalized by its RMS amplitude, producing a consistently scaled oscillator that works regardless of the underlying filter or price instrument.",
95 params: &[
96 ParamDef {
97 name: "band_edge",
98 default: "20",
99 description: "Critical period for the SuperSmoother filter",
100 },
101 ],
102 formula_source: "https://www.traders.com/Documentation/FEEDbk_docs/2015/01/TradersTips.html",
103 formula_latex: r#"
104\[
105WN = (Price - Price_{t-2}) / 2
106\]
107\[
108AvgWN = (WN + WN_{t-1}) / 2
109\]
110\[
111Filt = c_1 AvgWN + c_2 Filt_{t-1} + c_3 Filt_{t-2}
112\]
113\[
114Peak = \max(0.991 \times Peak_{t-1}, |Filt|)
115\]
116\[
117Universal = Filt / Peak
118\]
119"#,
120 gold_standard_file: "universal_oscillator.json",
121 category: "Ehlers DSP",
122};
123
124#[cfg(test)]
125mod tests {
126 use super::*;
127 use crate::traits::Next;
128 use proptest::prelude::*;
129
130 #[test]
131 fn test_universal_oscillator_basic() {
132 let mut uo = UniversalOscillator::new(20);
133 let prices = vec![10.0, 10.5, 11.0, 11.5, 12.0, 11.0, 10.0];
134 for p in prices {
135 let res = uo.next(p);
136 assert!(res >= -1.0 && res <= 1.0);
137 }
138 }
139
140 proptest! {
141 #[test]
142 fn test_universal_oscillator_parity(
143 inputs in prop::collection::vec(1.0..100.0, 50..100),
144 ) {
145 let band_edge = 20;
146 let mut uo = UniversalOscillator::new(band_edge);
147 let streaming_results: Vec<f64> = inputs.iter().map(|&x| uo.next(x)).collect();
148
149 let mut uo_batch = UniversalOscillator::new(band_edge);
151 let batch_results: Vec<f64> = inputs.iter().map(|&x| uo_batch.next(x)).collect();
152
153 for (s, b) in streaming_results.iter().zip(batch_results.iter()) {
154 approx::assert_relative_eq!(s, b, epsilon = 1e-10);
155 }
156 }
157 }
158}