Skip to main content

quantwave_core/indicators/
laguerre_rsi.rs

1use crate::indicators::metadata::{IndicatorMetadata, ParamDef};
2use crate::traits::Next;
3
4/// Laguerre RSI
5///
6/// Based on John Ehlers' "Time Warp – Without Space Travel" (2002).
7/// An RSI indicator generated over Laguerre time rather than linear time,
8/// providing rapid response to price changes with minimal lag.
9#[derive(Debug, Clone)]
10pub struct LaguerreRSI {
11    gamma: f64,
12    l0: f64,
13    l1: f64,
14    l2: f64,
15    l3: f64,
16    count: usize,
17}
18
19impl LaguerreRSI {
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 LaguerreRSI {
33    fn default() -> Self {
34        Self::new(0.5)
35    }
36}
37
38impl Next<f64> for LaguerreRSI {
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 0.0;
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        let mut cu = 0.0;
63        let mut cd = 0.0;
64
65        if self.l0 >= self.l1 {
66            cu += self.l0 - self.l1;
67        } else {
68            cd += self.l1 - self.l0;
69        }
70        if self.l1 >= self.l2 {
71            cu += self.l1 - self.l2;
72        } else {
73            cd += self.l2 - self.l1;
74        }
75        if self.l2 >= self.l3 {
76            cu += self.l2 - self.l3;
77        } else {
78            cd += self.l3 - self.l2;
79        }
80
81        let rsi = if cu + cd != 0.0 {
82            cu / (cu + cd)
83        } else {
84            0.0
85        };
86
87        rsi.clamp(0.0, 1.0)
88    }
89}
90
91pub const LAGUERRE_RSI_METADATA: IndicatorMetadata = IndicatorMetadata {
92    name: "Laguerre RSI",
93    description: "RSI calculated over Laguerre-warped time for faster response.",
94    usage: "Use as a faster lower-lag alternative to traditional RSI. Laguerre smoothing produces fewer whipsaws while remaining responsive to genuine momentum shifts.",
95    keywords: &["oscillator", "rsi", "ehlers", "dsp", "laguerre", "momentum"],
96    ehlers_summary: "Ehlers constructs the Laguerre RSI in Cybernetic Analysis by computing RSI on the four outputs of a Laguerre filter bank. The result has RSI-like scaling (0 to 1) but dramatically less lag and smoother behaviour than conventional RSI.",
97    params: &[ParamDef {
98        name: "gamma",
99        default: "0.5",
100        description: "Smoothing factor (0.0 to 1.0)",
101    }],
102    formula_source: "https://github.com/lavs9/quantwave/blob/main/references/Ehlers%20Papers/TimeWarp.pdf",
103    formula_latex: r#"
104\[
105L_0 = (1 - \gamma) \cdot Close + \gamma \cdot L_{0,t-1}
106\]
107\[
108L_1 = -\gamma L_0 + L_{0,t-1} + \gamma L_{1,t-1}
109\]
110\[
111L_2 = -\gamma L_1 + L_{1,t-1} + \gamma L_{2,t-1}
112\]
113\[
114L_3 = -\gamma L_2 + L_{2,t-1} + \gamma L_{3,t-1}
115\]
116\[
117CU = \sum \max(L_{i} - L_{i+1}, 0)
118\]
119\[
120CD = \sum \max(L_{i+1} - L_{i}, 0)
121\]
122\[
123RSI = \frac{CU}{CU + CD}
124\]
125"#,
126    gold_standard_file: "laguerre_rsi.json",
127    category: "Ehlers DSP",
128};
129
130#[cfg(test)]
131mod tests {
132    use super::*;
133    use crate::traits::Next;
134    use proptest::prelude::*;
135
136    #[test]
137    fn test_laguerre_rsi_basic() {
138        let mut lrsi = LaguerreRSI::new(0.5);
139        let inputs = vec![10.0, 11.0, 12.0, 13.0, 14.0];
140        for input in inputs {
141            let res = lrsi.next(input);
142            assert!(!res.is_nan());
143        }
144    }
145
146    proptest! {
147        #[test]
148        fn test_laguerre_rsi_parity(
149            inputs in prop::collection::vec(1.0..100.0, 10..100),
150        ) {
151            let gamma = 0.5;
152            let mut lrsi = LaguerreRSI::new(gamma);
153            let streaming_results: Vec<f64> = inputs.iter().map(|&x| lrsi.next(x)).collect();
154
155            let mut batch_results = Vec::with_capacity(inputs.len());
156            let mut l0 = 0.0;
157            let mut l1 = 0.0;
158            let mut l2 = 0.0;
159            let mut l3 = 0.0;
160
161            for (i, &input) in inputs.iter().enumerate() {
162                if i == 0 {
163                    l0 = input; l1 = input; l2 = input; l3 = input;
164                    batch_results.push(0.0);
165                } else {
166                    let prev_l0 = l0;
167                    let prev_l1 = l1;
168                    let prev_l2 = l2;
169                    let prev_l3 = l3;
170
171                    l0 = (1.0 - gamma) * input + gamma * prev_l0;
172                    l1 = -gamma * l0 + prev_l0 + gamma * prev_l1;
173                    l2 = -gamma * l1 + prev_l1 + gamma * prev_l2;
174                    l3 = -gamma * l2 + prev_l2 + gamma * prev_l3;
175
176                    let mut cu = 0.0;
177                    let mut cd = 0.0;
178
179                    if l0 >= l1 { cu += l0 - l1; } else { cd += l1 - l0; }
180                    if l1 >= l2 { cu += l1 - l2; } else { cd += l2 - l1; }
181                    if l2 >= l3 { cu += l2 - l3; } else { cd += l3 - l2; }
182
183                    let res = if cu + cd != 0.0 { cu / (cu + cd) } else { 0.0 };
184                    batch_results.push(res);
185                }
186            }
187
188            for (s, b) in streaming_results.iter().zip(batch_results.iter()) {
189                approx::assert_relative_eq!(s, b, epsilon = 1e-10);
190            }
191        }
192    }
193}