XIRR is an aggregate function that calculates the annualized internal rate of
return for a series of cash flows that occur on irregular dates. It is
Cube’s counterpart to the XIRR function in Excel and DAX, and solves the same equation.
The function is available in:
- The SQL API, in queries with post-processing (v1.3.8 and later).
- Cube Store (v1.3.12 and later). This is what serves
XIRRwhen it is used in a multi-stage measure queried through any Core Data API, including the REST (JSON) API, as long as the query hits a pre-aggregation. - The DAX API.
Syntax
The result is a double: an annualized rate expressed as a fraction, so
0.1 means
10% per year. XIRR(DISTINCT ...) is not supported.
How the rate is calculated
XIRR finds the rate r at which the net present value of all cash flows in the
group is zero:
t_i is the time of each cash flow in years and date_0 is the earliest date in
the group. Note that:
- Time is measured in years as the number of days since the earliest cash flow, divided by 365. Leap days count as ordinary days.
- The earliest date is the anchor regardless of row order, so rows can arrive in any order and the result is the same. Unlike Excel, the earliest cash flow does not need to be the first one.
- Cash flows on the same date are discounted identically, so it makes no difference whether they are summed per day before the call or passed as separate rows.
- Start at
initial_guess(0.1by default). - Compute
NPV(r)and its derivative at the current rate. Rows whose payment is exactly0are skipped. - If the absolute value of
NPV(r)is below0.000001, stop and returnr. - Otherwise, move the rate by
NPV(r) / NPV'(r)and repeat. - After 100 iterations without meeting the tolerance, or as soon as a step produces a rate that is not a number (for example, after stepping to -100% or below), the function reports no solution.
payment: the iteration stops when the
discounted cash flows sum to less than one millionth. Scaling every payment by the same
factor does not change the rate, so if your amounts are very large (billions), passing
them in thousands or millions gives the solver more floating-point headroom.
Results and errors
The function evaluates once per group and behaves as follows:Convergence
Newton’s method follows the slope from a single starting point, so the starting point matters for unusual series:- For strongly negative returns, such as a large loss over the period, the default
0.1guess can step past -100% and fail. Pass a negativeinitial_guess, for example-0.9, or evaluate the function twice with two guesses and combine the results withCOALESCE. - Because the result is annualized, a short window turns a small gain or loss into a
large rate. Read
XIRRover windows long enough for an annualized figure to be meaningful.
Where the function runs
XIRR exists in the SQL API’s post-processing engine and in Cube Store. It does not
exist in your upstream database. When you use it in the data model:
- Define it as a
multi_stagemeasure of typenumber_aggwhosegrainincludes a day-level time dimension, and serve it from a pre-aggregation withdaygranularity. Cube Store then receives one row per day and runs the solver. The XIRR recipe shows the full pattern. - Every dimension a query groups or filters by must be present in that pre-aggregation.
A query that cannot be matched falls through to the upstream database, which fails
with its own unknown-function error, for example
function xirr(numeric, date) does not existin Postgres. - Applying a time granularity to such a query groups the cash flows into buckets and
solves each bucket separately. A bucket that does not contain both an outflow and an
inflow has no root, so it returns
on_erroror fails.