quantwave_core/indicators/
generalized_laguerre.rs1use crate::indicators::metadata::{IndicatorMetadata, ParamDef};
2use crate::indicators::ultimate_smoother::UltimateSmoother;
3use crate::traits::Next;
4
5#[derive(Debug, Clone)]
11pub struct GeneralizedLaguerre {
12 us: UltimateSmoother,
13 gamma: f64,
14 order: usize,
15 lg_curr: [f64; 11], 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 for i in 1..=self.order {
42 self.lg_prev[i] = self.lg_curr[i];
43 }
44
45 self.lg_curr[1] = self.us.next(input);
47
48 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 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 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 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 lg_prev[1..=order].copy_from_slice(&lg_curr[1..=order]);
148
149 lg_curr[1] = us.next(input);
150
151 for i in 2..=order {
152 lg_curr[i] = -gamma * lg_prev[i-1] + lg_prev[i-1] + gamma * lg_prev[i];
153 }
154
155 if t == 0 {
156 let first = lg_curr[1];
157 for i in 1..=order { lg_curr[i] = first; }
158 }
159
160 let res = lg_curr[1..=order].iter().sum::<f64>() / (order as f64);
161 batch_results.push(res);
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}