quantwave_core/indicators/
reflex.rs1use crate::indicators::metadata::{IndicatorMetadata, ParamDef};
2use crate::traits::Next;
3use crate::indicators::super_smoother::SuperSmoother;
4use std::collections::VecDeque;
5
6#[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 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: &[
72 ParamDef { name: "length", default: "20", description: "Assumed cycle period" },
73 ],
74 formula_source: "https://github.com/lavs9/quantwave/blob/main/references/traderstipsreference/implemented/TRADERS’ TIPS - FEBRUARY 2020.html",
75 formula_latex: r#"
76\[
77Filt = \text{SuperSmoother}(Price, Length/2)
78\]
79\[
80Slope = \frac{Filt_{t-Length} - Filt_t}{Length}
81\]
82\[
83Sum = \frac{1}{Length} \sum_{n=1}^{Length} (Filt_t + n \cdot Slope - Filt_{t-n})
84\]
85\[
86MS = 0.04 \cdot Sum^2 + 0.96 \cdot MS_{t-1}
87\]
88\[
89Reflex = \frac{Sum}{\sqrt{MS}}
90\]
91"#,
92 gold_standard_file: "reflex.json",
93 category: "Ehlers DSP",
94};
95
96#[cfg(test)]
97mod tests {
98 use super::*;
99 use crate::traits::Next;
100 use proptest::prelude::*;
101
102 #[test]
103 fn test_reflex_basic() {
104 let mut reflex = Reflex::new(20);
105 let inputs = vec![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, 24.0, 25.0, 26.0, 27.0, 28.0, 29.0, 30.0];
106 for input in inputs {
107 let res = reflex.next(input);
108 assert!(!res.is_nan());
109 }
110 }
111
112 proptest! {
113 #[test]
114 fn test_reflex_parity(
115 inputs in prop::collection::vec(1.0..100.0, 50..100),
116 ) {
117 let length = 20;
118 let mut reflex = Reflex::new(length);
119 let streaming_results: Vec<f64> = inputs.iter().map(|&x| reflex.next(x)).collect();
120
121 let mut batch_results = Vec::with_capacity(inputs.len());
123 let mut smoother = SuperSmoother::new(length / 2);
124 let mut filt_vals = Vec::new();
125 let mut ms = 0.0;
126
127 for (i, &input) in inputs.iter().enumerate() {
128 let filt = smoother.next(input);
129 filt_vals.push(filt);
130
131 if filt_vals.len() <= length {
132 batch_results.push(0.0);
133 continue;
134 }
135
136 let filt_now = filt_vals[i];
137 let filt_n = filt_vals[i - length];
138 let slope = (filt_n - filt_now) / length as f64;
139
140 let mut sum = 0.0;
141 for count in 1..=length {
142 let val = filt_vals[i - count];
143 sum += (filt_now + count as f64 * slope) - val;
144 }
145 sum /= length as f64;
146
147 ms = 0.04 * sum * sum + 0.96 * ms;
148 let res = if ms > 0.0 { sum / ms.sqrt() } else { 0.0 };
149 batch_results.push(res);
150 }
151
152 for (s, b) in streaming_results.iter().zip(batch_results.iter()) {
153 approx::assert_relative_eq!(s, b, epsilon = 1e-10);
154 }
155 }
156 }
157}