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