Skip to main content

kestrel_chartkit/indicator/
mass_index.rs

1use super::smoothing::Ema;
2use super::{Indicator, IndicatorAlert, IndicatorOutput};
3use crate::model::Bar;
4use std::collections::VecDeque;
5
6/// Mass Index: the summed ratio of a single to a double exponential average of the bar range.
7///
8/// `range = high - low` (floored at `1e-8`); `e1 = Ema(9)(range)` and `e2 = Ema(9)(e1)`, both with
9/// the first-sample seed and running from the first bar; `ratio = e1 / e2` (`1` for a vanishing
10/// `e2`). The index is the sum of the last `period` ratios (default 25). A constant range gives
11/// exactly `period`; widening ranges push it up.
12///
13/// First output: with the `period`-th bar. [`Indicator::warmup_period`] nonetheless reports
14/// `period + 18`. [`Indicator::reset`] clears both averages and the window.
15#[derive(Debug, Clone)]
16pub struct MassIndexEngine {
17    period: usize,
18    ema1: Ema,
19    ema2: Ema,
20    ratio_window: VecDeque<f64>,
21}
22
23impl MassIndexEngine {
24    pub fn new(period: usize) -> Self {
25        Self {
26            period: period.max(1),
27            ema1: Ema::new(9),
28            ema2: Ema::new(9),
29            ratio_window: VecDeque::with_capacity(period),
30        }
31    }
32
33    pub fn with_defaults() -> Self {
34        Self::new(25)
35    }
36}
37
38impl Indicator for MassIndexEngine {
39    fn name(&self) -> &str {
40        "mass_index"
41    }
42
43    fn warmup_period(&self) -> usize {
44        self.period + 18
45    }
46
47    fn reset(&mut self) {
48        self.ema1.reset();
49        self.ema2.reset();
50        self.ratio_window.clear();
51    }
52
53    fn on_bar(&mut self, bar: &Bar) -> Option<IndicatorOutput> {
54        let range = (bar.high - bar.low).max(1e-8);
55        let e1 = self.ema1.update(range)?;
56        let e2 = self.ema2.update(e1)?;
57
58        let ratio = if e2 > 1e-8 { e1 / e2 } else { 1.0 };
59
60        self.ratio_window.push_back(ratio);
61        if self.ratio_window.len() > self.period {
62            self.ratio_window.pop_front();
63        }
64
65        if self.ratio_window.len() < self.period {
66            return None;
67        }
68
69        let mass_val: f64 = self.ratio_window.iter().sum();
70        Some(IndicatorOutput::new(mass_val))
71    }
72
73    fn alerts(&self) -> Vec<IndicatorAlert> {
74        Vec::new()
75    }
76}
77
78#[cfg(test)]
79mod tests {
80    use super::*;
81
82    #[test]
83    fn test_mass_index() {
84        let mut mi = MassIndexEngine::with_defaults();
85        let mut out = None;
86        for i in 0..50 {
87            let b = Bar::new(i, 100.0, 105.0, 95.0, 100.0 + i as f64, 1000.0);
88            out = mi.on_bar(&b);
89        }
90        assert!(out.is_some());
91    }
92}