wickra_core/indicators/
mass_index.rs1use std::collections::VecDeque;
4
5use crate::error::{Error, Result};
6use crate::ohlcv::Candle;
7use crate::traits::Indicator;
8
9use super::Ema;
10
11#[derive(Debug, Clone)]
46pub struct MassIndex {
47 ema_period: usize,
48 sum_period: usize,
49 ema1: Ema,
50 ema2: Ema,
51 window: VecDeque<f64>,
53 sum: f64,
54 last: Option<f64>,
55}
56
57impl MassIndex {
58 pub fn new(ema_period: usize, sum_period: usize) -> Result<Self> {
65 if ema_period == 0 || sum_period == 0 {
66 return Err(Error::PeriodZero);
67 }
68 Ok(Self {
69 ema_period,
70 sum_period,
71 ema1: Ema::new(ema_period)?,
72 ema2: Ema::new(ema_period)?,
73 window: VecDeque::with_capacity(sum_period),
74 sum: 0.0,
75 last: None,
76 })
77 }
78
79 pub const fn periods(&self) -> (usize, usize) {
81 (self.ema_period, self.sum_period)
82 }
83
84 pub const fn value(&self) -> Option<f64> {
86 self.last
87 }
88}
89
90impl Indicator for MassIndex {
91 type Input = Candle;
92 type Output = f64;
93
94 #[inline]
95 fn update(&mut self, candle: Candle) -> Option<f64> {
96 let range = candle.high - candle.low;
97 let single = self.ema1.update(range)?;
98 let double = self.ema2.update(single)?;
99 let ratio = if double == 0.0 {
100 1.0
102 } else {
103 single / double
104 };
105 if self.window.len() == self.sum_period {
106 self.sum -= self.window.pop_front().expect("window is non-empty");
107 }
108 self.window.push_back(ratio);
109 self.sum += ratio;
110 if self.window.len() < self.sum_period {
111 return None;
112 }
113 self.last = Some(self.sum);
114 Some(self.sum)
115 }
116
117 fn reset(&mut self) {
118 self.ema1.reset();
119 self.ema2.reset();
120 self.window.clear();
121 self.sum = 0.0;
122 self.last = None;
123 }
124
125 #[inline]
126 fn warmup_period(&self) -> usize {
127 2 * self.ema_period + self.sum_period - 2
130 }
131
132 #[inline]
133 fn is_ready(&self) -> bool {
134 self.last.is_some()
135 }
136
137 #[inline]
138 fn name(&self) -> &'static str {
139 "MassIndex"
140 }
141}
142
143#[cfg(test)]
144mod tests {
145 use super::*;
146 use crate::traits::BatchExt;
147 use approx::assert_relative_eq;
148
149 fn candle(mid: f64, span: f64, ts: i64) -> Candle {
151 Candle::new(mid, mid + span / 2.0, mid - span / 2.0, mid, 1.0, ts).unwrap()
152 }
153
154 #[test]
155 fn new_rejects_zero_period() {
156 assert!(matches!(MassIndex::new(0, 25), Err(Error::PeriodZero)));
157 assert!(matches!(MassIndex::new(9, 0), Err(Error::PeriodZero)));
158 }
159
160 #[test]
164 fn accessors_and_metadata() {
165 let mut mi = MassIndex::new(9, 25).unwrap();
166 assert_eq!(mi.periods(), (9, 25));
167 assert_eq!(mi.name(), "MassIndex");
168 assert_eq!(mi.value(), None);
169 for i in 0..mi.warmup_period() {
170 mi.update(candle(100.0, 2.0, i64::try_from(i).unwrap()));
171 }
172 assert!(mi.value().is_some());
173 }
174
175 #[test]
176 fn warmup_period_formula() {
177 let mi = MassIndex::new(9, 25).unwrap();
178 assert_eq!(mi.warmup_period(), 2 * 9 + 25 - 2);
179 }
180
181 #[test]
182 fn first_emission_at_warmup_period() {
183 let mut mi = MassIndex::new(3, 4).unwrap();
184 let warmup = mi.warmup_period(); assert_eq!(warmup, 8);
186 let candles: Vec<Candle> = (0..20).map(|i| candle(100.0 + i as f64, 2.0, i)).collect();
187 let out = mi.batch(&candles);
188 for v in out.iter().take(warmup - 1) {
189 assert!(v.is_none());
190 }
191 assert!(out[warmup - 1].is_some());
192 }
193
194 #[test]
195 fn constant_range_sums_to_sum_period() {
196 let mut mi = MassIndex::new(3, 4).unwrap();
199 let candles: Vec<Candle> = (0..40).map(|i| candle(100.0 + i as f64, 2.0, i)).collect();
200 for v in mi.batch(&candles).into_iter().flatten() {
201 assert_relative_eq!(v, 4.0, epsilon = 1e-9);
202 }
203 }
204
205 #[test]
206 fn zero_range_market_sums_to_sum_period() {
207 let mut mi = MassIndex::new(3, 4).unwrap();
208 let candles: Vec<Candle> = (0..40).map(|i| candle(100.0, 0.0, i)).collect();
209 for v in mi.batch(&candles).into_iter().flatten() {
210 assert_relative_eq!(v, 4.0, epsilon = 1e-12);
211 }
212 }
213
214 #[test]
215 fn reset_clears_state() {
216 let mut mi = MassIndex::new(3, 4).unwrap();
217 let candles: Vec<Candle> = (0..20).map(|i| candle(100.0 + i as f64, 2.0, i)).collect();
218 mi.batch(&candles);
219 assert!(mi.is_ready());
220 mi.reset();
221 assert!(!mi.is_ready());
222 assert_eq!(mi.update(candles[0]), None);
223 }
224
225 #[test]
226 fn batch_equals_streaming() {
227 let candles: Vec<Candle> = (0..120)
228 .map(|i| {
229 let span = 2.0 + (i as f64 * 0.3).sin().abs() * 3.0;
230 candle(100.0 + (i as f64 * 0.2).cos() * 5.0, span, i)
231 })
232 .collect();
233 let batch = MassIndex::new(9, 25).unwrap().batch(&candles);
234 let mut b = MassIndex::new(9, 25).unwrap();
235 let streamed: Vec<_> = candles.iter().map(|c| b.update(*c)).collect();
236 assert_eq!(batch, streamed);
237 }
238}