quantwave_core/indicators/
trendflex.rs1use crate::indicators::metadata::{IndicatorMetadata, ParamDef};
2use crate::indicators::super_smoother::SuperSmoother;
3use crate::traits::Next;
4use crate::utils::RingBuffer as 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: &[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 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}