# 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.

*https://stockmarketstack.com/how-to/calculate-portfolio-twr-and-xirr · next to Stock Portfolio Trackers*

**Answer:** 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 tools that do this

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

1. [Portfolio Performance](https://stockmarketstack.com/tools/portfolio-performance.md) — Free desktop app. TTWROR linked daily and an annualised IRR, over any reporting period, from imported statements, with fees and taxes in or out.
2. [Wealthfolio](https://stockmarketstack.com/tools/wealthfolio.md) — 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.
3. [Parqet](https://stockmarketstack.com/tools/parqet.md) — Simple return, TTWROR over the period chosen, and an XIRR it calls IZF, shown only once a portfolio has 90 days of history.
4. [Portseido](https://stockmarketstack.com/tools/portseido.md) — Simple, time-weighted and money-weighted returns side by side, money-weighted by default for benchmarks. Free to 100 transactions.
5. [Sharesight](https://stockmarketstack.com/tools/sharesight.md) — Money-weighted only, by a Modified Dietz variant rather than IRR, not annualised under a year of average capital. No time-weighted figure.

## 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](https://stockmarketstack.com/glossary/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](https://stockmarketstack.com/guides/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:

```text
=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:

```python
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](https://stockmarketstack.com/tools/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](https://stockmarketstack.com/tools/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](https://stockmarketstack.com/tools/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](https://stockmarketstack.com/tools/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](https://stockmarketstack.com/tools/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](https://stockmarketstack.com/tools/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](https://stockmarketstack.com/how-to/import-a-broker-csv-into-a-portfolio-tracker)
is the route into the trackers above, and the
[three-way comparison of the free ones](https://stockmarketstack.com/compare/ghostfolio-vs-portfolio-performance-vs-wealthfolio)
covers which suits whom. The rest are on the [portfolio trackers](https://stockmarketstack.com/categories/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.

## 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](https://www.gipsstandards.org/wp-content/uploads/2021/03/2020_gips_standards_firms.pdf) — CFA Institute, 2020-01-01. The current edition — it took effect on 1 January 2020 and CFA Institute has published no successor edition.
2. [XIRR function](https://support.microsoft.com/en-us/office/xirr-function-de1242ec-6477-445b-b11b-a303ad9adc9d) — Microsoft, read 2026-10-09
3. [XIRR — Google Docs Editors Help](https://support.google.com/docs/answer/3093266) — Google, read 2026-10-09
4. [True time-weighted rate of return](https://help.portfolio-performance.info/en/concepts/performance/time-weighted/) — Portfolio Performance, read 2026-10-09
5. [Money-weighted rate of return](https://help.portfolio-performance.info/en/concepts/performance/money-weighted/) — Portfolio Performance, read 2026-10-09
6. [Performance metrics](https://wealthfolio.app/docs/concepts/performance-metrics/) — Wealthfolio, read 2026-10-09
7. [Rendite vs. TTWROR](https://faq.parqet.com/de/articles/650587-rendite-vs-ttwror) — Parqet, read 2026-10-09
8. [IZF (Interner Zinsfuß)](https://faq.parqet.com/de/articles/650586-izf-interner-zinsfuss) — Parqet, read 2026-10-09
9. [Money-weighted vs time-weighted return](https://support.portseido.com/003-mwr-vs-twr/) — Portseido, read 2026-10-09
10. [Performance calculation method](https://help.sharesight.com/performance_calculation_method/) — Sharesight, read 2026-10-09
11. [Absolute and annualised return](https://help.sharesight.com/absolute-and-annualised-return/) — Sharesight, read 2026-10-09

*Last updated 2026-10-09. A reference page, corrected in place — not a dated post.*
