wickra_core/indicators/
ehlers_stochastic.rs1#![allow(clippy::doc_markdown)]
3
4use std::collections::VecDeque;
5
6use crate::error::{Error, Result};
7use crate::indicators::roofing_filter::RoofingFilter;
8use crate::traits::Indicator;
9
10#[derive(Debug, Clone)]
36pub struct EhlersStochastic {
37 period: usize,
38 roofing: RoofingFilter,
39 filtered_buf: VecDeque<f64>,
40 prev_stoch: f64,
42 has_prev: bool,
43 last_value: Option<f64>,
44}
45
46impl EhlersStochastic {
47 pub fn new(period: usize) -> Result<Self> {
53 if period == 0 {
54 return Err(Error::PeriodZero);
55 }
56 if period > crate::error::MAX_PERIOD {
57 return Err(Error::InvalidPeriod {
58 message: crate::error::PERIOD_ABOVE_MAX,
59 });
60 }
61 Ok(Self {
62 period,
63 roofing: RoofingFilter::new(10, 48)?,
65 filtered_buf: VecDeque::with_capacity(period),
66 prev_stoch: 0.0,
67 has_prev: false,
68 last_value: None,
69 })
70 }
71
72 pub const fn period(&self) -> usize {
74 self.period
75 }
76
77 pub const fn value(&self) -> Option<f64> {
79 self.last_value
80 }
81}
82
83impl Indicator for EhlersStochastic {
84 type Input = f64;
85 type Output = f64;
86
87 #[inline]
88 fn update(&mut self, input: f64) -> Option<f64> {
89 if !input.is_finite() {
90 return None;
91 }
92 let filtered = self.roofing.update(input)?;
93 if self.filtered_buf.len() == self.period {
94 self.filtered_buf.pop_front();
95 }
96 self.filtered_buf.push_back(filtered);
97 if self.filtered_buf.len() < self.period {
98 return None;
99 }
100 let max = self
101 .filtered_buf
102 .iter()
103 .copied()
104 .fold(f64::NEG_INFINITY, f64::max);
105 let min = self
106 .filtered_buf
107 .iter()
108 .copied()
109 .fold(f64::INFINITY, f64::min);
110 let range = max - min;
111 let raw = if range > 0.0 {
112 ((filtered - min) / range).mul_add(2.0, -1.0)
113 } else {
114 0.0
115 };
116 let smoothed = if self.has_prev {
118 f64::midpoint(raw, self.prev_stoch)
119 } else {
120 raw
121 };
122 self.prev_stoch = raw;
123 self.has_prev = true;
124 self.last_value = Some(smoothed);
125 Some(smoothed)
126 }
127
128 fn reset(&mut self) {
129 self.roofing.reset();
130 self.filtered_buf.clear();
131 self.prev_stoch = 0.0;
132 self.has_prev = false;
133 self.last_value = None;
134 }
135
136 #[inline]
137 fn warmup_period(&self) -> usize {
138 self.period + self.roofing.warmup_period() - 1
141 }
142
143 #[inline]
144 fn is_ready(&self) -> bool {
145 self.last_value.is_some()
146 }
147
148 #[inline]
149 fn name(&self) -> &'static str {
150 "EhlersStochastic"
151 }
152}
153
154#[cfg(test)]
155mod tests {
156 use super::*;
157 use crate::traits::BatchExt;
158
159 #[test]
160 fn new_rejects_zero_period() {
161 assert!(matches!(EhlersStochastic::new(0), Err(Error::PeriodZero)));
162 }
163
164 #[test]
165 fn accessors_and_metadata() {
166 let mut es = EhlersStochastic::new(20).unwrap();
167 assert_eq!(es.period(), 20);
168 assert_eq!(es.warmup_period(), 20);
169 assert_eq!(es.name(), "EhlersStochastic");
170 assert!(!es.is_ready());
171 let prices: Vec<f64> = (0..150)
172 .map(|i| 100.0 + (f64::from(i) * 0.4).sin() * 5.0)
173 .collect();
174 es.batch(&prices);
175 assert!(es.is_ready());
176 assert!(es.value().is_some());
177 }
178
179 #[test]
180 fn output_bounded_in_unit_interval() {
181 let prices: Vec<f64> = (0..200)
182 .map(|i| 100.0 + (f64::from(i) * 0.3).sin() * 5.0)
183 .collect();
184 let mut es = EhlersStochastic::new(20).unwrap();
185 for v in es.batch(&prices).into_iter().flatten() {
186 assert!((-1.0..=1.0).contains(&v), "value out of band: {v}");
187 }
188 }
189
190 #[test]
191 fn batch_equals_streaming() {
192 let prices: Vec<f64> = (0..150)
193 .map(|i| 100.0 + (f64::from(i) * 0.3).sin() * 5.0)
194 .collect();
195 let mut a = EhlersStochastic::new(20).unwrap();
196 let mut b = EhlersStochastic::new(20).unwrap();
197 let batch = a.batch(&prices);
198 let streamed: Vec<_> = prices.iter().map(|p| b.update(*p)).collect();
199 assert_eq!(batch, streamed);
200 }
201
202 #[test]
203 fn ignores_non_finite_input() {
204 let mut es = EhlersStochastic::new(20).unwrap();
205 let prices: Vec<f64> = (0..150)
206 .map(|i| 100.0 + (f64::from(i) * 0.3).sin() * 5.0)
207 .collect();
208 es.batch(&prices);
209 let before = es.value();
210 assert!(before.is_some());
211 assert_eq!(es.update(f64::NAN), None);
212 }
213
214 #[test]
215 fn reset_clears_state() {
216 let mut es = EhlersStochastic::new(20).unwrap();
217 let prices: Vec<f64> = (0..150)
218 .map(|i| 100.0 + (f64::from(i) * 0.3).sin() * 5.0)
219 .collect();
220 es.batch(&prices);
221 assert!(es.is_ready());
222 es.reset();
223 assert!(!es.is_ready());
224 }
225
226 #[test]
227 fn flat_window_emits_zero() {
228 let mut es = EhlersStochastic::new(20).unwrap();
231 for v in es.batch(&[100.0_f64; 150]).into_iter().flatten() {
232 assert_eq!(v, 0.0);
233 }
234 }
235}