Skip to main content

quantwave_core/indicators/
generalized_laguerre.rs

1use crate::indicators::metadata::{IndicatorMetadata, ParamDef};
2use crate::indicators::ultimate_smoother::UltimateSmoother;
3use crate::traits::Next;
4
5/// Generalized Laguerre Filter
6///
7/// Based on John Ehlers' "The Continuation Index" (TASC September 2025).
8/// This is a generalized Laguerre filter of arbitrary order (up to 10),
9/// using an UltimateSmoother as the first Laguerre component.
10#[derive(Debug, Clone)]
11pub struct GeneralizedLaguerre {
12    us: UltimateSmoother,
13    gamma: f64,
14    order: usize,
15    lg_curr: [f64; 11], // 1-indexed to match formula (LG[1..order])
16    lg_prev: [f64; 11],
17    count: usize,
18}
19
20impl GeneralizedLaguerre {
21    pub fn new(length: usize, gamma: f64, order: usize) -> Self {
22        let order = order.clamp(1, 10);
23        Self {
24            us: UltimateSmoother::new(length),
25            gamma,
26            order,
27            lg_curr: [0.0; 11],
28            lg_prev: [0.0; 11],
29            count: 0,
30        }
31    }
32}
33
34impl Next<f64> for GeneralizedLaguerre {
35    type Output = f64;
36
37    fn next(&mut self, input: f64) -> Self::Output {
38        self.count += 1;
39
40        // Update previous values
41        for i in 1..=self.order {
42            self.lg_prev[i] = self.lg_curr[i];
43        }
44
45        // Calculate current component LG[1] using UltimateSmoother
46        self.lg_curr[1] = self.us.next(input);
47
48        // Calculate subsequent components LG[2..order]
49        for i in 2..=self.order {
50            self.lg_curr[i] = -self.gamma * self.lg_prev[i - 1]
51                + self.lg_prev[i - 1]
52                + self.gamma * self.lg_prev[i];
53        }
54
55        if self.count == 1 {
56            // Initialization: set all components to the first value
57            let first_val = self.lg_curr[1];
58            for i in 1..=self.order {
59                self.lg_curr[i] = first_val;
60            }
61            return first_val;
62        }
63
64        // Simple average of components
65        let mut fir = 0.0;
66        for i in 1..=self.order {
67            fir += self.lg_curr[i];
68        }
69
70        fir / (self.order as f64)
71    }
72}
73
74pub const GENERALIZED_LAGUERRE_METADATA: IndicatorMetadata = IndicatorMetadata {
75    name: "Generalized Laguerre",
76    description: "A generalized Laguerre filter of arbitrary order using an UltimateSmoother as the primary component.",
77    usage: "Use when the standard 4-element Laguerre filter needs further customization. The additional gamma2 parameter allows independent control of the pole spacing for more flexible frequency response shaping.",
78    keywords: &["filter", "ehlers", "dsp", "smoothing", "laguerre"],
79    ehlers_summary: "The Generalized Laguerre Filter extends the classic 4-element Laguerre design with an additional parameter that controls the distribution of poles across the frequency spectrum. This gives finer control over the transition band slope and passband flatness, useful for specialized spectral analysis applications.",
80    params: &[
81        ParamDef {
82            name: "length",
83            default: "40",
84            description: "UltimateSmoother period",
85        },
86        ParamDef {
87            name: "gamma",
88            default: "0.8",
89            description: "Smoothing factor (0.0 to 1.0)",
90        },
91        ParamDef {
92            name: "order",
93            default: "8",
94            description: "Filter order (1 to 10)",
95        },
96    ],
97    formula_source: "https://github.com/lavs9/quantwave/blob/main/references/traderstipsreference/TRADERS%E2%80%99%20TIPS%20-%20SEPTEMBER%202025.html",
98    formula_latex: r#"
99\[
100LG_1 = UltimateSmoother(Price, Length)
101\]
102\[
103LG_i = -\gamma LG_{i-1,t-1} + LG_{i-1,t-1} + \gamma LG_{i,t-1} \text{ for } i=2 \dots Order
104\]
105\[
106Filter = \frac{1}{Order} \sum_{i=1}^{Order} LG_i
107\]
108"#,
109    gold_standard_file: "generalized_laguerre.json",
110    category: "Ehlers DSP",
111};
112
113#[cfg(test)]
114mod tests {
115    use super::*;
116    use crate::traits::Next;
117    use proptest::prelude::*;
118
119    #[test]
120    fn test_generalized_laguerre_basic() {
121        let mut gl = GeneralizedLaguerre::new(40, 0.8, 8);
122        let inputs = vec![10.0, 11.0, 12.0, 13.0, 14.0];
123        for input in inputs {
124            let res = gl.next(input);
125            assert!(!res.is_nan());
126        }
127    }
128
129    proptest! {
130        #[test]
131        fn test_generalized_laguerre_parity(
132            inputs in prop::collection::vec(1.0..100.0, 50..100),
133        ) {
134            let length = 40;
135            let gamma = 0.8;
136            let order = 8;
137            let mut gl = GeneralizedLaguerre::new(length, gamma, order);
138            let streaming_results: Vec<f64> = inputs.iter().map(|&x| gl.next(x)).collect();
139
140            // Reference implementation
141            let mut us = UltimateSmoother::new(length);
142            let mut lg_curr = vec![0.0; order + 1];
143            let mut lg_prev = vec![0.0; order + 1];
144            let mut batch_results = Vec::with_capacity(inputs.len());
145
146            for (t, &input) in inputs.iter().enumerate() {
147                for i in 1..=order {
148                    lg_prev[i] = lg_curr[i];
149                }
150
151                lg_curr[1] = us.next(input);
152
153                for i in 2..=order {
154                    lg_curr[i] = -gamma * lg_prev[i-1] + lg_prev[i-1] + gamma * lg_prev[i];
155                }
156
157                if t == 0 {
158                    let first = lg_curr[1];
159                    for i in 1..=order { lg_curr[i] = first; }
160                }
161
162                let res = lg_curr[1..=order].iter().sum::<f64>() / (order as f64);
163                batch_results.push(res);
164            }
165
166            for (s, b) in streaming_results.iter().zip(batch_results.iter()) {
167                approx::assert_relative_eq!(s, b, epsilon = 1e-10);
168            }
169        }
170    }
171}