wickra_core/indicators/
empirical_mode_decomposition.rs1use std::collections::VecDeque;
4use std::f64::consts::PI;
5
6use crate::error::{Error, Result};
7use crate::indicators::super_smoother::SuperSmoother;
8use crate::traits::Indicator;
9
10#[derive(Debug, Clone)]
38pub struct EmpiricalModeDecomposition {
39 period: usize,
40 fraction: f64,
41 bandpass: f64,
42 prev_bp_1: f64,
43 prev_bp_2: f64,
44 prev_in_1: Option<f64>,
45 prev_in_2: Option<f64>,
46 beta: f64,
47 alpha: f64,
48 smoother: SuperSmoother,
49 peak_smoother: SuperSmoother,
50 valley_smoother: SuperSmoother,
51 bp_buf: VecDeque<f64>,
52 bp_history_len: usize,
53 last_value: Option<f64>,
54}
55
56impl EmpiricalModeDecomposition {
57 pub fn new(period: usize, fraction: f64) -> Result<Self> {
68 if period == 0 {
69 return Err(Error::PeriodZero);
70 }
71 if period > crate::error::MAX_PERIOD {
72 return Err(Error::InvalidPeriod {
73 message: crate::error::PERIOD_ABOVE_MAX,
74 });
75 }
76 if !fraction.is_finite() || fraction <= 0.0 || fraction > 1.0 {
77 return Err(Error::InvalidPeriod {
78 message: "fraction must be in (0, 1]",
79 });
80 }
81 let beta = (2.0 * PI / period as f64).cos();
82 let gamma = 1.0 / (2.0 * PI * 0.25 / period as f64).cos();
83 let alpha = gamma - (gamma * gamma - 1.0).sqrt();
84 let history = (period as f64 * fraction).round().max(1.0) as usize;
85 Ok(Self {
86 period,
87 fraction,
88 bandpass: 0.0,
89 prev_bp_1: 0.0,
90 prev_bp_2: 0.0,
91 prev_in_1: None,
92 prev_in_2: None,
93 beta,
94 alpha,
95 smoother: SuperSmoother::new(period.max(2))?,
96 peak_smoother: SuperSmoother::new(period.max(2))?,
97 valley_smoother: SuperSmoother::new(period.max(2))?,
98 bp_buf: VecDeque::with_capacity(history),
99 bp_history_len: history,
100 last_value: None,
101 })
102 }
103
104 pub const fn period(&self) -> usize {
106 self.period
107 }
108
109 pub const fn fraction(&self) -> f64 {
111 self.fraction
112 }
113
114 pub const fn value(&self) -> Option<f64> {
116 self.last_value
117 }
118}
119
120impl Indicator for EmpiricalModeDecomposition {
121 type Input = f64;
122 type Output = f64;
123
124 fn update(&mut self, input: f64) -> Option<f64> {
125 if !input.is_finite() {
126 return None;
127 }
128 let bp = if let (Some(_x1), Some(x2)) = (self.prev_in_1, self.prev_in_2) {
130 0.5 * (1.0 - self.alpha) * (input - x2)
131 + self.beta * (1.0 + self.alpha) * self.prev_bp_1
132 - self.alpha * self.prev_bp_2
133 } else {
134 0.0
135 };
136 self.prev_bp_2 = self.prev_bp_1;
137 self.prev_bp_1 = bp;
138 self.bandpass = bp;
139 self.prev_in_2 = self.prev_in_1;
140 self.prev_in_1 = Some(input);
141
142 if self.bp_buf.len() == self.bp_history_len {
143 self.bp_buf.pop_front();
144 }
145 self.bp_buf.push_back(bp);
146 if self.bp_buf.len() < self.bp_history_len {
147 return None;
148 }
149
150 let peak = self
152 .bp_buf
153 .iter()
154 .copied()
155 .fold(f64::NEG_INFINITY, f64::max);
156 let valley = self.bp_buf.iter().copied().fold(f64::INFINITY, f64::min);
157
158 let avg_peak = self.peak_smoother.update(peak)?;
159 let avg_valley = self.valley_smoother.update(valley)?;
160
161 let mean = f64::midpoint(avg_peak, avg_valley);
163 let raw = bp - mean;
164 let v = self.smoother.update(raw)?;
165 self.last_value = Some(v);
166 Some(v)
167 }
168
169 fn reset(&mut self) {
170 self.bandpass = 0.0;
171 self.prev_bp_1 = 0.0;
172 self.prev_bp_2 = 0.0;
173 self.prev_in_1 = None;
174 self.prev_in_2 = None;
175 self.smoother.reset();
176 self.peak_smoother.reset();
177 self.valley_smoother.reset();
178 self.bp_buf.clear();
179 self.last_value = None;
180 }
181
182 #[inline]
183 fn warmup_period(&self) -> usize {
184 self.bp_history_len
185 }
186
187 #[inline]
188 fn is_ready(&self) -> bool {
189 self.last_value.is_some()
190 }
191
192 #[inline]
193 fn name(&self) -> &'static str {
194 "EmpiricalModeDecomposition"
195 }
196}
197
198#[cfg(test)]
199mod tests {
200 use super::*;
201 use crate::traits::BatchExt;
202
203 #[test]
204 fn new_rejects_invalid_params() {
205 assert!(matches!(
206 EmpiricalModeDecomposition::new(0, 0.5),
207 Err(Error::PeriodZero)
208 ));
209 assert!(matches!(
210 EmpiricalModeDecomposition::new(20, 0.0),
211 Err(Error::InvalidPeriod { .. })
212 ));
213 assert!(matches!(
214 EmpiricalModeDecomposition::new(20, 1.5),
215 Err(Error::InvalidPeriod { .. })
216 ));
217 assert!(matches!(
218 EmpiricalModeDecomposition::new(20, f64::NAN),
219 Err(Error::InvalidPeriod { .. })
220 ));
221 }
222
223 #[test]
224 fn accessors_and_metadata() {
225 let mut emd = EmpiricalModeDecomposition::new(20, 0.5).unwrap();
226 assert_eq!(emd.period(), 20);
227 assert!((emd.fraction() - 0.5).abs() < 1e-15);
228 assert_eq!(emd.name(), "EmpiricalModeDecomposition");
229 assert!(emd.warmup_period() >= 1);
230 assert!(!emd.is_ready());
231 let prices: Vec<f64> = (0..200)
232 .map(|i| 100.0 + (f64::from(i) * 0.3).sin() * 5.0)
233 .collect();
234 emd.batch(&prices);
235 assert!(emd.is_ready());
236 assert!(emd.value().is_some());
237 }
238
239 #[test]
240 fn batch_equals_streaming() {
241 let prices: Vec<f64> = (0..200)
242 .map(|i| 100.0 + (f64::from(i) * 0.2).cos() * 5.0)
243 .collect();
244 let mut a = EmpiricalModeDecomposition::new(20, 0.5).unwrap();
245 let mut b = EmpiricalModeDecomposition::new(20, 0.5).unwrap();
246 let batch = a.batch(&prices);
247 let streamed: Vec<_> = prices.iter().map(|p| b.update(*p)).collect();
248 assert_eq!(batch, streamed);
249 }
250
251 #[test]
252 fn ignores_non_finite_input() {
253 let mut emd = EmpiricalModeDecomposition::new(20, 0.5).unwrap();
254 let prices: Vec<f64> = (0..200)
255 .map(|i| 100.0 + (f64::from(i) * 0.3).sin() * 5.0)
256 .collect();
257 emd.batch(&prices);
258 let before = emd.value();
259 assert!(before.is_some());
260 assert_eq!(emd.update(f64::NAN), None);
261 }
262
263 #[test]
264 fn reset_clears_state() {
265 let mut emd = EmpiricalModeDecomposition::new(20, 0.5).unwrap();
266 let prices: Vec<f64> = (0..200)
267 .map(|i| 100.0 + (f64::from(i) * 0.3).sin() * 5.0)
268 .collect();
269 emd.batch(&prices);
270 assert!(emd.is_ready());
271 emd.reset();
272 assert!(!emd.is_ready());
273 }
274}