wickra_core/indicators/
mfi.rs1use std::collections::VecDeque;
4
5use crate::error::{Error, Result};
6use crate::indicators::rolling_moments::RollingSum;
7use crate::ohlcv::Candle;
8use crate::traits::Indicator;
9
10#[derive(Debug, Clone)]
32pub struct Mfi {
33 period: usize,
34 prev_tp: Option<f64>,
35 pos_window: VecDeque<f64>,
36 neg_window: VecDeque<f64>,
37 pos_sum: RollingSum,
38 neg_sum: RollingSum,
39}
40
41impl Mfi {
42 pub fn new(period: usize) -> Result<Self> {
45 if period == 0 {
46 return Err(Error::PeriodZero);
47 }
48 if period > crate::error::MAX_PERIOD {
49 return Err(Error::InvalidPeriod {
50 message: crate::error::PERIOD_ABOVE_MAX,
51 });
52 }
53 Ok(Self {
54 period,
55 prev_tp: None,
56 pos_window: VecDeque::with_capacity(period),
57 neg_window: VecDeque::with_capacity(period),
58 pos_sum: RollingSum::new(),
59 neg_sum: RollingSum::new(),
60 })
61 }
62
63 pub const fn period(&self) -> usize {
65 self.period
66 }
67}
68
69impl Indicator for Mfi {
70 type Input = Candle;
71 type Output = f64;
72
73 fn update(&mut self, candle: Candle) -> Option<f64> {
74 let tp = candle.typical_price();
75
76 let Some(prev) = self.prev_tp else {
81 self.prev_tp = Some(tp);
82 return None;
83 };
84
85 let mf = tp * candle.volume;
86 let (pos_flow, neg_flow) = if tp > prev {
87 (mf, 0.0)
88 } else if tp < prev {
89 (0.0, mf)
90 } else {
91 (0.0, 0.0)
92 };
93
94 if self.pos_window.len() == self.period {
95 let old_pos = self.pos_window.pop_front().expect("non-empty");
96 let old_neg = self.neg_window.pop_front().expect("non-empty");
97 self.pos_sum.evict(old_pos);
98 self.neg_sum.evict(old_neg);
99 }
100 self.pos_window.push_back(pos_flow);
101 self.neg_window.push_back(neg_flow);
102 self.pos_sum.push(pos_flow);
103 self.neg_sum.push(neg_flow);
104 if self.pos_sum.needs_reseed(self.period) {
105 self.pos_sum.reseed(self.pos_window.iter().copied());
106 self.neg_sum.reseed(self.neg_window.iter().copied());
107 }
108
109 self.prev_tp = Some(tp);
110
111 if self.pos_window.len() < self.period {
112 return None;
113 }
114 let (pos_sum, neg_sum) = (self.pos_sum.value(), self.neg_sum.value());
117 if pos_sum == 0.0 && neg_sum == 0.0 {
118 return Some(50.0);
119 }
120 if neg_sum == 0.0 {
121 return Some(100.0);
122 }
123 let mr = pos_sum / neg_sum;
124 Some(100.0 - 100.0 / (1.0 + mr))
125 }
126
127 fn reset(&mut self) {
128 self.prev_tp = None;
129 self.pos_window.clear();
130 self.neg_window.clear();
131 self.pos_sum.reset();
132 self.neg_sum.reset();
133 }
134
135 #[inline]
136 fn warmup_period(&self) -> usize {
137 self.period + 1
140 }
141
142 #[inline]
143 fn is_ready(&self) -> bool {
144 self.pos_window.len() == self.period
145 }
146
147 #[inline]
148 fn name(&self) -> &'static str {
149 "MFI"
150 }
151}
152
153#[cfg(test)]
154mod tests {
155 use super::*;
156 use crate::traits::BatchExt;
157 use approx::assert_relative_eq;
158
159 fn c(price: f64, volume: f64) -> Candle {
160 Candle::new(price, price, price, price, volume, 0).unwrap()
161 }
162
163 #[test]
164 fn pure_uptrend_yields_high_mfi() {
165 let candles: Vec<Candle> = (1..30).map(|i| c(f64::from(i), 100.0)).collect();
166 let mut mfi = Mfi::new(14).unwrap();
167 let last = mfi.batch(&candles).into_iter().flatten().last().unwrap();
168 assert_relative_eq!(last, 100.0, epsilon = 1e-9);
169 }
170
171 #[test]
172 fn pure_downtrend_yields_low_mfi() {
173 let candles: Vec<Candle> = (1..30).rev().map(|i| c(f64::from(i), 100.0)).collect();
174 let mut mfi = Mfi::new(14).unwrap();
175 let last = mfi.batch(&candles).into_iter().flatten().last().unwrap();
176 assert_relative_eq!(last, 0.0, epsilon = 1e-9);
177 }
178
179 #[test]
180 fn batch_equals_streaming() {
181 let candles: Vec<Candle> = (0..40).map(|i| c(f64::from(i) + 10.0, 50.0)).collect();
182 let mut a = Mfi::new(14).unwrap();
183 let mut b = Mfi::new(14).unwrap();
184 assert_eq!(
185 a.batch(&candles),
186 candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
187 );
188 }
189
190 #[test]
191 fn reset_clears_state() {
192 let candles: Vec<Candle> = (1..30).map(|i| c(f64::from(i), 100.0)).collect();
193 let mut mfi = Mfi::new(14).unwrap();
194 mfi.batch(&candles);
195 assert!(mfi.is_ready());
196 mfi.reset();
197 assert!(!mfi.is_ready());
198 }
199
200 #[test]
203 fn accessors_and_metadata() {
204 let mfi = Mfi::new(14).unwrap();
205 assert_eq!(mfi.period(), 14);
206 assert_eq!(mfi.name(), "MFI");
207 }
208
209 #[test]
214 fn flat_typical_prices_default_to_50() {
215 let mut mfi = Mfi::new(3).unwrap();
216 let candles: Vec<Candle> = (0..6)
217 .map(|i| Candle::new(10.0, 10.0, 10.0, 10.0, 1.0, i).unwrap())
218 .collect();
219 let last = mfi
220 .batch(&candles)
221 .into_iter()
222 .flatten()
223 .last()
224 .expect("emits");
225 assert_eq!(last, 50.0);
226 }
227
228 #[test]
229 fn rejects_zero_period() {
230 assert!(Mfi::new(0).is_err());
231 }
232
233 #[test]
234 fn first_value_emitted_on_period_plus_one_candle() {
235 let candles: Vec<Candle> = (1..=20).map(|i| c(f64::from(i), 100.0)).collect();
238 let mut mfi = Mfi::new(5).unwrap();
239 let out = mfi.batch(&candles);
240 for (i, v) in out.iter().enumerate().take(5) {
241 assert!(v.is_none(), "candle index {i} must be None during warmup");
242 }
243 assert!(
244 out[5].is_some(),
245 "first MFI value lands at index period (5)"
246 );
247 assert_eq!(mfi.warmup_period(), 6);
248 }
249
250 #[test]
251 fn known_value_period_2() {
252 let candles = vec![c(10.0, 100.0), c(12.0, 100.0), c(11.0, 100.0)];
257 let mut mfi = Mfi::new(2).unwrap();
258 let out = mfi.batch(&candles);
259 assert!(out[0].is_none());
260 assert!(out[1].is_none());
261 assert_relative_eq!(out[2].unwrap(), 1200.0 / 23.0, epsilon = 1e-9);
262 }
263}