quantwave_core/indicators/
reverse_ema.rs1use crate::indicators::metadata::{IndicatorMetadata, ParamDef};
2use crate::traits::Next;
3
4#[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 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 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}