quantwave_core/indicators/
laguerre_rsi.rs1use crate::indicators::metadata::{IndicatorMetadata, ParamDef};
2use crate::traits::Next;
3
4#[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 { cu / (cu + cd) } else { 0.0 };
82
83 rsi.clamp(0.0, 1.0)
84 }
85}
86
87pub const LAGUERRE_RSI_METADATA: IndicatorMetadata = IndicatorMetadata {
88 name: "Laguerre RSI",
89 description: "RSI calculated over Laguerre-warped time for faster response.",
90 usage: "Use as a faster lower-lag alternative to traditional RSI. Laguerre smoothing produces fewer whipsaws while remaining responsive to genuine momentum shifts.",
91 keywords: &["oscillator", "rsi", "ehlers", "dsp", "laguerre", "momentum"],
92 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.",
93 params: &[ParamDef {
94 name: "gamma",
95 default: "0.5",
96 description: "Smoothing factor (0.0 to 1.0)",
97 }],
98 formula_source: "https://github.com/lavs9/quantwave/blob/main/references/Ehlers%20Papers/TimeWarp.pdf",
99 formula_latex: r#"
100\[
101L_0 = (1 - \gamma) \cdot Close + \gamma \cdot L_{0,t-1}
102\]
103\[
104L_1 = -\gamma L_0 + L_{0,t-1} + \gamma L_{1,t-1}
105\]
106\[
107L_2 = -\gamma L_1 + L_{1,t-1} + \gamma L_{2,t-1}
108\]
109\[
110L_3 = -\gamma L_2 + L_{2,t-1} + \gamma L_{3,t-1}
111\]
112\[
113CU = \sum \max(L_{i} - L_{i+1}, 0)
114\]
115\[
116CD = \sum \max(L_{i+1} - L_{i}, 0)
117\]
118\[
119RSI = \frac{CU}{CU + CD}
120\]
121"#,
122 gold_standard_file: "laguerre_rsi.json",
123 category: "Ehlers DSP",
124};
125
126#[cfg(test)]
127mod tests {
128 use super::*;
129 use crate::traits::Next;
130 use proptest::prelude::*;
131
132 #[test]
133 fn test_laguerre_rsi_basic() {
134 let mut lrsi = LaguerreRSI::new(0.5);
135 let inputs = vec![10.0, 11.0, 12.0, 13.0, 14.0];
136 for input in inputs {
137 let res = lrsi.next(input);
138 assert!(!res.is_nan());
139 }
140 }
141
142 proptest! {
143 #[test]
144 fn test_laguerre_rsi_parity(
145 inputs in prop::collection::vec(1.0..100.0, 10..100),
146 ) {
147 let gamma = 0.5;
148 let mut lrsi = LaguerreRSI::new(gamma);
149 let streaming_results: Vec<f64> = inputs.iter().map(|&x| lrsi.next(x)).collect();
150
151 let mut batch_results = Vec::with_capacity(inputs.len());
152 let mut l0 = 0.0;
153 let mut l1 = 0.0;
154 let mut l2 = 0.0;
155 let mut l3 = 0.0;
156
157 for (i, &input) in inputs.iter().enumerate() {
158 if i == 0 {
159 l0 = input; l1 = input; l2 = input; l3 = input;
160 batch_results.push(0.0);
161 } else {
162 let prev_l0 = l0;
163 let prev_l1 = l1;
164 let prev_l2 = l2;
165 let prev_l3 = l3;
166
167 l0 = (1.0 - gamma) * input + gamma * prev_l0;
168 l1 = -gamma * l0 + prev_l0 + gamma * prev_l1;
169 l2 = -gamma * l1 + prev_l1 + gamma * prev_l2;
170 l3 = -gamma * l2 + prev_l2 + gamma * prev_l3;
171
172 let mut cu = 0.0;
173 let mut cd = 0.0;
174
175 if l0 >= l1 { cu += l0 - l1; } else { cd += l1 - l0; }
176 if l1 >= l2 { cu += l1 - l2; } else { cd += l2 - l1; }
177 if l2 >= l3 { cu += l2 - l3; } else { cd += l3 - l2; }
178
179 let res = if cu + cd != 0.0 { cu / (cu + cd) } else { 0.0 };
180 batch_results.push(res);
181 }
182 }
183
184 for (s, b) in streaming_results.iter().zip(batch_results.iter()) {
185 approx::assert_relative_eq!(s, b, epsilon = 1e-10);
186 }
187 }
188 }
189}