mantis-ta 0.6.2

Composable technical analysis and strategy engine for Rust
Documentation
use super::EMA;
use crate::indicators::Indicator;
use crate::types::{Candle, MacdOutput};

/// Moving Average Convergence Divergence over closing prices.
///
/// # Examples
/// ```rust
/// use mantis_ta::indicators::{Indicator, MACD};
/// use mantis_ta::types::Candle;
///
/// // 12/26/9 MACD emits after slow + signal warmup
/// let prices = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0];
/// let candles: Vec<Candle> = prices
///     .iter()
///     .enumerate()
///     .map(|(i, p)| Candle {
///         timestamp: i as i64,
///         open: *p,
///         high: *p,
///         low: *p,
///         close: *p,
///         volume: 0.0,
///     })
///     .collect();
///
/// let out = MACD::new(2, 4, 2).calculate(&candles);
/// assert!(out.iter().take(4).all(|v| v.is_none()));
/// assert!(out.iter().skip(3).any(|v| v.is_some()));
/// ```
#[derive(Debug, Clone)]
pub struct MACD {
    fast: EMA,
    slow: EMA,
    signal: EMA,
    slow_period: usize,
    signal_period: usize,
}

impl MACD {
    pub fn new(fast: usize, slow: usize, signal: usize) -> Self {
        assert!(fast > 0 && slow > 0 && signal > 0, "periods must be > 0");
        assert!(fast < slow, "fast period must be < slow period");
        Self {
            fast: EMA::new(fast),
            slow: EMA::new(slow),
            signal: EMA::new(signal),
            slow_period: slow,
            signal_period: signal,
        }
    }

    #[inline]
    fn macd_candle(value: f64) -> Candle {
        Candle {
            timestamp: 0,
            open: value,
            high: value,
            low: value,
            close: value,
            volume: 0.0,
        }
    }
}

impl Indicator for MACD {
    type Output = MacdOutput;

    fn next(&mut self, candle: &Candle) -> Option<Self::Output> {
        let slow_val = self.slow.next(candle);
        let fast_val = self.fast.next(candle);

        let macd_line = match (fast_val, slow_val) {
            (Some(f), Some(s)) => f - s,
            _ => return None,
        };

        let macd_candle = Self::macd_candle(macd_line);
        let signal_line = self.signal.next(&macd_candle)?;

        let histogram = macd_line - signal_line;
        Some(MacdOutput {
            macd_line,
            signal_line,
            histogram,
        })
    }

    fn reset(&mut self) {
        self.fast.reset();
        self.slow.reset();
        self.signal.reset();
    }

    fn warmup_period(&self) -> usize {
        // Need slow EMA warmup, then signal warmup on MACD line.
        self.slow_period + self.signal_period - 1
    }

    fn clone_boxed(&self) -> Box<dyn Indicator<Output = Self::Output>> {
        Box::new(self.clone())
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn macd_emits_after_warmup() {
        let mut macd = MACD::new(2, 4, 2);
        let prices = [1.0, 2.0, 3.0, 4.0, 5.0];
        let candles: Vec<Candle> = prices
            .iter()
            .map(|p| Candle {
                timestamp: 0,
                open: *p,
                high: *p,
                low: *p,
                close: *p,
                volume: 0.0,
            })
            .collect();

        let outputs: Vec<_> = candles.iter().map(|c| macd.next(c)).collect();
        assert!(
            outputs
                .iter()
                .take(macd.warmup_period() - 1)
                .all(|o| o.is_none())
        );
        assert!(
            outputs
                .iter()
                .skip(macd.warmup_period() - 1)
                .any(|o| o.is_some())
        );
    }
}