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 #[inline]
206 fn update(&mut self, input: f64) -> Option<MacdOutput> {
207 if !input.is_finite() {
208 return None;
209 }
210
211 let fast = self.fast.update(input);
212 let slow = self.slow.update(input);
213
214 match (fast, slow) {
215 (Some(f), Some(s)) => {
216 let macd = f - s;
217 let signal = self.signal_ema.update(macd)?;
218 let out = MacdOutput {
219 macd,
220 signal,
221 histogram: macd - signal,
222 };
223 self.last = Some(out);
224 Some(out)
225 }
226 _ => None,
227 }
228 }
229
230 fn reset(&mut self) {
231 self.fast.reset();
232 self.slow.reset();
233 self.signal_ema.reset();
234 self.last = None;
235 }
236
237 #[inline]
238 fn warmup_period(&self) -> usize {
239 self.slow_period + self.signal_period - 1
241 }
242
243 #[inline]
244 fn is_ready(&self) -> bool {
245 self.last.is_some()
246 }
247
248 #[inline]
249 fn name(&self) -> &'static str {
250 "MACD"
251 }
252}
253
254#[cfg(test)]
255mod tests {
256 use super::*;
257 use crate::traits::BatchExt;
258 use approx::assert_relative_eq;
259
260 #[test]
261 fn rejects_fast_geq_slow() {
262 assert!(matches!(
263 MacdIndicator::new(26, 12, 9),
264 Err(Error::InvalidPeriod { .. })
265 ));
266 assert!(matches!(
267 MacdIndicator::new(12, 12, 9),
268 Err(Error::InvalidPeriod { .. })
269 ));
270 }
271
272 #[test]
276 fn accessors_and_metadata() {
277 let mut m = MacdIndicator::new(12, 26, 9).unwrap();
278 assert_eq!(m.periods(), (12, 26, 9));
279 assert_eq!(m.name(), "MACD");
280 assert!(m.value().is_none());
281 for i in 1..=m.warmup_period() {
282 m.update(100.0 + f64::from(u32::try_from(i).unwrap()));
283 }
284 assert!(m.value().is_some());
285 }
286
287 #[test]
288 fn rejects_zero_periods() {
289 assert!(matches!(
290 MacdIndicator::new(0, 26, 9),
291 Err(Error::PeriodZero)
292 ));
293 assert!(matches!(
294 MacdIndicator::new(12, 0, 9),
295 Err(Error::PeriodZero)
296 ));
297 assert!(matches!(
298 MacdIndicator::new(12, 26, 0),
299 Err(Error::PeriodZero)
300 ));
301 }
302
303 #[test]
304 fn first_emission_matches_warmup_period() {
305 let prices: Vec<f64> = (1..=60).map(f64::from).collect();
306 let mut macd = MacdIndicator::classic();
307 let out = macd.batch(&prices);
308 let warmup = macd.warmup_period();
309 for x in out.iter().take(warmup - 1) {
313 assert!(x.is_none(), "expected None within warmup");
314 }
315 assert!(
316 out[warmup - 1].is_some(),
317 "expected first emission at warmup_period - 1 ({warmup} idx)"
318 );
319 }
320
321 #[test]
322 fn histogram_equals_macd_minus_signal() {
323 let prices: Vec<f64> = (1..=80).map(|i| f64::from(i) * 0.5).collect();
324 let mut macd = MacdIndicator::classic();
325 for v in macd.batch(&prices).into_iter().flatten() {
326 assert_relative_eq!(v.histogram, v.macd - v.signal, epsilon = 1e-12);
327 }
328 }
329
330 #[test]
331 fn constant_series_yields_zero_macd_eventually() {
332 let mut macd = MacdIndicator::classic();
333 let out = macd.batch(&[100.0_f64; 200]);
334 let last = out.iter().rev().flatten().next().expect("emits a value");
336 assert_relative_eq!(last.macd, 0.0, epsilon = 1e-9);
337 assert_relative_eq!(last.signal, 0.0, epsilon = 1e-9);
338 assert_relative_eq!(last.histogram, 0.0, epsilon = 1e-9);
339 }
340
341 #[test]
342 fn rising_series_macd_positive_then_signal_catches_up() {
343 let prices: Vec<f64> = (1..=200).map(f64::from).collect();
344 let mut macd = MacdIndicator::classic();
345 let out = macd.batch(&prices);
346 let last = out.iter().rev().flatten().next().unwrap();
347 assert!(last.macd > 0.0, "rising series must yield positive MACD");
348 }
349
350 #[test]
351 fn batch_equals_streaming() {
352 let prices: Vec<f64> = (1..=100)
353 .map(|i| (f64::from(i) * 0.4).cos() * 10.0)
354 .collect();
355 let mut a = MacdIndicator::classic();
356 let mut b = MacdIndicator::classic();
357 assert_eq!(
358 a.batch(&prices),
359 prices.iter().map(|p| b.update(*p)).collect::<Vec<_>>()
360 );
361 }
362
363 #[test]
364 fn reset_clears_state() {
365 let mut macd = MacdIndicator::classic();
366 macd.batch(&(1..=80).map(f64::from).collect::<Vec<_>>());
367 assert!(macd.is_ready());
368 macd.reset();
369 assert!(!macd.is_ready());
370 assert_eq!(macd.update(1.0), None);
371 }
372
373 fn bits_eq(a: &[f64], b: &[f64]) -> bool {
374 a.len() == b.len()
375 && a.iter()
376 .zip(b)
377 .all(|(x, y)| x == y || (x.is_nan() && y.is_nan()))
378 }
379
380 fn macd_replay(series: &[f64]) -> Vec<f64> {
382 let mut m = MacdIndicator::classic();
383 let mut out = Vec::with_capacity(series.len() * 3);
384 for &x in series {
385 match m.update(x) {
386 Some(o) => out.extend_from_slice(&[o.macd, o.signal, o.histogram]),
387 None => out.extend_from_slice(&[f64::NAN; 3]),
388 }
389 }
390 out
391 }
392
393 #[test]
394 fn batch_macd_fast_path_is_bit_identical() {
395 let series: Vec<f64> = (0..300)
396 .map(|i| (f64::from(i) * 0.4).cos() * 10.0 + 100.0)
397 .collect();
398 let mut macd = MacdIndicator::classic();
399 let got = macd.batch_macd(&series);
400 assert!(bits_eq(&got, &macd_replay(&series)));
401 let mut ref_macd = MacdIndicator::classic();
403 for &x in &series {
404 ref_macd.update(x);
405 }
406 let (a, b) = (macd.update(101.0), ref_macd.update(101.0));
407 assert_eq!(a.is_some(), b.is_some());
408 assert_relative_eq!(a.unwrap().macd, b.unwrap().macd, epsilon = 1e-12);
409 }
410
411 #[test]
412 fn batch_macd_falls_back_on_non_finite() {
413 let mut series: Vec<f64> = (0..60).map(|i| f64::from(i) + 100.0).collect();
414 series[40] = f64::NAN;
415 let mut macd = MacdIndicator::classic();
416 assert!(bits_eq(&macd.batch_macd(&series), &macd_replay(&series)));
417 }
418
419 #[test]
420 fn batch_macd_falls_back_when_not_fresh() {
421 let series: Vec<f64> = (0..60).map(|i| f64::from(i) + 100.0).collect();
422 let mut macd = MacdIndicator::classic();
423 macd.update(50.0);
424 let mut ref_macd = MacdIndicator::classic();
425 ref_macd.update(50.0);
426 let mut want = Vec::new();
427 for &x in &series {
428 match ref_macd.update(x) {
429 Some(o) => want.extend_from_slice(&[o.macd, o.signal, o.histogram]),
430 None => want.extend_from_slice(&[f64::NAN; 3]),
431 }
432 }
433 assert!(bits_eq(&macd.batch_macd(&series), &want));
434 }
435
436 #[test]
437 fn batch_macd_too_short_for_output_falls_back() {
438 let series: Vec<f64> = (0..20).map(|i| f64::from(i) + 100.0).collect();
440 let mut macd = MacdIndicator::classic();
441 let got = macd.batch_macd(&series);
442 assert!(bits_eq(&got, &macd_replay(&series)));
443 assert!(got.iter().all(|x| x.is_nan()));
444 }
445
446 #[test]
447 fn ignores_non_finite_input() {
448 let mut macd = MacdIndicator::classic();
449 macd.batch(&(1..=80).map(f64::from).collect::<Vec<_>>());
450 let before = macd.value();
451 assert!(before.is_some());
452 assert_eq!(macd.update(f64::NAN), None);
454 assert_eq!(macd.update(f64::INFINITY), None);
455 assert_eq!(macd.value(), before);
456 }
457}