quantwave_core/indicators/
laguerre_oscillator.rs1use crate::indicators::math::RMS;
2use crate::indicators::metadata::{IndicatorMetadata, ParamDef};
3use crate::indicators::ultimate_smoother::UltimateSmoother;
4use crate::traits::Next;
5
6#[derive(Debug, Clone)]
12pub struct LaguerreOscillator {
13 us: UltimateSmoother,
14 rms: RMS,
15 gamma: f64,
16 l1: f64,
17 prev_l0: f64,
18 count: usize,
19}
20
21impl LaguerreOscillator {
22 pub fn new(length: usize, gamma: f64, rms_period: usize) -> Self {
23 Self {
24 us: UltimateSmoother::new(length),
25 rms: RMS::new(rms_period),
26 gamma,
27 l1: 0.0,
28 prev_l0: 0.0,
29 count: 0,
30 }
31 }
32}
33
34impl Next<f64> for LaguerreOscillator {
35 type Output = f64;
36
37 fn next(&mut self, input: f64) -> Self::Output {
38 let l0 = self.us.next(input);
39 self.count += 1;
40
41 if self.count == 1 {
42 self.prev_l0 = l0;
43 self.l1 = l0;
44 let _ = self.rms.next(0.0);
45 return 0.0;
46 }
47
48 let next_l1 = -self.gamma * l0 + self.prev_l0 + self.gamma * self.l1;
50
51 let diff = l0 - next_l1;
52 let rms_val = self.rms.next(diff);
53
54 let res = if rms_val != 0.0 { diff / rms_val } else { 0.0 };
55
56 self.l1 = next_l1;
57 self.prev_l0 = l0;
58
59 res
60 }
61}
62
63pub const LAGUERRE_OSCILLATOR_METADATA: IndicatorMetadata = IndicatorMetadata {
64 name: "Laguerre Oscillator",
65 description: "A low-lag trend oscillator derived from Laguerre polynomials and normalized by RMS volatility.",
66 usage: "Use to detect overbought and oversold conditions with very low lag. The single gamma parameter lets you tune it from aggressive to smooth.",
67 keywords: &["oscillator", "ehlers", "dsp", "laguerre", "momentum"],
68 ehlers_summary: "Ehlers describes the Laguerre Oscillator in Cybernetic Analysis as measuring the difference between the first and last elements of a 4-element Laguerre filter bank, extracting the high-frequency component as a zero-lag momentum measure.",
69 params: &[
70 ParamDef {
71 name: "length",
72 default: "30",
73 description: "UltimateSmoother period",
74 },
75 ParamDef {
76 name: "gamma",
77 default: "0.5",
78 description: "Smoothing factor",
79 },
80 ParamDef {
81 name: "rms_period",
82 default: "100",
83 description: "RMS normalization period",
84 },
85 ],
86 formula_source: "https://github.com/lavs9/quantwave/blob/main/references/traderstipsreference/TRADERS%E2%80%99%20TIPS%20-%20JULY%202025.html",
87 formula_latex: r#"
88\[
89L_0 = UltimateSmoother(Close, Length)
90\]
91\[
92L_1 = -\gamma L_0 + L_{0,t-1} + \gamma L_{1,t-1}
93\]
94\[
95RMS = \sqrt{\frac{1}{n}\sum (L_0 - L_1)^2}
96\]
97\[
98Osc = (L_0 - L_1) / RMS
99\]
100"#,
101 gold_standard_file: "laguerre_oscillator.json",
102 category: "Ehlers DSP",
103};
104
105#[cfg(test)]
106mod tests {
107 use super::*;
108 use crate::traits::Next;
109 use proptest::prelude::*;
110
111 #[test]
112 fn test_laguerre_oscillator_basic() {
113 let mut lo = LaguerreOscillator::new(30, 0.5, 100);
114 let inputs = vec![10.0, 11.0, 12.0, 13.0, 14.0];
115 for input in inputs {
116 let res = lo.next(input);
117 assert!(!res.is_nan());
118 }
119 }
120
121 proptest! {
122 #[test]
123 fn test_laguerre_oscillator_parity(
124 inputs in prop::collection::vec(1.0..100.0, 110..200),
125 ) {
126 let length = 30;
127 let gamma = 0.5;
128 let rms_period = 100;
129 let mut lo = LaguerreOscillator::new(length, gamma, rms_period);
130 let streaming_results: Vec<f64> = inputs.iter().map(|&x| lo.next(x)).collect();
131
132 let mut us = UltimateSmoother::new(length);
134 let l0_vals: Vec<f64> = inputs.iter().map(|&x| us.next(x)).collect();
135
136 let mut batch_results = Vec::with_capacity(inputs.len());
137 let mut l1 = 0.0;
138 let mut diffs = Vec::new();
139
140 for (i, &l0) in l0_vals.iter().enumerate() {
141 if i == 0 {
142 l1 = l0;
143 diffs.push(0.0);
144 batch_results.push(0.0);
145 } else {
146 let prev_l0 = l0_vals[i-1];
147 l1 = -gamma * l0 + prev_l0 + gamma * l1;
148 let diff = l0 - l1;
149 diffs.push(diff);
150
151 let start = if diffs.len() > rms_period { diffs.len() - rms_period } else { 0 };
152 let window = &diffs[start..];
153 let sum_sq: f64 = window.iter().map(|&x| x*x).sum();
154 let rms = (sum_sq / window.len() as f64).sqrt();
155
156 let res = if rms != 0.0 { diff / rms } else { 0.0 };
157 batch_results.push(res);
158 }
159 }
160
161 for (s, b) in streaming_results.iter().zip(batch_results.iter()) {
162 approx::assert_relative_eq!(s, b, epsilon = 1e-10);
163 }
164 }
165 }
166}