quantwave_core/indicators/
ehlers_loops.rs1use crate::indicators::metadata::{IndicatorMetadata, ParamDef};
2use crate::traits::Next;
3use std::f64::consts::PI;
4
5#[derive(Debug, Clone)]
12pub struct EhlersLoops {
13 price_filter: NormalizedRoofing,
14 volume_filter: NormalizedRoofing,
15}
16
17#[derive(Debug, Clone)]
18struct NormalizedRoofing {
19 hpc1: f64,
20 hpc2: f64,
21 hpc3: f64,
22 ssc1: f64,
23 ssc2: f64,
24 ssc3: f64,
25 rms_alpha: f64,
26
27 input_history: [f64; 2],
28 hp_history: [f64; 2],
29 ss_history: [f64; 2],
30 ms: f64,
31 count: usize,
32}
33
34impl NormalizedRoofing {
35 fn new(lp_period: usize, hp_period: usize, rms_alpha: f64) -> Self {
36 let hp_period_f = hp_period as f64;
37 let lp_period_f = lp_period as f64;
38
39 let hpa1 = (-1.414 * PI / hp_period_f).exp();
41 let hpb1 = 2.0 * hpa1 * (1.414 * PI / hp_period_f).cos();
42 let hpc2 = hpb1;
43 let hpc3 = -hpa1 * hpa1;
44 let hpc1 = (1.0 + hpc2 - hpc3) / 4.0;
45
46 let ssa1 = (-1.414 * PI / lp_period_f).exp();
48 let ssb1 = 2.0 * ssa1 * (1.414 * PI / lp_period_f).cos();
49 let ssc2 = ssb1;
50 let ssc3 = -ssa1 * ssa1;
51 let ssc1 = 1.0 - ssc2 - ssc3;
52
53 Self {
54 hpc1,
55 hpc2,
56 hpc3,
57 ssc1,
58 ssc2,
59 ssc3,
60 rms_alpha,
61 input_history: [0.0; 2],
62 hp_history: [0.0; 2],
63 ss_history: [0.0; 2],
64 ms: 0.0,
65 count: 0,
66 }
67 }
68
69 fn next(&mut self, input: f64) -> f64 {
70 self.count += 1;
71
72 let hp = if self.count < 3 {
74 0.0
75 } else {
76 self.hpc1 * (input - 2.0 * self.input_history[0] + self.input_history[1])
77 + self.hpc2 * self.hp_history[0]
78 + self.hpc3 * self.hp_history[1]
79 };
80
81 let ss = if self.count < 3 {
83 0.0
84 } else {
85 self.ssc1 * (hp + self.hp_history[0]) / 2.0
86 + self.ssc2 * self.ss_history[0]
87 + self.ssc3 * self.ss_history[1]
88 };
89
90 if self.count == 1 {
92 self.ms = ss * ss;
93 } else {
94 self.ms = self.rms_alpha * (ss * ss) + (1.0 - self.rms_alpha) * self.ms;
95 }
96
97 let res = if self.ms > 0.0 {
98 ss / self.ms.sqrt()
99 } else {
100 0.0
101 };
102
103 self.hp_history[1] = self.hp_history[0];
105 self.hp_history[0] = hp;
106 self.input_history[1] = self.input_history[0];
107 self.input_history[0] = input;
108 self.ss_history[1] = self.ss_history[0];
109 self.ss_history[0] = ss;
110
111 res
112 }
113}
114
115impl EhlersLoops {
116 pub fn new(lp_period: usize, hp_period: usize) -> Self {
117 Self::with_rms_alpha(lp_period, hp_period, 0.0242)
119 }
120
121 pub fn with_rms_alpha(lp_period: usize, hp_period: usize, rms_alpha: f64) -> Self {
122 Self {
123 price_filter: NormalizedRoofing::new(lp_period, hp_period, rms_alpha),
124 volume_filter: NormalizedRoofing::new(lp_period, hp_period, rms_alpha),
125 }
126 }
127}
128
129impl Next<(f64, f64)> for EhlersLoops {
130 type Output = (f64, f64); fn next(&mut self, (price, volume): (f64, f64)) -> Self::Output {
133 (
134 self.price_filter.next(price),
135 self.volume_filter.next(volume),
136 )
137 }
138}
139
140pub const EHLERS_LOOPS_METADATA: IndicatorMetadata = IndicatorMetadata {
141 name: "Ehlers Loops",
142 description: "Converts price and volume into normalized standard deviation units for scatter plot analysis.",
143 params: &[
144 ParamDef {
145 name: "lp_period",
146 default: "20",
147 description: "Low-pass filter period (SuperSmoother)",
148 },
149 ParamDef {
150 name: "hp_period",
151 default: "125",
152 description: "High-pass filter period (Butterworth)",
153 },
154 ],
155 formula_source: "https://github.com/lavs9/quantwave/blob/main/references/traderstipsreference/TRADERS’%20TIPS%20-%20JUNE%202022.html",
156 formula_latex: r#"
157\[
158HP = c_1 (Price - 2 Price_{t-1} + Price_{t-2}) + c_2 HP_{t-1} + c_3 HP_{t-2}
159\]
160\[
161SS = s_1 \frac{HP + HP_{t-1}}{2} + s_2 SS_{t-1} + s_3 SS_{t-2}
162\]
163\[
164MS = \alpha SS^2 + (1 - \alpha) MS_{t-1}
165\]
166\[
167RMS = \frac{SS}{\sqrt{MS}}
168\]
169"#,
170 gold_standard_file: "ehlers_loops.json",
171 category: "Ehlers DSP",
172};
173
174#[cfg(test)]
175mod tests {
176 use super::*;
177 use crate::traits::Next;
178 use crate::test_utils::{load_gold_standard_loops, assert_indicator_parity_loops};
179 use proptest::prelude::*;
180
181 #[test]
182 fn test_ehlers_loops_gold_standard() {
183 let case = load_gold_standard_loops("ehlers_loops");
184 let el = EhlersLoops::new(20, 125);
185 assert_indicator_parity_loops(el, &case.input, &case.expected);
186 }
187
188 #[test]
189 fn test_ehlers_loops_basic() {
190 let mut el = EhlersLoops::new(20, 125);
191 let inputs = vec![(100.0, 1000.0), (101.0, 1100.0), (102.0, 1200.0)];
192 for input in inputs {
193 let (p_rms, v_rms) = el.next(input);
194 assert!(!p_rms.is_nan());
195 assert!(!v_rms.is_nan());
196 }
197 }
198
199 proptest! {
200 #[test]
201 fn test_ehlers_loops_parity(
202 prices in prop::collection::vec(1.0..100.0, 100..200),
203 volumes in prop::collection::vec(100.0..1000.0, 100..200),
204 ) {
205 let lp_period = 20;
206 let hp_period = 125;
207 let rms_alpha = 0.0242;
208 let mut el = EhlersLoops::with_rms_alpha(lp_period, hp_period, rms_alpha);
209
210 let min_len = prices.len().min(volumes.len());
211 let inputs: Vec<(f64, f64)> = prices[..min_len].iter().cloned().zip(volumes[..min_len].iter().cloned()).collect();
212 let streaming_results: Vec<(f64, f64)> = inputs.iter().map(|&x| el.next(x)).collect();
213
214 let mut price_results = Vec::with_capacity(inputs.len());
216 let hp_period_f = hp_period as f64;
217 let lp_period_f = lp_period as f64;
218
219 let hpa1 = (-1.414 * PI / hp_period_f).exp();
220 let hpb1 = 2.0 * hpa1 * (1.414 * PI / hp_period_f).cos();
221 let hpc2 = hpb1;
222 let hpc3 = -hpa1 * hpa1;
223 let hpc1 = (1.0 + hpc2 - hpc3) / 4.0;
224
225 let ssa1 = (-1.414 * PI / lp_period_f).exp();
226 let ssb1 = 2.0 * ssa1 * (1.414 * PI / lp_period_f).cos();
227 let ssc2 = ssb1;
228 let ssc3 = -ssa1 * ssa1;
229 let ssc1 = 1.0 - ssc2 - ssc3;
230
231 let mut p_input_hist = [0.0; 2];
232 let mut p_hp_hist = [0.0; 2];
233 let mut p_ss_hist = [0.0; 2];
234 let mut p_ms = 0.0;
235
236 for (i, &(p_input, _)) in inputs.iter().enumerate() {
237 let bar = i + 1;
238 let hp = if bar < 3 { 0.0 } else {
239 hpc1 * (p_input - 2.0 * p_input_hist[0] + p_input_hist[1]) + hpc2 * p_hp_hist[0] + hpc3 * p_hp_hist[1]
240 };
241 let ss = if bar < 3 { 0.0 } else {
242 ssc1 * (hp + p_hp_hist[0]) / 2.0 + ssc2 * p_ss_hist[0] + ssc3 * p_ss_hist[1]
243 };
244 if bar == 1 { p_ms = ss * ss; } else { p_ms = rms_alpha * ss * ss + (1.0 - rms_alpha) * p_ms; }
245 let res = if p_ms > 0.0 { ss / p_ms.sqrt() } else { 0.0 };
246
247 p_hp_hist[1] = p_hp_hist[0]; p_hp_hist[0] = hp;
248 p_input_hist[1] = p_input_hist[0]; p_input_hist[0] = p_input;
249 p_ss_hist[1] = p_ss_hist[0]; p_ss_hist[0] = ss;
250 price_results.push(res);
251 }
252
253 let mut vol_results = Vec::with_capacity(inputs.len());
255 let mut v_input_hist = [0.0; 2];
256 let mut v_hp_hist = [0.0; 2];
257 let mut v_ss_hist = [0.0; 2];
258 let mut v_ms = 0.0;
259
260 for (i, &(_, v_input)) in inputs.iter().enumerate() {
261 let bar = i + 1;
262 let hp = if bar < 3 { 0.0 } else {
263 hpc1 * (v_input - 2.0 * v_input_hist[0] + v_input_hist[1]) + hpc2 * v_hp_hist[0] + hpc3 * v_hp_hist[1]
264 };
265 let ss = if bar < 3 { 0.0 } else {
266 ssc1 * (hp + v_hp_hist[0]) / 2.0 + ssc2 * v_ss_hist[0] + ssc3 * v_ss_hist[1]
267 };
268 if bar == 1 { v_ms = ss * ss; } else { v_ms = rms_alpha * ss * ss + (1.0 - rms_alpha) * v_ms; }
269 let res = if v_ms > 0.0 { ss / v_ms.sqrt() } else { 0.0 };
270
271 v_hp_hist[1] = v_hp_hist[0]; v_hp_hist[0] = hp;
272 v_input_hist[1] = v_input_hist[0]; v_input_hist[0] = v_input;
273 v_ss_hist[1] = v_ss_hist[0]; v_ss_hist[0] = ss;
274 vol_results.push(res);
275 }
276
277 for (_i, (s, bp, bv)) in streaming_results.iter().zip(price_results.iter().zip(vol_results.iter())).map(|(s, (bp, bv))| (s, bp, bv)).enumerate() {
278 approx::assert_relative_eq!(s.0, *bp, epsilon = 1e-10);
279 approx::assert_relative_eq!(s.1, *bv, epsilon = 1e-10);
280 }
281 }
282 }
283}