Skip to main content

quantwave_core/indicators/
reverse_ema.rs

1use crate::indicators::metadata::{IndicatorMetadata, ParamDef};
2use crate::traits::Next;
3
4/// Reverse EMA
5///
6/// Based on John Ehlers' article "The Reverse EMA Indicator" (TASC September 2017).
7/// This indicator provides a causal forward and backward EMA that minimizes lag.
8/// It uses a series of filters to align the EMA and reduce aliasing.
9///
10/// By varying the alpha parameter, it can display trend or cycle information with very low lag.
11/// Typical values: Trend = 0.05, Cycle = 0.3.
12#[derive(Debug, Clone)]
13pub struct ReverseEMA {
14    alpha: f64,
15    prev_ema: f64,
16    prev_re: [f64; 8],
17    count: usize,
18}
19
20impl ReverseEMA {
21    pub fn new(alpha: f64) -> Self {
22        Self {
23            alpha,
24            prev_ema: 0.0,
25            prev_re: [0.0; 8],
26            count: 0,
27        }
28    }
29}
30
31impl Next<f64> for ReverseEMA {
32    type Output = f64;
33
34    fn next(&mut self, input: f64) -> Self::Output {
35        self.count += 1;
36
37        let cc = 1.0 - self.alpha;
38
39        if self.count == 1 {
40            self.prev_ema = input;
41            let mut val = (1.0 + cc) * input;
42            self.prev_re[0] = val;
43            let mut p = 2.0;
44            for i in 1..8 {
45                val = (cc.powf(p) + 1.0) * val;
46                self.prev_re[i] = val;
47                p *= 2.0;
48            }
49        }
50
51        let ema_now = self.alpha * input + cc * self.prev_ema;
52
53        let mut re_now = [0.0; 8];
54        re_now[0] = cc * ema_now + self.prev_ema;
55
56        let mut p = 2.0;
57        for i in 1..8 {
58            re_now[i] = cc.powf(p) * re_now[i - 1] + self.prev_re[i - 1];
59            p *= 2.0;
60        }
61
62        let wave = ema_now - self.alpha * re_now[7];
63
64        self.prev_ema = ema_now;
65        self.prev_re = re_now;
66
67        wave
68    }
69}
70
71pub const REVERSE_EMA_METADATA: IndicatorMetadata = IndicatorMetadata {
72    name: "Reverse EMA",
73    description: "A causal forward and backward EMA indicator that minimizes lag using a series of alignment filters.",
74    usage: "Use to identify trends or cycles with minimal lag. Higher alpha values (e.g., 0.3) isolate cycles, while lower values (e.g., 0.05) isolate trends.",
75    keywords: &["ema", "lag", "ehlers", "oscillator", "zero-lag"],
76    ehlers_summary: "Ehlers' Reverse EMA approximates a non-causal zero-lag filter by using a product series of Z-transform components. It achieves double smoothing at high frequencies and mitigates spectral dilation at low frequencies, providing a unique balance of smoothness and responsiveness.",
77    params: &[ParamDef {
78        name: "alpha",
79        default: "0.1",
80        description: "Smoothing factor (0.0 to 1.0)",
81    }],
82    formula_source: "https://github.com/lavs9/quantwave/blob/main/references/traderstipsreference/TRADERS%E2%80%99%20TIPS%20-%20SEPTEMBER%202017.html",
83    formula_latex: r#"
84\[
85EMA = \alpha \cdot Price + (1 - \alpha) \cdot EMA_{t-1}
86\]
87\[
88RE_1 = (1 - \alpha) \cdot EMA + EMA_{t-1}
89\]
90\[
91RE_i = (1 - \alpha)^{2^{i-1}} \cdot RE_{i-1} + RE_{i-1, t-1} \text{ for } i=2..8
92\]
93\[
94Wave = EMA - \alpha \cdot RE_8
95\]
96"#,
97    gold_standard_file: "reverse_ema.json",
98    category: "Ehlers DSP",
99};
100
101#[cfg(test)]
102mod tests {
103    use super::*;
104    use crate::traits::Next;
105    use proptest::prelude::*;
106
107    #[test]
108    fn test_reverse_ema_basic() {
109        let mut rema = ReverseEMA::new(0.1);
110        let inputs = vec![100.0, 101.0, 102.0, 101.0, 100.0];
111        for input in inputs {
112            let res = rema.next(input);
113            assert!(!res.is_nan());
114        }
115    }
116
117    proptest! {
118        #[test]
119        fn test_reverse_ema_parity(
120            inputs in prop::collection::vec(90.0..110.0, 50..100),
121        ) {
122            let alpha = 0.1;
123            let mut rema = ReverseEMA::new(alpha);
124            let streaming_results: Vec<f64> = inputs.iter().map(|&x| rema.next(x)).collect();
125
126            // Reference implementation (TradeStation style, starting from 0)
127            let mut ema = 0.0;
128            let mut re = [0.0; 8];
129            let mut prev_ema = 0.0;
130            let mut prev_re = [0.0; 8];
131            let cc = 1.0 - alpha;
132            let mut batch_results = Vec::with_capacity(inputs.len());
133
134            for (i, &input) in inputs.iter().enumerate() {
135                // To match our steady-state initialization in the test, we'd need to seed it.
136                // But let's just test that the logic is self-consistent.
137                // Our streaming version seeds it, so let's seed the batch version too.
138                if i == 0 {
139                    prev_ema = input;
140                    let mut val = (1.0 + cc) * input;
141                    prev_re[0] = val;
142                    let mut p = 2.0;
143                    for j in 1..8 {
144                        val = (cc.powf(p) + 1.0) * val;
145                        prev_re[j] = val;
146                        p *= 2.0;
147                    }
148                }
149
150                let ema = alpha * input + cc * prev_ema;
151                let mut re_now = [0.0; 8];
152                re_now[0] = cc * ema + prev_ema;
153                let mut p = 2.0;
154                for j in 1..8 {
155                    re_now[j] = cc.powf(p) * re_now[j-1] + prev_re[j-1];
156                    p *= 2.0;
157                }
158                let wave = ema - alpha * re_now[7];
159                batch_results.push(wave);
160                prev_ema = ema;
161                prev_re = re_now;
162            }
163
164            for (s, b) in streaming_results.iter().zip(batch_results.iter()) {
165                approx::assert_relative_eq!(s, b, epsilon = 1e-10);
166            }
167        }
168    }
169}