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