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 {
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 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 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 self.phase += 2.0 * PI / dc;
117
118 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 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 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}