Skip to main content

egui_charts/studies/builtin/
mass_index.rs

1use super::smoothing::Smoothing;
2use crate::model::Bar;
3use crate::studies::palette;
4use crate::studies::stepper::Stepper;
5use crate::studies::{Indicator, IndicatorValue, IndicatorValues};
6use egui::Color32;
7
8/// Mass Index
9/// Identifies trend reversals based on range expansion
10/// Developed by Donald Dorsey
11#[derive(Clone)]
12pub struct MassIndex {
13    /// EMA period for range smoothing (typically 9)
14    ema_period: usize,
15    /// Summation period (typically 25)
16    sum_period: usize,
17    values: IndicatorValues,
18    stepper: Stepper<State>,
19    color: Color32,
20    visible: bool,
21}
22
23#[derive(Clone, Default)]
24struct State {
25    index: usize,
26    count: usize,
27    ema: [Smoothing; 2],
28    ratios: std::collections::VecDeque<f64>,
29    pending: Vec<(usize, f64)>,
30}
31
32impl MassIndex {
33    /// Create with EMA period 9 and rolling-sum period 25.
34    pub fn new() -> Self {
35        Self {
36            ema_period: 9,
37            sum_period: 25,
38            values: IndicatorValues::new(),
39            stepper: Stepper::default(),
40            color: palette::LIGHT_BLUE,
41            visible: true,
42        }
43    }
44
45    /// Set the EMA and rolling-sum periods in bars, each at least one.
46    pub fn with_periods(mut self, ema_period: usize, sum_period: usize) -> Self {
47        self.ema_period = ema_period.max(1);
48        self.sum_period = sum_period.max(1);
49        self
50    }
51
52    /// Set the plot color.
53    pub fn with_color(mut self, color: Color32) -> Self {
54        self.color = color;
55        self
56    }
57}
58
59impl Default for MassIndex {
60    fn default() -> Self {
61        Self::new()
62    }
63}
64
65impl Indicator for MassIndex {
66    crate::studies::inputs::study_inputs!(self;
67        ema_period: length("EMA period", 1, usize::MAX),
68        sum_period: length("Sum period", 1, usize::MAX)
69    ; {});
70
71    fn name(&self) -> &str {
72        "Mass Index"
73    }
74
75    fn desc(&self) -> &str {
76        "Mass Index - Identifies reversals based on range expansion"
77    }
78
79    fn calculate(&mut self, data: &[Bar]) {
80        self.update(data, 0);
81    }
82
83    fn update(&mut self, data: &[Bar], from: usize) {
84        self.stepper
85            .run(data, from, &mut self.values, |state, bar| {
86                let index = state.index;
87                state.index += 1;
88                if !bar.high.is_finite() || !bar.low.is_finite() {
89                    return IndicatorValue::None;
90                }
91                state.count += 1;
92                let alpha = 2.0 / (self.ema_period as f64 + 1.0);
93                let first = state.ema[0].next(bar.high - bar.low, self.ema_period, alpha);
94                let second = state.ema[1].next(first, self.ema_period, alpha);
95                let ratio = if first.is_nan() || second.is_nan() || second == 0.0 {
96                    f64::NAN
97                } else {
98                    first / second
99                };
100                state.ratios.push_back(ratio);
101                if state.ratios.len() > self.sum_period {
102                    state.ratios.pop_front();
103                }
104                let mut sum = 0.0;
105                if state.ratios.len() < self.sum_period {
106                    return IndicatorValue::None;
107                }
108                for &value in &state.ratios {
109                    if value.is_nan() {
110                        return IndicatorValue::None;
111                    }
112                    sum += value;
113                }
114                let required = self
115                    .ema_period
116                    .saturating_mul(2)
117                    .saturating_add(self.sum_period);
118                if state.count < required {
119                    if sum.is_finite() {
120                        state.pending.push((index, sum));
121                    }
122                    IndicatorValue::None
123                } else {
124                    IndicatorValue::Single(sum)
125                }
126            });
127        let required = self
128            .ema_period
129            .saturating_mul(2)
130            .saturating_add(self.sum_period);
131        for &(index, value) in &self.stepper.state().pending {
132            self.values.set(
133                index,
134                if self.stepper.state().count < required {
135                    IndicatorValue::None
136                } else {
137                    IndicatorValue::Single(value)
138                },
139            );
140        }
141    }
142
143    fn is_incremental(&self) -> bool {
144        true
145    }
146
147    fn values(&self) -> &IndicatorValues {
148        &self.values
149    }
150
151    fn colors(&self) -> Vec<Color32> {
152        vec![self.color]
153    }
154
155    fn set_colors(&mut self, colors: Vec<Color32>) {
156        if !colors.is_empty() {
157            self.color = colors[0];
158        }
159    }
160
161    fn is_overlay(&self) -> bool {
162        false // Separate pane
163    }
164
165    fn is_visible(&self) -> bool {
166        self.visible
167    }
168
169    fn set_visible(&mut self, visible: bool) {
170        self.visible = visible;
171    }
172
173    fn clone_box(&self) -> Box<dyn Indicator> {
174        Box::new(self.clone())
175    }
176
177    fn line_names(&self) -> Vec<String> {
178        vec![format!("Mass({},{})", self.ema_period, self.sum_period)]
179    }
180}