Skip to main content

quantwave_core/indicators/
laguerre_oscillator.rs

1use crate::indicators::math::RMS;
2use crate::indicators::metadata::{IndicatorMetadata, ParamDef};
3use crate::indicators::ultimate_smoother::UltimateSmoother;
4use crate::traits::Next;
5
6/// Laguerre Oscillator
7///
8/// Based on John Ehlers' "Laguerre Filters" (TASC July 2025).
9/// The Laguerre Oscillator is a low-lag trend indicator where values above zero
10/// generally correspond to upward movement and values below zero to downward movement.
11#[derive(Debug, Clone)]
12pub struct LaguerreOscillator {
13    us: UltimateSmoother,
14    rms: RMS,
15    gamma: f64,
16    l1: f64,
17    prev_l0: f64,
18    count: usize,
19}
20
21impl LaguerreOscillator {
22    pub fn new(length: usize, gamma: f64, rms_period: usize) -> Self {
23        Self {
24            us: UltimateSmoother::new(length),
25            rms: RMS::new(rms_period),
26            gamma,
27            l1: 0.0,
28            prev_l0: 0.0,
29            count: 0,
30        }
31    }
32}
33
34impl Next<f64> for LaguerreOscillator {
35    type Output = f64;
36
37    fn next(&mut self, input: f64) -> Self::Output {
38        let l0 = self.us.next(input);
39        self.count += 1;
40
41        if self.count == 1 {
42            self.prev_l0 = l0;
43            self.l1 = l0;
44            let _ = self.rms.next(0.0);
45            return 0.0;
46        }
47
48        // L1 = -Gama * L0 + L0[1] + Gama * L1[1];
49        let next_l1 = -self.gamma * l0 + self.prev_l0 + self.gamma * self.l1;
50
51        let diff = l0 - next_l1;
52        let rms_val = self.rms.next(diff);
53
54        let res = if rms_val != 0.0 { diff / rms_val } else { 0.0 };
55
56        self.l1 = next_l1;
57        self.prev_l0 = l0;
58
59        res
60    }
61}
62
63pub const LAGUERRE_OSCILLATOR_METADATA: IndicatorMetadata = IndicatorMetadata {
64    name: "Laguerre Oscillator",
65    description: "A low-lag trend oscillator derived from Laguerre polynomials and normalized by RMS volatility.",
66    params: &[
67        ParamDef {
68            name: "length",
69            default: "30",
70            description: "UltimateSmoother period",
71        },
72        ParamDef {
73            name: "gamma",
74            default: "0.5",
75            description: "Smoothing factor",
76        },
77        ParamDef {
78            name: "rms_period",
79            default: "100",
80            description: "RMS normalization period",
81        },
82    ],
83    formula_source: "https://github.com/lavs9/quantwave/blob/main/references/traderstipsreference/TRADERS%E2%80%99%20TIPS%20-%20JULY%202025.html",
84    formula_latex: r#"
85\[
86L_0 = UltimateSmoother(Close, Length)
87\]
88\[
89L_1 = -\gamma L_0 + L_{0,t-1} + \gamma L_{1,t-1}
90\]
91\[
92RMS = \sqrt{\frac{1}{n}\sum (L_0 - L_1)^2}
93\]
94\[
95Osc = (L_0 - L_1) / RMS
96\]
97"#,
98    gold_standard_file: "laguerre_oscillator.json",
99    category: "Ehlers DSP",
100};
101
102#[cfg(test)]
103mod tests {
104    use super::*;
105    use crate::traits::Next;
106    use proptest::prelude::*;
107
108    #[test]
109    fn test_laguerre_oscillator_basic() {
110        let mut lo = LaguerreOscillator::new(30, 0.5, 100);
111        let inputs = vec![10.0, 11.0, 12.0, 13.0, 14.0];
112        for input in inputs {
113            let res = lo.next(input);
114            assert!(!res.is_nan());
115        }
116    }
117
118    proptest! {
119        #[test]
120        fn test_laguerre_oscillator_parity(
121            inputs in prop::collection::vec(1.0..100.0, 110..200),
122        ) {
123            let length = 30;
124            let gamma = 0.5;
125            let rms_period = 100;
126            let mut lo = LaguerreOscillator::new(length, gamma, rms_period);
127            let streaming_results: Vec<f64> = inputs.iter().map(|&x| lo.next(x)).collect();
128
129            // Reference implementation
130            let mut us = UltimateSmoother::new(length);
131            let l0_vals: Vec<f64> = inputs.iter().map(|&x| us.next(x)).collect();
132
133            let mut batch_results = Vec::with_capacity(inputs.len());
134            let mut l1 = 0.0;
135            let mut diffs = Vec::new();
136
137            for (i, &l0) in l0_vals.iter().enumerate() {
138                if i == 0 {
139                    l1 = l0;
140                    diffs.push(0.0);
141                    batch_results.push(0.0);
142                } else {
143                    let prev_l0 = l0_vals[i-1];
144                    l1 = -gamma * l0 + prev_l0 + gamma * l1;
145                    let diff = l0 - l1;
146                    diffs.push(diff);
147
148                    let start = if diffs.len() > rms_period { diffs.len() - rms_period } else { 0 };
149                    let window = &diffs[start..];
150                    let sum_sq: f64 = window.iter().map(|&x| x*x).sum();
151                    let rms = (sum_sq / window.len() as f64).sqrt();
152
153                    let res = if rms != 0.0 { diff / rms } else { 0.0 };
154                    batch_results.push(res);
155                }
156            }
157
158            for (s, b) in streaming_results.iter().zip(batch_results.iter()) {
159                approx::assert_relative_eq!(s, b, epsilon = 1e-10);
160            }
161        }
162    }
163}