Skip to main content

quantwave_core/indicators/
synthetic_oscillator.rs

1use crate::indicators::metadata::{IndicatorMetadata, ParamDef};
2use crate::traits::Next;
3use crate::indicators::hann::HannFilter;
4use crate::indicators::high_pass::HighPass;
5use crate::indicators::super_smoother::SuperSmoother;
6use crate::indicators::ultimate_smoother::UltimateSmoother;
7use std::collections::VecDeque;
8use std::f64::consts::PI;
9
10/// Synthetic Oscillator
11///
12/// Based on John Ehlers' "A Synthetic Oscillator" (April 2026).
13/// A nonlinear oscillator designed to reduce lag while maintaining smoothness.
14/// It adapts to the instantaneous dominant cycle and uses phase accumulation.
15#[derive(Debug, Clone)]
16pub struct SyntheticOscillator {
17    hann_price: HannFilter,
18    hp: HighPass,
19    ss: SuperSmoother,
20    lp_window: VecDeque<f64>,
21    lp_sum_sq: f64,
22    re_prev: f64,
23    roc_window: VecDeque<f64>,
24    roc_sum_sq: f64,
25    im_prev: f64,
26    dc_prev: f64,
27    hp2: HighPass,
28    us: UltimateSmoother,
29    bp_prev: f64,
30    phase: f64,
31    synth_prev: f64,
32    lower_bound: f64,
33    upper_bound: f64,
34}
35
36impl SyntheticOscillator {
37    pub fn new(lower_bound: usize, upper_bound: usize) -> Self {
38        let mid = ((lower_bound * upper_bound) as f64).sqrt();
39        Self {
40            hann_price: HannFilter::new(12),
41            hp: HighPass::new(upper_bound),
42            ss: SuperSmoother::new(lower_bound),
43            lp_window: VecDeque::with_capacity(100),
44            lp_sum_sq: 0.0,
45            re_prev: 0.0,
46            roc_window: VecDeque::with_capacity(100),
47            roc_sum_sq: 0.0,
48            im_prev: 0.0,
49            dc_prev: lower_bound as f64,
50            hp2: HighPass::new(mid as usize),
51            us: UltimateSmoother::new(mid as usize),
52            bp_prev: 0.0,
53            phase: 0.0,
54            synth_prev: 0.0,
55            lower_bound: lower_bound as f64,
56            upper_bound: upper_bound as f64,
57        }
58    }
59}
60
61impl Default for SyntheticOscillator {
62    fn default() -> Self {
63        Self::new(15, 25)
64    }
65}
66
67impl Next<f64> for SyntheticOscillator {
68    type Output = f64;
69
70    fn next(&mut self, input: f64) -> Self::Output {
71        let price = self.hann_price.next(input);
72
73        // Real component
74        let hp = self.hp.next(price);
75        let lp = self.ss.next(hp);
76
77        self.lp_window.push_back(lp);
78        self.lp_sum_sq += lp * lp;
79        if self.lp_window.len() > 100 {
80            if let Some(old) = self.lp_window.pop_front() {
81                self.lp_sum_sq -= old * old;
82            }
83        }
84
85        let rms_lp = (self.lp_sum_sq / self.lp_window.len() as f64).sqrt();
86        let re = if rms_lp > 1e-10 { lp / rms_lp } else { 0.0 };
87
88        // Imaginary component
89        let roc = re - self.re_prev;
90        self.roc_window.push_back(roc);
91        self.roc_sum_sq += roc * roc;
92        if self.roc_window.len() > 100 {
93            if let Some(old) = self.roc_window.pop_front() {
94                self.roc_sum_sq -= old * old;
95            }
96        }
97
98        let qrms = (self.roc_sum_sq / self.roc_window.len() as f64).sqrt();
99        let im = if qrms > 1e-10 { roc / qrms } else { 0.0 };
100
101        // Dominant Cycle
102        let denom = (re - self.re_prev) * im - (im - self.im_prev) * re;
103        let mut dc = if denom.abs() > 1e-10 {
104            (2.0 * PI * (re * re + im * im)) / denom
105        } else {
106            self.dc_prev
107        };
108
109        if dc < self.lower_bound { dc = self.lower_bound; }
110        if dc > self.upper_bound { dc = self.upper_bound; }
111
112        let hp2 = self.hp2.next(input);
113        let bp = self.us.next(hp2);
114
115        // Phase accumulation
116        self.phase += 2.0 * PI / dc;
117
118        // Reset at zero crossings
119        if self.bp_prev <= 0.0 && bp > 0.0 {
120            self.phase = PI / dc;
121        } else if self.bp_prev >= 0.0 && bp < 0.0 {
122            self.phase = PI + PI / dc;
123        }
124
125        let mut synth = self.phase.sin();
126
127        // Glitch removal
128        // Normalize phase to [0, 2*PI] for quadrant checks
129        let norm_phase = self.phase % (2.0 * PI);
130        if norm_phase > 0.0 && norm_phase < PI / 2.0 && synth < self.synth_prev {
131            synth = self.synth_prev;
132        } else if norm_phase > PI && norm_phase < 1.5 * PI && synth > self.synth_prev {
133            synth = self.synth_prev;
134        }
135
136        self.re_prev = re;
137        self.im_prev = im;
138        self.dc_prev = dc;
139        self.bp_prev = bp;
140        self.synth_prev = synth;
141
142        synth
143    }
144}
145
146pub const SYNTHETIC_OSCILLATOR_METADATA: IndicatorMetadata = IndicatorMetadata {
147    name: "Synthetic Oscillator",
148    description: "A nonlinear oscillator designed to reduce lag while maintaining smoothness by adapting to the dominant cycle.",
149    params: &[
150        ParamDef { name: "lower_bound", default: "15", description: "Lower bound of cycle period" },
151        ParamDef { name: "upper_bound", default: "25", description: "Upper bound of cycle period" },
152    ],
153    formula_source: "https://github.com/lavs9/quantwave/blob/main/references/traderstipsreference/TRADERS’%20TIPS%20-%20APRIL%202026.html",
154    formula_latex: r#"
155\[
156Price = \text{Hann}(Close, 12)
157\]
158\[
159LP = \text{SuperSmoother}(\text{HighPass}(Price, UB), LB)
160\]
161\[
162Re = \frac{LP}{RMS(LP, 100)}, \quad Im = \frac{Re - Re_{t-1}}{RMS(Re - Re_{t-1}, 100)}
163\]
164\[
165DC = \frac{2\pi(Re^2 + Im^2)}{(Re - Re_{t-1})Im - (Im - Im_{t-1})Re}
166\]
167\[
168BP = \text{UltimateSmoother}(\text{HighPass}(Close, Mid), Mid)
169\]
170\[
171Phase = Phase_{t-1} + \frac{2\pi}{DC}
172\]
173\[
174Synth = \sin(Phase)
175\]
176"#,
177    gold_standard_file: "synthetic_oscillator.json",
178    category: "Ehlers DSP",
179};
180
181#[cfg(test)]
182mod tests {
183    use super::*;
184    use crate::traits::Next;
185    use proptest::prelude::*;
186
187    #[test]
188    fn test_synthetic_oscillator_basic() {
189        let mut so = SyntheticOscillator::default();
190        let inputs = vec![10.0, 11.0, 12.0, 13.0, 14.0, 15.0];
191        for input in inputs {
192            let res = so.next(input);
193            assert!(!res.is_nan());
194        }
195    }
196
197    proptest! {
198        #[test]
199        fn test_synthetic_oscillator_parity(
200            inputs in prop::collection::vec(1.0..100.0, 200..300),
201        ) {
202            let lb = 15;
203            let ub = 25;
204            let mut so = SyntheticOscillator::new(lb, ub);
205            let streaming_results: Vec<f64> = inputs.iter().map(|&x| so.next(x)).collect();
206
207            // Batch implementation
208            let mut batch_results = Vec::with_capacity(inputs.len());
209            let mut hann = HannFilter::new(12);
210            let mut hp = HighPass::new(ub);
211            let mut ss = SuperSmoother::new(lb);
212            let mut lp_win = VecDeque::new();
213            let mut lp_sum_sq = 0.0;
214            let mut re_prev = 0.0;
215            let mut roc_win = VecDeque::new();
216            let mut roc_sum_sq = 0.0;
217            let mut im_prev = 0.0;
218            let mut dc_prev = lb as f64;
219            let mid = ((lb * ub) as f64).sqrt();
220            let mut hp2 = HighPass::new(mid as usize);
221            let mut us = UltimateSmoother::new(mid as usize);
222            let mut bp_prev = 0.0;
223            let mut phase = 0.0;
224            let mut synth_prev = 0.0;
225
226            for &input in &inputs {
227                let p = hann.next(input);
228                let h = hp.next(p);
229                let l = ss.next(h);
230                
231                lp_win.push_back(l);
232                lp_sum_sq += l * l;
233                if lp_win.len() > 100 {
234                    let old = lp_win.pop_front().unwrap();
235                    lp_sum_sq -= old * old;
236                }
237                let rms_lp = (lp_sum_sq / lp_win.len() as f64).sqrt();
238                let re = if rms_lp > 1e-10 { l / rms_lp } else { 0.0 };
239                
240                let roc = re - re_prev;
241                roc_win.push_back(roc);
242                roc_sum_sq += roc * roc;
243                if roc_win.len() > 100 {
244                    let old = roc_win.pop_front().unwrap();
245                    roc_sum_sq -= old * old;
246                }
247                let qrms = (roc_sum_sq / roc_win.len() as f64).sqrt();
248                let im = if qrms > 1e-10 { roc / qrms } else { 0.0 };
249                
250                let denom = (re - re_prev) * im - (im - im_prev) * re;
251                let mut dc = if denom.abs() > 1e-10 {
252                    (2.0 * PI * (re * re + im * im)) / denom
253                } else {
254                    dc_prev
255                };
256                if dc < lb as f64 { dc = lb as f64; }
257                if dc > ub as f64 { dc = ub as f64; }
258                
259                let h2 = hp2.next(input);
260                let bp = us.next(h2);
261                
262                phase += 2.0 * PI / dc;
263                if bp_prev <= 0.0 && bp > 0.0 {
264                    phase = PI / dc;
265                } else if bp_prev >= 0.0 && bp < 0.0 {
266                    phase = PI + PI / dc;
267                }
268                
269                let mut synth = phase.sin();
270                let norm_phase = phase % (2.0 * PI);
271                if norm_phase > 0.0 && norm_phase < PI / 2.0 && synth < synth_prev {
272                    synth = synth_prev;
273                } else if norm_phase > PI && norm_phase < 1.5 * PI && synth > synth_prev {
274                    synth = synth_prev;
275                }
276                
277                batch_results.push(synth);
278                
279                re_prev = re;
280                im_prev = im;
281                dc_prev = dc;
282                bp_prev = bp;
283                synth_prev = synth;
284            }
285
286            for (s, b) in streaming_results.iter().zip(batch_results.iter()) {
287                approx::assert_relative_eq!(s, b, epsilon = 1e-10);
288            }
289        }
290    }
291}