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)]
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}