Skip to main content

quantwave_core/indicators/
trendflex.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 Trendflex
7///
8/// Based on John Ehlers' "Reflex: A New Zero-Lag Indicator" (2020).
9/// It is a zero-lag averaging indicator that retains the trend component.
10#[derive(Debug, Clone)]
11pub struct Trendflex {
12    length: usize,
13    smoother: SuperSmoother,
14    filt_history: VecDeque<f64>,
15    ms: f64,
16}
17
18impl Trendflex {
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 Trendflex {
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 mut sum = 0.0;
46        for count in 1..=self.length {
47            let val = self.filt_history[count];
48            sum += filt - val;
49        }
50        sum /= self.length as f64;
51
52        self.ms = 0.04 * sum * sum + 0.96 * self.ms;
53
54        if self.ms > 0.0 {
55            sum / self.ms.sqrt()
56        } else {
57            0.0
58        }
59    }
60}
61
62pub const TRENDFLEX_METADATA: IndicatorMetadata = IndicatorMetadata {
63    name: "Trendflex",
64    description: "A zero-lag averaging indicator designed to retain the trend component while reducing lag.",
65    usage: "Use to recognize enduring trends with minimal lag. It is better at identifying the start of a new trend than standard moving averages.",
66    keywords: &["zero-lag", "trend", "ehlers", "dsp", "oscillator"],
67    ehlers_summary: "Trendflex is the companion to Reflex. While Reflex focuses on the cyclic component by removing the trend slope, Trendflex retains the trend information by measuring the cumulative difference between the current smoothed value and its history without slope adjustment.",
68    params: &[ParamDef {
69        name: "length",
70        default: "20",
71        description: "Assumed cycle period",
72    }],
73    formula_source: "https://github.com/lavs9/quantwave/blob/main/references/traderstipsreference/implemented/TRADERS’ TIPS - FEBRUARY 2020.html",
74    formula_latex: r#"
75\[
76Filt = \text{SuperSmoother}(Price, Length/2)
77\]
78\[
79Sum = \frac{1}{Length} \sum_{n=1}^{Length} (Filt_t - Filt_{t-n})
80\]
81\[
82MS = 0.04 \cdot Sum^2 + 0.96 \cdot MS_{t-1}
83\]
84\[
85Trendflex = \frac{Sum}{\sqrt{MS}}
86\]
87"#,
88    gold_standard_file: "trendflex.json",
89    category: "Ehlers DSP",
90};
91
92#[cfg(test)]
93mod tests {
94    use super::*;
95    use crate::traits::Next;
96    use proptest::prelude::*;
97
98    #[test]
99    fn test_trendflex_basic() {
100        let mut tf = Trendflex::new(20);
101        let inputs = vec![
102            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,
103            24.0, 25.0, 26.0, 27.0, 28.0, 29.0, 30.0,
104        ];
105        for input in inputs {
106            let res = tf.next(input);
107            assert!(!res.is_nan());
108        }
109    }
110
111    proptest! {
112        #[test]
113        fn test_trendflex_parity(
114            inputs in prop::collection::vec(1.0..100.0, 50..100),
115        ) {
116            let length = 20;
117            let mut tf = Trendflex::new(length);
118            let streaming_results: Vec<f64> = inputs.iter().map(|&x| tf.next(x)).collect();
119
120            // Batch implementation
121            let mut batch_results = Vec::with_capacity(inputs.len());
122            let mut smoother = SuperSmoother::new(length / 2);
123            let mut filt_vals = Vec::new();
124            let mut ms = 0.0;
125
126            for (i, &input) in inputs.iter().enumerate() {
127                let filt = smoother.next(input);
128                filt_vals.push(filt);
129
130                if filt_vals.len() <= length {
131                    batch_results.push(0.0);
132                    continue;
133                }
134
135                let filt_now = filt_vals[i];
136                let mut sum = 0.0;
137                for count in 1..=length {
138                    let val = filt_vals[i - count];
139                    sum += filt_now - val;
140                }
141                sum /= length as f64;
142
143                ms = 0.04 * sum * sum + 0.96 * ms;
144                let res = if ms > 0.0 { sum / ms.sqrt() } else { 0.0 };
145                batch_results.push(res);
146            }
147
148            for (s, b) in streaming_results.iter().zip(batch_results.iter()) {
149                approx::assert_relative_eq!(s, b, epsilon = 1e-10);
150            }
151        }
152    }
153}