Back to blog
Performance

Inside the Maths: What Happens When XIRR Solvers Break

What building the actual calculation taught me that the theory never did.

FolioTrack Team·15 Aug 2026·10 min read

Inside the maths: what happens when XIRR solvers break

What building the actual calculation taught me that the theory never did.

In my last post I explained why I use XIRR to track my own portfolio, and touched briefly on where it gets weird — short time horizons, missing prices, cash buffers. That post was the concept. This one is what actually happened when I sat down and built the thing.

Because here's what nobody tells you when they explain XIRR in the abstract: the formula assumes a well-behaved set of numbers that always has a clean answer. Real portfolios don't always cooperate. This is a walk through four real problems I hit — one in the solver itself, two in what counts as a cash flow, and one that's still sitting there, deliberately unfixed, because the honest fix isn't simple.

This is purely a build/technical write-up — not advice on what to invest in.

The day Newton-Raphson gave up

XIRR doesn't have a neat closed-form solution. You can't rearrange the equation and solve for the rate directly — you have to guess, check how wrong the guess is, adjust, and repeat until the error gets small enough to ignore. The standard way to do that is Newton-Raphson: start with a guess, use the slope of the error at that point to jump to a better guess, and repeat. It's fast, and for most portfolios it converges to an answer within a handful of iterations.

Most portfolios. Not all of them.

While testing against a real portfolio — CMC - Race Car, calculating its since-inception return — the solver simply didn't converge. Not a wrong number. Not a crash. It just kept iterating without ever settling on an answer within the tolerance I'd set.

The cause turned out to be a single flow: an early cash-buffer withdrawal of $68,586, sitting inside a history of 76 total flows. There was nothing mathematically invalid about it — the flows summed correctly, there were flows of both signs (which XIRR requires), a real rate genuinely existed that would balance the equation. But Newton-Raphson uses the slope of the error curve to decide where to jump next, and when one flow's size dominates the others like that, the slope can behave badly enough that each guess overshoots the last, bouncing around instead of homing in.

The fix wasn't a smarter Newton-Raphson. It was accepting that Newton-Raphson isn't always the right tool, and falling back to something slower but unconditionally reliable when it fails: bisection. Instead of using a slope to jump toward the answer, bisection just checks the midpoint between two bounds, sees which side of zero the error landed on, and narrows the bracket in half each time. It has no derivative to go unstable — it can't overshoot the way Newton-Raphson can. The trade-off is speed: bisection takes more iterations to reach the same precision. But it always gets there, as long as there's a genuine sign change across the bracket to guarantee a root exists inside it.

So the actual solver isn't one algorithm — it's Newton-Raphson first, with bisection as an automatic fallback the moment Newton-Raphson's derivative goes to zero, produces a non-finite step, or simply fails to converge within the iteration limit. Two methods, same equation, different reliability trade-offs, used together.

Margin loan interest: the cost XIRR never sees

Here's a subtler problem, and one that doesn't announce itself with a failed calculation — it just quietly gives you the wrong answer.

XIRR only understands one kind of event: money leaving your pocket, or money returning to it. A buy is an outflow. A sell is an inflow. That's the entire vocabulary.

A margin loan doesn't fit that vocabulary cleanly. When you draw down a margin loan to buy more shares, no new cash left your pocket — the broker's money did the buying. When you repay the loan, or pay interest on it, that's a real cost to you, but it's not structured as a "sell" or a "buy" in the transaction history. It's its own category of event.

The result: interest paid on a margin loan currently never enters the XIRR calculation at all. It's excluded, on purpose, along with dividends, reinvestments, and a handful of other transaction types that aren't genuine external cash flows into or out of your own pocket. That exclusion is correct for most of the list — a dividend you reinvest didn't leave your wallet, so it shouldn't count as an outflow. But interest paid on borrowed capital is different: it is a real cost to you, even though it never touched your bank account as a discrete transaction the way a share purchase does.

What that means in practice: a portfolio running on margin can show a strong XIRR while quietly overstating how well you actually did, because the cost of the leverage that helped generate those returns was never subtracted anywhere. The number isn't lying exactly — it's answering a narrower question than it looks like it's answering. It's telling you the return on the invested capital, not the return net of what it cost you to access some of that capital.

LVR (loan-to-value ratio) is a separate, complementary number, not a fix folded into XIRR itself. LVR tells you how much of the portfolio's value is funded by debt — a risk measure. XIRR tells you the return. They're answering different questions, and I don't think the right move is trying to cram leverage risk into a return calculation. The right move is reading both side by side: what's my return, and how much borrowed money did I carry to get it.

The double-counting bug: when a deposit and a buy share the same dollar

This one was a straightforward bug, not a design trade-off — and it's the kind of mistake that's easy to make and easy to miss, because both halves of it look correct in isolation.

Say you deposit $2,500 into your brokerage account, then use that $2,500 to buy shares. That's genuinely one event from your wallet's perspective: $2,500 left your pocket, once. But if the deposit gets logged as its own outflow, and the resulting share purchase also gets logged as an outflow, XIRR sees two withdrawals of $2,500 instead of one. The calculation ends up thinking you deployed $5,000 when you actually deployed $2,500 — which skews the return, usually making it look worse than it really was, since XIRR now thinks more capital went in for the same eventual outcome.

The fix was to only count genuine external cash flows — the transactions table drives this directly, using transaction type (buy, sell, spp) rather than every cash movement logged anywhere in the account. A cash deposit that immediately funds a buy isn't two events; it's one, and only the buy — the actual moment capital left your pocket and entered the market — gets counted.

The SMSF bug: eight years of history, quietly ignored

This is the one I'm most glad got caught, because it wasn't a dramatic failure — it was a silent, plausible-looking wrong answer, which is the worst kind of bug.

XIRR needs a starting date and a full cash flow history from that date onward. Early on, that history was being derived indirectly — from the same daily price series used to build performance charts, rather than straight from the transactions table. That seemed reasonable at the time: the price series already existed, why build a second data path.

The problem: price history for a specific ticker doesn't necessarily go back as far as the portfolio's real history. My SMSF's actual transaction history goes back to 23 November 2011. But at least one currently-held ticker only had about two years of daily price data available. Since XIRR was quietly built from the price series's date range rather than the portfolio's real inception, the calculation was silently starting from wherever the shortest available price history began — not from 2011, but from roughly two years ago. Eight-plus years of genuine transaction history simply weren't part of the calculation, and nothing about the output looked wrong enough to notice at a glance. It was just a number, sitting there, quietly measuring the wrong window.

The fix was building XIRR's flows directly from the transactions table — which has always had the full, correct history natively — instead of borrowing a date range from somewhere else that happened to be shorter. Worth noting: this fix was deliberately scoped to XIRR only. TWR (time-weighted return) still needs a continuous daily value series to work at all, since it segments returns period by period rather than working off discrete cash flow dates — so TWR still depends on price history the way it always did. XIRR didn't need to; it just used to, by accident of how it was first built.

The one I haven't fixed yet: scrip mergers

Not every edge case has a clean answer, and I'd rather say so than pretend everything's solved.

When a company gets taken over and you receive shares in the acquiring company rather than cash, that's recorded as an ordinary sell (the old holding, at its deemed value) paired with an ordinary buy (the new holding). Both legs look, structurally, exactly like any other trade — nothing marks them as a merger rather than two coincidental, unrelated transactions.

Depending on which cost-basis treatment applies, those two legs don't necessarily net to zero the way a normal sell-then-rebuy would. That's a real distortion sitting in the calculation today, for anyone who's been through a scrip-consideration takeover.

I know about it. I haven't patched it, on purpose — the tempting quick fix would be pattern-matching on the free-text notes field to guess which transactions are merger-related, but that's fragile: notes text isn't a stable contract, and a heuristic like that breaks the moment someone phrases a note slightly differently. The honest fix is a dedicated transaction type for corporate actions, which is a bigger piece of work than a two-line patch. Flagging it here rather than quietly working around it.

Takeaways

None of this means XIRR is broken as a concept — it means the gap between "the formula" and "a working calculation on real, messy data" is bigger than it looks from the outside. A few things worth taking away, whether you're building something similar or just trying to trust the number your own tracker shows you:

  • A single dominant cash flow can break the standard solving method, even when the maths is entirely valid. A robust implementation needs a fallback, not just a faster version of the same algorithm.
  • XIRR only sees what's structured as a cash flow. Costs that don't take the shape of a buy or sell — like margin loan interest — simply aren't part of the number, even though they're real. Read XIRR alongside other measures (like LVR for leverage) rather than expecting one number to capture everything.
  • The same dollar counted twice is a common, easy-to-miss bug — anywhere a deposit and the purchase it funds might both get logged as separate outflows.
  • XIRR is only as complete as the history it's actually built from. If a calculation quietly depends on a shorter dataset than your real history — even for a reason that seemed sensible at the time — the result can look plausible while measuring the wrong window entirely.
  • Not every edge case gets fixed immediately, and that's fine — as long as it's known and stated, not silently ignored.

— The FolioTrack Team