⚠ Trading involves significant risk of loss. Everything on this site is for education only and is not financial advice.
Lesson 5 of 13 · Free

Returns in Code: Simple versus Log Returns, Cumulative Returns, Annualised Return and Volatility, and Rolling Windows

Compute simple and log returns in pandas, compound them correctly, annualise return and volatility, and build rolling statistics.

Author: Moustafa HijaziContent reviewed: 2026-10-305 min readSelf-study course

What you will learn

  • How to compute simple and log returns from a close series and when each one is the right tool.
  • Why simple returns must be compounded with a product while log returns are simply summed.
  • How to annualise a daily mean return and a daily standard deviation, and what the square-root-of-time rule assumes.
  • How rolling windows produce time-varying statistics such as 20-day volatility, and why the first rows are empty.

Explanation

A simple return is the percentage change in price, rt = (Pt − Pt−1) ÷ Pt−1, in pandas r = df["close"].pct_change(). A log return is the natural logarithm of the price ratio, ℓt = ln(Pt ÷ Pt−1), in code np.log(df["close"]).diff(). Both start with a NaN. For small moves the two are almost identical; for large moves they diverge (a −50% simple return is a −69.3% log return).

Compounding. Over several periods, simple returns multiply: total = (1 + r1)(1 + r2)…(1 + rn) − 1. Log returns add: total log return = ℓ1 + ℓ2 + … + ℓn, and converting back gives total simple return = exp(sum) − 1. Adding simple returns is wrong and the error grows with volatility: +10% followed by −10% sums to 0% but actually leaves −1%. A cumulative return series is (1 + r).cumprod() − 1; an equity curve from €10,000 is 10_000 * (1 + r).cumprod(); with log returns, np.exp(lr.cumsum()).

When to use which. Log returns for aggregating over time and for statistics; simple returns whenever money changes hands or assets are combined, because a portfolio return is a weighted sum of simple returns. Report to humans in simple percentages.

Annualising. Convention assumes 252 trading days per year for equities (the true count varies slightly by exchange and year; state the convention you use). Mean daily return × 252 is the arithmetic annualised return, and the CAGR (compound annual growth rate, defined in the Systematic Trading course) is (ending ÷ starting)1 ÷ years − 1, which is what you should report for a backtest. Volatility is the standard deviation of returns; it scales with the square root of time, so annual volatility ≈ daily standard deviation × √252 ≈ daily × 15.87. For monthly data use 12 and √12, for weekly 52 and √52. The square-root rule assumes independent returns, which trending series violate; treat it as a convention.

Rolling windows. r.rolling(20).std() * np.sqrt(252) gives the annualised volatility of the most recent 20 days, recalculated on every row. The first 19 rows are NaN because the window is not full (min_periods can relax this). expanding() uses all data up to each row (the running equity maximum for drawdown in lesson 9). Both look backwards only, so they are free of look-ahead bias by construction.

Worked example

Hypothetical example: four closes of a share, €100.00, €105.00, €100.80, €105.84; no costs, dividends or taxes.

Simple returns: (105 − 100) ÷ 100 = +5.00%; (100.80 − 105) ÷ 105 = −4.00%; (105.84 − 100.80) ÷ 100.80 = +5.00%.

Log returns: ln(1.05) = 0.04879; ln(0.96) = −0.04082; ln(1.05) = 0.04879. Sum = 0.05676. exp(0.05676) − 1 = 0.0584, that is +5.84%.

Check: 1.05 × 0.96 × 1.05 = 1.0584 → +5.84%, and 105.84 ÷ 100 − 1 = 5.84%. Adding the simple returns would give 6.00%, overstating by 0.16 points in three days: €10,000 becomes €10,584, not €10,600.

Code: r = c.pct_change(); total = (1 + r.dropna()).prod() - 1 returns 0.0584. r.rolling(3).mean() places (0.05 − 0.04 + 0.05) ÷ 3 = 2.00% on the last row and NaN before.

Annualising a hypothetical longer sample: if daily returns have mean 0.04% and standard deviation 1.2%, the arithmetic annualised return is 0.0004 × 252 = 10.08% and the annualised volatility is 0.012 × 15.87 = 19.05%.

Common mistake

Summing simple returns or averaging them to get a totalQuick notebooks often use r.sum() as the period return. It ignores compounding and overstates results in volatile series, dramatically so for leveraged products. Use (1 + r).prod() − 1 for simple returns, or sum log returns and exponentiate.

Practice activity

On paper: closes €50.00, €48.00, €50.40, €49.392 (hypothetical). (a) Compute the three simple returns. (b) Compute the three log returns using ln(0.96) = −0.04082, ln(1.05) = 0.04879 and ln(0.98) = −0.02020. (c) Give the total return by compounding and by summing logs, and compare with the naive sum of simple returns. (d) If a different series shows a daily standard deviation of 1.5%, state its annualised volatility using 252 days, and explain why multiplying by 252 would be wrong.

Show solution

(a) 48 ÷ 50 − 1 = −4.00%; 50.40 ÷ 48 − 1 = +5.00%; 49.392 ÷ 50.40 − 1 = −2.00%.

(b) −0.04082, +0.04879, −0.02020; sum = −0.01223.

(c) Compounded: 0.96 × 1.05 × 0.98 = 0.98784 → −1.216%. From logs: exp(−0.01223) − 1 = −1.216%, and 49.392 ÷ 50 − 1 = −1.216% confirms it. The naive sum gives −4 + 5 − 2 = −1.00%, which understates the loss.

(d) Annualised volatility = 1.5% × √252 = 1.5% × 15.87 = 23.8%. Multiplying by 252 would give 378%, mixing up the scaling rule for means (linear in time) with the rule for standard deviations (square root of time).

Limitations: the √252 rule assumes independent daily returns; real series with autocorrelation have a different true annual volatility.

Key takeaways

  • pct_change() gives simple returns, np.log(c).diff() gives log returns; both leave a NaN in row one.
  • Compound simple returns with a product; sum log returns and exponentiate; never add simple returns.
  • Annualise means with × 252 and standard deviations with × √252, state the convention, and report CAGR for backtests.
  • Rolling and expanding windows look backwards only, produce NaN until full, and use the sample standard deviation by default.

Next step

Lesson 6 codes the standard indicators as vectorised functions, SMA, EMA, RSI, ATR and Bollinger Bands, and shows how to verify them against a charting platform.

Self-check

Answer the questions, then check your answers. You can retry as often as you like.

1. A series of daily simple returns is +10%, −10%, +10%, −10%. What is the total return?

2. Monthly returns of a fund have a standard deviation of 4%. Which is the annualised volatility under the usual convention?

3. Why do the first 19 rows of r.rolling(20).std() show NaN?

Automatic self-check for your own revision. Your answers are checked in your browser and are not seen or assessed by us. It is not an exam and not a qualification.

Your progress

“Complete” only means you marked the lesson as done. It is not a test of your understanding. Progress and notes are saved only in this browser on this device — not in an account.