quantwave_core/indicators/
synthetic_oscillator.rs1use 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#[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 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 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 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 self.phase += 2.0 * PI / dc;
113
114 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 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 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}