Skip to main content

quantwave_core/indicators/
reflex.rs

1use crate::indicators::metadata::{IndicatorMetadata, ParamDef};
2use crate::indicators::super_smoother::SuperSmoother;
3use crate::traits::Next;
4use crate::utils::RingBuffer as VecDeque;
5
6/// Ehlers Reflex
7///
8/// Based on John Ehlers' "Reflex: A New Zero-Lag Indicator" (2020).
9/// It is a zero-lag averaging indicator that synchronizes with the cycle component.
10#[derive(Debug, Clone)]
11pub struct Reflex {
12    length: usize,
13    smoother: SuperSmoother,
14    filt_history: VecDeque<f64>,
15    ms: f64,
16}
17
18impl Reflex {
19    pub fn new(length: usize) -> Self {
20        Self {
21            length,
22            // SuperSmoother period is half the cycle length as per paper
23            smoother: SuperSmoother::new(length / 2),
24            filt_history: VecDeque::with_capacity(length + 1),
25            ms: 0.0,
26        }
27    }
28}
29
30impl Next<f64> for Reflex {
31    type Output = f64;
32
33    fn next(&mut self, input: f64) -> Self::Output {
34        let filt = self.smoother.next(input);
35        self.filt_history.push_front(filt);
36
37        if self.filt_history.len() <= self.length {
38            return 0.0;
39        }
40
41        if self.filt_history.len() > self.length + 1 {
42            self.filt_history.pop_back();
43        }
44
45        let filt_n = self.filt_history[self.length];
46        let slope = (filt_n - filt) / self.length as f64;
47
48        let mut sum = 0.0;
49        for count in 1..=self.length {
50            let val = self.filt_history[count];
51            sum += (filt + count as f64 * slope) - val;
52        }
53        sum /= self.length as f64;
54
55        self.ms = 0.04 * sum * sum + 0.96 * self.ms;
56
57        if self.ms > 0.0 {
58            sum / self.ms.sqrt()
59        } else {
60            0.0
61        }
62    }
63}
64
65pub const REFLEX_METADATA: IndicatorMetadata = IndicatorMetadata {
66    name: "Reflex",
67    description: "A zero-lag averaging indicator designed to synchronize with the cycle component in price data.",
68    usage: "Use to identify cyclic reversals with minimal lag. It is more sensitive to significant reversals than standard moving averages.",
69    keywords: &["zero-lag", "cycle", "ehlers", "dsp", "oscillator"],
70    ehlers_summary: "Ehlers introduces Reflex as a way to reduce lag in averaging indicators by measuring the difference between the current SuperSmoother value and its historical values, adjusted for a linear slope. This 'reflexes' the indicator to show reversals as they happen rather than after the fact.",
71    params: &[ParamDef {
72        name: "length",
73        default: "20",
74        description: "Assumed cycle period",
75    }],
76    formula_source: "https://github.com/lavs9/quantwave/blob/main/references/traderstipsreference/implemented/TRADERS’ TIPS - FEBRUARY 2020.html",
77    formula_latex: r#"
78\[
79Filt = \text{SuperSmoother}(Price, Length/2)
80\]
81\[
82Slope = \frac{Filt_{t-Length} - Filt_t}{Length}
83\]
84\[
85Sum = \frac{1}{Length} \sum_{n=1}^{Length} (Filt_t + n \cdot Slope - Filt_{t-n})
86\]
87\[
88MS = 0.04 \cdot Sum^2 + 0.96 \cdot MS_{t-1}
89\]
90\[
91Reflex = \frac{Sum}{\sqrt{MS}}
92\]
93"#,
94    gold_standard_file: "reflex.json",
95    category: "Ehlers DSP",
96};
97
98#[cfg(test)]
99mod tests {
100    use super::*;
101    use crate::traits::Next;
102    use proptest::prelude::*;
103
104    #[test]
105    fn test_reflex_basic() {
106        let mut reflex = Reflex::new(20);
107        let inputs = vec![
108            10.0, 11.0, 12.0, 13.0, 14.0, 15.0, 16.0, 17.0, 18.0, 19.0, 20.0, 21.0, 22.0, 23.0,
109            24.0, 25.0, 26.0, 27.0, 28.0, 29.0, 30.0,
110        ];
111        for input in inputs {
112            let res = reflex.next(input);
113            assert!(!res.is_nan());
114        }
115    }
116
117    proptest! {
118        #[test]
119        fn test_reflex_parity(
120            inputs in prop::collection::vec(1.0..100.0, 50..100),
121        ) {
122            let length = 20;
123            let mut reflex = Reflex::new(length);
124            let streaming_results: Vec<f64> = inputs.iter().map(|&x| reflex.next(x)).collect();
125
126            // Batch implementation
127            let mut batch_results = Vec::with_capacity(inputs.len());
128            let mut smoother = SuperSmoother::new(length / 2);
129            let mut filt_vals = Vec::new();
130            let mut ms = 0.0;
131
132            for (i, &input) in inputs.iter().enumerate() {
133                let filt = smoother.next(input);
134                filt_vals.push(filt);
135
136                if filt_vals.len() <= length {
137                    batch_results.push(0.0);
138                    continue;
139                }
140
141                let filt_now = filt_vals[i];
142                let filt_n = filt_vals[i - length];
143                let slope = (filt_n - filt_now) / length as f64;
144
145                let mut sum = 0.0;
146                for count in 1..=length {
147                    let val = filt_vals[i - count];
148                    sum += (filt_now + count as f64 * slope) - val;
149                }
150                sum /= length as f64;
151
152                ms = 0.04 * sum * sum + 0.96 * ms;
153                let res = if ms > 0.0 { sum / ms.sqrt() } else { 0.0 };
154                batch_results.push(res);
155            }
156
157            for (s, b) in streaming_results.iter().zip(batch_results.iter()) {
158                approx::assert_relative_eq!(s, b, epsilon = 1e-10);
159            }
160        }
161    }
162}