quantwave_core/indicators/
trendflex.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 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 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 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}