An arithmetic average of annual returns does not necessarily reproduce the ending value of a series. A CAGR calculation should. That difference is useful as a test when building a spreadsheet, a performance widget or a data-analysis function.
This example uses four synthetic valuations, exactly one year apart:
| Observation | Value | Annual return |
|---|---|---|
| Start | 100 | Not applicable |
| Year 1 | 120 | +20% |
| Year 2 | 90 | −25% |
| Year 3 | 108 | +20% |
There are four values and three annual return periods. The cumulative return is 8%. The arithmetic mean of the annual percentages is 5%. The CAGR is approximately 2.5985568% per year.
All three outputs describe different measurements. Treating 5% as the constant annual growth rate would produce 115.7625 after three years, not 108.
Implement an explicit annual-series contract
The function below accepts positive finite numerical values at consecutive annual anniversaries, with no unhandled external contributions or withdrawals. It cannot establish those timing and cash-flow assumptions from the array; the caller must do that before calling it. Daily closes are a different input contract.
export function summarizeAnnualValues(values) {
if (!Array.isArray(values) || values.length < 2 ||
values.some(value => typeof value !== "number" ||
!Number.isFinite(value) || value <= 0)) {
throw new TypeError("Provide at least two positive finite annual values.");
}
const annualReturns = [];
for (let i = 1; i < values.length; i++) {
const factor = values[i] / values[i - 1];
if (!Number.isFinite(factor) || factor <= 0) {
throw new RangeError("Growth factor is outside the supported numeric range.");
}
annualReturns.push(factor - 1);
}
const years = values.length - 1;
const cumulativeReturn = values.at(-1) / values[0] - 1;
const arithmeticAverageAnnualReturn =
annualReturns.reduce((sum, value) => sum + value, 0) / years;
const cagr = Math.expm1(
(Math.log(values.at(-1)) - Math.log(values[0])) / years
);
if (![cumulativeReturn, arithmeticAverageAnnualReturn, cagr]
.every(Number.isFinite) || cagr <= -1) {
throw new RangeError("Result is outside the supported numeric range.");
}
return { years, cumulativeReturn, arithmeticAverageAnnualReturn, cagr };
}
const result = summarizeAnnualValues([100, 120, 90, 108]);
// years: 3
// cumulativeReturn: approximately 0.08
// arithmeticAverageAnnualReturn: approximately 0.05
// cagr: approximately 0.025985568006
The calculation returns decimal rates. Multiply by 100 when you need a numerical percentage, or use a percentage formatter directly on the decimal. Do not multiply twice.
The logarithms compute the equivalent annual endpoint growth without first raising an endpoint ratio to a fractional power. The checks reject invalid inputs and unsupported numeric ranges; they do not turn JavaScript's binary floating-point arithmetic into exact decimal accounting.
Test the ending-value identity
The essential invariant is:
start × (1 + CAGR)^years ≈ end
Use a tolerance rather than exact equality for ordinary floating-point results. For the example, 100 * (1 + result.cagr) ** result.years should be close to 108. Compounding result.arithmeticAverageAnnualReturn instead should not pass that same test.
A second fixture is [100, 110, 99]. It gives a 0% arithmetic average, a −1% cumulative return and approximately −0.5012563% CAGR per year. The same 10% loss applies to a larger value than the preceding 10% gain.
Keep the data contract beside the result
| Assumption | Failure to catch upstream |
|---|---|
| Consecutive annual observations | Daily values mistaken for yearly periods, or missing years ignored |
| Comparable value basis | A share split changes the unit represented by a per-share price |
| Stated income treatment | Price growth labelled total return without distributions and reinvestment |
| No unhandled external cash flows | New deposits counted as investment performance |
| One reporting currency | Unintended FX changes mixed into a comparison |
For irregular dates, the caller needs an explicit elapsed-year calculation rather than values.length - 1. For a daily series, endpoint CAGR depends on elapsed time; an arithmetic mean of daily returns is not an arithmetic mean of annual returns.
Also define the words used in the UI. Here, “average annual return” means the arithmetic average of yearly observations. Some formal reports use “average annual total return” for a compounded measure, so importing a label does not establish its formula.
Further reading
- SEC Form N-1A, for the compounded use of “average annual total return”.
- Microsoft RRI documentation, for the equivalent growth rate per specified period.
- MDN Math.expm1, for the numerical operation used above.
The inputs are invented educational fixtures. The function summarizes a historical series under its stated assumptions; it does not forecast returns or measure the risk of the path.
Top comments (0)