Skip to main content

quantwave_core/indicators/
cybernetic_oscillator.rs

1use crate::indicators::metadata::{IndicatorMetadata, ParamDef};
2use crate::traits::Next;
3use crate::indicators::high_pass::HighPass;
4use crate::indicators::super_smoother::SuperSmoother;
5use std::collections::VecDeque;
6
7/// Cybernetic Oscillator
8///
9/// Based on John Ehlers' "Making A Better Oscillator" (TASC June 2025).
10/// Combines a HighPass filter and a SuperSmoother, then normalizes by RMS.
11#[derive(Debug, Clone)]
12pub struct CyberneticOscillator {
13    hp: HighPass,
14    ss: SuperSmoother,
15    rms_window: VecDeque<f64>,
16    rms_len: usize,
17    sum_sq: f64,
18}
19
20impl CyberneticOscillator {
21    pub fn new(hp_length: usize, lp_length: usize, rms_len: usize) -> Self {
22        Self {
23            hp: HighPass::new(hp_length),
24            ss: SuperSmoother::new(lp_length),
25            rms_window: VecDeque::with_capacity(rms_len),
26            rms_len,
27            sum_sq: 0.0,
28        }
29    }
30}
31
32impl Default for CyberneticOscillator {
33    fn default() -> Self {
34        Self::new(30, 20, 100)
35    }
36}
37
38impl Next<f64> for CyberneticOscillator {
39    type Output = f64;
40
41    fn next(&mut self, input: f64) -> Self::Output {
42        let hp_val = self.hp.next(input);
43        let lp_val = self.ss.next(hp_val);
44
45        // Update RMS
46        let val_sq = lp_val * lp_val;
47        self.rms_window.push_back(lp_val);
48        self.sum_sq += val_sq;
49
50        if self.rms_window.len() > self.rms_len {
51            let oldest = self.rms_window.pop_front().unwrap();
52            self.sum_sq -= oldest * oldest;
53        }
54
55        // Avoid precision issues resulting in small negative sum_sq
56        if self.sum_sq < 0.0 {
57            self.sum_sq = 0.0;
58        }
59
60        let rms = (self.sum_sq / self.rms_len as f64).sqrt();
61
62        if rms != 0.0 {
63            lp_val / rms
64        } else {
65            0.0
66        }
67    }
68}
69
70pub const CYBERNETIC_OSCILLATOR_METADATA: IndicatorMetadata = IndicatorMetadata {
71    name: "CyberneticOscillator",
72    description: "Combined HighPass and SuperSmoother filters normalized by RMS.",
73    params: &[
74        ParamDef {
75            name: "hp_length",
76            default: "30",
77            description: "HighPass filter length",
78        },
79        ParamDef {
80            name: "lp_length",
81            default: "20",
82            description: "LowPass (SuperSmoother) length",
83        },
84        ParamDef {
85            name: "rms_len",
86            default: "100",
87            description: "RMS normalization length",
88        },
89    ],
90    formula_source: "https://github.com/lavs9/quantwave/blob/main/references/traderstipsreference/TRADERS’%20TIPS%20-%20JUNE%202025.html",
91    formula_latex: r#"
92\[
93HP = HighPass(Price, HPLen)
94\]
95\[
96LP = SuperSmoother(HP, LPLen)
97\]
98\[
99RMS = \sqrt{\frac{1}{N} \sum_{i=0}^{N-1} LP_{t-i}^2}
100\]
101\[
102CO = \frac{LP}{RMS}
103\]
104"#,
105    gold_standard_file: "cybernetic_oscillator.json",
106    category: "Ehlers DSP",
107};
108
109#[cfg(test)]
110mod tests {
111    use super::*;
112    use crate::traits::Next;
113    use proptest::prelude::*;
114
115    #[test]
116    fn test_cybernetic_oscillator_basic() {
117        let mut co = CyberneticOscillator::new(30, 20, 100);
118        for i in 0..150 {
119            let val = co.next(100.0 + (i as f64).sin());
120            assert!(!val.is_nan());
121        }
122    }
123
124    proptest! {
125        #[test]
126        fn test_cybernetic_oscillator_parity(
127            inputs in prop::collection::vec(1.0..100.0, 150..250),
128        ) {
129            let hp_len = 30;
130            let lp_len = 20;
131            let rms_len = 100;
132            let mut co = CyberneticOscillator::new(hp_len, lp_len, rms_len);
133            let streaming_results: Vec<f64> = inputs.iter().map(|&x| co.next(x)).collect();
134
135            // Batch implementation
136            let mut batch_results = Vec::with_capacity(inputs.len());
137            
138            let mut hp = HighPass::new(hp_len);
139            let mut ss = SuperSmoother::new(lp_len);
140            let lp_vals: Vec<f64> = inputs.iter().map(|&x| ss.next(hp.next(x))).collect();
141
142            for i in 0..lp_vals.len() {
143                let start = if i >= rms_len - 1 { i + 1 - rms_len } else { 0 };
144                let window = &lp_vals[start..i + 1];
145                
146                let mut sum_sq = 0.0;
147                for &v in window {
148                    sum_sq += v * v;
149                }
150                
151                // Note: The denominator in Ehlers' EasyLanguage is constant (Length)
152                // whereas the window may be smaller initially. 
153                // But Ehlers' code: $RMS = SquareRoot(SumSq / Length)
154                // So we always divide by rms_len.
155                let rms = (sum_sq / rms_len as f64).sqrt();
156                
157                if rms != 0.0 {
158                    batch_results.push(lp_vals[i] / rms);
159                } else {
160                    batch_results.push(0.0);
161                }
162            }
163
164            for (s, b) in streaming_results.iter().zip(batch_results.iter()) {
165                approx::assert_relative_eq!(s, b, epsilon = 1e-10);
166            }
167        }
168    }
169}