1use crate::error::{Error, Result};
4use crate::indicators::ema::Ema;
5use crate::traits::Indicator;
6
7#[derive(Debug, Clone, Copy, PartialEq)]
9pub struct MacdOutput {
10 pub macd: f64,
12 pub signal: f64,
14 pub histogram: f64,
16}
17
18#[derive(Debug, Clone)]
38pub struct MacdIndicator {
39 fast: Ema,
40 slow: Ema,
41 signal_ema: Ema,
42 fast_period: usize,
43 slow_period: usize,
44 signal_period: usize,
45 last: Option<MacdOutput>,
46}
47
48impl MacdIndicator {
49 pub fn new(fast: usize, slow: usize, signal: usize) -> Result<Self> {
56 if fast == 0 || slow == 0 || signal == 0 {
57 return Err(Error::PeriodZero);
58 }
59 if fast >= slow {
60 return Err(Error::InvalidPeriod {
61 message: "fast period must be strictly less than slow period",
62 });
63 }
64 Ok(Self {
65 fast: Ema::new(fast)?,
66 slow: Ema::new(slow)?,
67 signal_ema: Ema::new(signal)?,
68 fast_period: fast,
69 slow_period: slow,
70 signal_period: signal,
71 last: None,
72 })
73 }
74
75 pub fn classic() -> Self {
77 Self::new(12, 26, 9).expect("classic MACD periods are valid")
78 }
79
80 pub const fn periods(&self) -> (usize, usize, usize) {
82 (self.fast_period, self.slow_period, self.signal_period)
83 }
84
85 pub const fn value(&self) -> Option<MacdOutput> {
87 self.last
88 }
89
90 pub fn batch_macd(&mut self, inputs: &[f64]) -> Vec<f64> {
104 let n = inputs.len();
105 let (fp, sp, gp) = (self.fast_period, self.slow_period, self.signal_period);
106 if self.last.is_some()
109 || !self.fast.is_fresh()
110 || !self.slow.is_fresh()
111 || !self.signal_ema.is_fresh()
112 || n < sp + gp - 1
113 || !inputs.iter().all(|x| x.is_finite())
114 {
115 let mut out = vec![f64::NAN; n * 3];
116 for (i, &x) in inputs.iter().enumerate() {
117 if let Some(o) = self.update(x) {
118 out[i * 3] = o.macd;
119 out[i * 3 + 1] = o.signal;
120 out[i * 3 + 2] = o.histogram;
121 }
122 }
123 return out;
124 }
125
126 let mut out = vec![f64::NAN; n * 3];
129 let (fa, fo) = (self.fast.alpha(), 1.0 - self.fast.alpha());
130 let (sa, so) = (self.slow.alpha(), 1.0 - self.slow.alpha());
131 let (ga, go) = (self.signal_ema.alpha(), 1.0 - self.signal_ema.alpha());
132 let (fp_f, sp_f, gp_f) = (fp as f64, sp as f64, gp as f64);
133
134 let (mut fast_val, mut slow_val, mut sig) = (0.0_f64, 0.0_f64, 0.0_f64);
135 let (mut fsum, mut ssum, mut gsum) = (0.0_f64, 0.0_f64, 0.0_f64);
136 let mut sig_count = 0usize; let mut sig_seeded = false;
138 let mut last = MacdOutput {
139 macd: 0.0,
140 signal: 0.0,
141 histogram: 0.0,
142 };
143
144 for (i, &x) in inputs.iter().enumerate() {
145 if i < fp {
147 fsum += x;
148 if i == fp - 1 {
149 fast_val = fsum / fp_f;
150 }
151 } else {
152 fast_val = fa.mul_add(x, fo * fast_val);
153 }
154 if i < sp {
156 ssum += x;
157 if i == sp - 1 {
158 slow_val = ssum / sp_f;
159 }
160 } else {
161 slow_val = sa.mul_add(x, so * slow_val);
162 }
163 if i + 1 < sp {
164 continue; }
166 let macd = fast_val - slow_val;
167 let signal = if sig_seeded {
169 sig = ga.mul_add(macd, go * sig);
170 sig
171 } else {
172 gsum += macd;
173 sig_count += 1;
174 if sig_count < gp {
175 continue; }
177 sig = gsum / gp_f;
178 sig_seeded = true;
179 sig
180 };
181 let histogram = macd - signal;
182 out[i * 3] = macd;
183 out[i * 3 + 1] = signal;
184 out[i * 3 + 2] = histogram;
185 last = MacdOutput {
186 macd,
187 signal,
188 histogram,
189 };
190 }
191
192 self.fast.seed_to(fast_val);
194 self.slow.seed_to(slow_val);
195 self.signal_ema.seed_to(sig);
196 self.last = Some(last);
197 out
198 }
199}
200
201impl Indicator for MacdIndicator {
202 type Input = f64;
203 type Output = MacdOutput;
204
205 fn update(&mut self, input: f64) -> Option<MacdOutput> {
206 if !input.is_finite() {
207 return self.last;
208 }
209
210 let fast = self.fast.update(input);
211 let slow = self.slow.update(input);
212
213 match (fast, slow) {
214 (Some(f), Some(s)) => {
215 let macd = f - s;
216 let signal = self.signal_ema.update(macd)?;
217 let out = MacdOutput {
218 macd,
219 signal,
220 histogram: macd - signal,
221 };
222 self.last = Some(out);
223 Some(out)
224 }
225 _ => None,
226 }
227 }
228
229 fn reset(&mut self) {
230 self.fast.reset();
231 self.slow.reset();
232 self.signal_ema.reset();
233 self.last = None;
234 }
235
236 fn warmup_period(&self) -> usize {
237 self.slow_period + self.signal_period - 1
239 }
240
241 fn is_ready(&self) -> bool {
242 self.last.is_some()
243 }
244
245 fn name(&self) -> &'static str {
246 "MACD"
247 }
248}
249
250#[cfg(test)]
251mod tests {
252 use super::*;
253 use crate::traits::BatchExt;
254 use approx::assert_relative_eq;
255
256 #[test]
257 fn rejects_fast_geq_slow() {
258 assert!(matches!(
259 MacdIndicator::new(26, 12, 9),
260 Err(Error::InvalidPeriod { .. })
261 ));
262 assert!(matches!(
263 MacdIndicator::new(12, 12, 9),
264 Err(Error::InvalidPeriod { .. })
265 ));
266 }
267
268 #[test]
272 fn accessors_and_metadata() {
273 let mut m = MacdIndicator::new(12, 26, 9).unwrap();
274 assert_eq!(m.periods(), (12, 26, 9));
275 assert_eq!(m.name(), "MACD");
276 assert!(m.value().is_none());
277 for i in 1..=m.warmup_period() {
278 m.update(100.0 + f64::from(u32::try_from(i).unwrap()));
279 }
280 assert!(m.value().is_some());
281 }
282
283 #[test]
284 fn rejects_zero_periods() {
285 assert!(matches!(
286 MacdIndicator::new(0, 26, 9),
287 Err(Error::PeriodZero)
288 ));
289 assert!(matches!(
290 MacdIndicator::new(12, 0, 9),
291 Err(Error::PeriodZero)
292 ));
293 assert!(matches!(
294 MacdIndicator::new(12, 26, 0),
295 Err(Error::PeriodZero)
296 ));
297 }
298
299 #[test]
300 fn first_emission_matches_warmup_period() {
301 let prices: Vec<f64> = (1..=60).map(f64::from).collect();
302 let mut macd = MacdIndicator::classic();
303 let out = macd.batch(&prices);
304 let warmup = macd.warmup_period();
305 for x in out.iter().take(warmup - 1) {
309 assert!(x.is_none(), "expected None within warmup");
310 }
311 assert!(
312 out[warmup - 1].is_some(),
313 "expected first emission at warmup_period - 1 ({warmup} idx)"
314 );
315 }
316
317 #[test]
318 fn histogram_equals_macd_minus_signal() {
319 let prices: Vec<f64> = (1..=80).map(|i| f64::from(i) * 0.5).collect();
320 let mut macd = MacdIndicator::classic();
321 for v in macd.batch(&prices).into_iter().flatten() {
322 assert_relative_eq!(v.histogram, v.macd - v.signal, epsilon = 1e-12);
323 }
324 }
325
326 #[test]
327 fn constant_series_yields_zero_macd_eventually() {
328 let mut macd = MacdIndicator::classic();
329 let out = macd.batch(&[100.0_f64; 200]);
330 let last = out.iter().rev().flatten().next().expect("emits a value");
332 assert_relative_eq!(last.macd, 0.0, epsilon = 1e-9);
333 assert_relative_eq!(last.signal, 0.0, epsilon = 1e-9);
334 assert_relative_eq!(last.histogram, 0.0, epsilon = 1e-9);
335 }
336
337 #[test]
338 fn rising_series_macd_positive_then_signal_catches_up() {
339 let prices: Vec<f64> = (1..=200).map(f64::from).collect();
340 let mut macd = MacdIndicator::classic();
341 let out = macd.batch(&prices);
342 let last = out.iter().rev().flatten().next().unwrap();
343 assert!(last.macd > 0.0, "rising series must yield positive MACD");
344 }
345
346 #[test]
347 fn batch_equals_streaming() {
348 let prices: Vec<f64> = (1..=100)
349 .map(|i| (f64::from(i) * 0.4).cos() * 10.0)
350 .collect();
351 let mut a = MacdIndicator::classic();
352 let mut b = MacdIndicator::classic();
353 assert_eq!(
354 a.batch(&prices),
355 prices.iter().map(|p| b.update(*p)).collect::<Vec<_>>()
356 );
357 }
358
359 #[test]
360 fn reset_clears_state() {
361 let mut macd = MacdIndicator::classic();
362 macd.batch(&(1..=80).map(f64::from).collect::<Vec<_>>());
363 assert!(macd.is_ready());
364 macd.reset();
365 assert!(!macd.is_ready());
366 assert_eq!(macd.update(1.0), None);
367 }
368
369 fn bits_eq(a: &[f64], b: &[f64]) -> bool {
370 a.len() == b.len()
371 && a.iter()
372 .zip(b)
373 .all(|(x, y)| x == y || (x.is_nan() && y.is_nan()))
374 }
375
376 fn macd_replay(series: &[f64]) -> Vec<f64> {
378 let mut m = MacdIndicator::classic();
379 let mut out = Vec::with_capacity(series.len() * 3);
380 for &x in series {
381 match m.update(x) {
382 Some(o) => out.extend_from_slice(&[o.macd, o.signal, o.histogram]),
383 None => out.extend_from_slice(&[f64::NAN; 3]),
384 }
385 }
386 out
387 }
388
389 #[test]
390 fn batch_macd_fast_path_is_bit_identical() {
391 let series: Vec<f64> = (0..300)
392 .map(|i| (f64::from(i) * 0.4).cos() * 10.0 + 100.0)
393 .collect();
394 let mut macd = MacdIndicator::classic();
395 let got = macd.batch_macd(&series);
396 assert!(bits_eq(&got, &macd_replay(&series)));
397 let mut ref_macd = MacdIndicator::classic();
399 for &x in &series {
400 ref_macd.update(x);
401 }
402 let (a, b) = (macd.update(101.0), ref_macd.update(101.0));
403 assert_eq!(a.is_some(), b.is_some());
404 assert_relative_eq!(a.unwrap().macd, b.unwrap().macd, epsilon = 1e-12);
405 }
406
407 #[test]
408 fn batch_macd_falls_back_on_non_finite() {
409 let mut series: Vec<f64> = (0..60).map(|i| f64::from(i) + 100.0).collect();
410 series[40] = f64::NAN;
411 let mut macd = MacdIndicator::classic();
412 assert!(bits_eq(&macd.batch_macd(&series), &macd_replay(&series)));
413 }
414
415 #[test]
416 fn batch_macd_falls_back_when_not_fresh() {
417 let series: Vec<f64> = (0..60).map(|i| f64::from(i) + 100.0).collect();
418 let mut macd = MacdIndicator::classic();
419 macd.update(50.0);
420 let mut ref_macd = MacdIndicator::classic();
421 ref_macd.update(50.0);
422 let mut want = Vec::new();
423 for &x in &series {
424 match ref_macd.update(x) {
425 Some(o) => want.extend_from_slice(&[o.macd, o.signal, o.histogram]),
426 None => want.extend_from_slice(&[f64::NAN; 3]),
427 }
428 }
429 assert!(bits_eq(&macd.batch_macd(&series), &want));
430 }
431
432 #[test]
433 fn batch_macd_too_short_for_output_falls_back() {
434 let series: Vec<f64> = (0..20).map(|i| f64::from(i) + 100.0).collect();
436 let mut macd = MacdIndicator::classic();
437 let got = macd.batch_macd(&series);
438 assert!(bits_eq(&got, &macd_replay(&series)));
439 assert!(got.iter().all(|x| x.is_nan()));
440 }
441
442 #[test]
443 fn ignores_non_finite_input() {
444 let mut macd = MacdIndicator::classic();
445 macd.batch(&(1..=80).map(f64::from).collect::<Vec<_>>());
446 let before = macd.value();
447 assert!(before.is_some());
448 assert_eq!(macd.update(f64::NAN), before);
450 assert_eq!(macd.update(f64::INFINITY), before);
451 assert_eq!(macd.value(), before);
452 }
453}