Skip to main content

quantwave_core/indicators/
trendflex.rs

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