Skip to main content

quantwave_core/indicators/
classic_laguerre.rs

1use crate::indicators::metadata::{IndicatorMetadata, ParamDef};
2use crate::traits::Next;
3
4/// Classic Laguerre Filter
5///
6/// Based on John Ehlers' "Time Warp – Without Space Travel" (2002).
7/// This filter uses the Laguerre Transform to provide superior smoothing
8/// with minimal lag using only four data samples.
9#[derive(Debug, Clone)]
10pub struct ClassicLaguerre {
11    gamma: f64,
12    l0: f64,
13    l1: f64,
14    l2: f64,
15    l3: f64,
16    count: usize,
17}
18
19impl ClassicLaguerre {
20    pub fn new(gamma: f64) -> Self {
21        Self {
22            gamma,
23            l0: 0.0,
24            l1: 0.0,
25            l2: 0.0,
26            l3: 0.0,
27            count: 0,
28        }
29    }
30}
31
32impl Default for ClassicLaguerre {
33    fn default() -> Self {
34        Self::new(0.8)
35    }
36}
37
38impl Next<f64> for ClassicLaguerre {
39    type Output = f64;
40
41    fn next(&mut self, input: f64) -> Self::Output {
42        self.count += 1;
43
44        if self.count == 1 {
45            self.l0 = input;
46            self.l1 = input;
47            self.l2 = input;
48            self.l3 = input;
49            return input;
50        }
51
52        let prev_l0 = self.l0;
53        let prev_l1 = self.l1;
54        let prev_l2 = self.l2;
55        let prev_l3 = self.l3;
56
57        self.l0 = (1.0 - self.gamma) * input + self.gamma * prev_l0;
58        self.l1 = -self.gamma * self.l0 + prev_l0 + self.gamma * prev_l1;
59        self.l2 = -self.gamma * self.l1 + prev_l1 + self.gamma * prev_l2;
60        self.l3 = -self.gamma * self.l2 + prev_l2 + self.gamma * prev_l3;
61
62        (self.l0 + 2.0 * self.l1 + 2.0 * self.l2 + self.l3) / 6.0
63    }
64}
65
66pub const CLASSIC_LAGUERRE_METADATA: IndicatorMetadata = IndicatorMetadata {
67    name: "Classic Laguerre Filter",
68    description: "The original Laguerre filter from John Ehlers' 2002 'Time Warp' paper.",
69    usage: "Use when a smooth trend estimate with controllable lag using only 4 state variables is needed. Preferred over long EMAs when computational memory is constrained.",
70    keywords: &["filter", "ehlers", "dsp", "smoothing", "laguerre"],
71    ehlers_summary: "The Classic Laguerre Filter uses four first-order IIR sections sharing the same gamma coefficient. In Cybernetic Analysis (2004) Ehlers shows gamma maps directly to an effective period, making it highly tunable with minimal computation.",
72    params: &[ParamDef {
73        name: "gamma",
74        default: "0.8",
75        description: "Smoothing factor (0.0 to 1.0)",
76    }],
77    formula_source: "https://github.com/lavs9/quantwave/blob/main/references/Ehlers%20Papers/TimeWarp.pdf",
78    formula_latex: r#"
79\[
80L_0 = (1 - \gamma) \cdot Price + \gamma \cdot L_{0,t-1}
81\]
82\[
83L_1 = -\gamma L_0 + L_{0,t-1} + \gamma L_{1,t-1}
84\]
85\[
86L_2 = -\gamma L_1 + L_{1,t-1} + \gamma L_{2,t-1}
87\]
88\[
89L_3 = -\gamma L_2 + L_{2,t-1} + \gamma L_{3,t-1}
90\]
91\[
92Filt = \frac{L_0 + 2L_1 + 2L_2 + L_3}{6}
93\]
94"#,
95    gold_standard_file: "classic_laguerre.json",
96    category: "Ehlers DSP",
97};
98
99#[cfg(test)]
100mod tests {
101    use super::*;
102    use crate::traits::Next;
103    use proptest::prelude::*;
104
105    #[test]
106    fn test_classic_laguerre_basic() {
107        let mut cl = ClassicLaguerre::new(0.8);
108        let inputs = vec![10.0, 11.0, 12.0, 13.0, 14.0];
109        for input in inputs {
110            let res = cl.next(input);
111            assert!(!res.is_nan());
112        }
113    }
114
115    proptest! {
116        #[test]
117        fn test_classic_laguerre_parity(
118            inputs in prop::collection::vec(1.0..100.0, 10..100),
119        ) {
120            let gamma = 0.8;
121            let mut cl = ClassicLaguerre::new(gamma);
122            let streaming_results: Vec<f64> = inputs.iter().map(|&x| cl.next(x)).collect();
123
124            let mut batch_results = Vec::with_capacity(inputs.len());
125            let mut l0 = 0.0;
126            let mut l1 = 0.0;
127            let mut l2 = 0.0;
128            let mut l3 = 0.0;
129
130            for (i, &input) in inputs.iter().enumerate() {
131                if i == 0 {
132                    l0 = input; l1 = input; l2 = input; l3 = input;
133                    batch_results.push(input);
134                } else {
135                    let prev_l0 = l0;
136                    let prev_l1 = l1;
137                    let prev_l2 = l2;
138                    let prev_l3 = l3;
139
140                    l0 = (1.0 - gamma) * input + gamma * prev_l0;
141                    l1 = -gamma * l0 + prev_l0 + gamma * prev_l1;
142                    l2 = -gamma * l1 + prev_l1 + gamma * prev_l2;
143                    l3 = -gamma * l2 + prev_l2 + gamma * prev_l3;
144
145                    let res = (l0 + 2.0 * l1 + 2.0 * l2 + l3) / 6.0;
146                    batch_results.push(res);
147                }
148            }
149
150            for (s, b) in streaming_results.iter().zip(batch_results.iter()) {
151                approx::assert_relative_eq!(s, b, epsilon = 1e-10);
152            }
153        }
154    }
155}