Money Flow Index
The Money Flow Index (MFI) is a volume-weighted momentum oscillator that measures buying and selling pressure. Often referred to as the "volume-weighted RSI," MFI combines price and volume data to identify overbought and oversold conditions in the market.
MFI oscillates between 0 and 100, with readings above 80 typically indicating overbought conditions and readings below 20 suggesting oversold conditions. Unlike traditional momentum indicators that rely solely on price, MFI incorporates trading volume to provide a more complete picture of market pressure.
What It Measures
MFI measures the flow of money into and out of an asset over a specified period. It calculates the ratio of positive money flow (when typical price increases) to negative money flow (when typical price decreases), weighted by volume. Higher MFI values indicate stronger buying pressure, while lower values suggest selling pressure dominates.
When to Use
Use MFI when you want to identify potential reversal points by detecting overbought or oversold conditions with volume confirmation. It's particularly effective in ranging markets where prices oscillate between support and resistance levels. MFI is also valuable for spotting divergences between price action and money flow, which can signal trend weakening or reversals.
Interpretation
- High values (above 80): Overbought conditions, potential sell signal or short opportunity
- Low values (below 20): Oversold conditions, potential buy signal or long opportunity
- Divergences: When price makes new highs/lows but MFI doesn't confirm, it may indicate a weakening trend
- 50 level: Acts as a centerline—above 50 suggests bullish momentum, below 50 indicates bearish momentum
Default Usage
use rust_ti::momentum_indicators::bulk::money_flow_index;
pub fn main() {
// fetch the data in your preferred way
// let close = vec![...]; // closing prices
// let volume = vec![...]; // volume data
let mfi = money_flow_index(&close, &volume, 14);
println!("{:?}", mfi);
}
import pytechnicalindicators as pti
# fetch the data in your preferred way
# close = [...] # closing prices
# volume = [...] # volume data
mfi = pti.momentum_indicators.bulk.money_flow_index(close, volume, period=14)
print(mfi)
// WASM import
import init, { momentum_bulk_moneyFlowIndex } from 'https://cdn.jsdelivr.net/npm/ti-engine@latest/dist/web/ti_engine.js';
await init();
// fetch the data in your preferred way
// const close = [...]; // closing prices
// const volume = [...]; // volume data
const mfi = momentum_bulk_moneyFlowIndex(close, volume, 14);
console.log(mfi);
Optimization
The best way to determine what the best parameters for your indicator are is to build a simple optimization loop that tests all possible parameter combinations between a defined min and max value, and rate the output.
Below is an example of how to do this in Rust.
use rust_ti::chart_trends::{peaks, valleys};
use rust_ti::momentum_indicators::bulk::money_flow_index;
fn proximity_rating(fuzzed_location: &usize, price_location: &usize) -> f64 {
1.0 / (*fuzzed_location as f64 - *price_location as f64).abs()
}
pub fn main() {
// fetch the data in your preferred way
let indicator_loop = Instant::now();
// get buy and sell points, in an ideal world we would buy at the lowest point in the dip and sell at the highest point in the peak
// In the course of a 20-day period (1 month of trading days), we want to find the highest peak and lowest valley within 5 days of each other
let sell_points = peaks(&close, 20, 5).into_iter().map(|(_, i)| i).collect::<Vec<usize>>();
let buy_points = valleys(&close, 20, 5).into_iter().map(|(_, i)| i).collect::<Vec<usize>>();
// Define the ranges for optimization
let max_period = 150;
let min_period = 2;
let min_oversold = 0;
let max_oversold = 50;
let min_overbought = 50;
let max_overbought = 100;
let fuzz_parameter = 5; // Allowable distance from buy/sell points
// Store the best parameters found
let mut best_rating = 0.0;
let mut best_period = 0;
let mut best_oversold = 0;
let mut best_overbought = 0;
let mut best_indicators = vec![];
for oversold in min_oversold..=max_oversold {
for overbought in min_overbought..=max_overbought {
for period in min_period..=max_period {
let indicators = money_flow_index(&close, &volume, period);
let mut rating = vec![];
let mut matched_sell = vec![];
let mut matched_buy = vec![];
for i in 0..indicators.len() {
let price_location = i + period + 1; // Adjust for indicator lag
if price_location >= close.len() { break; }
if indicators[i] > overbought as f64 {
if sell_points.contains(&price_location) {
// If sell point == mfi, rate positively
rating.push(1.0);
matched_sell.push(price_location);
} else if buy_points.contains(&price_location) {
// If buy point == mfi, rate negatively
rating.push(-1.0);
} else {
let mut found_sell = false;
for fuzzed_location in (price_location.saturating_sub(fuzz_parameter))..=(price_location + fuzz_parameter) {
// It's ok if we count multiple times for fuzzed locations as we reduce the rating
// based off of distance from the actual sell point which will impact the final rating
if sell_points.contains(&fuzzed_location) {
rating.push(proximity_rating(&fuzzed_location, &price_location));
matched_sell.push(fuzzed_location);
found_sell = true;
}
if buy_points.contains(&fuzzed_location) {
// Note the `-` here to penalize for selling instead of buying
if !matched_sell.contains(&fuzzed_location) {
rating.push(-proximity_rating(&fuzzed_location, &price_location));
}
}
}
if !found_sell {
rating.push(0.0);
}
}
} else if indicators[i] < oversold as f64 {
if buy_points.contains(&price_location) {
// If buy point == mfi, rate positively
rating.push(1.0);
matched_buy.push(price_location);
} else if sell_points.contains(&price_location) {
rating.push(-1.0);
} else {
let mut found_buy = false;
for fuzzed_location in (price_location.saturating_sub(fuzz_parameter))..=(price_location + fuzz_parameter) {
// It's ok if we count multiple times for fuzzed locations as we reduce the rating
// based off of distance from the actual buy point which will impact the final rating
if buy_points.contains(&fuzzed_location) {
rating.push(proximity_rating(&fuzzed_location, &price_location));
matched_buy.push(fuzzed_location);
found_buy = true;
}
if sell_points.contains(&fuzzed_location) {
// Note the `-` here to penalize for buying instead of selling
if !matched_buy.contains(&fuzzed_location) {
rating.push(-proximity_rating(&fuzzed_location, &price_location));
}
}
}
if !found_buy {
rating.push(0.0);
}
}
}
}
// Look for any missed buy/sell points and penalize
for missed_sell in sell_points.iter() {
if !matched_sell.contains(missed_sell) {
rating.push(-1.0);
}
}
for missed_buy in buy_points.iter() {
if !matched_buy.contains(missed_buy) {
rating.push(-1.0);
}
}
let total_rating: f64 = rating.iter().sum::<f64>() / (rating.len() as f64);
if total_rating > best_rating {
best_rating = total_rating;
best_period = period;
best_oversold = oversold;
best_overbought = overbought;
best_indicators = indicators.clone();
}
}
}
}
println!(
"Indicators optimization loop took {} ms to run",
indicator_loop.elapsed().as_millis()
);
println!("\nBest Money Flow Index parameters found:");
println!("period = {}", best_period);
println!("oversold threshold = {}", best_oversold);
println!("overbought threshold = {}", best_overbought);
println!("Rating: {}", best_rating);
println!("Best Indicator values: {:?}", best_indicators);
}
Optimization Output
Below is an example output from the optimization code above run on a year of S&P data.
Best Money Flow Index parameters found:
period = 4
oversold threshold = 38
overbought threshold = 67
Rating: 0.021183800623052952
Best Indicator values: [74.44742348213569, 66.95676847723158, 100.0, ... ]
Interactive Chart
To better illustrate how the indicator performs with different parameters, an interactive chart is provided below comparing default parameters (blue) with optimized parameters (green).
Analysis
The optimized Money Flow Index parameters (period=4, oversold=38, overbought=67) generated significantly more trading signals compared to the default settings. The default MFI parameters (period=14, oversold=20, overbought=80) produced no trades at all during the simulation period, resulting in $0.00 profit/loss.
In contrast, the optimized parameters identified numerous overbought and oversold conditions, enabling both long and short positions throughout the trading period. The strategy invested 20% of remaining capital on each trade, opening long positions when MFI fell below 38 and short positions when it rose above 67. This more sensitive configuration yielded a total profit of $23.97 (2.4% return), demonstrating that the tighter thresholds and shorter period effectively captured short-term momentum shifts in the market.
Optimized trading simulation
Default trading simulation
Trading simulation code
For those you want to run their own simulation to compare results
use rust_ti::momentum_indicators::bulk::money_flow_index;
fn simulate_trading(best_indicator: &[f64], best_period: usize, close: &[f64], best_oversold: usize, best_overbought: usize) {
println!("\n--- Trading Simulation ---");
let initial_capital = 1000.0;
let mut capital = initial_capital;
let investment_pct = 0.20;
struct Position {
entry_price: f64,
shares: f64,
}
let mut open_long: Option<Position> = None;
let mut open_short: Option<Position> = None;
// Print table header
println!("{:<5} | {:<19} | {:<10} | {:<10} | {:<12} | {:<15} | {:<10}",
"Day", "Event", "MFI", "Price", "Shares", "Capital", "P/L");
println!("{}", "-".repeat(95));
for i in 0..best_indicator.len() {
let price_index = i + best_period + 1;
if price_index >= close.len() { break; }
let mfi_val = best_indicator[i];
let current_price = close[price_index];
let day = price_index;
// --- Handle Long Position ---
if let Some(long_pos) = open_long.take() {
if mfi_val > best_overbought as f64 {
let sale_value = long_pos.shares * current_price;
let profit = sale_value - (long_pos.shares * long_pos.entry_price);
capital += sale_value;
println!("{:<5} | {:<19} | {:<10.2} | ${:<9.2} | {:<12.4} | ${:<14.2} | ${:<9.2}",
day, "Sell (Close Long)", mfi_val, current_price, long_pos.shares, capital, profit);
} else {
open_long = Some(long_pos); // Put it back if not selling
}
} else if mfi_val < best_oversold as f64 && open_short.is_none() { // Don't buy if short is open
let investment = capital * investment_pct;
let shares_bought = investment / current_price;
open_long = Some(Position { entry_price: current_price, shares: shares_bought });
capital -= investment;
println!("{:<5} | {:<19} | {:<10.2} | ${:<9.2} | {:<12.4} | ${:<14.2} | {}",
day, "Buy (Open Long)", mfi_val, current_price, shares_bought, capital, "-");
}
// --- Handle Short Position ---
if let Some(short_pos) = open_short.take() {
if mfi_val < best_oversold as f64 {
let cost_to_cover = short_pos.shares * current_price;
let profit = (short_pos.shares * short_pos.entry_price) - cost_to_cover;
capital += profit; // Add profit to capital
println!("{:<5} | {:<19} | {:<10.2} | ${:<9.2} | {:<12.4} | ${:<14.2} | ${:<9.2}",
day, "Cover (Close Short)", mfi_val, current_price, short_pos.shares, capital, profit);
} else {
open_short = Some(short_pos); // Put it back if not covering
}
} else if mfi_val > best_overbought as f64 && open_long.is_none() { // Don't short if long is open
let short_value = capital * investment_pct;
let shares_shorted = short_value / current_price;
open_short = Some(Position { entry_price: current_price, shares: shares_shorted });
// Capital doesn't change when opening a short, it's held as collateral
println!("{:<5} | {:<19} | {:<10.2} | ${:<9.2} | {:<12.4} | ${:<14.2} | {}",
day, "Short (Open Short)", mfi_val, current_price, shares_shorted, capital, "-");
}
}
println!("\n--- Final Results ---");
if let Some(pos) = open_long {
println!("Simulation ended with an OPEN LONG position:");
println!(" - Shares: {:.4}", pos.shares);
println!(" - Entry Price: ${:.2}", pos.entry_price);
let last_price = close.last().unwrap_or(&0.0);
let current_value = pos.shares * last_price;
capital += current_value;
println!(" - Position value at last price (${:.2}): ${:.2}", last_price, current_value);
}
if let Some(pos) = open_short {
println!("Simulation ended with an OPEN SHORT position:");
println!(" - Shares: {:.4}", pos.shares);
println!(" - Entry Price: ${:.2}", pos.entry_price);
let last_price = close.last().unwrap_or(&0.0);
let cost_to_cover = pos.shares * last_price;
let pnl = (pos.shares * pos.entry_price) - cost_to_cover;
capital += pnl;
println!(" - Unrealized P/L at last price (${:.2}): ${:.2}", last_price, pnl);
}
let final_pnl = capital - initial_capital;
println!("\nInitial Capital: ${:.2}", initial_capital);
println!("Final Capital: ${:.2}", capital);
println!("Total P/L: ${:.2}", final_pnl);
}
fn main() {
// Fetch data and perform optimization as shown in the optimization code above
simulate_trading(&best_indicators, best_period, &close, best_oversold, best_overbought);
// Compare with default parameters (typically period=14, oversold=20, overbought=80)
println!("\n\nDefault Indicator values for comparison:");
let default_mfi = money_flow_index(&close, &volume, 14);
println!("{:?}", default_mfi);
simulate_trading(&default_mfi, 14, &close, 20, 80);
}