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 Engine.
7/// Mass Index = Sum(EMA(High - Low, 9) / EMA(EMA(High - Low, 9), 9), 25)
8#[derive(Debug, Clone)]
9pub struct MassIndexEngine {
10    period: usize,
11    ema1: Ema,
12    ema2: Ema,
13    ratio_window: VecDeque<f64>,
14}
15
16impl MassIndexEngine {
17    pub fn new(period: usize) -> Self {
18        Self {
19            period: period.max(1),
20            ema1: Ema::new(9),
21            ema2: Ema::new(9),
22            ratio_window: VecDeque::with_capacity(period),
23        }
24    }
25
26    pub fn with_defaults() -> Self {
27        Self::new(25)
28    }
29}
30
31impl Indicator for MassIndexEngine {
32    fn name(&self) -> &str {
33        "mass_index"
34    }
35
36    fn warmup_period(&self) -> usize {
37        self.period + 18
38    }
39
40    fn reset(&mut self) {
41        self.ema1.reset();
42        self.ema2.reset();
43        self.ratio_window.clear();
44    }
45
46    fn on_bar(&mut self, bar: &Bar) -> Option<IndicatorOutput> {
47        let range = (bar.high - bar.low).max(1e-8);
48        let e1 = self.ema1.update(range);
49        let e2 = self.ema2.update(e1);
50
51        let ratio = if e2 > 1e-8 { e1 / e2 } else { 1.0 };
52
53        self.ratio_window.push_back(ratio);
54        if self.ratio_window.len() > self.period {
55            self.ratio_window.pop_front();
56        }
57
58        if self.ratio_window.len() < self.period {
59            return None;
60        }
61
62        let mass_val: f64 = self.ratio_window.iter().sum();
63        Some(IndicatorOutput::new(mass_val))
64    }
65
66    fn alerts(&self) -> Vec<IndicatorAlert> {
67        Vec::new()
68    }
69}
70
71#[cfg(test)]
72mod tests {
73    use super::*;
74
75    #[test]
76    fn test_mass_index() {
77        let mut mi = MassIndexEngine::with_defaults();
78        let mut out = None;
79        for i in 0..50 {
80            let b = Bar::new(i, 100.0, 105.0, 95.0, 100.0 + i as f64, 1000.0);
81            out = mi.on_bar(&b);
82        }
83        assert!(out.is_some());
84    }
85}