A bank account that follows the short rate
The short rate is the interest rate a bank pays at time on money left with it for the next instant. It is a rate per year, compounded continuously (case study 1.4.6). This case study takes it as known in advance, a deterministic short rate: , with in years.
Let be the balance of an account that starts with . Over a short time it earns about . Dividing by and letting it shrink gives a differential equation.
Rule: A bank account
The equation is separable (lesson 6.1.1): , so , with the constant fixed by . With a constant rate it is the familiar .
The letter inside the integral is the time of each instant, since is already the upper limit. The integral is the area under the short rate (lesson 2.5.3), a pure number: a rate per year times years. For the rising rate,
After 10 years that area is 0.8, so the account holds .
The price of a zero-coupon bond
A zero-coupon bond pays 1 at its maturity and nothing before (case study 2.2.4). Write for its price today. The rates are known, so the bond and the account are both riskless, and they must grow alike.
Put in the account today: at it has grown to , which must equal the bond's 1. Any other price allows an arbitrage (case study 3.3.4). If the bond were cheaper, you could borrow from the account, buy bonds, and keep a profit with nothing to pay later.
Rule: Bond price under a deterministic short rate
Discount by the area under the short rate up to maturity. For the rising rate, .
| maturity | 1 | 2 | 5 | 10 | 20 |
|---|---|---|---|---|---|
| rising rate | 0.965605 | 0.923116 | 0.759572 | 0.449329 | 0.074274 |
| flat 3% | 0.970446 | 0.941765 | 0.860708 | 0.740818 | 0.548812 |
At one year the two are close. The gap widens with maturity, because each extra year is discounted at a higher rate than the last.
The price solves an equation in the maturity
Seen as a function of its maturity, the price solves a differential equation of its own. The fundamental theorem (lesson 2.5.3) gives , and the chain rule then gives
A bond maturing now is worth its payment. Each extra instant of maturity is discounted at the short rate of that instant.
The explorer below draws this equation, , with for and for ; the exact curve ends at 0.44933. Euler's method with lands on 0.45637, too high by 0.00705. Try and : the error falls to 0.00331, then 0.00063.
Explore
Each short line has the slope the equation gives at that point. A solution follows the lines. Move the starting value, then step along the field with Euler's method and shrink the step to follow the exact curve.
The dark curve is the exact solution; the coloured one is Euler's method.
- Exact solution at t = 10
- 0.44933
- Euler, 20 steps of 0.5
- 0.45264, off by 0.00331
Try it
P is the bond price from the rule; flat discounts at a constant 3%. The loop prints the table.
The last line is the account after 10 years, and its reciprocal, which is the 10-year price.
Yields and forward rates
The continuous yield is the one constant rate that gives the same price: . It is case study 1.6.4's continuous zero rate. Taking logs gives it from the price.
Rule: Yield
The area under the short rate divided by the time: the average short rate up to maturity.
For the rising rate, : 3.5% at one year, 8% at ten and 13% at twenty. The yield curve, the yield against maturity, rises, but half as steeply as the short rate.
The instantaneous forward rate
Case study 1.6.4 read one-year forward rates off a curve, : the growth the curve implies for one year alone. With continuous rates, the forward rate from to is . Shrink the gap to an instant and it becomes a derivative.
Rule: Instantaneous forward rate
The rate the curve fixes today for lending over an instant at time . With a known short rate, the fundamental theorem makes it the short rate itself.
At the forward rate is 13%, and a central difference of (lesson 2.8.1) agrees. The one-year forward rate from 9 to 10 is 12.5%, the short rate at mid-year, 9.5. The yield is the average of the forward rates up to , so on a rising curve it trails them: 8% against 13% at ten years.
The rising short rate drags the price far below a flat 3%. The forward rate is the short rate itself, 13% at ten years; the yield, its average so far, is 8%.
Try it
Each line prints the yield and the short rate at a maturity. Then a central difference of at 10 with , rounded.
The last line is the one-year forward rate from 9 to 10, against the short rate at 9.5.
Checking with solve_ivp
The exact price needs in closed form; solve_ivp (lesson 6.1.2) needs only itself. On from , with and , it matches the table at every maturity to about . With the default tolerances the 10-year price comes out 0.449235, off by about : too rough for a price.
Backwards from maturity
A bond also has a value at each date before maturity: . It solves , and what is known is its end: . A condition at the final time is a terminal condition, and the equation is solved backwards from it.
Given a t_span that runs from 10 down to 0, solve_ivp steps backwards and lands on 0.449329. Option values are found the same way, backwards from their payoff (lesson 6.3.2 and case study 6.3.4).
A short rate from a table
A desk's short-rate curve comes from market quotes at a few dates, joined by straight lines with np.interp (lesson 1.6.1). Take rates of 3.0%, 4.2%, 5.5%, 6.0% and 6.2% at 0, 1, 3, 5 and 10 years: there is no formula to integrate.
The area under straight-line pieces is exactly the trapezoid rule on the knots (lesson 2.8.2), so is the exact price: 0.575222 at 10 years, a yield of 5.53%. solve_ivp agrees to within ; the corners in the rate cost it some of the accuracy that rtol asks for.
Try it
The first loop solves for the price at five maturities, with the default and with tight tolerances. Each line prints the prices and the largest error, rounded to one figure.
Then the backward solve from , and the price under the rate from a table, against from np.trapezoid.
A stock that pays dividends
The same equations price a forward contract on a stock: an agreement today to buy the stock at time for a price , fixed now and paid at . Nothing changes hands today, so the fair is the one that makes the deal worth nothing now.
Copy the deal with the bank: borrow to buy the stock today, hold it to , and repay the loan then. Whatever you owe at is the fair forward price, because at you hold one share either way. Dividends paid along the way change both sides.
A dividend yield
Say the stock pays dividends continuously at a dividend yield : per year, times its price, reinvested at once in more shares. The number of shares then grows by , a separable equation with .
To hold one share at , buy shares today: 0.818731 of a share for and . Borrow their cost, , from the account; by the account equation the debt grows to at .
Rule: Forward price with a dividend yield
Without dividends, : today's price grown at the short rate. A yield lowers it by the factor , the shares the dividends will add.
With $100 and , the forward price is $222.5541 without dividends and $182.2119 with . That is : the yield takes 0.2 off the area of 0.8 under the short rate.
Dividends in dollars
Now say the stock pays a fixed $2 a year in cash, continuously, instead of a yield. Buy one share today with a loan of $100, and use the dividends to pay the loan down. The loan grows with interest and shrinks with the dividends:
This is linear (lesson 6.1.2), with . Its integrating factor is the discount factor itself. So , and integrating from 0 to gives the forward price, the loan left at :
Rule: Forward price with cash dividends
is the present value of the dividends: case study 2.5.5's integral, with in place of . Take it off the stock price, then grow what is left to .
Completing the square in turns into a bell curve, which has no antiderivative formula (lesson 2.5.2), so quad finds the area: . The dividends pay $20 in all, worth $14.9605 today, and $189.2588. Solving the loan equation with solve_ivp gives the same to about .
Try it
The first lines are the dividend yield: the shares to buy today, then the forward price without and with .
Then the cash dividends: quad gives the present value of the stream, and solve_ivp follows the loan to year 10. The last line prints the two forward prices and their gap, rounded.
Try it
The desk notes in numbers: the price with the rates typed in percent, the annual rate for an 8% continuous yield, a negative rate, and the straight-line rate pushed to 30 years.
A curve from a short rate
Write three functions. Each takes rate, a function of time giving the short rate per year as a decimal, and maturities in years.
zero_price(rate, T)returns by solving from withsolve_ivp, usingrtol=1e-10andatol=1e-12. Return 1.0 whenTis 0, and raiseValueErrorwhenTis negative.zero_yield(rate, T)returns the continuous yield . RaiseValueErrorunlessTis positive.forward_rate(rate, T1, T2)returns the forward rate . RaiseValueErrorunless .
The tests use the rising rate, a flat rate, a negative rate and a rate read from a table with np.interp.
See the tests
import math
import numpy as np
def rising(t):
return 0.03 + 0.01 * t
def test_rising_rate_price():
"""Under the short rate 0.03 + 0.01t, 1 paid in 10 years costs 0.449329 today"""
expect_close(zero_price(rising, 10), math.exp(-0.8), what="zero_price(rising, 10)", rel=1e-8)
def test_flat_rate_price():
"""A flat 3% for 5 years gives the price e^(-0.15)"""
expect_close(zero_price(lambda t: 0.03, 5), math.exp(-0.15), what="zero_price(flat 3%, 5)", rel=1e-8)
def test_maturity_now():
"""A bond maturing now is worth 1"""
expect_close(zero_price(rising, 0), 1.0, what="zero_price(rising, 0)")
def test_negative_maturity():
"""A negative maturity raises ValueError"""
try:
zero_price(rising, -1)
except ValueError:
return
raise AssertionError("expected ValueError for T = -1")
def test_negative_rate():
"""A flat -0.5% for 5 years makes the bond cost more than 1: e^0.025"""
expect_close(zero_price(lambda t: -0.005, 5), math.exp(0.025), what="zero_price(flat -0.5%, 5)", rel=1e-8)
def test_yield_is_average_rate():
"""The 10-year yield under the rising rate is its average, 8%"""
expect_close(zero_yield(rising, 10), 0.08, what="zero_yield(rising, 10)", rel=1e-7)
def test_yield_needs_positive_maturity():
"""A yield at maturity 0 raises ValueError"""
try:
zero_yield(rising, 0)
except ValueError:
return
raise AssertionError("expected ValueError for T = 0: the yield divides by T")
def test_forward_rate():
"""The forward rate from 9 to 10 years is 12.5%, the short rate at 9.5"""
expect_close(forward_rate(rising, 9, 10), 0.125, what="forward_rate(rising, 9, 10)", rel=1e-6)
def test_forward_rate_order():
"""A forward rate from 5 back to 2 raises ValueError"""
try:
forward_rate(rising, 5, 2)
except ValueError:
return
raise AssertionError("expected ValueError when T2 is not after T1")
def test_rate_from_a_table():
"""A rate joined by straight lines between quotes gives e^(-area) by the trapezoid rule: 0.575222"""
knots, rates = [0, 1, 3, 5, 10], [0.030, 0.042, 0.055, 0.060, 0.062]
price = zero_price(lambda t: np.interp(t, knots, rates), 10)
expect_close(price, math.exp(-np.trapezoid(rates, knots)), what="zero_price(table rate, 10)", rel=1e-7)
Summary card
- A bank account under a short rate grows by , so .
- With known rates, a zero-coupon bond costs ; as a function of maturity it solves with . For , .
- The continuous yield is the average short rate, ; the instantaneous forward rate is the short rate itself.
solve_ivpprices from alone, forwards from or backwards from the terminal condition ; set tight tolerances.- Forward prices: with a dividend yield ; with cash dividends at a year, whose integrating factor is the discount factor.