Money-Weighted Return in Python: IRR, Dates and Cash Flows
Master money-weighted return (MWR) and XIRR in Python: formulate cash-flow polynomials, solve robust root-finding with Brent's method, and reconcile MWR vs TWR.
Conceptual illustration: unequal cash flows at different times are balanced against terminal portfolio value.
When evaluating the performance of an algorithmic trading strategy, institutional portfolio managers and retail allocators frequently arrive at opposing conclusions about the exact same trading record. A quantitative fund manager may report an audited strategy gain of +15.0% over an annual cycle, while an allocator invested in the fund logs a net dollar loss on their personal brokerage statement.
This divergence is not a reporting error. It stems from the fundamental divide between strategy-level execution and investor-level cash flow dynamics.
In quantitative research, strategies are typically simulated on a static capital base: a fixed starting cash pool compounds uninterrupted over historical bars. In live portfolio operations, however, capital is dynamic:
Allocators inject capital after observing profitable runs.
Margin calls or risk mandates force capital injections during market dislocations.
Redemptions occur during drawdowns as capital is reallocated or withdrawn for liquidity.
Performance fees and dividend distributions continuously extract cash from the portfolio envelope.
Whenever external capital crosses the portfolio perimeter, raw equity changes cease to reflect strategy quality alone. To measure actual capital efficiency, finance relies on two distinct methodologies:
Time-Weighted Return (TWR): Measures the compound growth rate of a single unit of currency invested in the strategy, completely removing the distortive effects of external deposits and withdrawals. TWR evaluates manager skill.
Money-Weighted Return (MWR): Measures the internal rate of return (IRR) earned across all deployed capital, heavily weighting intervals where the capital base was largest. MWR evaluates investor dollar outcomes.
Understanding money weighted return—and knowing how to implement generalized, date-aware Extended Internal Rate of Return (XIRR) in Python—is essential for reconciling algorithmic backtests with real-world capital accounting.
Code
+-----------------------------------------------------------------------------+| CAPITAL TIMING DIVERGENCE OVERVIEW |+-----------------------------------------------------------------------------+| || Year 1: +50% Strategy Gain Year 2: -30% Strategy Loss || || Manager View (TWR): || $1.00 -------> $1.50 ---------------> $1.05 (+5.0% Cumulative Alpha) || || Investor A (Late Performance Chaser): || $10k start --> $15k + $90k deposit -> $105k base --> -$31.5k loss = $73.5k || Result: Injected $100k, ending balance $73.5k (MWR: -24.64% Loss) || || Investor B (Contrarian / Early Allocator): || $90k start --> $135k + $10k deposit -> $145k base -> -$43.5k loss = $101.5k|| Result: Injected $100k, ending balance $101.5k (MWR: +0.79% Gain) || |+-----------------------------------------------------------------------------+
The money-weighted return is mathematically defined as the discount rate r that equates the Net Present Value (NPV) of all external cash inflows and outflows—including the terminal portfolio valuation—to zero.
When transactions occur at irregular calendar dates, this metric is formalized as the Extended Internal Rate of Return (XIRR).
Let an investment account experience N external cash-flow events C_0, C_1, ..., C_{N-1} occurring at calendar dates d_0, d_1, ..., d_{N-1}, terminating with an ending portfolio liquidation value V_end on final audit date d_end.
The money-weighted annualized return r is the root of the generalized discounted cash-flow equation:
d_0: The inception timestamp of the portfolio (reference date where time fraction \Delta t_0 = 0).
d_i: The specific calendar date of the i-th external cash flow.
\Delta t_i = \frac{d_i - d_0}{365}: The year fraction elapsed under the standard Actual/365 day-count convention.
C_i: The magnitude and direction of the external cash flow at date d_i.
V_{\text{end}}: The final fair market value (unrealized mark-to-market equity) of the account at date d_{\text{end}}.
r: The annualized money-weighted return (internal rate of return).
[!NOTE]
In day-count mathematics, financial libraries default to 365.0 days per year regardless of leap years. For hyper-precise institutional fixed-income reconciliation, conventions such as Actual/Actual or 30/360 can be substituted into \Delta t_i, but Actual/365 remains the universal standard for equity and algorithmic trading performance auditing.
Throughout this guide, we adopt the standard Investor Balance Sheet convention: cash leaving the investor's bank account to fund trading is negative, while cash returned or remaining in portfolio equity is positive.
If cash flows occur at strictly uniform intervals (e.g., annual bars where \Delta t_i = i), the equation transforms via substitution w = (1 + r)^{-1} into a standard polynomial of degree N:
By the Abel-Ruffini theorem, no general algebraic solution using radicals exists for polynomials of degree five or higher (N ≥ 5). When cash flows occur at arbitrary timestamps, the exponents \Delta t_i are non-integer real numbers, turning the expression into a transcendental function. Consequently, computing money-weighted return requires numerical root-finding algorithms.
Quantitative researchers frequently debate whether MWR or TWR is the "correct" metric. The answer depends strictly on governance: who controls the capital allocation decisions?
Code
PERFORMANCE ATTRIBUTION GOVERNANCE │ Does the trading manager control the timing and sizing of external cash injections? │ ┌─────────────────┴─────────────────┐ ▼ ▼ YES NO │ │ Use Money-Weighted Use Time-Weighted Return (MWR) Return (TWR) │ │ ┌────────────┴────────────┐ ┌────────────┴────────────┐ ▼ ▼ ▼ ▼ Private Equity / Personal Wealth Public Mutual Funds / Systematic Trading Venture Capital & Desk Treasury GIPS Mandates Alpha Research
Time-Weighted Return (TWR) measures the compound growth of one unit of capital over time. By segmenting the evaluation timeline at each cash-flow boundary, calculating holding period returns, and geometrically compounding them, TWR completely isolates strategy performance from capital volume. TWR answers: "Did the algorithm make profitable trading decisions?"
Money-Weighted Return (MWR) accounts for both the rate of return and the volume of capital exposed during each interval. An algorithm that gains +50% on $10,000 and subsequently loses -10% on $1,000,000 generates positive strategy alpha but massive net dollar losses. MWR answers: "Did the investor build or destroy net wealth across their actual capital commitments?"
Quantitative alpha benchmarks, systematic fund evaluation
When designing algorithmic models spanning multiple trading assets, managing cash rebalancing and asset allocations is paramount. To examine how multi-asset liquidity pools handle capital allocation, review our guide on How to Backtest a Portfolio alongside our reference on Time-Weighted Return in Python.
Three different allocators deploy a total of $100,000 into this exact same strategy:
Investor A (The Performance Chaser): Starts with $10,000. Sees the +50% Year 1 gain, gets enthusiastic, and injects $90,000 on 2024-12-31 right before the Year 2 drawdown.
Investor B (The Early Contrarian): Starts with $90,000. Following the Year 1 run, adds a modest $10,000 on 2024-12-31.
Investor C (The Dollar-Cost Averager): Injects $50,000 at inception, and deposits a balanced $50,000 on 2024-12-31.
The following ledger tracks the cash flow events, account valuations immediately prior to cash flows (V_{pre}), external deposits (C_t), post-cash valuations (V_{post}), and terminal balances:
Date
Event
Investor A Equity
Investor B Equity
Investor C Equity
Strategy Return
2024-01-01
Initial Deposit
$10,000 (-$10k flow)
$90,000 (-$90k flow)
$50,000 (-$50k flow)
Inception
2024-12-31
Pre-Flow Value (V_{pre})
$15,000
$135,000
$75,000
+50.0%
2024-12-31
External Deposit (C_1)
+$90,000 (-$90k flow)
+$10,000 (-$10k flow)
+$50,000 (-$50k flow)
External Capital
2024-12-31
Post-Flow Base (V_{post})
$105,000
$145,000
$125,000
Reset Base
2025-12-31
Final Liquidation Value
$73,500
$101,500
$87,500
-30.0%
Let us inspect the terminal performance across the three participants:
Code
───────────────────────────────────────────────────────────────────────────────────PERFORMANCE RECONCILIATION SUMMARY───────────────────────────────────────────────────────────────────────────────────Strategy Benchmark: Cumulative Time-Weighted Return (TWR): +5.00% Annualized Time-Weighted Return (CAGR): +2.47%Investor A (Performance Chaser): Total Cash Contributed: $100,000 Ending Portfolio Balance: $73,500 Net Dollar Profit / Loss: -$26,500 (-26.50%) Annualized Money-Weighted Return (XIRR): -24.64%Investor B (Early Contrarian): Total Cash Contributed: $100,000 Ending Portfolio Balance: $101,500 Net Dollar Profit / Loss: +$1,500 (+1.50%) Annualized Money-Weighted Return (XIRR): +0.79%Investor C (Dollar-Cost Averager): Total Cash Contributed: $100,000 Ending Portfolio Balance: $87,500 Net Dollar Profit / Loss: -$12,500 (-12.50%) Annualized Money-Weighted Return (XIRR): -8.58%───────────────────────────────────────────────────────────────────────────────────
Despite identical underlying execution, Investor A lost over 24% annualized, while Investor B achieved a positive annual yield. The timing and magnitude of capital deployment exerted far greater influence over the final dollar outcome than the strategy's trading alpha.
In historical versions of Python's numerical ecosystem, numpy.irr provided a basic internal rate of return calculation. However, NumPy deprecated this function, delegating financial math to numpy-financial.irr.
Crucially, numpy-financial.irrcannot compute XIRR: it assumes all cash flows occur at strictly equidistant intervals (e.g. exactly 30 days apart). Real trading accounts experience irregular cash flows across non-uniform calendar days.
Failure Mode: Highly sensitive to the initial guess r_0. If the derivative approaches zero or if a step projects r \le -1.0, the algorithm diverges or produces complex numbers.
Brent's Method (scipy.optimize.brentq): A robust hybrid algorithm combining root-bracketing bisection with secant and inverse quadratic interpolation. Provided that f(a) and f(b) have opposing signs (f(a) \times f(b) < 0), Brent's method is mathematically guaranteed to converge.
A production-grade XIRR implementation employs a multi-grid bracket search combined with scipy.optimize.brentq, followed by an analytical Newton-Raphson fallback.
Below is a self-contained, typed, and mathematically robust Python implementation capable of handling date strings, Python datetime objects, and pandas Timestamp instances:
Code
"""Robust Money-Weighted Return (XIRR) Engine.Combines Scipy Brent's method with multi-grid bracketing and Newton fallback."""from __future__ import annotationsimport datetime as dtfrom typing import Sequence, Unionimport numpy as npimport pandas as pdimport scipy.optimize as scodef xnpv( rate: float, cash_flows: Sequence[float], dates: Sequence[Union[str, dt.date, dt.datetime, pd.Timestamp]],) -> float: """ Calculate the Net Present Value (NPV) for irregular cash flows. Parameters ---------- rate : float Discount rate (e.g. 0.10 for 10% annual rate). cash_flows : Sequence[float] Array of cash flow values (inflows/outflows). dates : Sequence[Union[str, dt.date, dt.datetime, pd.Timestamp]] Calendar dates corresponding to each cash flow. Returns ------- float The calculated Net Present Value. """ if rate <= -1.0: return float("inf") parsed_dates = [pd.to_datetime(d) for d in dates] t0 = parsed_dates[0] days = np.array([(d - t0).days for d in parsed_dates], dtype=np.float64) cfs = np.array(cash_flows, dtype=np.float64) time_fractions = days / 365.0 return float(np.sum(cfs / ((1.0 + rate) ** time_fractions)))def dxnpv_dr( rate: float, cash_flows: Sequence[float], dates: Sequence[Union[str, dt.date, dt.datetime, pd.Timestamp]],) -> float: """ Calculate the exact analytical first derivative of XNPV with respect to rate. """ if rate <= -1.0: return float("-inf") parsed_dates = [pd.to_datetime(d) for d in dates] t0 = parsed_dates[0] days = np.array([(d - t0).days for d in parsed_dates], dtype=np.float64) cfs = np.array(cash_flows, dtype=np.float64) time_fractions = days / 365.0 # d/dr [ (1 + r)^(-t) ] = -t * (1 + r)^(-t - 1) return float(-np.sum(time_fractions * cfs / ((1.0 + rate) ** (time_fractions + 1.0))))def xirr( cash_flows: Sequence[float], dates: Sequence[Union[str, dt.date, dt.datetime, pd.Timestamp]], guess: float = 0.10, tol: float = 1e-8, maxiter: int = 200,) -> float: """ Compute the Extended Internal Rate of Return (XIRR) for irregular dates. Employs a multi-grid search to locate a valid sign-bracket for Brent's method. Falls back to Newton-Raphson with analytical derivative if no bracket is resolved on standard intervals. Parameters ---------- cash_flows : Sequence[float] Series of cash flows (negative for investments, positive for proceeds). dates : Sequence[Union[str, dt.date, dt.datetime, pd.Timestamp]] Chronological list of cash flow dates. guess : float, optional Starting rate guess for Newton fallback, default is 0.10. tol : float, optional Convergence tolerance, default is 1e-8. maxiter : int, optional Maximum iterations for root solver, default is 200. Returns ------- float The annualized money-weighted return (XIRR). """ cfs = np.array(cash_flows, dtype=np.float64) parsed_dates = [pd.to_datetime(d) for d in dates] # Structural validations if len(cfs) != len(parsed_dates): raise ValueError("cash_flows and dates must have identical lengths") if len(cfs) < 2: raise ValueError("XIRR requires at least two cash flow events") if not (np.any(cfs > 0) and np.any(cfs < 0)): raise ValueError("Cash flows must contain at least one positive and one negative value") t0 = parsed_dates[0] days = np.array([(d - t0).days for d in parsed_dates], dtype=np.float64) if np.any(np.diff(days) < 0): raise ValueError("Dates must be sorted in strictly ascending chronological order") # Objective function wrappers def objective(r: float) -> float: if r <= -0.999999999: return 1e12 return float(np.sum(cfs / ((1.0 + r) ** (days / 365.0)))) def derivative(r: float) -> float: if r <= -0.999999999: return -1e12 t = days / 365.0 return float(-np.sum(t * cfs / ((1.0 + r) ** (t + 1.0)))) # Multi-grid search intervals covering -99.9% to +5000% search_intervals = [ (-0.999, -0.50), (-0.50, 0.0), (0.0, 0.50), (0.50, 2.0), (2.0, 10.0), (10.0, 50.0), ] for low, high in search_intervals: f_low = objective(low) f_high = objective(high) if f_low * f_high <= 0: return float(sco.brentq(objective, low, high, xtol=tol, maxiter=maxiter)) # Fallback to Newton-Raphson if bracketing fails try: sol = sco.newton( objective, x0=guess, fprime=derivative, tol=tol, maxiter=maxiter, ) if sol > -0.999 and not np.isnan(sol): return float(sol) except (RuntimeError, OverflowError): pass raise RuntimeError("XIRR failed to converge: no valid economic root found.")
While internal rates of return appear intuitive on corporate balance sheets, mathematical edge cases frequently trip up quantitative software pipelines.
If an investment experiences an absolute total loss (account equity drops to $0), the true mathematical rate of return is -100.0% (r = -1.0). Any standard Newton or secant solver will encounter division by zero or infinite overflow.
Engineering Remedy: Pre-screen the cash flow vector. If the terminal equity V_end == 0 and no positive distributions were ever made, immediately return -1.0 without triggering the numerical solver.
A foundational flaw of IRR is the potential existence of multiple real roots.
By Descartes' Rule of Signs, the number of positive real roots of a polynomial is at most equal to the number of sign changes in the sequence of its coefficients.
Code
Example of Alternating Cash Flows: t = 0: Initial Capital Injection: C_0 = -$100.00 (Outflow from investor) t = 1: Substantial Profit Distribution: C_1 = +$230.00 (Inflow to investor) [Sign Change #1] t = 2: Collateral Margin Injection: C_2 = -$132.00 (Outflow from investor) [Sign Change #2]
Because the cash flow series changes sign twice (- to +, then + to -), the discounted cash-flow polynomial can have up to two positive real roots.
Setting w = (1 + r)^{-1}:
Code
-100 + 230 w - 132 w^2 = 0 \iff 132 w^2 - 230 w + 100 = 0
Both +10.0% and +20.0% are mathematically correct solutions that set Net Present Value to zero. In algorithmic pipelines where accounts make periodic distributions followed by capital calls, single-root solvers can arbitrarily jump between roots depending on the initial guess.
[!WARNING]
When cash flow series exhibit multiple sign alternations, pure MWR/IRR becomes economically ambiguous. In such environments, institutions utilize the Modified Internal Rate of Return (MIRR), which explicitly decouples the negative cash-flow financing rate from the positive cash-flow reinvestment rate.
If an account records only deposits (C_i < 0) and the final valuation is zero, or conversely, if an account records only distributions with no recorded investment base, the polynomial has zero sign changes. The NPV curve never intersects the zero-axis, and no real root exists. Robust code must validate sign diversity before entering numerical search routines.
Modern algorithmic backtesting engines—such as OpenBacktesting 0.2.1, designed for single-asset historical OHLCV bar-by-bar research in Python >=3.12,<3.15 with SQLite persistence and Studio UI inspection—focus on deterministic order fills, trade logging, and equity curve generation.
In production institutional environments, quantitative engineers bridge strategy research with operational reality by maintaining a dual-ledger architecture:
The following Python script illustrates how to track both unit NAV (TWR) and investor cash flows (MWR) concurrently over a multi-month trading schedule:
Code
"""Dual-Ledger Performance Auditor: Reconciles Strategy TWR with Investor MWR."""from __future__ import annotationsimport pandas as pdimport numpy as npclass PerformanceAuditor: def __init__(self, initial_nav: float = 1.0): self.unit_nav: float = initial_nav self.nav_history: list[dict] = [] self.cash_flows: list[float] = [] self.flow_dates: list[str] = [] def record_period( self, date: str, trading_pnl_pct: float, external_cash_flow: float = 0.0, ) -> None: """ Record a periodic trading return and any end-of-period external cash flow. Parameters ---------- date : str ISO date string (YYYY-MM-DD). trading_pnl_pct : float Periodic trading return (e.g. 0.05 for +5.0%). external_cash_flow : float, optional Net cash injected (+) or withdrawn (-), default 0.0. """ # 1. Update Strategy Unit NAV (TWR Engine) self.unit_nav *= (1.0 + trading_pnl_pct) # 2. Record Treasury Cash Flows (MWR Engine) # Note: From investor perspective, deposit is out-of-pocket (negative) if external_cash_flow != 0.0: self.cash_flows.append(-external_cash_flow) self.flow_dates.append(date) self.nav_history.append({ "date": date, "pnl_pct": trading_pnl_pct, "unit_nav": self.unit_nav, "external_flow": external_cash_flow, }) def audit(self, terminal_date: str, terminal_portfolio_equity: float) -> dict: """ Produce a reconciled audit of Strategy TWR vs Investor XIRR. """ # Terminal valuation is positive cash flow to the investor audit_cfs = list(self.cash_flows) + [terminal_portfolio_equity] audit_dates = list(self.flow_dates) + [terminal_date] strategy_twr = self.unit_nav - 1.0 investor_mwr = xirr(audit_cfs, audit_dates) return { "Strategy Cumulative TWR": f"{strategy_twr:+.2%}", "Investor Annualized MWR": f"{investor_mwr:+.2%}", "Terminal Portfolio Value": f"${terminal_portfolio_equity:,.2f}", }
Before presenting track records to risk committees, allocators, or institutional clients, audit your performance pipeline against this technical checklist:
Enforce Clear Metric Ownership: Use Time-Weighted Return (TWR) to benchmark quantitative strategies, evaluate alpha models, and publish marketing tearsheets. Use Money-Weighted Return (MWR / XIRR) exclusively to audit investor capital efficiency, advisory accounts, and proprietary treasury returns.
Strict Cash Flow Sign Verification: Ensure your data ingest layer rigidly adheres to sign conventions: capital contributions must enter the XIRR vector as negative numbers (C_i < 0), distributions as positive numbers (C_i > 0), and terminal equity as a final positive liquidation credit.
Audit Multiple Root Vulnerabilities: Inspect historical cash flow sequences for alternating signs. If an account has experienced both massive distributions and follow-on recapitalizations, verify whether multiple IRR roots exist using Descartes' Rule of Signs, or report the Modified Internal Rate of Return (MIRR).
Handle Total Loss Singularities Gracefully: Implement guard rails for r \to -1.0. Accounts liquidated to zero must immediately return -100.0% without triggering division-by-zero exceptions in Newton solvers.
Separate Execution from Treasury Ledgers: In backtesting engines, maintain distinct tables for order fills and balance adjustments. Strategy mark-to-market returns must never be contaminated by external margin deposits.
Standardize Day-Count Exponents: Explicitly document whether your XIRR engine applies Actual/365, Actual/Actual, or 30/360. Ensure all date comparisons rely on explicit calendar difference mathematics.
Audit Multi-Asset Portfolio Rebalancing: In multi-asset backtests, ensure internal asset rebalancings are treated as internal book transfers rather than external cash flows crossing the fund perimeter.
The distinction between money-weighted return and time-weighted return is not a mere accounting nuance—it is a fundamental mathematical reality of capital allocation.
TWR removes capital distortions, allowing quantitative developers to evaluate true trading edge and systematic alpha independent of investor timing luck.
MWR / XIRR incorporates both the timing and magnitude of cash flows, reflecting the true dollar experience of the capital allocator.
By implementing robust numerical root-finding using scipy.optimize.brentq and structuring backtesting engines with decoupled execution and treasury ledgers, quantitative engineers can deliver audit-grade performance reporting that satisfies both strategy researchers and capital allocators.