Skip to main content

finance_solution/stocks/ta/
mfi.rs

1//! # Money Flow Index (MFI)
2//!
3//! Volume-weighted RSI-style oscillator on typical price:
4//!
5//! ```text
6//! TP = (H + L + C) / 3
7//! raw money flow = TP * volume
8//! +MF / −MF over period by TP direction vs prior TP
9//! MFI = 100 − 100 / (1 + +MF/−MF)
10//! ```
11//!
12//! Warm-up: needs `period` money-flow samples after the first bar (first TP has no prior).
13//! First MFI at index `period` (period flows from bars 1..=period).  
14//! If −MF = 0 and +MF > 0 → MFI = 100; if both zero → `None`.
15//!
16//! Default: **period 14** ([`MfiParams::period_14`]).
17//!
18//! ---
19//!
20//! ## Trading perspective
21//!
22//! | Region | Habit (classic) |
23//! |--------|-----------------|
24//! | MFI \> 80 | “Overbought” with volume |
25//! | MFI \< 20 | “Oversold” with volume |
26//! | Divergence vs price | Volume not confirming price extreme |
27//!
28//! ## vs RSI / OBV
29//!
30//! | | MFI | RSI | OBV |
31//! |--|-----|-----|-----|
32//! | Uses volume | Yes (× TP) | No | Yes (cumulative) |
33//! | Bounded | 0–100 | 0–100 | Unbounded |
34//! | Narrative | Money flow heat | Close momentum | Flow confirmation |
35//!
36//! Prefer **MFI** when volume quality matters for OB/OS; **RSI** when volume is noisy or
37//! missing; **OBV** for cumulative divergence without bounds.
38//!
39//! ## Pairs well with
40//!
41//! - **Price oscillators (RSI/WillR)** — agreement at extremes is stronger; disagreement is a flag.
42//! - **VWAP** — intraday location vs session VWAP + MFI.
43//! - **ADX** — high MFI in a strong ADX trend can stay elevated (trend, not auto-fade).
44//!
45//! ---
46//!
47//! ## Engineering
48//!
49//! [`MfiParams`] → [`mfi`] / [`MfiState`] → [`mfi_solution`]. Batch via state.
50//! After warm-up each push is **O(1)** via rings of +MF/−MF contributions.
51
52use crate::stocks::ta::common::{opt_cell, require_hlc, validate_positive_volume};
53use crate::stocks::ta::ring::RingF64;
54use crate::util::error::{require_finite, FinanceError, FinanceResult};
55use crate::util::primitives::PeriodLength;
56use crate::{columns_with_strings, print_table_locale_opt};
57
58#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
59pub struct MfiParams {
60    pub period: usize,
61}
62
63impl MfiParams {
64    pub const fn new(period: usize) -> Self {
65        Self { period }
66    }
67
68    pub const fn period_14() -> Self {
69        Self { period: 14 }
70    }
71}
72
73#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
74pub struct ValidatedMfi {
75    params: MfiParams,
76}
77
78impl ValidatedMfi {
79    pub fn new(params: MfiParams) -> FinanceResult<Self> {
80        PeriodLength::new(params.period)?;
81        Ok(Self { params })
82    }
83
84    pub fn params(self) -> MfiParams {
85        self.params
86    }
87
88    pub fn compute(
89        self,
90        high: &[f64],
91        low: &[f64],
92        close: &[f64],
93        volume: &[f64],
94    ) -> FinanceResult<MfiSeries> {
95        mfi_validated(high, low, close, volume, self)
96    }
97}
98
99#[derive(Clone, Debug, PartialEq)]
100pub struct MfiSeries {
101    pub mfi: Vec<Option<f64>>,
102    pub params: MfiParams,
103}
104
105impl MfiSeries {
106    pub fn last(&self) -> Option<f64> {
107        self.mfi.iter().rev().find_map(|x| *x)
108    }
109}
110
111/// Incremental MFI.
112#[derive(Clone, Debug)]
113pub struct MfiState {
114    params: MfiParams,
115    prev_tp: Option<f64>,
116    pos: RingF64,
117    neg: RingF64,
118    last: Option<f64>,
119}
120
121impl MfiState {
122    pub fn new(params: MfiParams) -> FinanceResult<Self> {
123        let _ = ValidatedMfi::new(params)?;
124        Ok(Self {
125            params,
126            prev_tp: None,
127            pos: RingF64::with_capacity(params.period),
128            neg: RingF64::with_capacity(params.period),
129            last: None,
130        })
131    }
132
133    pub fn from_history(
134        params: MfiParams,
135        high: &[f64],
136        low: &[f64],
137        close: &[f64],
138        volume: &[f64],
139    ) -> FinanceResult<Self> {
140        let mut s = Self::new(params)?;
141        let _ = s.push_bars(high, low, close, volume)?;
142        Ok(s)
143    }
144
145    pub fn params(&self) -> MfiParams {
146        self.params
147    }
148
149    pub fn reset(&mut self) {
150        self.prev_tp = None;
151        self.pos.clear();
152        self.neg.clear();
153        self.last = None;
154    }
155
156    pub fn push(
157        &mut self,
158        high: f64,
159        low: f64,
160        close: f64,
161        volume: f64,
162    ) -> FinanceResult<Option<f64>> {
163        require_finite("high", high)?;
164        require_finite("low", low)?;
165        require_finite("close", close)?;
166        require_finite("volume", volume)?;
167        if high < low {
168            return Err(FinanceError::InvalidCashflow {
169                message: "high must be >= low for each bar",
170            });
171        }
172        if volume < 0.0 {
173            return Err(FinanceError::InvalidCashflow {
174                message: "volume must be non-negative",
175            });
176        }
177        let tp = (high + low + close) / 3.0;
178        let rmf = tp * volume;
179        let out = match self.prev_tp {
180            None => {
181                self.prev_tp = Some(tp);
182                self.last = None;
183                None
184            }
185            Some(ptp) => {
186                let (p, n) = if tp > ptp {
187                    (rmf, 0.0)
188                } else if tp < ptp {
189                    (0.0, rmf)
190                } else {
191                    (0.0, 0.0)
192                };
193                let _ = self.pos.push(p);
194                let _ = self.neg.push(n);
195                self.prev_tp = Some(tp);
196                if !self.pos.is_full() {
197                    self.last = None;
198                    None
199                } else {
200                    let pos_sum = self.pos.sum();
201                    let neg_sum = self.neg.sum();
202                    let mfi = if neg_sum == 0.0 && pos_sum == 0.0 {
203                        None
204                    } else if neg_sum == 0.0 {
205                        Some(100.0)
206                    } else {
207                        let ratio = pos_sum / neg_sum;
208                        Some(100.0 - 100.0 / (1.0 + ratio))
209                    };
210                    self.last = mfi;
211                    mfi
212                }
213            }
214        };
215        Ok(out)
216    }
217
218    pub fn push_bars(
219        &mut self,
220        high: &[f64],
221        low: &[f64],
222        close: &[f64],
223        volume: &[f64],
224    ) -> FinanceResult<Vec<Option<f64>>> {
225        require_hlc(high, low, close)?;
226        validate_positive_volume(volume)?;
227        if close.len() != volume.len() {
228            return Err(FinanceError::LengthMismatch {
229                left: close.len(),
230                right: volume.len(),
231                context: "close/volume",
232            });
233        }
234        let mut out = Vec::with_capacity(close.len());
235        for i in 0..close.len() {
236            out.push(self.push(high[i], low[i], close[i], volume[i])?);
237        }
238        Ok(out)
239    }
240
241    pub fn last(&self) -> Option<f64> {
242        self.last
243    }
244}
245
246pub fn mfi(
247    high: &[f64],
248    low: &[f64],
249    close: &[f64],
250    volume: &[f64],
251    params: MfiParams,
252) -> FinanceResult<MfiSeries> {
253    ValidatedMfi::new(params)?.compute(high, low, close, volume)
254}
255
256fn mfi_validated(
257    high: &[f64],
258    low: &[f64],
259    close: &[f64],
260    volume: &[f64],
261    eng: ValidatedMfi,
262) -> FinanceResult<MfiSeries> {
263    let mut st = MfiState::new(eng.params)?;
264    let mfi = st.push_bars(high, low, close, volume)?;
265    Ok(MfiSeries {
266        mfi,
267        params: eng.params,
268    })
269}
270
271#[derive(Clone, Debug)]
272pub struct MfiSolution {
273    series: MfiSeries,
274    close: Vec<f64>,
275    formula: String,
276}
277
278impl MfiSolution {
279    pub fn series(&self) -> &MfiSeries {
280        &self.series
281    }
282    pub fn formula(&self) -> &str {
283        &self.formula
284    }
285
286    pub fn print_table(&self) {
287        self.print_table_locale_opt(None, None);
288    }
289
290    pub fn print_table_locale(&self, locale: &num_format::Locale, precision: usize) {
291        self.print_table_locale_opt(Some(locale), Some(precision));
292    }
293
294    fn print_table_locale_opt(
295        &self,
296        locale: Option<&num_format::Locale>,
297        precision: Option<usize>,
298    ) {
299        let columns = columns_with_strings(&[
300            ("period", "i", true),
301            ("close", "f", true),
302            ("mfi", "f", true),
303        ]);
304        let data = self
305            .close
306            .iter()
307            .enumerate()
308            .map(|(i, c)| vec![i.to_string(), c.to_string(), opt_cell(self.series.mfi[i])])
309            .collect();
310        print_table_locale_opt(&columns, data, locale, precision);
311    }
312}
313
314/// # Examples
315/// ```
316/// use finance_solution::stocks::ta::{mfi_solution, MfiParams};
317/// let n = 30usize;
318/// let h: Vec<_> = (0..n).map(|i| 101.0 + i as f64 * 0.1).collect();
319/// let l: Vec<_> = (0..n).map(|i| 99.0 + i as f64 * 0.1).collect();
320/// let c: Vec<_> = (0..n).map(|i| 100.0 + i as f64 * 0.1).collect();
321/// let v: Vec<_> = (0..n).map(|i| 1000.0 + i as f64).collect();
322/// let sol = mfi_solution(&h, &l, &c, &v, MfiParams::period_14()).unwrap();
323/// assert!(sol.formula().contains("14"));
324/// ```
325pub fn mfi_solution(
326    high: &[f64],
327    low: &[f64],
328    close: &[f64],
329    volume: &[f64],
330    params: MfiParams,
331) -> FinanceResult<MfiSolution> {
332    let series = mfi(high, low, close, volume, params)?;
333    Ok(MfiSolution {
334        series,
335        close: close.to_vec(),
336        formula: format!(
337            "MFI({}) = 100 - 100/(1 + +MF/-MF); TP=(H+L+C)/3; MF=TP*vol",
338            params.period
339        ),
340    })
341}
342
343#[cfg(test)]
344mod tests {
345    use super::*;
346
347    #[test]
348    fn rising_high_mfi() {
349        let n = 40usize;
350        let h: Vec<_> = (0..n).map(|i| 101.0 + i as f64).collect();
351        let l: Vec<_> = (0..n).map(|i| 99.0 + i as f64).collect();
352        let c: Vec<_> = (0..n).map(|i| 100.0 + i as f64).collect();
353        let v = vec![1000.0; n];
354        let s = mfi(&h, &l, &c, &v, MfiParams::period_14()).unwrap();
355        assert!(s.last().unwrap() > 80.0);
356    }
357
358    #[test]
359    fn state_parity() {
360        let n = 35usize;
361        let h: Vec<_> = (0..n).map(|i| 12.0 + (i as f64).sin()).collect();
362        let l: Vec<_> = (0..n).map(|i| 10.0 + (i as f64).sin()).collect();
363        let c: Vec<_> = (0..n).map(|i| 11.0 + (i as f64).sin()).collect();
364        let v: Vec<_> = (0..n).map(|i| 500.0 + i as f64).collect();
365        let p = MfiParams::period_14();
366        let batch = mfi(&h, &l, &c, &v, p).unwrap();
367        let mut st = MfiState::new(p).unwrap();
368        for i in 0..n {
369            let o = st.push(h[i], l[i], c[i], v[i]).unwrap();
370            match (o, batch.mfi[i]) {
371                (None, None) => {}
372                (Some(a), Some(b)) => assert!((a - b).abs() < 1e-9),
373                other => panic!("{other:?}"),
374            }
375        }
376    }
377}