use crate::indicators::metadata::{IndicatorMetadata, ParamDef};
use crate::indicators::ultimate_smoother::UltimateSmoother;
use crate::traits::Next;
#[derive(Debug, Clone)]
pub struct GeneralizedLaguerre {
us: UltimateSmoother,
gamma: f64,
order: usize,
lg_curr: [f64; 11], lg_prev: [f64; 11],
count: usize,
}
impl GeneralizedLaguerre {
pub fn new(length: usize, gamma: f64, order: usize) -> Self {
let order = order.clamp(1, 10);
Self {
us: UltimateSmoother::new(length),
gamma,
order,
lg_curr: [0.0; 11],
lg_prev: [0.0; 11],
count: 0,
}
}
}
impl Next<f64> for GeneralizedLaguerre {
type Output = f64;
fn next(&mut self, input: f64) -> Self::Output {
self.count += 1;
for i in 1..=self.order {
self.lg_prev[i] = self.lg_curr[i];
}
self.lg_curr[1] = self.us.next(input);
for i in 2..=self.order {
self.lg_curr[i] = -self.gamma * self.lg_prev[i - 1]
+ self.lg_prev[i - 1]
+ self.gamma * self.lg_prev[i];
}
if self.count == 1 {
let first_val = self.lg_curr[1];
for i in 1..=self.order {
self.lg_curr[i] = first_val;
}
return first_val;
}
let mut fir = 0.0;
for i in 1..=self.order {
fir += self.lg_curr[i];
}
fir / (self.order as f64)
}
}
pub const GENERALIZED_LAGUERRE_METADATA: IndicatorMetadata = IndicatorMetadata {
name: "Generalized Laguerre",
description: "A generalized Laguerre filter of arbitrary order using an UltimateSmoother as the primary component.",
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.",
keywords: &["filter", "ehlers", "dsp", "smoothing", "laguerre"],
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.",
params: &[
ParamDef {
name: "length",
default: "40",
description: "UltimateSmoother period",
},
ParamDef {
name: "gamma",
default: "0.8",
description: "Smoothing factor (0.0 to 1.0)",
},
ParamDef {
name: "order",
default: "8",
description: "Filter order (1 to 10)",
},
],
formula_source: "https://github.com/lavs9/quantwave/blob/main/references/traderstipsreference/TRADERS%E2%80%99%20TIPS%20-%20SEPTEMBER%202025.html",
formula_latex: r#"
\[
LG_1 = UltimateSmoother(Price, Length)
\]
\[
LG_i = -\gamma LG_{i-1,t-1} + LG_{i-1,t-1} + \gamma LG_{i,t-1} \text{ for } i=2 \dots Order
\]
\[
Filter = \frac{1}{Order} \sum_{i=1}^{Order} LG_i
\]
"#,
gold_standard_file: "generalized_laguerre.json",
category: "Ehlers DSP",
};
#[cfg(test)]
mod tests {
use super::*;
use crate::traits::Next;
use proptest::prelude::*;
#[test]
fn test_generalized_laguerre_basic() {
let mut gl = GeneralizedLaguerre::new(40, 0.8, 8);
let inputs = vec![10.0, 11.0, 12.0, 13.0, 14.0];
for input in inputs {
let res = gl.next(input);
assert!(!res.is_nan());
}
}
proptest! {
#[test]
fn test_generalized_laguerre_parity(
inputs in prop::collection::vec(1.0..100.0, 50..100),
) {
let length = 40;
let gamma = 0.8;
let order = 8;
let mut gl = GeneralizedLaguerre::new(length, gamma, order);
let streaming_results: Vec<f64> = inputs.iter().map(|&x| gl.next(x)).collect();
let mut us = UltimateSmoother::new(length);
let mut lg_curr = vec![0.0; order + 1];
let mut lg_prev = vec![0.0; order + 1];
let mut batch_results = Vec::with_capacity(inputs.len());
for (t, &input) in inputs.iter().enumerate() {
for i in 1..=order {
lg_prev[i] = lg_curr[i];
}
lg_curr[1] = us.next(input);
for i in 2..=order {
lg_curr[i] = -gamma * lg_prev[i-1] + lg_prev[i-1] + gamma * lg_prev[i];
}
if t == 0 {
let first = lg_curr[1];
for i in 1..=order { lg_curr[i] = first; }
}
let res = lg_curr[1..=order].iter().sum::<f64>() / (order as f64);
batch_results.push(res);
}
for (s, b) in streaming_results.iter().zip(batch_results.iter()) {
approx::assert_relative_eq!(s, b, epsilon = 1e-10);
}
}
}
}