kestrel-chartkit 0.1.0

High-performance Rust technical analysis library for indicator math, market regime classification, composite scoring, and SVG visualization.
Documentation
use std::collections::HashMap;

use crate::model::Bar;

use super::smoothing::{crossed_over, crossed_under, Ema};
use super::{Indicator, IndicatorAlert, IndicatorOutput};

pub struct Macd {
    fast_ema: Ema,
    slow_ema: Ema,
    signal_ema: Ema,
    slow_len: usize,

    prev_macd: Option<f64>,
    prev_signal: Option<f64>,
    bars_seen: usize,

    alerts: MacdAlerts,
}

#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub struct MacdAlerts {
    pub bull_cross: bool,
    pub bear_cross: bool,
    pub bull_zero_cross: bool,
    pub bear_zero_cross: bool,
}

impl Macd {
    pub fn new(fast_len: usize, slow_len: usize, signal_len: usize) -> Self {
        Self {
            fast_ema: Ema::new(fast_len),
            slow_ema: Ema::new(slow_len),
            signal_ema: Ema::new(signal_len),
            slow_len,
            prev_macd: None,
            prev_signal: None,
            bars_seen: 0,
            alerts: MacdAlerts::default(),
        }
    }

    pub fn with_defaults() -> Self {
        Self::new(12, 26, 9)
    }
}

impl Indicator for Macd {
    fn name(&self) -> &str {
        "macd"
    }

    fn warmup_period(&self) -> usize {
        self.slow_len
    }

    fn on_bar(&mut self, bar: &Bar) -> Option<IndicatorOutput> {
        self.alerts = MacdAlerts::default();
        self.bars_seen += 1;

        let close = bar.close;
        let fast = self.fast_ema.update(close);
        let slow = self.slow_ema.update(close);

        if self.bars_seen < self.slow_len {
            return None;
        }

        let macd_line = fast - slow;
        let signal_line = self.signal_ema.update(macd_line);
        let hist = macd_line - signal_line;

        if let (Some(prev_m), Some(prev_s)) = (self.prev_macd, self.prev_signal) {
            self.alerts.bull_cross = crossed_over(prev_m, prev_s, macd_line, signal_line);
            self.alerts.bear_cross = crossed_under(prev_m, prev_s, macd_line, signal_line);
            self.alerts.bull_zero_cross = crossed_over(prev_m, 0.0, macd_line, 0.0);
            self.alerts.bear_zero_cross = crossed_under(prev_m, 0.0, macd_line, 0.0);
        }

        self.prev_macd = Some(macd_line);
        self.prev_signal = Some(signal_line);

        let mut extra = HashMap::new();
        extra.insert("signal".to_string(), signal_line);
        extra.insert("hist".to_string(), hist);

        Some(IndicatorOutput::with_extra(macd_line, extra))
    }

    fn reset(&mut self) {
        self.fast_ema.reset();
        self.slow_ema.reset();
        self.signal_ema.reset();
        self.prev_macd = None;
        self.prev_signal = None;
        self.bars_seen = 0;
        self.alerts = MacdAlerts::default();
    }

    fn alerts(&self) -> Vec<IndicatorAlert> {
        let a = self.alerts;
        let mut out = Vec::new();
        if a.bull_cross {
            out.push(IndicatorAlert {
                kind: "bull_cross".to_string(),
                note: "MACD · BULL CROSS SIGNAL".to_string(),
                strength: 1.0,
            });
        }
        if a.bear_cross {
            out.push(IndicatorAlert {
                kind: "bear_cross".to_string(),
                note: "MACD · BEAR CROSS SIGNAL".to_string(),
                strength: 1.0,
            });
        }
        if a.bull_zero_cross {
            out.push(IndicatorAlert {
                kind: "bull_zero_cross".to_string(),
                note: "MACD · CROSS ABOVE ZERO".to_string(),
                strength: 1.0,
            });
        }
        if a.bear_zero_cross {
            out.push(IndicatorAlert {
                kind: "bear_zero_cross".to_string(),
                note: "MACD · CROSS BELOW ZERO".to_string(),
                strength: 1.0,
            });
        }
        out
    }
}