MFI
Money Flow Index — a volume-weighted RSI built on typical price times volume.
Quick reference
| Field | Value |
|---|---|
| Family | Momentum Oscillators |
| Input type | Candle (volume needed) |
| Output type | f64 |
| Output range | [0, 100] |
| Default parameters | period = 14 (Python) |
| Warmup period | period (14 for period = 14) |
| Interpretation | overbought above 80, oversold below 20 |
Formula
For each new candle:
TP_t = (high_t + low_t + close_t) / 3 (typical price)
MF_t = TP_t · volume_t (money flow)
positive MF = MF_t if TP_t > TP_{t-1}, else 0
negative MF = MF_t if TP_t < TP_{t-1}, else 0
(both zero when TP_t == TP_{t-1})Maintain rolling sums of positive and negative money flow over the last period bars. Then:
MR_t = positive_sum / negative_sum
MFI_t = 100 − 100 / (1 + MR_t)The implementation guards both special cases: when both rolling sums are zero, MFI returns 50 (neutral); when only negative_sum == 0, MFI returns 100; otherwise the standard formula.
Parameters
| Name | Type | Default (Python) | Valid range | Description |
|---|---|---|---|---|
period | usize | 14 | >= 1 | Rolling window length for the positive/negative money-flow sums. |
Mfi::new(0) returns Error::PeriodZero.
Inputs / Outputs
From impl Indicator for Mfi:
use wickra::{Indicator, Mfi, Candle};
// Mfi: Input = Candle, Output = f64
const _: fn(&mut Mfi, Candle) -> Option<f64> = <Mfi as Indicator>::update;Volume is consumed via candle.volume — it is not optional. Calling the indicator with a zero-volume candle is legal (every money flow on that bar is zero), but mass zero-volume bars will dilute the sums.
Python's MFI.batch(high, low, close, volume) returns a 1-D float64np.ndarray (warmup → NaN). Node's MFI.batch(high, low, close, volume) returns a flat number[] (warmup → NaN); only batch is exposed on the Node binding.
Warmup
warmup_period() returns period. The first candle has no previous TP to compare against, so its money flow is classified as neither positive nor negative — it sits in the window as a 0 / 0 slot but still counts toward filling the window. The first Some is therefore emitted at the period-th update, exactly when the rolling positive and negative sums first contain period − 1 real comparisons.
Edge cases
- Pure uptrend. Every
TP_t > TP_{t-1}, sonegative_sum == 0and the implementation returns100directly (testpure_uptrend_yields_high_mfi). Pure downtrend mirrors at0(testpure_downtrend_yields_low_mfi). - Flat input (all
TPequal). Both sums stay at zero; the implementation returns50(the same neutral convention as RSI on flat input). - Zero-volume candle. Money flow on that bar is zero. The window still advances; the indicator just gets one less data point of influence.
- Reset.
reset()clearsprev_tp, both rolling windows, and both sums.
Examples
Rust
use wickra::{BatchExt, Candle, Indicator, Mfi};
let candles: Vec<Candle> = (1..=20)
.map(|i| Candle::new(i as f64, i as f64, i as f64, i as f64, 100.0, 0).unwrap())
.collect();
let mut mfi = Mfi::new(14)?;
let out = mfi.batch(&candles);
println!("row 13 = {}", out[13].unwrap());
println!("row 19 = {}", out[19].unwrap());
# Ok::<(), wickra::Error>(())Verified output:
row 13 = 100
row 19 = 100Python
import numpy as np
import wickra as ta
n = 20
i = np.arange(1, n + 1, dtype=float)
high = low = close = i
volume = np.full(n, 100.0)
mfi = ta.MFI(14)
out = mfi.batch(high, low, close, volume)
print('warmup:', mfi.warmup_period())
print('row 13:', out[13])
print('row 19:', out[19])Verified output:
warmup: 14
row 13: 100.0
row 19: 100.0Node
const wickra = require('wickra');
const n = 20;
const high = [], low = [], close = [], vol = [];
for (let i = 1; i <= n; i++) {
high.push(i); low.push(i); close.push(i); vol.push(100);
}
const m = new wickra.MFI(14);
const out = m.batch(high, low, close, vol);
console.log('row 13:', out[13]);
console.log('row 19:', out[19]);Verified output:
row 13: 100
row 19: 100Interpretation
- Overbought / oversold. The conventional MFI thresholds are
80 / 20— tighter than RSI's70 / 30because the volume weighting amplifies sustained one-way moves. - Divergence. MFI divergences are read like RSI divergences: a new price high without a confirming MFI high is bearish, and vice versa. Because volume is in the mix, MFI divergences are often interpreted as "the move is happening on weak participation" — i.e. structurally more meaningful than a pure-price divergence.
- Compare with OBV. OBV (the unsmoothed cumulative volume) tells you accumulated participation; MFI tells you participation pressure over a fixed horizon. The two often diverge interestingly near trend exhaustion.
Common pitfalls
- MFI requires volume. Unlike RSI (close only) or Stochastic (high/low/close), MFI's per-bar money flow is
TP × volume. Passing a candle stream withvolume == 0throughout will collapse MFI to50regardless of price action. Validate your data source before reaching for MFI. - Same flat-input convention as RSI. A perfectly flat window yields
50(notNaN, not "no value"). Treat the value as informational only until the underlying TP series starts moving.
References
- Gene Quong and Avrum Soudack, "Volume-Weighted RSI: Money Flow", Technical Analysis of Stocks & Commodities, March 1989 — the original publication of the MFI as a volume-weighted RSI variant.
See also
- Indicator: Rsi — the price-only ancestor.
- Indicator: Adx — directional/trend strength to pair with MFI's overbought/oversold reading.
- Warmup Periods — bare
period(no off-by-one).