quantwave_core/indicators/
emd.rs1use crate::indicators::metadata::{IndicatorMetadata, ParamDef};
2use crate::indicators::smoothing::SMA;
3use crate::traits::Next;
4use std::f64::consts::PI;
5
6#[derive(Debug, Clone)]
15pub struct EMD {
16 alpha: f64,
17 beta: f64,
18 fraction: f64,
19 price_prev1: f64,
20 price_prev2: f64,
21 bp_history: [f64; 2],
22 mean_sma: SMA,
23 peak_sma: SMA,
24 valley_sma: SMA,
25 peak: f64,
26 valley: f64,
27 count: usize,
28}
29
30impl EMD {
31 pub fn new(period: usize, delta: f64, fraction: f64) -> Self {
32 let beta = (2.0 * PI / period as f64).cos();
34 let gamma = 1.0 / (4.0 * PI * delta / period as f64).cos();
36 let alpha = gamma - (gamma * gamma - 1.0).sqrt();
38
39 Self {
40 alpha,
41 beta,
42 fraction,
43 price_prev1: 0.0,
44 price_prev2: 0.0,
45 bp_history: [0.0; 2],
46 mean_sma: SMA::new(2 * period),
47 peak_sma: SMA::new(50),
48 valley_sma: SMA::new(50),
49 peak: 0.0,
50 valley: 0.0,
51 count: 0,
52 }
53 }
54}
55
56impl Next<f64> for EMD {
57 type Output = (f64, f64, f64); fn next(&mut self, input: f64) -> Self::Output {
60 self.count += 1;
61
62 let bp = 0.5 * (1.0 - self.alpha) * (input - self.price_prev2)
64 + self.beta * (1.0 + self.alpha) * self.bp_history[0]
65 - self.alpha * self.bp_history[1];
66
67 let mean = self.mean_sma.next(bp);
69
70 if self.count > 2 {
73 if self.bp_history[0] > bp && self.bp_history[0] > self.bp_history[1] {
74 self.peak = self.bp_history[0];
75 }
76 if self.bp_history[0] < bp && self.bp_history[0] < self.bp_history[1] {
77 self.valley = self.bp_history[0];
78 }
79 }
80
81 let avg_peak = self.peak_sma.next(self.peak);
83 let avg_valley = self.valley_sma.next(self.valley);
84
85 self.bp_history[1] = self.bp_history[0];
87 self.bp_history[0] = bp;
88
89 self.price_prev2 = self.price_prev1;
90 self.price_prev1 = input;
91
92 (mean, self.fraction * avg_peak, self.fraction * avg_valley)
93 }
94}
95
96pub const EMD_METADATA: IndicatorMetadata = IndicatorMetadata {
97 name: "EMD",
98 description: "Empirical Mode Decomposition separates cycles from trends using bandpass filtering and identifies market modes via adaptive thresholds.",
99 usage: "Use to decompose price into Intrinsic Mode Functions to separate cycles of different periods without any a priori period assumption. Useful for multi-timescale analysis.",
100 keywords: &["decomposition", "cycle", "spectral", "dsp"],
101 ehlers_summary: "Empirical Mode Decomposition is a data-driven method developed by Huang et al. (1998) that decomposes a signal into Intrinsic Mode Functions by iteratively sifting local extrema. Unlike Fourier methods, it requires no predetermined basis functions, making it adaptive to non-stationary market data.",
102 params: &[
103 ParamDef {
104 name: "period",
105 default: "20",
106 description: "Bandpass center period",
107 },
108 ParamDef {
109 name: "delta",
110 default: "0.5",
111 description: "Bandwidth half-width",
112 },
113 ParamDef {
114 name: "fraction",
115 default: "0.1",
116 description: "Threshold multiplier for peaks/valleys",
117 },
118 ],
119 formula_source: "https://github.com/lavs9/quantwave/blob/main/references/Ehlers%20Papers/EmpiricalModeDecomposition.pdf",
120 formula_latex: r#"
121\[
122\beta = \cos\left(\frac{360}{P}\right), \gamma = \frac{1}{\cos\left(\frac{720\delta}{P}\right)}, \alpha = \gamma - \sqrt{\gamma^2 - 1}
123\]
124\[
125BP = 0.5(1 - \alpha)(Price - Price_{t-2}) + \beta(1 + \alpha)BP_{t-1} - \alpha BP_{t-2}
126\]
127\[
128Mean = \text{SMA}(BP, 2P)
129\]
130\[
131Threshold = \text{Fraction} \cdot \text{SMA}(\text{Peak/Valley}, 50)
132\]
133"#,
134 gold_standard_file: "emd.json",
135 category: "Ehlers DSP",
136};
137
138#[cfg(test)]
139mod tests {
140 use super::*;
141 use crate::traits::Next;
142 use proptest::prelude::*;
143
144 #[test]
145 fn test_emd_basic() {
146 let mut emd = EMD::new(20, 0.5, 0.1);
147 let inputs = vec![10.0, 11.0, 12.0, 13.0, 14.0, 15.0];
148 for input in inputs {
149 let (m, u, l) = emd.next(input);
150 assert!(!m.is_nan());
151 assert!(!u.is_nan());
152 assert!(!l.is_nan());
153 }
154 }
155
156 proptest! {
157 #[test]
158 fn test_emd_parity(
159 inputs in prop::collection::vec(1.0..100.0, 150..250),
160 ) {
161 let period = 20;
162 let delta = 0.5;
163 let fraction = 0.1;
164 let mut emd = EMD::new(period, delta, fraction);
165 let streaming_results: Vec<(f64, f64, f64)> = inputs.iter().map(|&x| emd.next(x)).collect();
166
167 let mut batch_results = Vec::with_capacity(inputs.len());
169 let beta = (2.0 * PI / period as f64).cos();
170 let gamma = 1.0 / (4.0 * PI * delta / period as f64).cos();
171 let alpha = gamma - (gamma * gamma - 1.0).sqrt();
172
173 let mut price_hist = vec![0.0; inputs.len() + 4];
174 let mut bp_hist = vec![0.0; inputs.len() + 4];
175 let mut peak = 0.0;
176 let mut valley = 0.0;
177 let mut peak_hist = Vec::new();
178 let mut valley_hist = Vec::new();
179
180 for (i, &input) in inputs.iter().enumerate() {
181 let bar = i + 1;
182 let idx = i + 2;
183 price_hist[idx] = input;
184
185 let bp = 0.5 * (1.0 - alpha) * (price_hist[idx] - price_hist[idx-2])
186 + beta * (1.0 + alpha) * bp_hist[idx-1]
187 - alpha * bp_hist[idx-2];
188 bp_hist[idx] = bp;
189
190 let mut mean_sum = 0.0;
191 let mean_len = (2 * period).min(bar);
192 for j in 0..mean_len {
193 mean_sum += bp_hist[idx-j];
194 }
195 let mean = mean_sum / mean_len as f64;
196
197 if bar > 2 {
198 if bp_hist[idx-1] > bp && bp_hist[idx-1] > bp_hist[idx-2] {
199 peak = bp_hist[idx-1];
200 }
201 if bp_hist[idx-1] < bp && bp_hist[idx-1] < bp_hist[idx-2] {
202 valley = bp_hist[idx-1];
203 }
204 }
205 peak_hist.push(peak);
206 valley_hist.push(valley);
207
208 let mut p_sum = 0.0;
209 let p_len = 50.min(bar);
210 for j in 0..p_len {
211 p_sum += peak_hist[i-j];
212 }
213 let avg_p = p_sum / p_len as f64;
214
215 let mut v_sum = 0.0;
216 for j in 0..p_len {
217 v_sum += valley_hist[i-j];
218 }
219 let avg_v = v_sum / p_len as f64;
220
221 batch_results.push((mean, fraction * avg_p, fraction * avg_v));
222 }
223
224 for (s, b) in streaming_results.iter().zip(batch_results.iter()) {
225 approx::assert_relative_eq!(s.0, b.0, epsilon = 1e-10);
226 approx::assert_relative_eq!(s.1, b.1, epsilon = 1e-10);
227 approx::assert_relative_eq!(s.2, b.2, epsilon = 1e-10);
228 }
229 }
230 }
231}