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