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