Price as a function of yield
A bond pays a coupon at the end of each year and its face value after years. At the yield (a decimal: 5% is 0.05), its price is the present value of those payments (case study 1.5.5):
A higher yield divides every payment by more, so the price falls as the yield rises. Its derivative , written , is negative: the graph slopes down (lesson 2.2.2).
The price of the 5-year 4% bond against its yield, with the tangent at 5%. The tangent's error is tiny for small moves, grows about four times when the move doubles, and is positive both ways.
The tangent at 5% is the duration picture: its slope is . The rest of this case study measures that slope, puts it in traders' units, and tests how far the tangent holds.
Bumping the yield
The formula is a sum of powers of . Differentiating it exactly needs the rules of module 2.3. Instead, do what lesson 2.2.2 did with a function you can only evaluate: move the yield a little each way and reprice.
Rule: Price sensitivity by bumping
The central difference of lesson 2.2.2. Reprice at a slightly higher and a slightly lower yield, and divide by the total move, . A bump of one basis point is .
Try it
A 1 bp rise moves the price by ; a 1 bp fall moves it by . The two are not mirror images, because the curve bends.
So the forward difference gives , the backward one and the central one . SymPy's exact derivative is : the central difference is off by 0.00003, the one-sided ones by about 0.12.
How big a bump?
Lesson 2.2.2 showed that a finite difference fails if is too big or too small. The same holds here.
Try it
A 100 bp bump () is off by 0.26: the curve bends too much over that range. One basis point is off by 0.00003, and by about .
At rounding takes over and the error grows to 0.0044. One basis point is accurate enough, and it is a move markets really make.
Units: per unit, per 1%, per basis point
The slope is in price per unit of yield. One unit is a move of 1 in : 100 percentage points. No market moves that far, so the slope is rescaled to a realistic move.
| yield move | 1 (100%) | 0.01 (1%) | 0.0001 (1 bp) |
|---|---|---|---|
| price change |
The last column has a name. The DV01 (dollar value of an 01, a basis point) is the price fall for a 1 bp rise in yield. It is quoted as a positive number.
Rule: DV01
For the bond, 0.0421 per $100 of face value. A position of $10 million face loses about $4,209.80 for each basis point that yields rise.
A slope of , a change of per 1% and a DV01 of 0.0421 describe the same bond. Always check which unit a number is in before comparing two of them.
Try it
The central-difference slope in the three units, then the DV01 per $100 and on $10 million of face value.
Duration
A slope in dollars grows with the size of the position. Dividing by the price turns it into a percentage, which compares bonds of any size.
Rule: Modified duration
The fall in price, as a fraction of the price, per unit rise in yield. With , a 1% rise in yield () cuts the price by about .
For the 5-year 4% bond, . Other bonds at the same 5% yield:
| bond | price | |
|---|---|---|
| 5-year, 8% coupon | 112.99 | 4.1496 |
| 5-year, 4% coupon | 95.67 | 4.4003 |
| 5-year, zero coupon | 78.35 | 4.7619 |
| 10-year, 4% coupon | 92.28 | 7.9615 |
| 10-year, zero coupon | 61.39 | 9.5238 |
| 30-year, 4% coupon | 84.63 | 16.0942 |
Longer bonds are more sensitive. For the same maturity, a lower coupon means a higher duration, because more of the money arrives at the end. Duration also moves with the yield: the 5-year 4% bond has 4.5042 at 3% and 4.2997 at 7%.
Try it
duration divides the bumped slope by the price. The loop reproduces the table.
The zero-coupon bond: the clean case
A zero-coupon bond pays only its face value, at time : . Its duration is exactly : for 5 years at 5%, , as the bump found.
So for a zero, duration is its maturity divided by . A coupon bond is a bundle of payments at different times. Weight each payment's time by its share of the price and average: that is the Macaulay duration, .
For the 5-year 4% bond, 85% of the value arrives at year 5 and the rest earlier, so the average time is 4.62 years. Dividing by gives 4.40, the bumped duration. For yearly payments, ; case study 2.3.7 shows why, with the differentiation rules.
Try it
The present value of each payment, its share of the price, and the weighted average time.
The last line compares the Macaulay duration over with the zero's .
How far does the tangent go?
The derivative gives the tangent line of lesson 2.2.1: , where (delta y) is the change in yield. Here it is against the true price, both ways.
| yield move | true price | tangent | true − tangent |
|---|---|---|---|
| 104.5797 | 104.0901 | 0.48959 | |
| 100.0000 | 99.8803 | 0.11968 | |
| bp | 95.7126 | 95.7126 | 0.00001 |
| bp | 95.6284 | 95.6284 | 0.00001 |
| 91.5753 | 91.4607 | 0.11455 | |
| 87.6994 | 87.2509 | 0.44848 |
For a 1 bp move the tangent is off by only 0.00001. For 1% it is off by about 0.12, and for 2% by about 0.45 to 0.49. Doubling the move roughly quadruples the error, like the gap in lesson 2.2.1.
The true price is above the tangent in every row. So duration overstates the loss when yields rise, and understates the gain when they fall. That bend is convexity, the subject of case study 2.4.5.
The row is a yield of 4%, equal to the coupon rate. There the price is exactly 100: a bond whose yield equals its coupon rate is worth its face value.
Try it
The table above, from the central-difference slope. The gap prints with five decimals, so 1 bp shows as 0.00001.
Price sensitivity, DV01 and duration
Write three functions for a bond with yearly coupons. Yields are decimals.
price_slope(coupon, face, years, y, h=1e-4)returns the central-difference estimate of .dv01(coupon, face, years, y)returns the price fall for a 1 bp rise, , as a positive number.modified_duration(coupon, face, years, y)returns .
Write your own price function inside, or copy the one from the cells above.
See the tests
def test_slope():
"""The 5-year 4% bond at 5% has dP/dy of about −420.98"""
expect_close(price_slope(4, 100, 5, 0.05), -420.97969897625376, what="price_slope(4, 100, 5, 0.05)", rel=1e-6)
def test_slope_is_negative():
"""Prices fall as yields rise, so the slope is negative"""
s = price_slope(6, 100, 10, 0.03)
assert s < 0, f"expected a negative slope, got {s}"
def test_dv01():
"""Its DV01 is about 0.0421 per 100 of face value, a positive number"""
expect_close(dv01(4, 100, 5, 0.05), 0.042097969897625376, what="dv01(4, 100, 5, 0.05)", rel=1e-6)
def test_dv01_scales_with_face():
"""On 10 million of face value (coupon 400,000 a year) the DV01 is about 4,209.80"""
expect_close(dv01(400_000, 10_000_000, 5, 0.05), 4209.796989762537, what="dv01(400000, 10000000, 5, 0.05)", rel=1e-6)
def test_duration():
"""Its modified duration is about 4.40"""
expect_close(modified_duration(4, 100, 5, 0.05), 4.4003072662927565, what="modified_duration(4, 100, 5, 0.05)", rel=1e-6)
def test_zero_coupon():
"""A 5-year zero-coupon bond at 5% has duration 5 / 1.05"""
expect_close(modified_duration(0, 100, 5, 0.05), 5 / 1.05, what="modified_duration(0, 100, 5, 0.05)", rel=1e-6)
def test_zero_yield():
"""At a yield of exactly 0 the 5-year 4% bond has duration about 4.6667 (no division by zero)"""
expect_close(modified_duration(4, 100, 5, 0.0), 4.666666981666623, what="modified_duration(4, 100, 5, 0.0)", rel=1e-6)
def test_longer_is_riskier():
"""A 10-year bond has a higher duration than a 5-year one"""
d5, d10 = modified_duration(4, 100, 5, 0.05), modified_duration(4, 100, 10, 0.05)
assert d10 > d5, f"expected the 10-year duration ({d10:.3f}) above the 5-year ({d5:.3f})"
Summary card
- A bond's price falls as its yield rises; the derivative measures how fast. It is the slope of the tangent to the price curve.
- Estimate it by bumping: with of one basis point. A much bigger bump suffers from the bend, a much smaller one from rounding.
- Units: per unit of yield, per 1% (× 0.01) or per basis point (× 0.0001). DV01 , quoted positive.
- Modified duration : the percentage price change per unit change in yield. Longer and lower-coupon bonds have higher duration; a zero has .
- The tangent is excellent for small moves and always below the true price; the gap for large moves is convexity.