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
is a payment at the end of year , and is the yearly rate as a decimal (8% is 0.08), compounded once a year. 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: .
The $300 saved in year 4 is worth only $220.51 today. Each later payment is worth less than the one before.
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:
Read it as in lesson 0.2.1: "NPV is the sum, for from 0 to , of the cash flow at time divided by to the power ." Here and is the price. A positive NPV means the cash flows are worth more than they cost.
Each symbol has a direct translation into code:
| Maths | Python |
|---|---|
cf[t]. The list starts at year 0, so there is no shift by one | |
| from 0 to | range(len(cf)) |
(1 + r) ** t | |
a 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, . Writing a formula two ways and checking they agree is a habit worth keeping.
How the rate changes the answer
At a 0% rate nothing is discounted, so the NPV is the plain total: . 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 .
Net present value
Write npv(rate, cashflows). cashflows[0] happens today, cashflows[1] in one year, and so on. Return .
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 adds up a portfolio. Put $6,000 in a stock, $3,000 in a bond and $1,000 in cash. Write for the dollars in asset and for the total, $10,000. Each asset's weight is its share of the total:
Here is the return on asset and is the portfolio's return. The weights always add up to 1: by the constant-multiple rule, .
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 . 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.
Lesson 1.4.7 explains why this weighted sum works for simple returns but not for log returns.
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 .
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 in years is worth today: divide by once per year.
- , 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))), ornp.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 , they add up to 1, and .