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