How to calculate a portfolio's time-weighted return and XIRR

XIRR needs only dated cash flows and a closing value. A time-weighted return needs a valuation at every flow. The tools that compute each, and a script.

XIRR needs only your dated deposits and withdrawals plus today's value, so a spreadsheet's XIRR function or a few lines of Python will do it. A time-weighted return needs the portfolio's value on every day money moved, which a list of trades does not contain; a tracker that prices the holdings daily, such as Portfolio Performance, Wealthfolio or Parqet, computes it for you. What breaks is short periods, where both get annualised into nonsense.

The short way

Decide which number you want, because the two need different inputs.

  • A money-weighted return (XIRR) answers what your money earned, given when it went in. It needs a list of dated external cash flows and one closing value. Any spreadsheet does it.
  • A time-weighted return answers what the holdings earned, with the timing of your deposits removed; it is the figure comparable with a fund or an index. It needs the portfolio's value at every cash flow, which only a priced history of every holding gives you.

The definitions, and why neither is wrong, are in the time-weighted return entry. If the question is why your tracker and your broker print different percentages, start at why your tracker and broker disagree instead. This page is the arithmetic.

XIRR in a spreadsheet. Put dates in one column and amounts in the next, from your side of the account: money you paid in is negative, money you took out is positive, and the closing value goes in as a positive amount on the closing date, as if you withdrew everything. Then:

=XIRR(B2:B4, A2:A4)

Excel and Google Sheets both require at least one negative and one positive amount, and both default the guess to 10%. Microsoft documents a 365-day year, a tolerance of 0.000001 percent and up to 100 iterations before #NUM!.

XIRR in Python, the same thing with a bracketing solver, and the time-weighted figure beside it for the same account:

from datetime import date
from scipy.optimize import brentq

# Your side of the account: money in is negative, money out positive.
# The closing value goes in as if you withdrew everything on the last date.
flows = [
    (date(2025, 1, 2), -10_000.00),   # first deposit
    (date(2025, 7, 1),  -5_000.00),   # second deposit
    (date(2025, 12, 31), 15_600.00),  # closing value
]

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

def xirr(flows, lo=-0.99, hi=10.0):
    if npv(lo, flows) * npv(hi, flows) > 0:
        raise ValueError("no sign change in the bracket: no rate, or more than one")
    return brentq(lambda r: npv(r, flows), lo, hi, xtol=1e-12)

print(f"XIRR: {xirr(flows):.4%}")

# Time-weighted: the value just before each deposit closes one sub-period.
# Here the portfolio was worth 9,000 on 1 July before the 5,000 arrived.
valuations = [
    (10_000.00, 9_000.00),    # 2 Jan to 1 Jul
    (14_000.00, 15_600.00),   # 1 Jul (9,000 + 5,000) to 31 Dec
]
growth = 1.0
for start, end in valuations:
    growth *= end / start
print(f"TWR:  {growth - 1:.4%}")

It prints an XIRR of 4.8304% and a time-weighted return of 0.2857%. Both are correct. The holdings fell 10% in the first half and rose about 11.4% in the second, which nets to almost nothing; the second deposit arrived just before the rise, so your money did better than the holdings did. The 9,000 is the number a trade list does not have. Drop it, and the time-weighted figure cannot be computed at all.

A bracketing solver such as brentq is used here rather than Newton's method because it either finds the root inside the bracket or says there is no sign change, instead of wandering off from a bad guess. Excel and Sheets take a guess and iterate from it.

What the options are

Trackers that compute both from your transactions. These price every holding every day, so the valuation at each cash flow is there without you supplying it. Portfolio Performance values the portfolio daily and links one-day periods into its TTWROR, treating money in as arriving at the start of the day and money out at the end; its IRR is the annual rate that reconciles the opening value, the flows and the closing value. At the portfolio level fees and taxes are included, with a pre-tax option. Wealthfolio also links daily returns, solves its money-weighted rate by bisection on 365.25-day years, and reports it unavailable when the flows never change sign. Parqet breaks its TTWROR at every activity and does not annualise it; its XIRR, which the German help centre calls IZF, is solved by Newton-Raphson and includes dividends, realised gains, interest, taxes and fees.

A tracker that shows all three side by side. Portseido prints a simple return, a time-weighted one and a money-weighted one, and uses money-weighted by default when you benchmark. Its help centre describes the money-weighted figure as the rate that equates discounted inflows with the current value, which is an IRR in all but name.

A tracker that will not compute a time-weighted return. Sharesight reports money-weighted only and avoids IRR on purpose: its documentation says IRR can produce unreasonable or multiple results, or none, and uses a variation of Modified Dietz instead. Read its percentage as money-weighted and do not compare it with a fund's published return.

Ghostfolio is not on the list. Its source code defines time-weighted and money-weighted calculation types, but on 9 October 2026 both calculators were unimplemented stubs, and the one method it runs is return on average investment, annualised from the first activity.

The approximation when you have no daily values

The GIPS standards require a valuation on the date of every large cash flow and at least monthly, with sub-period returns geometrically linked; where daily returns are not calculated, flows below that threshold are handled by a return that adjusts for daily-weighted cash flows. That adjustment is the Modified Dietz idea: weight each flow by the fraction of the period it was invested, and skip the valuation.

Applied to the account above over the whole year, it gives 600 of gain over 12,521 of day-weighted capital, about 4.79%, which is close to the XIRR and nowhere near the time-weighted 0.29%. That is the trap. Modified Dietz over one long period is a money-weighted number. It approximates a time-weighted return only when it is computed over short sub-periods, monthly at the longest, and those are chained together.

Where this breaks

Short periods. Both figures can be annualised, and over weeks the annualisation dominates. Portfolio Performance's help gives the example of a 2% TTWROR in January becoming 26.26% a year. Sharesight cites a 5% gain on the first day of ownership annualising to 1,825%. The trackers draw the line in different places: Sharesight does not annualise below one year of average capital invested, Parqet waits for 90 days of history before showing its XIRR, and Wealthfolio's code declines to annualise below 30 days. The GIPS standards say returns for periods of less than a year must not be annualised. A spreadsheet XIRR has no such guard, which is why a purchase made last week, with today's value in the next row, returns a figure in the hundreds.

Flows that change sign more than once. XIRR solves a polynomial in the discount factor. With deposits, then a withdrawal, then more deposits, there can be two rates that satisfy it, or none, and the one Excel returns depends on the guess. Sharesight's documentation gives this as its reason for not using IRR. The Python above refuses when the bracket shows no sign change; it cannot detect two roots inside the bracket, so look at the cash flows before trusting any IRR on an account that has paid out and been topped up.

Dates. Microsoft's documentation says XIRR truncates dates to whole days and returns #NUM! if any date precedes the first row's, so the first row has to be the earliest flow. An intraday purchase and today's closing value land on the same day, and if most of the money went in within the last few days, the short-period problem above applies in full.

What counts as a cash flow. Dividends that stay in the account are income, not flows, under the GIPS definition. Imported as a deposit, a dividend is counted as your money and understates both returns; withdrawn to a bank, it is a flow out. Fees and taxes are inside some trackers' figures and outside others: Parqet's help notes that paying taxes and fees can make its TTWROR higher than not paying them, because of how cash in the clearing account is weighted.

Currency. Compute in one currency. A foreign holding needs its value converted at each flow date, and the time-weighted return in your home currency then includes a currency gain that the same holding's return in its own currency does not. Sharesight splits that out as a separate component and notes the components do not sum to the total.

Year length. Excel discounts on 365 days, Wealthfolio on 365.25. On the account above that moves the XIRR from 4.8304% to 4.8338%, which is enough to make a reconciliation look wrong when it is not.

If you outgrow this

When the account has years of history and several brokers, typing valuations is the part that does not scale. Importing a broker CSV into a tracker is the route into the trackers above, and the three-way comparison of the free ones covers which suits whom. The rest are on the portfolio trackers page.

When the numbers are for clients, the GIPS standards are the reference, and they set the valuation frequency, the treatment of large cash flows and the cases in which a money-weighted return may be shown at all.

The tools that do this

In the order this page recommends trying them. Paid placement does not affect this order.

  1. Portfolio Performance

    Free desktop app. TTWROR linked daily and an annualised IRR, over any reporting period, from imported statements, with fees and taxes in or out.

    Free open-source desktop tracker that reads your bank's PDF statements.

    €3/moFree tierOpen source

  2. Wealthfolio

    Free and local. Daily-linked TWR and an XIRR-style money-weighted rate on 365.25-day years; periods under 30 days are not annualised.

    Local-first portfolio and net-worth tracker for desktop, mobile and your own Docker host.

    $2.99/moFree tierOpen source

  3. Parqet

    Simple return, TTWROR over the period chosen, and an XIRR it calls IZF, shown only once a portfolio has 90 days of history.

    German portfolio tracker built on broker PDF imports, with a read-only MCP server.

    €11.99/moFree tier

  4. Portseido

    Simple, time-weighted and money-weighted returns side by side, money-weighted by default for benchmarks. Free to 100 transactions.

    Multi-currency tracking for portfolios spread across several brokers and countries.

    $10/moFree tier

  5. Sharesight

    Money-weighted only, by a Modified Dietz variant rather than IRR, not annualised under a year of average capital. No time-weighted figure.

    Money-weighted portfolio tracking with real ATO, IRD and CRA tax reports.

    $9.33/moFree tier

FAQ

Why does XIRR return an absurd percentage on a recent purchase?

Because XIRR is an annual rate, and over a few days the exponent that annualises it is enormous. A 2% gain in one month becomes about 26% a year; a few days of gain become hundreds of percent. Nothing is wrong with the arithmetic. The GIPS standards forbid annualising returns for periods under a year, and Parqet will not show its XIRR figure before 90 days of history.

Can I compute a time-weighted return from my trade list alone?

Not exactly. A time-weighted return links the returns between cash flows, so it needs the portfolio's market value just before each deposit or withdrawal, and a trade list records prices of what you bought, not the value of everything else that day. Either price every holding on every flow date, use a tracker that does, or accept a Modified Dietz approximation.

Do dividends count as cash flows?

Not for the portfolio as a whole, as long as they stay in the account. The GIPS definition excludes dividend and interest income from external cash flows, and Portfolio Performance treats a dividend as a portfolio cash flow only when it leaves the account. For a single holding's return, Portfolio Performance does count the dividend as a flow out of that security.

Why does Excel's XIRR differ slightly from my tracker's money-weighted return?

Usually the year length or the method. Excel discounts on a 365-day year and Wealthfolio on 365.25 days; Sharesight uses a Modified Dietz variant instead of solving for a rate; and trackers differ on whether fees, taxes and cash balances are in the flows. Match the flows first, then expect differences in the second decimal.

Sources

  1. Global Investment Performance Standards (GIPS) for Firms, 2020 edition — CFA Institute, . The current edition — it took effect on 1 January 2020 and CFA Institute has published no successor edition.
  2. XIRR function — Microsoft, read
  3. XIRR — Google Docs Editors Help — Google, read
  4. True time-weighted rate of return — Portfolio Performance, read
  5. Money-weighted rate of return — Portfolio Performance, read
  6. Performance metrics — Wealthfolio, read
  7. Rendite vs. TTWROR — Parqet, read
  8. IZF (Interner Zinsfuß) — Parqet, read
  9. Money-weighted vs time-weighted return — Portseido, read
  10. Performance calculation method — Sharesight, read
  11. Absolute and annualised return — Sharesight, read

The catalogue next door

This page names a handful of cards. The rest of them are in Stock Portfolio Trackers, each filled in against the same schema, with the fields to narrow it yourself.

Last updated . Corrected in place: this is a reference page, not a dated post.

Report an error on this page

Quote the line and say what it should be.

The vendor’s page, filing or documentation that says otherwise.

Only if you want to hear back. Never published.

Read by a person. It fixes a fact and moves nothing else.