Skip to main content

kestrel_chartkit/indicator/
mcginley.rs

1use super::{Indicator, IndicatorAlert, IndicatorOutput};
2use crate::model::Bar;
3
4/// McGinley Dynamic over the closing price.
5///
6/// A moving average whose effective length adapts to how fast price moves away from it:
7///
8/// ```text
9/// MD_t = MD_{t-1} + (close_t - MD_{t-1}) / (period * (close_t / MD_{t-1})^4)
10/// ```
11///
12/// This is the form with `period` itself in the denominator. Some descriptions scale the length
13/// by a constant `0.6`; this type does not.
14///
15/// The first value is the first close, and output starts with the first bar — the recursion needs
16/// no window. [`Indicator::warmup_period`] nonetheless reports `period`. [`Indicator::reset`]
17/// clears the average.
18#[derive(Debug, Clone)]
19pub struct McGinleyDynamicEngine {
20    period: usize,
21    state: Option<f64>,
22}
23
24impl McGinleyDynamicEngine {
25    pub fn new(period: usize) -> Self {
26        Self {
27            period: period.max(1),
28            state: None,
29        }
30    }
31}
32
33impl Indicator for McGinleyDynamicEngine {
34    fn name(&self) -> &str {
35        "mcginley"
36    }
37
38    fn warmup_period(&self) -> usize {
39        self.period
40    }
41
42    fn reset(&mut self) {
43        self.state = None;
44    }
45
46    fn on_bar(&mut self, bar: &Bar) -> Option<IndicatorOutput> {
47        let price = bar.close;
48        let k = self.period as f64;
49
50        let next = match self.state {
51            None => price,
52            Some(prev) => {
53                let ratio = (price / prev.max(1e-8)).powi(4);
54                prev + (price - prev) / (k * ratio).max(1e-6)
55            }
56        };
57
58        self.state = Some(next);
59        Some(IndicatorOutput::new(next))
60    }
61
62    fn alerts(&self) -> Vec<IndicatorAlert> {
63        Vec::new()
64    }
65}
66
67#[cfg(test)]
68mod tests {
69    use super::*;
70
71    #[test]
72    fn test_mcginley_basic() {
73        let mut mg = McGinleyDynamicEngine::new(14);
74        let b1 = Bar::new(1, 100.0, 105.0, 95.0, 100.0, 1000.0);
75        let out1 = mg.on_bar(&b1).unwrap();
76        assert_eq!(out1.value, 100.0);
77    }
78}