Skip to content
All library documents

Bond Convexity: Yield Scaling, Exact Formulas, and Market Conventions

Article Quant Q&A · Author: aalberti333

Summary

The document examines why a finite-difference bond convexity calculation can differ substantially from a market quote. Its central explanation is that convexity depends on how yield is expressed: using yield in percentage points versus decimal form changes the scale of the reported measure. The response notes that the common market convention cited in the discussion uses percentage yields, and recommends adjusting the computed scale accordingly.

The answer also distinguishes a numerical approximation from the exact yield-based convexity obtained by differentiating the bond pricing formula twice. It cautions that matching market values requires more than choosing a formula: day-count rules and other bond pricing conventions affect the result. The example implementation uses simplified date and yield assumptions, so its output need not match a professional quote. The discussion suggests a well-tested fixed-income library when precise convention handling matters, but does not provide a full implementation or evaluate the user's code line by line.

Key ideas

  • Convexity scale depends on whether yield is measured in percentage points or decimals.
  • A finite-difference estimate approximates the second derivative of price with respect to yield.
  • Vanilla bond convexity can be derived exactly from the bond price formula.
  • Day-count and other market conventions can create differences from quoted values.
  • A simplified calculation may be inadequate when precise market convention handling is required.

Tags

Full text
# Convexity for historical bond data


# Convexity for historical bond data












I'm trying to write a program to calculate the convexity of a bond. The bigger idea is, that if I have access to the actual price for each point in time, I should be able to calculate various features of the bond given that date. The convexity program is written as follows:

```
""" Calculate convexity of a bond """
    def bond_convexity(price, par, T, coup, freq, dy=0.0001):
        ytm = bond_ytm(price, par, T, coup, freq)

        ytm_minus = ytm - dy
        price_minus = bond_price(par, T, ytm_minus, coup, freq)

        ytm_plus = ytm + dy
        price_plus = bond_price(par, T, ytm_plus, coup, freq)

        convexity = (price_minus+price_plus-2*price)/(price*dy**2)
        return convexity
```

Where the yield to maturity and bond price functions are as follows:

```
"""Interpolate yield to maturity"""
def bond_ytm(price, par, coupdates, coup, freq=2, guess=0.05):
     freq = float(freq)
     MAT = coupdates[len(coupdates) - 1] #maturity date
     timedif = list() #init
     periods = list() #init
     for i in range(0,len(coupdates)-1):
         timedif.append( round((MAT - coupdates[i]).days/365,1) )     #collection of time to maturity - coupon date
         periods.append(timedif[i]*freq)
     coupon = coup/100.*par/freq
     dt = [(x)/freq for x in periods]
     ytm_func = ytm_func = lambda y : \
         sum([coupon/(1+y/freq)**(freq*t) for t in dt]) + par/(1+y/freq)**\
(freq*max(dt)) - price

 return optimize.newton(ytm_func, guess)

 """Price bond"""
 def bond_price(par, coupdates, ytm, coup, freq=2):
      freq = float(freq)
      MAT = coupdates[len(coupdates) - 1] #maturity date
      timedif = list() #init
      periods = list() #init
      for i in range(0,len(coupdates)-1):
          timedif.append( round((MAT - coupdates[i]).days/365,1) ) #collection of time to maturity - coupon date
          periods.append(timedif[i]*freq)
      coupon = coup/100.*par/freq
      dt = [(x)/freq for x in periods]
      price = sum([coupon/(1+ytm/freq)**(freq*t) for t in dt]) + par/(1+ytm/freq)**(freq*max(dt))
 return price
```

Now, the following data shows the upcoming coupon dates for the bond and today's date at the top. I've saved this variable as cdd:

```
cdd
Out[28]: 
[datetime.datetime(2017, 10, 31, 15, 20, 0, 480212),
 datetime.datetime(2018, 2, 1, 0, 0),
 datetime.datetime(2018, 8, 1, 0, 0),
 datetime.datetime(2019, 2, 1, 0, 0),
 datetime.datetime(2019, 8, 1, 0, 0),
 datetime.datetime(2020, 2, 1, 0, 0),
 datetime.datetime(2020, 8, 1, 0, 0),
 datetime.datetime(2021, 2, 1, 0, 0),
 datetime.datetime(2021, 8, 1, 0, 0),
 datetime.datetime(2022, 2, 1, 0, 0),
 datetime.datetime(2022, 8, 1, 0, 0),
 datetime.datetime(2023, 2, 1, 0, 0),
 datetime.datetime(2023, 8, 1, 0, 0),
 datetime.datetime(2024, 2, 1, 0, 0),
 datetime.datetime(2024, 8, 1, 0, 0),
 datetime.datetime(2025, 2, 1, 0, 0),
 datetime.datetime(2025, 8, 1, 0, 0),
 datetime.datetime(2026, 2, 1, 0, 0),
 datetime.datetime(2026, 8, 1, 0, 0),
 datetime.datetime(2027, 2, 1, 0, 0),
 datetime.datetime(2027, 8, 1, 0, 0),
 datetime.datetime(2028, 2, 1, 0, 0),
 datetime.datetime(2028, 8, 1, 0, 0),
 datetime.datetime(2029, 2, 1, 0, 0),
 datetime.datetime(2029, 8, 1, 0, 0),
 datetime.datetime(2030, 2, 1, 0, 0),
 datetime.datetime(2030, 8, 1, 0, 0),
 datetime.datetime(2031, 2, 1, 0, 0),
 datetime.datetime(2031, 8, 1, 0, 0),
 datetime.datetime(2032, 2, 1, 0, 0),
 datetime.datetime(2032, 8, 1, 0, 0),
 datetime.datetime(2033, 2, 1, 0, 0),
 datetime.datetime(2033, 8, 1, 0, 0),
 datetime.datetime(2034, 2, 1, 0, 0),
 datetime.datetime(2034, 8, 1, 0, 0),
 datetime.datetime(2035, 2, 1, 0, 0),
 datetime.datetime(2035, 8, 1, 0, 0),
 datetime.datetime(2036, 2, 1, 0, 0),
 datetime.datetime(2036, 8, 1, 0, 0),
 datetime.datetime(2037, 2, 1, 0, 0),
 datetime.datetime(2037, 8, 1, 0, 0),
 datetime.datetime(2038, 2, 1, 0, 0),
 datetime.datetime(2038, 8, 1, 0, 0),
 datetime.datetime(2039, 2, 1, 0, 0),
 datetime.datetime(2039, 8, 1, 0, 0),
 datetime.datetime(2040, 2, 1, 0, 0),
 datetime.datetime(2040, 8, 1, 0, 0),
 datetime.datetime(2041, 2, 1, 0, 0),
 datetime.datetime(2041, 8, 1, 0, 0),
 datetime.datetime(2042, 2, 1, 0, 0),
 datetime.datetime(2042, 8, 1, 0, 0),
 datetime.datetime(2043, 2, 1, 0, 0),
 datetime.datetime(2043, 8, 1, 0, 0),
 datetime.datetime(2044, 2, 1, 0, 0),
 datetime.datetime(2044, 8, 1, 0, 0),
 datetime.datetime(2045, 2, 1, 0, 0),
 datetime.datetime(2045, 8, 1, 0, 0)]
```

However, if I run the program to calculate convexity for a bond priced at 112.057, par = 100, dates as cdd, coupon of 4.9, and semi annual frequency, I get the following solution:

```
bond_convexity(112.057, 100, cdd, 4.9, 2)
Out[32]: 351.98162487756656
```

Bloomberg is showing that the convexity should be approximately 3.467. What could I possibly be doing wrong? Any help is appreciated.

## Answer by Helin (score 1, accepted)

https://quant.stackexchange.com/a/36683

A couple of thoughts:

- There are different ways to express convexity, depending how you express yields. An example might help: If yields are expressed in percent (e.g., 5 for 5%), then the convexity of a zero coupon bond with a duration of 15 is roughly $15\times15/100 = 2.25$. Alternatively, if you expressed yields in decimals (0.05 for 5%), then the corresponding convexity is expressed as $15\times 15 = 225$. The first approach is more common and is used by BBG. All you have to do is to divide your number by 100 to be consistent.

- You are using an approximation formula for convexity, but yield-based convexity for vanilla bonds has an exact closed-form solution (simply take the second derivative of the price/yield formula relative to yield).

- Your implementation does not conform to market conventions (stuff like Actual/Actual day count). There are a lot of subtleties involved in bond pricing and I recommend using a professional implemented solution if precision is important. QuantLib, for example, has a very easy to use Python binding that you can use.

Shown in full with attribution under the source's licence. Licence: CC BY-SA 4.0 (Stack Exchange)

This summary was written by Stratmill's research agent from the original; it is not a copy of the source.