wickra_core/indicators/
trendflex.rs1#![allow(clippy::doc_markdown)]
3
4use std::collections::VecDeque;
5
6use crate::error::{Error, Result};
7use crate::indicators::super_smoother::SuperSmoother;
8use crate::traits::Indicator;
9
10#[derive(Debug, Clone)]
47pub struct Trendflex {
48 period: usize,
49 smoother: SuperSmoother,
50 filt: VecDeque<f64>,
51 ms: f64,
52 last: Option<f64>,
53}
54
55impl Trendflex {
56 pub fn new(period: usize) -> Result<Self> {
62 if period == 0 {
63 return Err(Error::PeriodZero);
64 }
65 if period > crate::error::MAX_PERIOD {
66 return Err(Error::InvalidPeriod {
67 message: crate::error::PERIOD_ABOVE_MAX,
68 });
69 }
70 Ok(Self {
71 period,
72 smoother: SuperSmoother::new(period)?,
73 filt: VecDeque::with_capacity(period + 1),
74 ms: 0.0,
75 last: None,
76 })
77 }
78
79 pub const fn period(&self) -> usize {
81 self.period
82 }
83
84 pub const fn value(&self) -> Option<f64> {
86 self.last
87 }
88}
89
90impl Indicator for Trendflex {
91 type Input = f64;
92 type Output = f64;
93
94 #[inline]
95 fn update(&mut self, price: f64) -> Option<f64> {
96 if !price.is_finite() {
97 return None;
98 }
99 let filt = self.smoother.update(price)?;
100 if self.filt.len() == self.period + 1 {
101 self.filt.pop_front();
102 }
103 self.filt.push_back(filt);
104 if self.filt.len() < self.period + 1 {
105 return None;
106 }
107 let newest = self.filt[self.period];
108 let mut sum = 0.0;
109 for i in 1..=self.period {
110 sum += newest - self.filt[self.period - i];
111 }
112 sum /= self.period as f64;
113 self.ms = 0.04 * sum * sum + 0.96 * self.ms;
114 let trendflex = if self.ms > 0.0 {
115 sum / self.ms.sqrt()
116 } else {
117 0.0
118 };
119 self.last = Some(trendflex);
120 Some(trendflex)
121 }
122
123 fn reset(&mut self) {
124 self.smoother.reset();
125 self.filt.clear();
126 self.ms = 0.0;
127 self.last = None;
128 }
129
130 #[inline]
131 fn warmup_period(&self) -> usize {
132 self.period + 1
133 }
134
135 #[inline]
136 fn is_ready(&self) -> bool {
137 self.last.is_some()
138 }
139
140 #[inline]
141 fn name(&self) -> &'static str {
142 "Trendflex"
143 }
144}
145
146#[cfg(test)]
147mod tests {
148 use super::*;
149 use crate::traits::BatchExt;
150 use approx::assert_relative_eq;
151
152 #[test]
153 fn rejects_zero_period() {
154 assert!(matches!(Trendflex::new(0), Err(Error::PeriodZero)));
155 }
156
157 #[test]
158 fn accessors_and_metadata() {
159 let t = Trendflex::new(20).unwrap();
160 assert_eq!(t.period(), 20);
161 assert_eq!(t.warmup_period(), 21);
162 assert_eq!(t.name(), "Trendflex");
163 assert!(!t.is_ready());
164 assert_eq!(t.value(), None);
165 }
166
167 #[test]
168 fn first_emission_at_warmup_period() {
169 let mut t = Trendflex::new(5).unwrap();
170 let xs: Vec<f64> = (0..12).map(f64::from).collect();
171 let out = t.batch(&xs);
172 for v in out.iter().take(5) {
173 assert!(v.is_none());
174 }
175 assert!(out[5].is_some());
176 }
177
178 #[test]
179 fn constant_input_is_zero() {
180 let mut t = Trendflex::new(10).unwrap();
181 for v in t.batch(&[50.0; 100]).into_iter().flatten() {
182 assert_relative_eq!(v, 0.0, epsilon = 1e-9);
183 }
184 }
185
186 #[test]
187 fn uptrend_is_positive() {
188 let mut t = Trendflex::new(10).unwrap();
190 let out: Vec<f64> = t
191 .batch(&(0..200).map(f64::from).collect::<Vec<_>>())
192 .into_iter()
193 .flatten()
194 .skip(100)
195 .collect();
196 for v in out {
197 assert!(v > 0.0, "uptrend should be positive, got {v}");
198 }
199 }
200
201 #[test]
202 fn downtrend_is_negative() {
203 let mut t = Trendflex::new(10).unwrap();
204 let out: Vec<f64> = t
205 .batch(&(0..200).map(|i| 200.0 - f64::from(i)).collect::<Vec<_>>())
206 .into_iter()
207 .flatten()
208 .skip(100)
209 .collect();
210 for v in out {
211 assert!(v < 0.0, "downtrend should be negative, got {v}");
212 }
213 }
214
215 #[test]
216 fn ignores_non_finite() {
217 let mut t = Trendflex::new(10).unwrap();
218 t.batch(&(0..40).map(f64::from).collect::<Vec<_>>());
219 let before = t.value();
220 assert_eq!(t.update(f64::NAN), None);
221 assert_eq!(t.value(), before);
223 }
224
225 #[test]
226 fn reset_clears_state() {
227 let mut t = Trendflex::new(10).unwrap();
228 t.batch(&(0..40).map(f64::from).collect::<Vec<_>>());
229 assert!(t.is_ready());
230 t.reset();
231 assert!(!t.is_ready());
232 assert_eq!(t.value(), None);
233 }
234
235 #[test]
236 fn batch_equals_streaming() {
237 let xs: Vec<f64> = (0..120)
238 .map(|i| 100.0 + (f64::from(i) * 0.25).sin() * 9.0)
239 .collect();
240 let batch = Trendflex::new(20).unwrap().batch(&xs);
241 let mut b = Trendflex::new(20).unwrap();
242 let streamed: Vec<_> = xs.iter().map(|x| b.update(*x)).collect();
243 assert_eq!(batch, streamed);
244 }
245}