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 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}