SigmaField
Mathematical Foundations for Quant Finance
0 of 256 lessons
    1. 0.1Placement checkSoon
      1. 0.2.1 Symbols, sets and function notationConcept · 15 min
      2. 0.2.2 Sums and products: Σ and ΠConcept · 15 min
      3. 0.2.3 Practice: reading formulasPractice set · 15 min
      4. 0.2.4 Case study: net present value from formula to codeCase study · 20 min

Level 0, Orientation and fluency 0.2 Reading maths Lesson 4 of 4

Case studyLesson 0.2.4 in Level 0, Orientation and fluency. About 20 minutes.

Case study: net present value from formula to code

A dollar later is worth less than a dollar now

If you can earn 8% a year, $100 today becomes $108 in a year. So $108 promised in a year is worth $100 today. To find today's value of money that arrives later, divide by 1.08 once for each year you wait.

Rule: Present value of one payment

PV=Ct(1+r)tPV = \frac{C_t}{(1 + r)^t}

CtC_t is a payment at the end of year tt, and rr is the yearly rate as a decimal (8% is 0.08), compounded once a year. PVPV is its present value: what it is worth today.

Try it

The cell builds the machine's table. Year 0 is today, so the price is not discounted: (1.08)0=1(1.08)^0 = 1.

The $300 saved in year 4 is worth only $220.51 today. Each later payment is worth less than the one before.

⌘/Ctrl + Enter runs the cell

Net present value is a sum

The net present value (NPV) adds up the present value of every cash flow, with money paid out counted as negative:

NPV=∑t=0TCt(1+r)t\text{NPV} = \sum_{t=0}^{T} \frac{C_t}{(1 + r)^t}

Read it as in lesson 0.2.1: "NPV is the sum, for tt from 0 to TT, of the cash flow at time tt divided by 1+r1 + r to the power tt." Here T=4T = 4 and C0=−1000C_0 = -1000 is the price. A positive NPV means the cash flows are worth more than they cost.

Each symbol has a direct translation into code:

MathsPython
CtC_tcf[t]. The list starts at year 0, so there is no shift by one
tt from 0 to TTrange(len(cf))
(1+r)t(1 + r)^t(1 + r) ** t
∑\suma loop, or sum(...), or np.sum

Try it

Here is the sum written three ways: a loop with a running total, a one-line sum, and NumPy on whole arrays.

All three print the same NPV, −6.36-6.36. Writing a formula two ways and checking they agree is a habit worth keeping.

⌘/Ctrl + Enter runs the cell
Check yourself
The cash flows are stored as cf = [-1000, 300, 300, 300, 300], with cf[0] paid today. Which line computes the NPV at rate r?

How the rate changes the answer

At a 0% rate nothing is discounted, so the NPV is the plain total: −1000+4×300=200-1000 + 4 \times 300 = 200. A higher rate shrinks every future payment, so the NPV falls.

Try it

The cell prints the NPV at five rates and plots it against the rate.

The line crosses zero between 4% and 8%, at about 7.71%. That rate is the machine's internal rate of return. Case study 1.3.5 finds it precisely, by solving NPV =0= 0.

⌘/Ctrl + Enter runs the cell
Check yourself
At a 4% rate the machine's NPV is $88.97. What does that tell you?
Exercise

Net present value

Write npv(rate, cashflows). cashflows[0] happens today, cashflows[1] in one year, and so on. Return ∑tCt/(1+r)t\sum_t C_t / (1 + r)^t.

The tests use lists and NumPy arrays, a zero rate, payments in later years, and an empty list. An empty sum is 0, as in lesson 0.2.2.

See the tests
import numpy as np

MACHINE = [-1000, 300, 300, 300, 300]


def test_zero_rate():
    """At a 0% rate nothing is discounted: the machine's NPV is the plain total, 200"""
    expect_close(npv(0.0, MACHINE), 200.0, what="npv(0, [-1000, 300, 300, 300, 300])")


def test_machine():
    """At 8% the machine's NPV is about −6.36"""
    expected = -1000 + 300 / 1.08 + 300 / 1.08**2 + 300 / 1.08**3 + 300 / 1.08**4
    expect_close(npv(0.08, MACHINE), expected, what="npv(0.08, [-1000, 300, 300, 300, 300])")


def test_today_not_discounted():
    """A single cash flow today is worth its face value"""
    expect_close(npv(0.05, [250]), 250.0, what="npv(0.05, [250])")


def test_later_year():
    """121 received in 2 years at 10% is worth 100 today"""
    expect_close(npv(0.10, [0, 0, 121]), 100.0, what="npv(0.10, [0, 0, 121])")


def test_higher_rate_lower_npv():
    """A higher rate gives a lower NPV"""
    low, high = npv(0.04, MACHINE), npv(0.12, MACHINE)
    assert high < low, f"expected npv at 12% ({high}) to be below npv at 4% ({low})"


def test_numpy_input():
    """A NumPy array of cash flows gives the same answer as a list"""
    expect_close(npv(0.08, np.array(MACHINE, dtype=float)), npv(0.08, MACHINE), what="npv with a NumPy array")


def test_empty():
    """No cash flows at all: an empty sum is 0"""
    expect_close(npv(0.08, []), 0.0, what="npv(0.08, [])")

A portfolio's return is a weighted sum

The same Σ\Sigma adds up a portfolio. Put $6,000 in a stock, $3,000 in a bond and $1,000 in cash. Write ViV_i for the dollars in asset ii and V=∑jVjV = \sum_j V_j for the total, $10,000. Each asset's weight is its share of the total:

wi=ViVRp=∑i=1nwiRi\begin{gathered} w_i = \frac{V_i}{V} \\[6pt] R_p = \sum_{i=1}^{n} w_i R_i \end{gathered}

Here RiR_i is the return on asset ii and RpR_p is the portfolio's return. The weights always add up to 1: by the constant-multiple rule, ∑iVi/V=1V∑iVi=1\sum_i V_i / V = \frac{1}{V} \sum_i V_i = 1.

Try it

Say the stock returns 10%, the bond 2% and the cash 0%. The weights are 0.6, 0.3 and 0.1, so Rp=0.06+0.006+0=6.6%R_p = 0.06 + 0.006 + 0 = 6.6\%. Their sum prints as 0.9999999999999999, which is 1 up to rounding.

The last line checks the return the long way. The holdings grow to $6,600, $3,060 and $1,000, a total of $10,660.

⌘/Ctrl + Enter runs the cell

Lesson 1.4.7 explains why this weighted sum works for simple returns but not for log returns.

Exercise

Portfolio return from dollar holdings

Write portfolio_return(values, returns). values holds the dollars in each asset and returns holds each asset's return over the period. Work out the weights from the values, then return ∑wiRi\sum w_i R_i.

The tests pass lists or NumPy arrays. One test doubles every holding: the weights, and so the answer, should not change.

See the tests
import numpy as np


def test_example():
    """$6,000, $3,000 and $1,000 returning 10%, 2% and 0% give 6.6%"""
    expect_close(portfolio_return([6000, 3000, 1000], [0.10, 0.02, 0.00]), 0.066, what="portfolio_return([6000, 3000, 1000], [0.10, 0.02, 0.00])")


def test_equal_holdings():
    """Equal holdings give the plain average of the returns"""
    expect_close(portfolio_return([500, 500], [0.10, 0.30]), 0.20, what="portfolio_return([500, 500], [0.10, 0.30])")


def test_scale_does_not_matter():
    """Doubling every holding leaves the return unchanged"""
    one = portfolio_return(np.array([6000, 3000, 1000]), np.array([0.10, 0.02, 0.00]))
    two = portfolio_return(np.array([12000, 6000, 2000]), np.array([0.10, 0.02, 0.00]))
    expect_close(two, one, what="return with every holding doubled")


def test_single_asset():
    """A portfolio of one asset has that asset's return"""
    expect_close(portfolio_return([2500], [0.07]), 0.07, what="portfolio_return([2500], [0.07])")


def test_losses():
    """$500 losing 20% and $1,500 gaining 4% give −2%"""
    expect_close(portfolio_return([500, 1500], [-0.20, 0.04]), -0.02, what="portfolio_return([500, 1500], [-0.20, 0.04])")

Summary card

  • A payment CtC_t in tt years is worth Ct/(1+r)tC_t / (1 + r)^t today: divide by 1+r1 + r once per year.
  • NPV=∑t=0TCt/(1+r)t\text{NPV} = \sum_{t=0}^{T} C_t / (1 + r)^t, with money paid out counted as negative. Positive NPV: worth more than it costs.
  • In Python: sum(cf[t] / (1 + r) ** t for t in range(len(cf))), or np.sum(c / (1 + r) ** np.arange(len(c))).
  • NPV falls as the rate rises. The rate where it is zero is the internal rate of return.
  • Portfolio weights are wi=Vi/∑jVjw_i = V_i / \sum_j V_j, they add up to 1, and Rp=∑iwiRiR_p = \sum_i w_i R_i.

Free further reading

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