Skip to main content

quantwave_core/indicators/
zero_lag.rs

1use crate::indicators::metadata::{IndicatorMetadata, ParamDef};
2use crate::indicators::smoothing::EMA;
3use crate::traits::Next;
4
5/// ZeroLag Indicator
6///
7/// Based on John Ehlers' "Zero Lag (well, almost)"
8/// The indicator acknowledgement that the EMA filter has an error term: Error = Price - EMA[1].
9/// It introduces this error term into the equation in addition to the value of the new data sample,
10/// and applies a gain term to minimize the lag.
11#[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); // (EC, EMA)
33
34    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    params: &[
73        ParamDef {
74            name: "length",
75            default: "20",
76            description: "Equivalent SMA length",
77        },
78        ParamDef {
79            name: "gain_limit",
80            default: "50.0",
81            description: "Gain limit (divided by 10 for actual gain)",
82        },
83    ],
84    formula_source: "https://github.com/lavs9/quantwave/blob/main/references/Ehlers%20Papers/implemented/ZeroLag.pdf",
85    formula_latex: r#"
86\[
87\alpha = \frac{2}{Length + 1}
88\]
89\[
90EMA = \alpha \times Close + (1 - \alpha) \times EMA_{t-1}
91\]
92\[
93EC = \alpha \times (EMA + Gain \times (Close - EC_{t-1})) + (1 - \alpha) \times EC_{t-1}
94\]
95"#,
96    gold_standard_file: "zero_lag.json",
97    category: "Ehlers DSP",
98};
99
100#[cfg(test)]
101mod tests {
102    use super::*;
103    use crate::traits::Next;
104    use proptest::prelude::*;
105
106    #[test]
107    fn test_zero_lag_basic() {
108        let mut zl = ZeroLag::new(20, 50.0);
109        let inputs = vec![10.0, 11.0, 12.0, 11.0, 10.0];
110        for input in inputs {
111            let (ec, ema) = zl.next(input);
112            println!("Input: {}, EC: {}, EMA: {}", input, ec, ema);
113            assert!(!ec.is_nan());
114            assert!(!ema.is_nan());
115        }
116    }
117
118    proptest! {
119        #[test]
120        fn test_zero_lag_parity(
121            inputs in prop::collection::vec(1.0..100.0, 10..100),
122        ) {
123            let length = 20;
124            let gain_limit = 50.0;
125            let mut zl = ZeroLag::new(length, gain_limit);
126
127            let streaming_results: Vec<(f64, f64)> = inputs.iter().map(|&x| zl.next(x)).collect();
128
129            // Batch implementation
130            let mut batch_results = Vec::with_capacity(inputs.len());
131            let alpha = 2.0 / (length as f64 + 1.0);
132            let mut ema_prev = None;
133            let mut ec_prev = None;
134
135            for &input in &inputs {
136                let ema = match ema_prev {
137                    Some(prev) => alpha * input + (1.0 - alpha) * prev,
138                    None => input,
139                };
140                ema_prev = Some(ema);
141
142                let ec = match ec_prev {
143                    Some(prev) => {
144                        let mut least_err = f64::MAX;
145                        let mut best_g = 0.0;
146                        for i in -50..=50 {
147                            let g = i as f64 / 10.0;
148                            let ec_val: f64 = alpha * (ema + g * (input - prev)) + (1.0 - alpha) * prev;
149                            let err = (input - ec_val).abs();
150                            if err < least_err {
151                                least_err = err;
152                                best_g = g;
153                            }
154                        }
155                        alpha * (ema + best_g * (input - prev)) + (1.0 - alpha) * prev
156                    }
157                    None => input,
158                };
159                ec_prev = Some(ec);
160                batch_results.push((ec, ema));
161            }
162
163            for (s, b) in streaming_results.iter().zip(batch_results.iter()) {
164                approx::assert_relative_eq!(s.0, b.0, epsilon = 1e-10);
165                approx::assert_relative_eq!(s.1, b.1, epsilon = 1e-10);
166            }
167        }
168    }
169}