A stock goes from 100 to 110, then back to 99. Up 10%, down 10% — so flat, right? Your P&L says −1%. That one percent is the difference between simple and log returns, and it compounds into real reconciliation breaks.
import numpy as np px = np.array([100.0, 110.0, 99.0]) simple = px[1:] / px[:-1] - 1 log_r = np.diff(np.log(px)) print(f"sum of simple returns: {simple.sum():+.2%}") print(f"exp(sum of log returns) - 1: {np.exp(log_r.sum()) - 1:+.2%}")
Output:
sum of simple returns: +0.00% exp(sum of log returns) - 1: -1.00%
The rule of thumb:
- Across time → log returns. They add exactly: the two-day log return is the sum of the daily log returns, so
log_r.sum()over any window is the true holding-period figure. This is why volatility models, backtests and anything that aggregates a single asset through time work in logs. - Across assets → simple returns. A portfolio return is the value-weighted average of simple returns — 60% in asset A and 40% in asset B gives exactly 0.6·rA + 0.4·rB. Log returns do not aggregate this way (the log of a sum is not the sum of logs), so portfolio attribution and NAV calculations live in simple-return space.
The classic failure mode is mixing the two: a risk system that sums simple returns through time overstates every drawdown recovery, and a report that averages log returns across a book understates the portfolio's actual gain. For daily equity moves the gap per observation is tiny (≈ r²/2, so a 1% move differs by 0.005%) — which is exactly why the bug survives review until a volatile month makes it visible.
If you remember one line: logs add through time, simples average across a portfolio — convert at the boundary with exp() and log(), never inside the aggregation.