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.min(10).max(1);
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    params: &[
78        ParamDef {
79            name: "length",
80            default: "40",
81            description: "UltimateSmoother period",
82        },
83        ParamDef {
84            name: "gamma",
85            default: "0.8",
86            description: "Smoothing factor (0.0 to 1.0)",
87        },
88        ParamDef {
89            name: "order",
90            default: "8",
91            description: "Filter order (1 to 10)",
92        },
93    ],
94    formula_source: "https://github.com/lavs9/quantwave/blob/main/references/traderstipsreference/TRADERS%E2%80%99%20TIPS%20-%20SEPTEMBER%202025.html",
95    formula_latex: r#"
96\[
97LG_1 = UltimateSmoother(Price, Length)
98\]
99\[
100LG_i = -\gamma LG_{i-1,t-1} + LG_{i-1,t-1} + \gamma LG_{i,t-1} \text{ for } i=2 \dots Order
101\]
102\[
103Filter = \frac{1}{Order} \sum_{i=1}^{Order} LG_i
104\]
105"#,
106    gold_standard_file: "generalized_laguerre.json",
107    category: "Ehlers DSP",
108};
109
110#[cfg(test)]
111mod tests {
112    use super::*;
113    use crate::traits::Next;
114    use proptest::prelude::*;
115
116    #[test]
117    fn test_generalized_laguerre_basic() {
118        let mut gl = GeneralizedLaguerre::new(40, 0.8, 8);
119        let inputs = vec![10.0, 11.0, 12.0, 13.0, 14.0];
120        for input in inputs {
121            let res = gl.next(input);
122            assert!(!res.is_nan());
123        }
124    }
125
126    proptest! {
127        #[test]
128        fn test_generalized_laguerre_parity(
129            inputs in prop::collection::vec(1.0..100.0, 50..100),
130        ) {
131            let length = 40;
132            let gamma = 0.8;
133            let order = 8;
134            let mut gl = GeneralizedLaguerre::new(length, gamma, order);
135            let streaming_results: Vec<f64> = inputs.iter().map(|&x| gl.next(x)).collect();
136
137            // Reference implementation
138            let mut us = UltimateSmoother::new(length);
139            let mut lg_curr = vec![0.0; order + 1];
140            let mut lg_prev = vec![0.0; order + 1];
141            let mut batch_results = Vec::with_capacity(inputs.len());
142
143            for (t, &input) in inputs.iter().enumerate() {
144                for i in 1..=order {
145                    lg_prev[i] = lg_curr[i];
146                }
147
148                lg_curr[1] = us.next(input);
149
150                for i in 2..=order {
151                    lg_curr[i] = -gamma * lg_prev[i-1] + lg_prev[i-1] + gamma * lg_prev[i];
152                }
153
154                if t == 0 {
155                    let first = lg_curr[1];
156                    for i in 1..=order { lg_curr[i] = first; }
157                }
158
159                let res = lg_curr[1..=order].iter().sum::<f64>() / (order as f64);
160                batch_results.push(res);
161            }
162
163            for (s, b) in streaming_results.iter().zip(batch_results.iter()) {
164                approx::assert_relative_eq!(s, b, epsilon = 1e-10);
165            }
166        }
167    }
168}