Skip to main content

quantwave_core/indicators/
synthetic_oscillator.rs

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