SigmaField
Mathematical Foundations for Quant Finance
0 of 256 lessons
      1. 1.3.1 Polynomials and their rootsConcept · 25 min
      2. 1.3.2 Bisection: finding a root by halvingConcept · 25 min
      3. 1.3.3 Newton's method, a first lookConcept · 25 min
      4. 1.3.4 Practice set: rootsPractice set · 20 min
      5. 1.3.5 Case study: IRR and yield to maturityCase study · 35 min

Level 1, Precalculus 1.3 Equations and roots Lesson 5 of 5

Case studyLesson 1.3.5 in Level 1, Precalculus. About 35 minutes.

Case study: IRR and yield to maturity

The internal rate of return is a root

Case study 0.2.4 wrote the net present value at a yearly rate rr as a sum over the cash flows CtC_t. Each CtC_t arrives at the end of year tt, with C0C_0 today, and money paid out counts as negative:

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

The internal rate of return (IRR) is the rate that makes this zero. It is the yearly return the project itself earns, quoted in percent per year. A project is worth doing when its IRR beats the rate that money costs you.

To bracket it, look for a sign change (lesson 1.3.2). At r=0r = 0 nothing is discounted, so the machine's NPV is −1000+4×300=200-1000 + 4 \times 300 = 200. At r=1r = 1, or 100% a year, the later savings are worth little: the NPV is −718.75-718.75, so a root lies in [0,1][0, 1].

The guarantee of lesson 1.3.2 also needs a continuous function. The NPV is continuous for every rate above −100%-100\%; at r=−1r = -1 the bottom 1+r1 + r is 0, and the formula divides by zero.

The machine's NPV falls as the rate rises. It crosses zero once, at the IRR of 7.71%: below that rate the machine is worth buying, above it not.

Try it

The cell finds the root twice: with the bisection function of lesson 1.3.2 and with SciPy's brentq.

Both give 0.07714, or 7.71%. Bisection needs 27 halvings to reach 10−810^{-8}.

⌘/Ctrl + Enter runs the cell
Check yourself
The machine's IRR is 7.71%. Money costs the firm 8% a year. Should it buy the machine?

Several sign changes, several IRRs

Write x=11+rx = \frac{1}{1 + r}, the value today of $1 paid in a year. Then 1(1+r)t=xt\frac{1}{(1 + r)^t} = x^t, and the NPV becomes a polynomial in xx (lesson 1.3.1), with the cash flows as its coefficients:

NPV=C0+C1x+C2x2+⋯+CTxT\text{NPV} = C_0 + C_1 x + C_2 x^2 + \cdots + C_T x^T

A rate above −100%-100\% means a positive xx, so each IRR is a positive root of this polynomial. Its degree is TT, so a project lasting TT years has at most TT IRRs (lesson 1.3.1).

A sharper count is Descartes' rule of signs. A polynomial has at most as many positive roots as its list of coefficients has sign changes.

The machine's cash flows, −1000,+300,+300,+300,+300-1000, +300, +300, +300, +300, change sign once, so it has at most one IRR. So does any project that pays out first and earns afterwards.

A mine with two IRRs

A mining project costs $100 million to open. The ore brings in $260 million in year 1, and closing the mine and restoring the land costs $165 million in year 2. The cash flows −100,+260,−165-100, +260, -165 change sign twice, and the NPV is zero at both 10% and 50%.

Cash flows that change sign twice give two IRRs. The NPV is positive only between them, and it is negative at both ends of the bracket from 0% to 100%.

So the decision flips twice as the cost of money rises. At 5% a year the mine loses $2.04 million, at 25% it gains $2.4 million, and at 60% it loses $1.95 million. Cheap money makes the final clean-up cost count almost in full.

Neither IRR on its own says whether to go ahead. The NPV at your own cost of money does, so desks report it alongside any IRR.

Try it

np.roots finds every root of the polynomial in xx. NumPy wants the highest power first, so the cell reverses the cash flows with cf[::-1].

For the mine, the roots x=0.909x = 0.909 and 0.6670.667 give r=1x−1r = \frac{1}{x} - 1: 10% and 50%. The machine's polynomial has degree 4, but only one positive real root, so one IRR.

⌘/Ctrl + Enter runs the cell
Check yourself
The mine's IRRs are 10% and 50%. Money costs the firm 5% a year. Should it open the mine?

Bad brackets, and no IRR at all

A root-finder returns one root from one bracket, or a refusal. These are the traps a desk meets.

  • Equal signs hide two IRRs. For the mine, the NPV is −5-5 at 0% and −11.25-11.25 at 100%. brentq on [0,1][0, 1] refuses, as if there were no IRR, yet there are two (lesson 1.3.2).
  • Too narrow a bracket. The machine on [0,0.05][0, 0.05] has NPV 200 and 63.79, both positive, so brentq refuses although the IRR is just outside.
  • Negative rates. A project that never earns back its cost has a negative IRR: −1000,+400,+400-1000, +400, +400 gives −13.67%-13.67\%. A search that starts at 0 misses it, and one that starts at −1-1 divides by zero. Start at −0.99-0.99.
  • No IRR. When every cash flow has the same sign, the NPV is never zero. Two sign changes do not promise a root either: −100,+200,−150-100, +200, -150 has none.
  • A guess picks the IRR. Spreadsheet IRR functions start from a guess, often 10%, and step towards a root, as Newton's method does (lesson 1.3.3). With two IRRs, the guess decides which one you get.

For −100,+200,−150-100, +200, -150, the NPV in xx is −150x2+200x−100-150x^2 + 200x - 100. Its discriminant is 2002−4×150×100=−20,000200^2 - 4 \times 150 \times 100 = -20{,}000, negative, so it has no real root (lesson 1.2.2). The NPV is negative at every rate, and at best −33.33-33.33, at 50%.

Try it

SciPy's newton without a slope (lesson 1.3.3) shows the guess at work on the mine. From 5% it finds 10%; from 30% it finds 50%.

The tiny differences from 0.1 and 0.5 in the last digits are rounding.

⌘/Ctrl + Enter runs the cell

The fix: scan, then bracket

The safe method scans first. Work out the NPV on a grid of rates, from −99%-99\% to 1,000% in steps of 1%. Then hand each pair of neighbours with a sign change to brentq.

A grid rate where the NPV is exactly 0 is an IRR itself, so keep it too. A step of 1% can still miss two IRRs less than 1% apart, because the NPV may not change sign between neighbours. So a careful desk also looks at the NPV curve.

Try it

all_irrs scans 1,100 rates and returns every IRR it brackets. It finds the machine's one IRR, both of the mine's, the negative IRR, and nothing for −100,+200,−150-100, +200, -150.

⌘/Ctrl + Enter runs the cell

A bond's yield is an IRR too

A bond pays a fixed coupon cc at the end of every year and repays its face value FF after TT years. Its price at a yearly yield yy discounts each payment and adds them, as NPV does:

P(y)=∑t=1Tc(1+y)t+F(1+y)TP(y) = \sum_{t=1}^{T} \frac{c}{(1 + y)^t} + \frac{F}{(1 + y)^T}

Take a 5-year bond with a $4 coupon on a face value of $100. At y=4%y = 4\% its price is exactly 100: a bond whose yield equals its coupon rate is priced at face value. Raise the yield and the price falls.

yield yy2%3%4%5%6%
price P(y)P(y)109.43104.58100.0095.6791.58

Markets quote the price, and traders want the yield. The yield to maturity is the yy that solves P(y)=market priceP(y) = \text{market price}. It is the IRR of buying the bond: pay the price today, then receive the coupons and the face value.

At a price of 96 those cash flows are −96,4,4,4,4,104-96, 4, 4, 4, 4, 104. They change sign once, so by the rule of signs there is at most one yield. The price always falls as the yield rises, so each price has exactly one: the inverse function of lesson 1.1.3.

Try it

Subtract the market price and find the root with brentq. At 96 the yield is about 4.92%, and at 104 about 3.12%: a bond priced above face value yields less than its coupon rate.

At 125, more than the $120 the bond will ever pay, the yield is negative, about −0.87%-0.87\%. The bracket must start below 0 to find it.

⌘/Ctrl + Enter runs the cell

Three conventions trip people up with bonds.

  • Decimals, not percent. In code, 4% is 0.04. price(4) gives 1.03, a nonsense price that a quick look catches.
  • Negative yields are real. Some government bonds in Europe and Japan traded above all their future payments in the late 2010s. Search from −0.99-0.99, as for IRR.
  • Coupon frequency. Many bonds pay coupons twice a year, and quote a yield per half-year, doubled. This case study uses yearly coupons throughout.
Check yourself
A bond with a 6% coupon trades at 92, below its face value of 100. What can you say about its yield to maturity?
Exercise

Every internal rate of return

Write irrs(cashflows) that returns a list of every rate rr between −0.99-0.99 and 10 with NPV(r)=0\text{NPV}(r) = 0, smallest first. cashflows[0] happens today.

  • Work out the NPV at rates from −0.99-0.99 to 10 in steps of 0.01. Wherever it changes sign between two neighbouring rates, find the root between them with brentq or your own bisection.
  • A grid rate where the NPV is exactly 0 is an IRR too: keep it.
  • Return an empty list when there is no IRR.

The tests include the machine, the mine with two IRRs, a negative IRR, and two sets of cash flows with no IRR.

See the tests
def _npv(r, cf):
    return sum(c / (1 + r) ** t for t, c in enumerate(cf))


def _check(found, expected, what):
    assert isinstance(found, list), f"{what}: expected a list, got {found!r}"
    assert len(found) == len(expected), f"{what}: expected {len(expected)} IRRs, {expected}, got {len(found)}: {found}"
    for got, want in zip(found, expected):
        expect_close(got, want, what=what, rel=1e-6, abs_tol=1e-9)


def test_machine():
    """The machine (−1000, then 300 for four years) has one IRR, about 7.71%"""
    _check(irrs([-1000, 300, 300, 300, 300]), [0.0771384729520836], "irrs(machine)")


def test_npv_is_zero():
    """At each IRR you return, the NPV is zero: −500, 200, 200, 200"""
    cf = [-500, 200, 200, 200]
    found = irrs(cf)
    assert len(found) == 1, f"expected one IRR, got {found}"
    expect_close(_npv(found[0], cf), 0.0, what="NPV at your IRR", abs_tol=1e-6)


def test_one_period():
    """Pay 100 today, get 110 in a year: the only IRR is 10%"""
    _check(irrs([-100, 110]), [0.10], "irrs([-100, 110])")


def test_two_irrs():
    """The mine (−100, +260, −165) changes sign twice: its IRRs are 10% and 50%, smallest first"""
    _check(irrs([-100, 260, -165]), [0.10, 0.50], "irrs(mine)")


def test_two_irrs_off_grid():
    """−100, +245, −148 has two IRRs, about 8.14% and 36.86%"""
    _check(irrs([-100, 245, -148]), [0.08138593383654945, 0.3686140661634508], "irrs([-100, 245, -148])")


def test_negative_irr():
    """−1000, then 400 twice never earns its cost back: the IRR is negative"""
    _check(irrs([-1000, 400, 400]), [-0.1366750419291627], "irrs([-1000, 400, 400])")


def test_all_positive():
    """Cash flows that are all positive have no IRR: return []"""
    _check(irrs([100, 50, 50]), [], "irrs([100, 50, 50])")


def test_no_real_root():
    """−100, +200, −150 changes sign twice but its NPV is negative at every rate: return []"""
    _check(irrs([-100, 200, -150]), [], "irrs([-100, 200, -150])")
Exercise

Yield to maturity

Write ytm(price, coupon, face, years) for a bond paying coupon once a year and face at the end. Return the yield yy that makes the bond's price equal price.

The tests include bonds priced at, above and below face value, and a bond with a negative yield. One is a zero-coupon bond, which pays only the face value at the end.

See the tests
def test_at_face_value():
    """A 4% bond priced at its face value of 100 yields 4%"""
    expect_close(ytm(100, 4, 100, 5), 0.04, what="ytm(100, 4, 100, 5)", rel=1e-6)


def test_below_face():
    """Priced at 96, the 5-year 4% bond yields about 4.92%"""
    expect_close(ytm(96, 4, 100, 5), 0.04921905611384293, what="ytm(96, 4, 100, 5)", rel=1e-6)


def test_above_face():
    """Priced at 104, it yields about 3.12%, below its coupon rate"""
    expect_close(ytm(104, 4, 100, 5), 0.03123499524165618, what="ytm(104, 4, 100, 5)", rel=1e-6)


def test_ten_years():
    """A 10-year 5% bond priced at 97 yields about 5.40%"""
    expect_close(ytm(97, 5, 100, 10), 0.05396020680608085, what="ytm(97, 5, 100, 10)", rel=1e-6)


def test_zero_coupon():
    """A zero-coupon bond priced at 90, paying 100 in 3 years, yields (100/90)^(1/3) − 1"""
    expect_close(ytm(90, 0, 100, 3), (100 / 90) ** (1 / 3) - 1, what="ytm(90, 0, 100, 3)", rel=1e-6)


def test_negative_yield():
    """Priced at 125, above the 120 it will ever pay, the 5-year 4% bond has a negative yield, about −0.87%"""
    expect_close(ytm(125, 4, 100, 5), -0.008702265943250905, what="ytm(125, 4, 100, 5)", rel=1e-6)

Summary card

  • IRR: the rate that makes ∑tCt/(1+r)t=0\sum_t C_t / (1 + r)^t = 0. Bracket it with a sign change, then use bisection or brentq.
  • With x=11+rx = \frac{1}{1 + r} the NPV is a polynomial in xx. Its number of IRRs is at most the number of sign changes in the cash flows.
  • Two sign changes can give two IRRs, or none. Then the IRR rule fails: judge the project by its NPV at your cost of money.
  • Scan a grid of rates from −0.99-0.99 and bracket every sign change; one bracket or one guess finds at most one IRR.
  • Bond price: P(y)=∑t=1Tc(1+y)t+F(1+y)TP(y) = \sum_{t=1}^{T} \frac{c}{(1 + y)^t} + \frac{F}{(1 + y)^T}, falling as the yield rises. The yield to maturity is the IRR of buying the bond; it can be negative.

Free further reading

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