SigmaField
Mathematical Foundations for Quant Finance
0 of 256 lessons
      1. 1.4.1 Exponents and their rulesConcept · 25 min
      2. 1.4.2 Exponential functions and the number eConcept · 25 min
      3. 1.4.3 Logarithms and their rulesConcept · 25 min
      4. 1.4.4 Solving exponential and logarithmic equationsConcept · 25 min
      5. 1.4.5 Practice set: exponents and logarithmsPractice set · 25 min
      6. 1.4.6 Case study: compounding and continuous ratesCase study · 35 min
      7. 1.4.7 Case study: simple returns and log returnsCase study · 30 min

Level 1, Precalculus 1.4 Exponents and logarithms Lesson 7 of 7

Case studyLesson 1.4.7 in Level 1, Precalculus. About 30 minutes.

Case study: simple returns and log returns

Two ways to measure a return

For a price that moves from Pt−1P_{t-1} to PtP_t over one period:

simple return Rt=PtPt−1−1log return rt=ln⁡PtPt−1=ln⁡(1+Rt)\begin{gathered} \text{simple return } R_t = \frac{P_t}{P_{t-1}} - 1 \\[6pt] \text{log return } r_t = \ln\frac{P_t}{P_{t-1}} = \ln(1 + R_t) \end{gathered}

The ratio Pt/Pt−1=1+RtP_t / P_{t-1} = 1 + R_t is the gross return: what each dollar became. Both returns summarise it, in two different ways.

A return has no units, but it always belongs to a period: a day, a month, a year. Both are decimals in formulas, so 0.05 is 5%.

Try it

Here is the stock from the opening: 100, then 150, then 75.

The average simple return says 0%, but the total return is −25%. Simple returns do not add up across time.

⌘/Ctrl + Enter runs the cell

The two side by side

Plot the log return against the simple return and three facts appear.

The log return against the simple return. They agree near 0, the log return is always the smaller, and it plunges as RR nears −100%-100\%. A doubling and a halving have log returns of equal size and opposite sign.

  • Near 0 they agree. Daily returns are usually well under 1%, so the two are almost identical from day to day.
  • The log return is always the smaller. ln⁡(1+R)≤R\ln(1 + R) \le R, with equality only at R=0R = 0. The gap grows with the size of the move.
  • Log returns are symmetric. Doubling gives ln⁡2≈0.6931\ln 2 \approx 0.6931 and halving gives ln⁡0.5≈−0.6931\ln 0.5 \approx -0.6931, so a move and its undoing cancel. In simple returns the same pair reads +100%+100\% and −50%-50\%.

A total loss, R=−100%R = -100\%, has no log return at all: ln⁡0\ln 0 does not exist (lesson 1.4.3). Near it, the log return heads to minus infinity.

Try it

For small returns, ln⁡(1+R)≈R−R2/2\ln(1 + R) \approx R - R^2/2. Lesson 2.7.2 shows where this comes from; for now, check it with numbers.

np.log1p(R) computes ln⁡(1+R)\ln(1 + R), accurately even for tiny RR.

⌘/Ctrl + Enter runs the cell

Log returns add across time

Take a stock that goes from 100 to 110, then to 99. The two log returns are ln⁡1.1\ln 1.1 and ln⁡0.9\ln 0.9. Their sum is the log return over the whole span:

ln⁡1.1+ln⁡0.9=ln⁡(1.1×0.9)=ln⁡0.99=ln⁡99100\begin{aligned} \ln 1.1 + \ln 0.9 &= \ln(1.1 \times 0.9) \\ &= \ln 0.99 = \ln\frac{99}{100} \end{aligned}

That is the product rule of logs. It works for any number of periods, because the gross returns multiply and the middle prices cancel:

PTP0=P1P0⋅P2P1⋯PTPT−1sor0→T=r1+r2+⋯+rT\begin{gathered} \frac{P_T}{P_0} = \frac{P_1}{P_0}\cdot\frac{P_2}{P_1}\cdots\frac{P_T}{P_{T-1}} \\[6pt] \text{so}\quad r_{0\to T} = r_1 + r_2 + \cdots + r_T \end{gathered}

Try it

Check it with the 100, 110, 99 path. The sum of the one-period log returns equals the log of the total gross return.

Simple returns compound instead of adding: 1+R0→T=(1+R1)(1+R2)⋯(1+RT)1 + R_{0\to T} = (1+R_1)(1+R_2)\cdots(1+R_T). Sums are easier to model than products, which is why the time-series models of the Financial Econometrics course work with log returns.

⌘/Ctrl + Enter runs the cell
Check yourself
A stock goes from 100 to 110, then to 99. Which statement is true?

Average and annual returns

The hook's 0% is the arithmetic mean of the simple returns, and it overstates growth. The honest average is the steady return per period that gives the same total: the geometric mean.

(PTP0)1/T−1=erˉ−1\begin{gathered} \left(\frac{P_T}{P_0}\right)^{1/T} - 1 \\ = e^{\bar r} - 1 \end{gathered}

Here TT is the number of periods and rˉ\bar r is the mean of the log returns. For the hook, 1.5×0.5−1≈−13.40%\sqrt{1.5 \times 0.5} - 1 \approx -13.40\% a month: two months at that rate also lose 25%.

Annualising

Log returns scale with time because they add. Over 252 trading days, the usual count for daily stock data, a mean daily log return of 0.0004 becomes 0.1008 for the year. As a simple return that is e0.1008−1≈10.61%e^{0.1008} - 1 \approx 10.61\%.

Simple returns compound instead. The same daily return is e0.0004−1≈0.040008%e^{0.0004} - 1 \approx 0.040008\% as a simple return, and (1.00040008)252−1(1.00040008)^{252} - 1 gives the same 10.61%. Multiplying it by 252 gives about 10.08%, which is wrong.

Try it

The cell computes the hook's geometric mean two ways, then annualises the daily return.

⌘/Ctrl + Enter runs the cell
Check yourself
A fund's monthly log returns over a year average 0.01. What is its simple return for the year?

Simple returns add across assets

A portfolio's weights are the fractions of its starting value in each asset. They add up to 1. Each asset contributes its weight times its own return:

Rule: Portfolio return

Rp=∑iwiRi=w1R1+w2R2+⋯+wnRn\begin{gathered} R_p = \sum_i w_i R_i \\ = w_1 R_1 + w_2 R_2 + \cdots + w_n R_n \end{gathered}

This works for simple returns only. The log of a weighted sum is not the weighted sum of logs. So portfolio and risk totals, such as value at risk across desks, are built from simple returns.

Try it

Put $10,000 into a portfolio: 60% in a stock that returns +10% and 40% in a bond that returns −5%.

The stock grows from $6,000 to $6,600 and the bond falls from $4,000 to $3,800. The total is $10,400, a 4% return, which matches 0.6×0.10+0.4×(−0.05)0.6 \times 0.10 + 0.4 \times (-0.05).

⌘/Ctrl + Enter runs the cell

The weighted sum of log returns can be badly wrong. Split money equally between assets returning +30% and −20%. The portfolio's simple return is 5%, so its log return is ln⁡1.05≈0.0488\ln 1.05 \approx 0.0488. The weighted sum of the two log returns is only 0.0196.

Check yourself
A portfolio is 70% in an asset that returns +8% and 30% in an asset that returns −2%. What is the portfolio's simple return?

Across assets and over time together

A desk report needs both at once: a portfolio's return over many periods. The recipe is to work across assets with simple returns in each period, then across time with log returns.

Keep the $10,000 portfolio for a second period, in which the stock returns −8% and the bond +3%. Nobody trades, so the weights drift: after period 1 the stock is $6,600 of $10,400, a weight of 63.46%, not 60%.

  • Period 1: Rp,1=4%R_{p,1} = 4\%, and the portfolio is worth $10,400.
  • Period 2, with the drifted weights: Rp,2≈−3.9808%R_{p,2} \approx -3.9808\%, and the portfolio is worth $9,986.
  • Over both: ln⁡1.04+ln⁡(1−0.039808)=ln⁡0.9986\ln 1.04 + \ln(1 - 0.039808) = \ln 0.9986, a total return of −0.14%-0.14\%.

Using 60% and 40% again in period 2 would give −3.60%-3.60\%. That is right only if someone rebalances back to 60/40 at the start of period 2.

Try it

The loop recomputes the weights from the current values at the start of each period, then grows each holding by its return.

The second line ends with 9986.0, the value after both periods. The last line adds the two log returns; they match the log of the total growth.

⌘/Ctrl + Enter runs the cell

Why up and down loses money

A rise of xx followed by a fall of xx multiplies your money by (1+x)(1−x)=1−x2(1 + x)(1 - x) = 1 - x^2. That is always less than 1, so you always end up poorer. The loss is x2x^2: 25% for a 50% swing, but only 0.01% for a 1% swing.

Try it

Bigger swings cost far more, because the loss grows with the square of the swing. This is called volatility drag.

You will meet it again in the Stochastic Calculus course, where Itô's lemma produces a −12σ2-\tfrac{1}{2}\sigma^2 term for the same reason.

⌘/Ctrl + Enter runs the cell

Bad inputs on a desk

Real price files break both formulas in predictable ways. Check for each of these before computing a single return.

  • A price of 0. After a default the simple return is −100%-100\% and the log return is minus infinity. NumPy prints -inf.
  • A negative price. Some futures and price spreads can trade below zero. Then Pt/Pt−1P_t / P_{t-1} can be negative, the log return does not exist, and desks track price changes in dollars instead.
  • Splits and dividends. A 2-for-1 split halves the price without anyone losing money. On raw prices it looks like a return of −50%-50\%, a log return of −0.6931-0.6931. Use prices adjusted for splits and dividends.
  • Missing prices. One missing price spoils the two returns on either side of it, and every sum that includes them.

Try it

The first line shows a fall to 0 and to a negative price: -inf and nan, for "not a number". np.errstate hides the warnings NumPy would print; on a desk you would want to see them.

The second line shows one missing price, written np.nan, spoiling two returns.

⌘/Ctrl + Enter runs the cell
Exercise

Returns from prices

Write three functions. Each takes a NumPy array of prices in time order.

  1. simple_returns(prices) returns the array of one-period simple returns.
  2. log_returns(prices) returns the array of one-period log returns.
  3. total_log_return(prices) returns the log return from the first price to the last, as a single number.

Both arrays should be one element shorter than prices. NumPy slicing (prices[1:], prices[:-1]) avoids any loop.

See the tests
import numpy as np

PRICES = np.array([100.0, 110.0, 99.0, 105.0])


def test_simple_returns():
    """Simple returns of 100, 110, 99, 105"""
    expect_close(simple_returns(PRICES), [0.10, -0.10, 105 / 99 - 1], what="simple_returns")


def test_log_returns():
    """Log returns of 100, 110, 99, 105"""
    expect_close(log_returns(PRICES), np.log([1.10, 0.90, 105 / 99]), what="log_returns")


def test_lengths():
    """Both arrays are one shorter than the prices"""
    expect_equal(len(simple_returns(PRICES)), 3, what="len(simple_returns)")
    expect_equal(len(log_returns(PRICES)), 3, what="len(log_returns)")


def test_total_is_sum_of_logs():
    """The total log return equals the sum of the one-period log returns"""
    expect_close(total_log_return(PRICES), float(np.sum(log_returns(PRICES))), what="total_log_return vs sum")
    expect_close(total_log_return(PRICES), np.log(1.05), what="total_log_return(100 to 105)")


def test_random_prices():
    """Works on a long random price path"""
    rng = np.random.default_rng(7)
    path = 50 * np.exp(np.cumsum(rng.normal(0, 0.01, 500)))
    expect_close(np.exp(np.sum(log_returns(path))), path[-1] / path[0], what="exp(sum of log returns)")
    expect_close(np.prod(1 + simple_returns(path)), path[-1] / path[0], what="product of gross returns")
Exercise

A portfolio's return

Write portfolio_return(weights, returns) returning the portfolio's simple return, given NumPy arrays of weights and of the assets' simple returns.

Then write portfolio_log_return(weights, returns) returning the portfolio's log return. Careful: it is not the weighted sum of the assets' log returns. Work out the simple return first, then convert.

See the tests
import numpy as np


def test_sixty_forty():
    """60% at +10% and 40% at -5% returns 4%"""
    expect_close(portfolio_return(np.array([0.6, 0.4]), np.array([0.10, -0.05])), 0.04, what="portfolio_return")


def test_three_assets():
    """Three assets with weights 0.5, 0.3, 0.2"""
    w = np.array([0.5, 0.3, 0.2])
    r = np.array([0.02, -0.01, 0.05])
    expect_close(portfolio_return(w, r), 0.5 * 0.02 - 0.3 * 0.01 + 0.2 * 0.05, what="portfolio_return")


def test_log_return_converts_at_the_end():
    """The portfolio log return is ln(1 + portfolio simple return)"""
    w = np.array([0.6, 0.4])
    r = np.array([0.10, -0.05])
    expect_close(portfolio_log_return(w, r), np.log(1.04), what="portfolio_log_return")


def test_log_return_is_not_weighted_logs():
    """It differs from the weighted sum of log returns"""
    w = np.array([0.5, 0.5])
    r = np.array([0.30, -0.20])
    wrong = float(np.dot(w, np.log1p(r)))
    got = portfolio_log_return(w, r)
    assert got is not None and abs(got - wrong) > 1e-3, (
        f"got {got}, which matches the weighted sum of log returns ({wrong:.6f}); convert the portfolio simple return instead"
    )

Summary card

  • Simple return Rt=Pt/Pt−1−1R_t = P_t/P_{t-1} - 1; log return rt=ln⁡(Pt/Pt−1)=ln⁡(1+Rt)r_t = \ln(P_t/P_{t-1}) = \ln(1+R_t). They agree near 0; the log return is always the smaller.
  • Log returns add across time: r0→T=∑trtr_{0\to T} = \sum_t r_t, by the product rule of logs.
  • The geometric mean erˉ−1e^{\bar r} - 1 is the honest average return; annualise log returns by multiplying by the number of periods.
  • Simple returns add across assets: Rp=∑iwiRiR_p = \sum_i w_i R_i, with start-of-period weights adding up to 1. Weights drift unless rebalanced.
  • Over time and assets: simple returns across assets each period, then add the log returns over time.
  • A rise and fall of xx leaves 1−x21 - x^2 of your money: volatility drag.
  • Zero or negative prices, splits and missing prices break returns: check the data first.
Next lesson1.5.1 Sequences →

Free further reading

Optional. This lesson covers what you need; these add more examples and practice. All are free to read.