mantis_ta/indicators/momentum/
mfi.rs1use crate::indicators::Indicator;
2use crate::types::Candle;
3use crate::utils::ringbuf::RingBuf;
4
5#[derive(Debug, Clone)]
47pub struct MFI {
48 period: usize,
49 prev_tp: Option<f64>,
50 flow_window: RingBuf<(f64, f64)>,
51 pos_sum: f64,
52 neg_sum: f64,
53}
54
55impl MFI {
56 pub fn new(period: usize) -> Self {
57 assert!(period > 0, "period must be > 0");
58 Self {
59 period,
60 prev_tp: None,
61 flow_window: RingBuf::new(period, (0.0, 0.0)),
62 pos_sum: 0.0,
63 neg_sum: 0.0,
64 }
65 }
66
67 #[inline]
68 fn compute_mfi(pos_sum: f64, neg_sum: f64) -> f64 {
69 if neg_sum == 0.0 {
70 return 100.0;
71 }
72 let money_ratio = pos_sum / neg_sum;
73 100.0 - 100.0 / (1.0 + money_ratio)
74 }
75
76 #[inline]
77 fn update(&mut self, high: f64, low: f64, close: f64, volume: f64) -> Option<f64> {
78 let typical_price = (high + low + close) / 3.0;
79 let Some(prev) = self.prev_tp else {
80 self.prev_tp = Some(typical_price);
81 return None;
82 };
83 self.prev_tp = Some(typical_price);
84
85 let raw_flow = typical_price * volume;
86 let (pos, neg) = match typical_price.partial_cmp(&prev) {
87 Some(std::cmp::Ordering::Greater) => (raw_flow, 0.0),
88 Some(std::cmp::Ordering::Less) => (0.0, raw_flow),
89 _ => (0.0, 0.0),
90 };
91
92 if let Some((old_pos, old_neg)) = self.flow_window.push((pos, neg)) {
93 self.pos_sum -= old_pos;
94 self.neg_sum -= old_neg;
95 }
96 self.pos_sum += pos;
97 self.neg_sum += neg;
98
99 if self.flow_window.len() < self.period {
100 None
101 } else {
102 Some(Self::compute_mfi(self.pos_sum, self.neg_sum))
103 }
104 }
105}
106
107impl Indicator for MFI {
108 type Output = f64;
109
110 fn next(&mut self, candle: &Candle) -> Option<Self::Output> {
111 self.update(candle.high, candle.low, candle.close, candle.volume)
112 }
113
114 fn reset(&mut self) {
115 self.prev_tp = None;
116 self.flow_window = RingBuf::new(self.period, (0.0, 0.0));
117 self.pos_sum = 0.0;
118 self.neg_sum = 0.0;
119 }
120
121 fn warmup_period(&self) -> usize {
122 self.period + 1
123 }
124
125 fn clone_boxed(&self) -> Box<dyn Indicator<Output = Self::Output>> {
126 Box::new(self.clone())
127 }
128}
129
130#[cfg(test)]
131mod tests {
132 use super::*;
133
134 fn candle(h: f64, l: f64, c: f64, v: f64) -> Candle {
135 Candle {
136 timestamp: 0,
137 open: c,
138 high: h,
139 low: l,
140 close: c,
141 volume: v,
142 }
143 }
144
145 #[test]
146 fn mfi_warmup_period_is_period_plus_one() {
147 let mfi = MFI::new(14);
148 assert_eq!(mfi.warmup_period(), 15);
149 }
150
151 #[test]
152 fn mfi_emits_none_until_warmup() {
153 let mut mfi = MFI::new(3);
154 let candles = vec![
155 candle(11.0, 9.0, 10.0, 100.0),
156 candle(12.0, 10.0, 11.0, 110.0),
157 candle(13.0, 11.0, 12.0, 120.0),
158 ];
159 let outputs: Vec<_> = candles.iter().map(|c| mfi.next(c)).collect();
160 assert!(outputs.iter().all(|v| v.is_none()));
161 }
162
163 #[test]
164 fn mfi_all_rising_typical_price_saturates_at_100() {
165 let mut mfi = MFI::new(3);
166 let candles = vec![
167 candle(11.0, 9.0, 10.0, 100.0),
168 candle(12.0, 10.0, 11.0, 110.0),
169 candle(13.0, 11.0, 12.0, 120.0),
170 candle(14.0, 12.0, 13.0, 130.0),
171 ];
172 let outputs: Vec<_> = candles.iter().map(|c| mfi.next(c)).collect();
173 assert!(outputs[3].is_some());
174 assert!((outputs[3].unwrap() - 100.0).abs() < 1e-9);
175 }
176
177 #[test]
178 fn mfi_all_falling_typical_price_saturates_at_0() {
179 let mut mfi = MFI::new(3);
180 let candles = vec![
181 candle(14.0, 12.0, 13.0, 130.0),
182 candle(13.0, 11.0, 12.0, 120.0),
183 candle(12.0, 10.0, 11.0, 110.0),
184 candle(11.0, 9.0, 10.0, 100.0),
185 ];
186 let outputs: Vec<_> = candles.iter().map(|c| mfi.next(c)).collect();
187 assert!(outputs[3].is_some());
188 assert!(outputs[3].unwrap().abs() < 1e-9);
189 }
190
191 #[test]
192 fn mfi_flat_typical_price_excluded_from_both_sums() {
193 let mut mfi = MFI::new(3);
196 let candles = vec![
197 candle(11.0, 9.0, 9.5, 100.0),
198 candle(12.0, 10.0, 10.5, 110.0),
199 candle(12.0, 10.0, 10.5, 120.0), candle(13.0, 11.0, 11.5, 130.0),
201 ];
202 let outputs: Vec<_> = candles.iter().map(|c| mfi.next(c)).collect();
203 assert!(outputs[3].is_some());
206 assert!((outputs[3].unwrap() - 100.0).abs() < 1e-9);
207 }
208
209 #[test]
210 fn mfi_reset_clears_state() {
211 let mut mfi = MFI::new(3);
212 let candles = vec![
213 candle(11.0, 9.0, 10.0, 100.0),
214 candle(12.0, 10.0, 11.0, 110.0),
215 candle(13.0, 11.0, 12.0, 120.0),
216 candle(14.0, 12.0, 13.0, 130.0),
217 ];
218 for c in &candles {
219 mfi.next(c);
220 }
221 mfi.reset();
222 assert!(mfi.next(&candles[0]).is_none());
223 assert_eq!(mfi.pos_sum, 0.0);
224 assert_eq!(mfi.neg_sum, 0.0);
225 }
226
227 #[test]
228 fn mfi_streaming_matches_batch() {
229 let candles = vec![
230 candle(11.0, 9.0, 10.0, 100.0),
231 candle(12.0, 10.0, 11.0, 110.0),
232 candle(13.0, 11.0, 12.0, 120.0),
233 candle(14.0, 12.0, 13.0, 130.0),
234 candle(12.0, 10.0, 11.0, 90.0),
235 candle(11.0, 9.0, 10.0, 95.0),
236 ];
237 let batch = MFI::new(3).calculate(&candles);
238 let mut streaming_out = Vec::new();
239 let mut mfi = MFI::new(3);
240 for c in &candles {
241 streaming_out.push(mfi.next(c));
242 }
243 assert_eq!(batch, streaming_out);
244 }
245}