The exact slope of a bond price
A bond pays at each time (in years). At the yield , that payment is worth today.
As a function of , this is a power of the inner function , whose slope is 1. The power rule (lesson 2.3.1) with the chain rule (lesson 2.3.3) gives
The sum rule (lesson 2.3.1) adds these over all payments, so is exact, with no bump. Each term is times the payment's present value , times one extra factor .
Rule: Modified duration, exactly
is the Macaulay duration of case study 2.2.4: the payment times, each weighted by its present value. The factor is the chain rule's extra power in .
Try it
SymPy differentiates the price formula exactly and evaluates it at 5%. The second line is the 1 bp bump of case study 2.2.4, for comparison.
The exact slope is about , and the bump is off by about 0.00003. The duration is .
Duration is a balance point
Picture each payment's present value as a weight placed on a timeline at its time. The Macaulay duration is the point where the timeline balances.
For the 5-year 4% bond at 5%, the coupons weigh 3.81, 3.63, 3.46 and 3.29. The last payment weighs 81.49, which is 85% of the price.
Present values of each payment, for 5-year bonds at a 5% yield. Bigger coupons put more weight early, so the balance point (the Macaulay duration) moves from 4.62 to 4.36 years.
Dividing by turns 4.6203 into the modified duration 4.4003, and 4.3570 into 4.1496, the 8% bond's duration in case study 2.2.4. A zero-coupon bond has one weight, at , so and : the clean case of case study 2.2.4, now from the formula.
Try it
The cell lists each payment's present value and its share of the price, then both durations, for the 4% and 8% bonds.
On the desk: conventions and edge inputs
Case study 2.2.4 covered signs, units and DV01. The exact formula adds a convention to get right and some inputs to guard against.
The compounding convention
The factor came from differentiating , so it depends on how the yield compounds. Many government bonds pay coupons twice a year, with the yield compounded twice a year. Then a payment at time is worth , and the chain rule, with inner slope , gives
So the factor becomes . Paid as 2 every half year, the 5-year 4% bond at 5% has and , which a bump confirms. Dividing by 1.05 instead gives 4.3519, off by 0.1.
Edge inputs
- Negative yields have happened on some government bonds. The formula still works while : a 5-year 1% bond at is priced at 107.61, with .
- A yield of or below makes zero or negative. The discount factors then divide by zero or mean nothing, so a function should refuse such a yield.
- Payments that move. The formula, like the bump, assumes the payments do not depend on (case study 2.2.4). For bonds the issuer may repay early, they do.
Try it
The first lines price the twice-a-year bond, compare the two factors, and check with a bump. Then come the negative yield and the yield of .
Python's ZeroDivisionError is what an unchecked price function raises at .
Duration is still a slope. For moves of 1% or more, the bend of the price curve matters: that is convexity, in case study 2.4.5.
With continuous discounting, duration is an average time
Discount continuously instead (case study 1.4.6): each payment is worth . Lesson 2.3.4 gives , so
There is no to divide by: the duration is the present-value-weighted average time. It is also , by the log-of-a-function rule of lesson 2.3.4: duration is the relative rate at which the price falls.
Try it
At a continuous yield of 5% the bond is worth about 95.14. The slope over the price, the weighted average time, and the slope of all give 4.6191 years.
Elasticity
How much does demand respond to the price ? The slope depends on units: quote prices in cents instead of dollars and it shrinks a hundredfold. The elasticity compares percentage changes instead, so it has no units.
Rule: Elasticity
The percentage change in quantity per 1% change in price. For ordinary goods it is negative; many reports quote its size instead, so check the sign convention.
Constant elasticity. For , the power rule gives . Then at every price: any has elasticity .
Straight-line demand. For , changes along the line. It is 0 at , at , at and at . At demand is 0 and is undefined; just below it, falls without limit ( at ).
The same demand with prices in cents is . Its slope is per cent, not per dollar, but at cents the elasticity is still .
Revenue and the product rule
Revenue is price times quantity, . The product rule (lesson 2.3.2) gives its slope, and the elasticity appears inside it.
Rule: Revenue and elasticity
If (inelastic demand), revenue rises with the price. At it is flat, at its peak. If (elastic demand), a higher price loses revenue.
For , the slope of revenue is . At , and , so the slope is . At it is .
It is 0 at , where revenue peaks at 3125.
Revenue for the demand . Where demand is inelastic, , revenue climbs with the price; it peaks where , at ; beyond, demand is elastic and revenue falls.
Elasticity is a slope
Like duration, elasticity describes small changes. For , a 2% price rise cuts the quantity by about ; the curve gives 2.93%.
For a 20% rise the estimate is 30%, but the curve gives only 23.93%. For big moves, use the demand curve itself.
Try it
SymPy computes both elasticities, checks that the revenue slope equals , and repeats the elasticity in cents.
The last lines compare the elasticity estimate with the curve for price rises of 2% and 20%.
Duration from the formula
Write exact_duration(coupon, face, years, y, freq=1), which returns the modified duration from the exact formula: no bumping. The bond pays the yearly coupon in freq equal parts, every of a year, and face at the end. The yield y is a decimal, compounded freq times a year.
- Payment (for ,
years * freq) comes at time years and is worth its amount divided by . - Return the Macaulay duration divided by .
- Raise
ValueErrorwhen .
The tests compare it with bump estimates and known values, try a negative yield, and check the yearly and twice-a-year conventions.
See the tests
def _price(c, F, T, y, freq=1):
return sum((c / freq) / (1 + y / freq) ** k for k in range(1, T * freq + 1)) + F / (1 + y / freq) ** (T * freq)
def _bump(c, F, T, y, freq=1, h=1e-6):
return -(_price(c, F, T, y + h, freq) - _price(c, F, T, y - h, freq)) / (2 * h) / _price(c, F, T, y, freq)
def test_five_year_bond():
"""The 5-year 4% bond at 5% has duration about 4.40031"""
expect_close(exact_duration(4, 100, 5, 0.05), 4.400306998058356, what="exact_duration(4, 100, 5, 0.05)", rel=1e-9)
def test_matches_bump():
"""It agrees with a bump estimate for a 10-year 6% bond at 3%"""
expect_close(exact_duration(6, 100, 10, 0.03), _bump(6, 100, 10, 0.03), what="exact vs bump", rel=1e-6)
def test_zero_coupon():
"""A 7-year zero-coupon bond at 4% has duration 7 / 1.04"""
expect_close(exact_duration(0, 100, 7, 0.04), 7 / 1.04, what="exact_duration(0, 100, 7, 0.04)")
def test_one_year():
"""A 1-year bond has duration 1 / (1 + y)"""
expect_close(exact_duration(5, 100, 1, 0.05), 1 / 1.05, what="exact_duration(5, 100, 1, 0.05)")
def test_semiannual():
"""With freq=2, the 5-year 4% bond at 5% has duration about 4.45806, not 4.35191"""
expect_close(exact_duration(4, 100, 5, 0.05, freq=2), 4.458056324352842, what="exact_duration(4, 100, 5, 0.05, freq=2)", rel=1e-9)
def test_semiannual_matches_bump():
"""With freq=2, it agrees with a bump estimate for a 30-year 5% bond at 4.5%"""
expect_close(exact_duration(5, 100, 30, 0.045, freq=2), _bump(5, 100, 30, 0.045, freq=2), what="exact vs bump, freq=2", rel=1e-6)
def test_negative_yield():
"""A negative yield is allowed: a 5-year 1% bond at -0.5% has duration about 4.93079"""
expect_close(exact_duration(1, 100, 5, -0.005), 4.9307916502902485, what="exact_duration(1, 100, 5, -0.005)", rel=1e-9)
def test_rejects_impossible_yield():
"""A yield of -100% or below has no discount factors: raise ValueError"""
for y in (-1, -1.5):
try:
exact_duration(4, 100, 5, y)
except ValueError:
continue
raise AssertionError(f"expected ValueError for y = {y}")
Summary card
- The chain rule gives each discounted payment's slope, ; the sum rule gives the exact .
- Modified duration , where is the balance point of the payments' present values in time.
- The factor depends on compounding: for twice a year, none for continuous discounting, where is the average time.
- Elasticity has no units; has constant elasticity .
- Revenue: . Revenue rises with price when , peaks at and falls when .