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 && let Some(old) = self.lp_window.pop_front() {
80            self.lp_sum_sq -= old * old;
81        }
82
83        let rms_lp = (self.lp_sum_sq / self.lp_window.len() as f64).sqrt();
84        let re = if rms_lp > 1e-10 { lp / rms_lp } else { 0.0 };
85
86        // Imaginary component
87        let roc = re - self.re_prev;
88        self.roc_window.push_back(roc);
89        self.roc_sum_sq += roc * roc;
90        if self.roc_window.len() > 100 && let Some(old) = self.roc_window.pop_front() {
91            self.roc_sum_sq -= old * old;
92        }
93
94        let qrms = (self.roc_sum_sq / self.roc_window.len() as f64).sqrt();
95        let im = if qrms > 1e-10 { roc / qrms } else { 0.0 };
96
97        // Dominant Cycle
98        let denom = (re - self.re_prev) * im - (im - self.im_prev) * re;
99        let mut dc = if denom.abs() > 1e-10 {
100            (2.0 * PI * (re * re + im * im)) / denom
101        } else {
102            self.dc_prev
103        };
104
105        if dc < self.lower_bound { dc = self.lower_bound; }
106        if dc > self.upper_bound { dc = self.upper_bound; }
107
108        let hp2 = self.hp2.next(input);
109        let bp = self.us.next(hp2);
110
111        // Phase accumulation
112        self.phase += 2.0 * PI / dc;
113
114        // Reset at zero crossings
115        if self.bp_prev <= 0.0 && bp > 0.0 {
116            self.phase = PI / dc;
117        } else if self.bp_prev >= 0.0 && bp < 0.0 {
118            self.phase = PI + PI / dc;
119        }
120
121        let mut synth = self.phase.sin();
122
123        // Glitch removal
124        // Normalize phase to [0, 2*PI] for quadrant checks
125        let norm_phase = self.phase % (2.0 * PI);
126        if (norm_phase > 0.0 && norm_phase < PI / 2.0 && synth < self.synth_prev) || (norm_phase > PI && norm_phase < 1.5 * PI && synth > self.synth_prev) {
127            synth = self.synth_prev;
128        }
129
130        self.re_prev = re;
131        self.im_prev = im;
132        self.dc_prev = dc;
133        self.bp_prev = bp;
134        self.synth_prev = synth;
135
136        synth
137    }
138}
139
140pub const SYNTHETIC_OSCILLATOR_METADATA: IndicatorMetadata = IndicatorMetadata {
141    name: "Synthetic Oscillator",
142    description: "A nonlinear oscillator designed to reduce lag while maintaining smoothness by adapting to the dominant cycle.",
143    usage: "Use to construct a synthetic oscillator from dominant cycle sine components when direct price oscillators are too noisy. Most effective in clearly cyclical markets.",
144    keywords: &["oscillator", "ehlers", "dsp", "cycle", "synthetic"],
145    ehlers_summary: "Ehlers constructs a Synthetic Oscillator by generating a synthetic sine wave at the measured dominant cycle period and comparing it to price. The phase difference between the synthetic sine and actual price reveals whether the market is ahead of or behind its expected cycle position.",
146    params: &[
147        ParamDef { name: "lower_bound", default: "15", description: "Lower bound of cycle period" },
148        ParamDef { name: "upper_bound", default: "25", description: "Upper bound of cycle period" },
149    ],
150    formula_source: "https://github.com/lavs9/quantwave/blob/main/references/traderstipsreference/TRADERS’%20TIPS%20-%20APRIL%202026.html",
151    formula_latex: r#"
152\[
153Price = \text{Hann}(Close, 12)
154\]
155\[
156LP = \text{SuperSmoother}(\text{HighPass}(Price, UB), LB)
157\]
158\[
159Re = \frac{LP}{RMS(LP, 100)}, \quad Im = \frac{Re - Re_{t-1}}{RMS(Re - Re_{t-1}, 100)}
160\]
161\[
162DC = \frac{2\pi(Re^2 + Im^2)}{(Re - Re_{t-1})Im - (Im - Im_{t-1})Re}
163\]
164\[
165BP = \text{UltimateSmoother}(\text{HighPass}(Close, Mid), Mid)
166\]
167\[
168Phase = Phase_{t-1} + \frac{2\pi}{DC}
169\]
170\[
171Synth = \sin(Phase)
172\]
173"#,
174    gold_standard_file: "synthetic_oscillator.json",
175    category: "Ehlers DSP",
176};
177
178#[cfg(test)]
179mod tests {
180    use super::*;
181    use crate::traits::Next;
182    use proptest::prelude::*;
183
184    #[test]
185    fn test_synthetic_oscillator_basic() {
186        let mut so = SyntheticOscillator::default();
187        let inputs = vec![10.0, 11.0, 12.0, 13.0, 14.0, 15.0];
188        for input in inputs {
189            let res = so.next(input);
190            assert!(!res.is_nan());
191        }
192    }
193
194    proptest! {
195        #[test]
196        fn test_synthetic_oscillator_parity(
197            inputs in prop::collection::vec(1.0..100.0, 200..300),
198        ) {
199            let lb = 15;
200            let ub = 25;
201            let mut so = SyntheticOscillator::new(lb, ub);
202            let streaming_results: Vec<f64> = inputs.iter().map(|&x| so.next(x)).collect();
203
204            // Batch implementation
205            let mut batch_results = Vec::with_capacity(inputs.len());
206            let mut hann = HannFilter::new(12);
207            let mut hp = HighPass::new(ub);
208            let mut ss = SuperSmoother::new(lb);
209            let mut lp_win = VecDeque::new();
210            let mut lp_sum_sq = 0.0;
211            let mut re_prev = 0.0;
212            let mut roc_win = VecDeque::new();
213            let mut roc_sum_sq = 0.0;
214            let mut im_prev = 0.0;
215            let mut dc_prev = lb as f64;
216            let mid = ((lb * ub) as f64).sqrt();
217            let mut hp2 = HighPass::new(mid as usize);
218            let mut us = UltimateSmoother::new(mid as usize);
219            let mut bp_prev = 0.0;
220            let mut phase = 0.0;
221            let mut synth_prev = 0.0;
222
223            for &input in &inputs {
224                let p = hann.next(input);
225                let h = hp.next(p);
226                let l = ss.next(h);
227                
228                lp_win.push_back(l);
229                lp_sum_sq += l * l;
230                if lp_win.len() > 100 {
231                    let old = lp_win.pop_front().unwrap();
232                    lp_sum_sq -= old * old;
233                }
234                let rms_lp = (lp_sum_sq / lp_win.len() as f64).sqrt();
235                let re = if rms_lp > 1e-10 { l / rms_lp } else { 0.0 };
236                
237                let roc = re - re_prev;
238                roc_win.push_back(roc);
239                roc_sum_sq += roc * roc;
240                if roc_win.len() > 100 {
241                    let old = roc_win.pop_front().unwrap();
242                    roc_sum_sq -= old * old;
243                }
244                let qrms = (roc_sum_sq / roc_win.len() as f64).sqrt();
245                let im = if qrms > 1e-10 { roc / qrms } else { 0.0 };
246                
247                let denom = (re - re_prev) * im - (im - im_prev) * re;
248                let mut dc = if denom.abs() > 1e-10 {
249                    (2.0 * PI * (re * re + im * im)) / denom
250                } else {
251                    dc_prev
252                };
253                if dc < lb as f64 { dc = lb as f64; }
254                if dc > ub as f64 { dc = ub as f64; }
255                
256                let h2 = hp2.next(input);
257                let bp = us.next(h2);
258                
259                phase += 2.0 * PI / dc;
260                if bp_prev <= 0.0 && bp > 0.0 {
261                    phase = PI / dc;
262                } else if bp_prev >= 0.0 && bp < 0.0 {
263                    phase = PI + PI / dc;
264                }
265                
266                let mut synth = phase.sin();
267                let norm_phase = phase % (2.0 * PI);
268                if norm_phase > 0.0 && norm_phase < PI / 2.0 && synth < synth_prev {
269                    synth = synth_prev;
270                } else if norm_phase > PI && norm_phase < 1.5 * PI && synth > synth_prev {
271                    synth = synth_prev;
272                }
273                
274                batch_results.push(synth);
275                
276                re_prev = re;
277                im_prev = im;
278                dc_prev = dc;
279                bp_prev = bp;
280                synth_prev = synth;
281            }
282
283            for (s, b) in streaming_results.iter().zip(batch_results.iter()) {
284                approx::assert_relative_eq!(s, b, epsilon = 1e-10);
285            }
286        }
287    }
288}