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