egui_charts/studies/builtin/
mass_index.rs1use 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#[derive(Clone)]
12pub struct MassIndex {
13 ema_period: usize,
15 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 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 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 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 }
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}