A bond's yield from its price¶
The annual yield at which the present value is the price, by bisection, deterministic to the last bit (the price falls as the yield rises, so the root is bracketed).
- Requires
- the
Priceand the bond's terms. Provides: theYield. - Source
- Fabozzi, ch. 3 (yield to maturity).
- Proof
- invariant, limit.
No Trait form: not now: a per-item bisection, which could be a kernel in a `repeat until stable` stage if a game ever needs it on the GPU each step, Bourse pricing its bonds live; until then a tool.
finance.bond_yield(price, face, coupon, years, freq, lo, hi)Uses, without proving it here: finance.bond_price.
In [1]:
Copied!
import os, sys
root = os.getcwd()
while not os.path.isfile(os.path.join(root, 'docsite.json')):
root = os.path.dirname(root)
sys.path.insert(0, os.path.join(root, 'tools'))
import numpy as np
import matplotlib.pyplot as plt
from lusine import dmath as D, library
from lusine.kernels.finance.bond_price import bond_price
from lusine.kernels.finance.bond_yield import bond_yield
import os, sys
root = os.getcwd()
while not os.path.isfile(os.path.join(root, 'docsite.json')):
root = os.path.dirname(root)
sys.path.insert(0, os.path.join(root, 'tools'))
import numpy as np
import matplotlib.pyplot as plt
from lusine import dmath as D, library
from lusine.kernels.finance.bond_price import bond_price
from lusine.kernels.finance.bond_yield import bond_yield
The reference¶
The kernel's reference, in numpy: the body under the declaration above, and what every section below runs. It stays as the kernel's test beside its Trait form.
In [2]:
Copied!
library.reference(bond_yield)
library.reference(bond_yield)
Out[2]:
the reference, bond_yield.py
def bond_yield(price, face, coupon, years, freq=2, lo=-0.5, hi=1.0):
"""YIELD FROM PRICE: the annual yield at which the present value is the
price, by bisection, deterministic to the last bit (the price falls as
the yield rises, so the root is bracketed).
Requires: the `Price` and the bond's terms. Provides: the `Yield`.
Source: Fabozzi, ch. 3 (yield to maturity).
Proof: invariant, limit."""
p = float(price)
a, b = float(lo), float(hi)
if not float(bond_price(face, coupon, years, b, freq)) <= p <= float(bond_price(face, coupon, years, a, freq)):
raise ValueError(f"bond_yield: a price of {p} is outside the yields {lo}..{hi}")
for _ in range(80):
m = 0.5 * (a + b)
if float(bond_price(face, coupon, years, m, freq)) > p:
a = m
else:
b = m
return 0.5 * (a + b)
Invariant: the price of the yield is the price¶
Priced at the yield it finds, a bond has the price it was given.
In [3]:
Copied!
face, coupon = 100.0, 0.05
for years in (2, 10, 30):
for price in (80.0, 95.0, 100.0, 112.5):
y = float(bond_yield(price, face, coupon, years))
assert abs(float(bond_price(face, coupon, years, y)) - price) < 1e-7
print('round trips')
face, coupon = 100.0, 0.05
for years in (2, 10, 30):
for price in (80.0, 95.0, 100.0, 112.5):
y = float(bond_yield(price, face, coupon, years))
assert abs(float(bond_price(face, coupon, years, y)) - price) < 1e-7
print('round trips')
round trips
Limit: at par the yield is the coupon¶
A bond priced at its face yields its coupon, and a dearer bond yields less.
In [4]:
Copied!
face, coupon = 100.0, 0.05
assert abs(float(bond_yield(face, face, coupon, 10)) - coupon) < 1e-9
assert float(bond_yield(105.0, face, coupon, 10)) < coupon < float(bond_yield(95.0, face, coupon, 10))
print('par yields the coupon')
face, coupon = 100.0, 0.05
assert abs(float(bond_yield(face, face, coupon, 10)) - coupon) < 1e-9
assert float(bond_yield(105.0, face, coupon, 10)) < coupon < float(bond_yield(95.0, face, coupon, 10))
print('par yields the coupon')
par yields the coupon