Cumulative vs annualised vs calendar returns: formulas and conversions
The three fund return conventions, the formulas behind them, how to convert between them, and the mistakes that produce plausible but wrong numbers.Published 7 September 2026 · 4 min read · by FundFacts API"The fund returned 8%" means nothing until you know over which period, compounded how, and in which currency. Fund documents use three conventions, cumulative, annualised and calendar-year returns, and they answer different questions. This guide gives the formula for each, shows how to convert between them, and lists the mistakes that produce numbers that look plausible and are wrong.
The three conventions
| Convention | Question it answers | Formula | API field |
|---|---|---|---|
| Cumulative | How much did 100 grow to over the period? | V_end / V_start - 1 | cumulativePerformance (period → %) |
| Annualised (CAGR) | What constant yearly rate would produce that growth? | (1 + R_cum)^(1 / T) - 1 | `annualisedReturns` |
| Calendar year | What happened between one 31 December and the next? | V_dec(Y) / V_dec(Y-1) - 1 | `calendarReturns.years / .fund / .benchmark` |
All three are computed from the NAV of one share class, with distributions reinvested (total return). Only the periods differ.
Cumulative return
The cumulative return over any period is the product of the periodic growth factors minus one:
textR_cum = (1 + r_1) * (1 + r_2) * ... * (1 + r_n) - 1= V_end / V_start - 1
In cumulativePerformance the keys are periods such as "1M", "YTD", "1Y", "3Y" and "5Y", the values are percentages, and every period is measured back from dataAsOf. A 3Y cumulative return of 29.5% means 100 became 129.5 over the three years ending on that date. Cumulative returns are intuitive but not comparable across periods: 29.5% over three years and 29.5% over ten years are very different outcomes.
Annualised return
Annualising converts a cumulative return into a constant compound yearly rate, the compound annual growth rate (CAGR):
textR_ann = (1 + R_cum)^(1 / T) - 1T = number of years = days / 365.25
The reverse conversion is R_cum = (1 + R_ann)^T - 1. annualisedReturns holds these figures for the multi-year periods. Fund documents do not annualise periods shorter than a year, and neither should you: turning a strong quarter into a yearly rate is misleading, so show short periods as cumulative.
Calendar-year returns
Calendar returns are cumulative returns over fixed windows: from the last NAV of the previous year to the last NAV of the current year. calendarReturns gives them as three parallel arrays:
json{"calendarReturns": {"years": [2021, 2022, 2023, 2024, 2025],"fund": [20.0, -15.0, 12.0, 8.0, 5.0],"benchmark": [21.2, -14.1, 13.5, 8.9, 5.8]}}
Example values. Index i of each array refers to the same year, so iterate the arrays together rather than assuming a fixed length or start year. A fund launched mid-2022 will have no 2021 entry and normally no 2022 entry either, because a partial first year is not a calendar return.
Converting between them
Calendar returns compound into a cumulative return; the cumulative return annualises into a CAGR. Working through the example values above:
textR_cum = 1.20 * 0.85 * 1.12 * 1.08 * 1.05 - 1= 1.2955 - 1= 29.55 %R_ann = 1.2955^(1/5) - 1= 5.31 % per year
Now look at what the arithmetic average of the five calendar returns gives: (20 - 15 + 12 + 8 + 5) / 5 = 6.0%. That is not the annualised return, and the gap is not a rounding issue.
The averaging mistake
The arithmetic mean of yearly returns always overstates the compound growth rate when returns vary, and the gap grows with volatility. A useful approximation:
textgeometric mean ≈ arithmetic mean - variance / 2
In the example the variance of the five returns is about 136 (in percentage points squared), so variance / 2 is about 0.68 percentage points, which is the 6.0% vs 5.31% gap. The extreme case makes the point: a fund that returns +50% then -50% has an arithmetic average of 0% and a cumulative return of -25%. Never average annual returns; compound them.
Code: from calendar years to cumulative and annualised
typescripttype CalendarReturns = { years: number[]; fund: number[]; benchmark?: number[] };export function compound(returnsPct: number[]): number {const growth = returnsPct.reduce((g, r) => g * (1 + r / 100), 1);return (growth - 1) * 100;}export function annualise(cumulativePct: number, years: number): number {if (years < 1) throw new Error("do not annualise periods shorter than a year");return (Math.pow(1 + cumulativePct / 100, 1 / years) - 1) * 100;}export async function calendarToCagr(isin: string) {const res = await fetch(`https://fundfactsapi.com/api/v1/funds/${isin}`, {headers: { Authorization: `Bearer ${process.env.FUNDFACTS_API_KEY}` },});if (!res.ok) throw new Error(`FundFacts API ${res.status}`);const { data } = await res.json();const cr: CalendarReturns = data.calendarReturns;const cumulative = compound(cr.fund);const cagr = annualise(cumulative, cr.years.length);return { years: cr.years, cumulative, cagr };}
Because years.length full calendar years is exactly T years, no day count is needed here. For cumulativePerformance periods measured back from dataAsOf, use the actual day count divided by 365.25.
Other mistakes that produce plausible wrong numbers
- Mixing price return and total return. A distributing share class's NAV drops on every ex-dividend date. Compute returns from raw NAV without reinvesting distributions and you get a price return, which is not comparable to the total return on the factsheet. The API's performance fields are total return; see accumulating vs distributing.
- Wrong share class. Two share classes of the same fund can differ by a percentage point a year through fees alone. Returns belong to an ISIN, not to a fund name.
- Currency. Returns are expressed in the share-class currency (
keyFacts.currency). A USD share class showing +10% is not +10% for a euro investor unless it is a hedged share class. Convert with period-matched FX rates, or compare only share classes in the same currency. - Period end dates.
cumulativePerformanceends atdataAsOf; calendar returns end at 31 December. Do not compound a "1Y" cumulative figure with calendar years, because the windows overlap. - Since-inception figures.
annualisedReturnsmay include a since-inception value. CheckkeyFacts.inceptionbefore comparing it with another fund's since-inception number over a different length. - Benchmark basis.
calendarReturns.benchmarkfollows the fund house's stated benchmark, named inbenchmarkName. A net total return index and a price index are not interchangeable, so read the name before quoting a relative return.
Which convention to show
Client-facing reports (see the client reporting use case) usually pair calendar years, which are the easiest to audit, with 1, 3 and 5-year annualised figures. Screeners and comparison tools (how to build one) should rank on annualised returns over identical windows, never on cumulative returns of different lengths. Whichever you show, label the currency, the end date and whether the figure is annualised. The performance data guide covers where each field comes from and how often it refreshes.