quantwave_core/indicators/
zero_lag.rs1use crate::indicators::metadata::{IndicatorMetadata, ParamDef};
2use crate::indicators::smoothing::EMA;
3use crate::traits::Next;
4
5#[derive(Debug, Clone)]
12pub struct ZeroLag {
13 alpha: f64,
14 gain_limit: f64,
15 ema: EMA,
16 ec_prev: Option<f64>,
17}
18
19impl ZeroLag {
20 pub fn new(length: usize, gain_limit: f64) -> Self {
21 let alpha = 2.0 / (length as f64 + 1.0);
22 Self {
23 alpha,
24 gain_limit,
25 ema: EMA::new(length),
26 ec_prev: None,
27 }
28 }
29}
30
31impl Next<f64> for ZeroLag {
32 type Output = (f64, f64); fn next(&mut self, input: f64) -> Self::Output {
35 let ema_val = self.ema.next(input);
36
37 let ec_prev = match self.ec_prev {
38 Some(prev) => prev,
39 None => {
40 self.ec_prev = Some(input);
41 return (input, ema_val);
42 }
43 };
44
45 let mut least_error = f64::MAX;
46 let mut best_gain = 0.0;
47
48 let gain_limit_steps = (self.gain_limit) as i32;
49
50 for i in -gain_limit_steps..=gain_limit_steps {
51 let gain = i as f64 / 10.0;
52 let ec =
53 self.alpha * (ema_val + gain * (input - ec_prev)) + (1.0 - self.alpha) * ec_prev;
54 let error = (input - ec).abs();
55 if error < least_error {
56 least_error = error;
57 best_gain = gain;
58 }
59 }
60
61 let ec =
62 self.alpha * (ema_val + best_gain * (input - ec_prev)) + (1.0 - self.alpha) * ec_prev;
63 self.ec_prev = Some(ec);
64
65 (ec, ema_val)
66 }
67}
68
69pub const ZERO_LAG_METADATA: IndicatorMetadata = IndicatorMetadata {
70 name: "Zero Lag EC",
71 description: "Zero Lag Error Corrected EMA attempts to eliminate lag by adding an error term to the EMA.",
72 usage: "Use as a near-zero-lag moving average for trend-following systems. The error-correction term removes the lag inherent in the standard EMA without introducing significant overshoot.",
73 keywords: &["moving-average", "zero-lag", "ehlers", "ema", "smoothing"],
74 ehlers_summary: "Ehlers introduces the Zero Lag indicator in Cybernetic Analysis as an EMA with an added error-correction term that subtracts the average lag from the output. The resulting EC (Error Corrected) line tracks price with near-zero delay while the ZL-EMA provides a smoothed reference, with crossovers between them providing trade signals.",
75 params: &[
76 ParamDef {
77 name: "length",
78 default: "20",
79 description: "Equivalent SMA length",
80 },
81 ParamDef {
82 name: "gain_limit",
83 default: "50.0",
84 description: "Gain limit (divided by 10 for actual gain)",
85 },
86 ],
87 formula_source: "https://github.com/lavs9/quantwave/blob/main/references/Ehlers%20Papers/implemented/ZeroLag.pdf",
88 formula_latex: r#"
89\[
90\alpha = \frac{2}{Length + 1}
91\]
92\[
93EMA = \alpha \times Close + (1 - \alpha) \times EMA_{t-1}
94\]
95\[
96EC = \alpha \times (EMA + Gain \times (Close - EC_{t-1})) + (1 - \alpha) \times EC_{t-1}
97\]
98"#,
99 gold_standard_file: "zero_lag.json",
100 category: "Ehlers DSP",
101};
102
103#[cfg(test)]
104mod tests {
105 use super::*;
106 use crate::traits::Next;
107 use proptest::prelude::*;
108
109 #[test]
110 fn test_zero_lag_basic() {
111 let mut zl = ZeroLag::new(20, 50.0);
112 let inputs = vec![10.0, 11.0, 12.0, 11.0, 10.0];
113 for input in inputs {
114 let (ec, ema) = zl.next(input);
115 println!("Input: {}, EC: {}, EMA: {}", input, ec, ema);
116 assert!(!ec.is_nan());
117 assert!(!ema.is_nan());
118 }
119 }
120
121 proptest! {
122 #[test]
123 fn test_zero_lag_parity(
124 inputs in prop::collection::vec(1.0..100.0, 10..100),
125 ) {
126 let length = 20;
127 let gain_limit = 50.0;
128 let mut zl = ZeroLag::new(length, gain_limit);
129
130 let streaming_results: Vec<(f64, f64)> = inputs.iter().map(|&x| zl.next(x)).collect();
131
132 let mut batch_results = Vec::with_capacity(inputs.len());
134 let alpha = 2.0 / (length as f64 + 1.0);
135 let mut ema_prev = None;
136 let mut ec_prev = None;
137
138 for &input in &inputs {
139 let ema = match ema_prev {
140 Some(prev) => alpha * input + (1.0 - alpha) * prev,
141 None => input,
142 };
143 ema_prev = Some(ema);
144
145 let ec = match ec_prev {
146 Some(prev) => {
147 let mut least_err = f64::MAX;
148 let mut best_g = 0.0;
149 for i in -50..=50 {
150 let g = i as f64 / 10.0;
151 let ec_val: f64 = alpha * (ema + g * (input - prev)) + (1.0 - alpha) * prev;
152 let err = (input - ec_val).abs();
153 if err < least_err {
154 least_err = err;
155 best_g = g;
156 }
157 }
158 alpha * (ema + best_g * (input - prev)) + (1.0 - alpha) * prev
159 }
160 None => input,
161 };
162 ec_prev = Some(ec);
163 batch_results.push((ec, ema));
164 }
165
166 for (s, b) in streaming_results.iter().zip(batch_results.iter()) {
167 approx::assert_relative_eq!(s.0, b.0, epsilon = 1e-10);
168 approx::assert_relative_eq!(s.1, b.1, epsilon = 1e-10);
169 }
170 }
171 }
172}