Returns & compounding
Before any strategy, edge, or backtest, you need to measure change correctly. Two ways to write down a return look almost identical and behave completely differently once you chain them together. Getting this wrong quietly corrupts every metric downstream — Sharpe, CAGR, drawdown — so we start here.
Simple returns#
The simple (arithmetic) return over one period is the fractional change in price. If is the close at time :
A move from 100 to 110 is — a 10% return. This is the number you actually earn on capital over the period, which is exactly why it is the right unit for combining across assetsat a point in time: a portfolio’s simple return is the weighted sum of its holdings’ simple returns.
Log returns#
The log (continuously-compounded) return is the natural log of the price ratio:
For small moves the two are nearly equal — when is small — so a 1% day is ~0.00995 in log space. The difference only bites on big moves and over long chains. The reason log returns matter is one property:
Why log returns add#
Because the log of a product is the sum of the logs, the multi-period log return is just the sum of the single-period log returns:
Start from the identity that the intermediate prices cancel. Write the terminal price ratio as a product of the single-period ratios:
Every for appears once in a numerator and once in a denominator, so they cancel — that is the “telescope.” Now take of both sides and use , which turns the product into a sum:
That telescoping is the whole point. Compounding, which is multiplicative in simple returns, becomes plainaddition in log space. Sums of many terms are exactly what the central limit theorem and every variance calculation are built for, so almost all statistics — mean, standard deviation, the Normal approximation — are done on log returns, then converted back at the end.
| Property | Simple return | Log return |
|---|---|---|
| Aggregate across assets (one period) | Adds (weighted) — use this | Does not add cleanly |
| Aggregate across time | Multiplies | Adds — use this |
| Symmetric around zero | No (+10% ≠ −10%) | Yes |
| Bounded below | ≥ −100% | Unbounded (−∞ possible) |
Arithmetic vs geometric mean#
Given a series of simple returns, you can summarise them two ways. The arithmetic mean is the plain average:
The geometric mean is the constant per-period rate that reproduces the actual ending wealth:
The geometric mean is what you actually compound at; the arithmetic mean is what you would expect on the next single period. They are equal only when every return is identical, and otherwise the geometric mean is always smaller. The gap between them is caused by volatility — and that gap has a name.
Proof: the geometric mean never exceeds the arithmetic mean#
Write the gross returns . The claim is the classical AM–GM inequality. The cleanest one-line argument uses the concavity of the logarithm.
The function is concave (its second derivative is negative everywhere). Jensen’s inequality for a concave function says the function of the average is at least the average of the function. Apply it to the gross returns, each weighted :
Because is strictly increasing, we can drop it from both sides without flipping the inequality:
Equality holds only when all are equal — i.e. a perfectly flat return stream. Any dispersion at all pushes GM strictly below AM.
Compounding & CAGR#
Wealth compounds multiplicatively. Starting from , after periods:
The Compound Annual Growth Rate collapses that whole path into the single annualised rate that would have produced the same ending value (with measured in years):
CAGR is a geometric mean by another name, so it is honest about the round-trip: a strategy that doubles then halves has a CAGR of 0%, which is exactly right. This is the number edgekit.metrics.equity_stats reports as cagr.
import numpy as np
from edgekit.metrics import equity_stats
# daily simple returns -> ending wealth and annualised rate
V0, VT = 100_000, 138_400
years = 2.5
cagr = (VT / V0) ** (1 / years) - 1 # -> 0.1357 (13.6%/yr)
es = equity_stats(daily_returns) # fractional daily returns
print(es["cagr"], es["max_dd_pct"], es["mar"])Volatility drag#
Here is the result that trips up newcomers. The rate you compound at is not your average return — it is your average return minus a penalty for variance. To a good approximation, the geometric growth rate is:
where is the arithmetic mean return and its standard deviation (both per period). The term is volatility drag: the mathematical cost of bouncing around. It falls straight out of the second-order Taylor expansion of — the same reason the geometric mean sits below the arithmetic one.
Where the σ²/2 comes from#
The geometric growth rate is the expected log return, — the per-period version of the additive log return we derived above. Expand the log to second order and take expectations.
Taylor-expand around :
Take the expectation term by term, keeping to second order:
Now use the variance identity :
The last step drops : for a per-period return, is small and is negligible next to the variance . What survives is the drag term. ■
There is a second, assumption-free way to see the same inequality: Jensen’s inequality. For any concave function , . Because is concave, applying it with gives
so the compounded growth rate can never exceed the arithmetic mean — the drag is exactly the size of that Jensen gap, and it is zero only when . This is the same fact as above, now stated in expectation.
The intuition: a loss hurts more than the same-size gain helps, because after a loss you compound from a smaller base. Down 50% needs a +100% recovery just to break even. Two strategies with the same average return but different volatility do not end up in the same place — the wilder one lags, and the gap widens with time.

Where edgekit measures this#
Two summary builders in edgekit.metrics speak these two languages. Trade-level analysis lives in R-multiples (return normalised by risk — the subject of The math of edge); equity-curve analysis lives in returns and compounds them into CAGR.
from edgekit import trade_stats
from edgekit.metrics import equity_stats
# Per-trade lens: expectancy in R, profit factor, MAR
st = trade_stats(trades.r, dates=trades.date)
print(st["ev_r"], st["pf"], st["mar"])
# Equity-curve lens: compounded growth and the drawdown it cost
es = equity_stats(daily_pnl_dollars, account=100_000)
print(es["cagr"], es["max_dd_pct"])Notice equity_stats reports mar (CAGR divided by max drawdown) alongside cagr. That ratio is the honest one: it asks not just how fast you grew, but how much of a stomach-churning drawdown you had to survive to get there — the volatility-drag lesson turned into a scorecard.
Next: returns are random, so to reason about them we need the language of chance — Probability & distributions.
