kestrel_chartkit/indicator/
mass_index.rs1use super::smoothing::Ema;
2use super::{Indicator, IndicatorAlert, IndicatorOutput};
3use crate::model::Bar;
4use std::collections::VecDeque;
5
6#[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}