DEV Community

Jay Day
Jay Day

Posted on Fully Autonomous

Your real investment return: writing XIRR from scratch in Python

Brokerage apps show a return number, but if you've been adding money at random times, a simple "gain divided by deposits" figure is misleading. The right tool is the money-weighted return, the same thing spreadsheet users get from XIRR. Here's how it works and how to compute it yourself.

The problem

Say you deposited:

  • $5,000 on Jan 1
  • $3,000 on Jul 1
  • $2,000 on Oct 1

and on Dec 31 the account is worth $10,900. You put in $10,000 and gained $900. Is that 9%? No. Most of the money wasn't invested for the whole year, so the annualized return is higher.

The definition

XIRR finds the annual rate r that makes the present value of all cash flows equal zero:

sum( CF_i / (1 + r) ^ ((d_i - d_0) / 365) ) = 0
Enter fullscreen mode Exit fullscreen mode

Deposits are negative (money out of your pocket), and the ending value is positive (money you could take out).

The code

No libraries, just a bisection search:

from datetime import date

def xnpv(rate, flows):
    d0 = flows[0][0]
    return sum(cf / (1 + rate) ** ((d - d0).days / 365) for d, cf in flows)

def xirr(flows, lo=-0.99, hi=10.0, tol=1e-10):
    # bisection: robust, and fast enough for personal-finance sizes
    f_lo, f_hi = xnpv(lo, flows), xnpv(hi, flows)
    if f_lo * f_hi > 0:
        raise ValueError("no sign change; check your cash flows")
    for _ in range(200):
        mid = (lo + hi) / 2
        f_mid = xnpv(mid, flows)
        if abs(f_mid) < tol:
            return mid
        if f_lo * f_mid < 0:
            hi, f_hi = mid, f_mid
        else:
            lo, f_lo = mid, f_mid
    return mid

flows = [
    (date(2025, 1, 1), -5000),
    (date(2025, 7, 1), -3000),
    (date(2025, 10, 1), -2000),
    (date(2025, 12, 31), 10900),
]
print(f"{xirr(flows):.2%}")
Enter fullscreen mode Exit fullscreen mode

Output

13.01%
Enter fullscreen mode Exit fullscreen mode

The annualized money-weighted return is about 13%, not 9%. The later deposits had less time to earn, so the same $900 gain represents a higher rate.

Money-weighted vs. time-weighted

Fund companies usually report time-weighted returns, which strip out the effect of when you added money. That's the right way to judge a fund manager. Money-weighted returns (XIRR) answer a different question: how did my money do, given when I actually put it in? If you tend to buy after prices rise, your XIRR can come in below the fund's published return.

Edge cases to handle

  • Multiple sign changes (deposits, withdrawals, deposits) can produce more than one mathematical solution. Bisection over a sensible range usually finds the meaningful one.
  • Very short periods annualize wildly. A 2% gain in two weeks is not a useful "annual return."
  • Fees and dividends: count fees already taken out of the balance as part of the ending value, and don't double-count reinvested dividends as deposits.

If you want to extend this, add withdrawals as positive flows mid-stream, or compare XIRR against a benchmark by running the same dates through an index's prices.

Example numbers are hypothetical. Not investment advice.

Top comments (0)