Saturday, October 20

Treasury Yield Curve Bootstrapping

Link: IPython notebook

In the previous post, we have introduced readers to basic principles of time value of money and presented Python implementation of the calculator. The time value of money is an essential principle applied in almost all areas of the financial mathematics. Today, we will discuss one of them - the basics of yield curve construction and bootstrapping. Please note that full implementation of this example can be found here.

Calculation of Yield Curve from Market Prices

When calculating yield curves from  market prices, the big question is which securities we should consider in the calculation. If we generalize the problem to a set of credit risk-free government securities, our choices will be as follows:
  • All on-the-run securities
  • All on-the-run and some off-the-run securities (to fill the gaps)
  • All securities (both on and off-the run, with aggregated YTMs)
On-the-run securities are those which have been issued recently, thus the most liquid ones. Unfortunately, there are often wide maturity gaps between them. Because of this, analysts often fill these gaps with additional off-the-run issues. For example, a 50-year bond issued 2 years ago effectively serves as a security filling the maturity gap of 48 years.

It is even possible to combine all on and off-the run securities in such a way they will expire on the same date. This will naturally lead to collisions in yields and maturities, which can be then aggregated and interpolated for even better results.

Another solution is to use treasury coupon strips. The coupon strip with a single payment is priced in a way which yields directly to the spot rate for given maturity. With this approach, the bootstrapping process discussed later will not be necessary. However, the drawback using coupon strips is different tax treatment skewing the whole calculation.

With respect to the facts above and for the purpose of this simple example, we will be using on-the-run market prices of UK Gilts, as of 19th of September 2012. The quoted gilts span maturities from 6 months to 48 years, sufficient enough for the sake of this analysis. The relevant prices were obtained from www.bondscape.net and look as follows:

epic, description,          coupon, maturity,  bid,    ask
TR13, Uk Gilt Treasury Stk, 4.5,    07-Mar-13, 101.92, 102.07
T813, Uk Gilt Treasury Stk, 8,      27-Sep-13, 107.86, 107.98
TR14, Uk Gilt Treasury Stk, 2.25,   07-Mar-14, 102.90, 103.05

Calculated yields to maturity don't necessarily correspond to those quoted in data file due to accrued interest and a fact that coupon payments are bound to specific calendar date, which is not necessarily one semi-annual period from now.

The code performing the calculation in Python would look as follows. The input is a set of bonds, each with given maturity, price and coupon rate. These values are passed into the TVM calculator introduced in one of the previous articles to calculate the bond's yield to maturity:

tr = [] # list of raw (not interpolated) times to maturity
yr = [] # list of raw (not interpolated) yields
for b in bonds:
    ttm = (b.maturity - localtime).days / 360
    price = (b.bid+b.ask)/2
    ytm = TVM(n=ttm*b.freq, pv=-price, pmt=b.couponRate/b.freq, fv=1).calc_r() * b.freq
    tr.append(ttm)
    yr.append(ytm)

These yields, together with maturities are then aggregated in lists tr (time to maturity) and yr (yield to maturity)

TTM     YTM
0.47    0.23%
1.04    0.34%
1.48    0.24%
1.99    0.30%
...     ...    

Interpolation

As the time passes, the maturities of all issues are shortened by one day each day. However, if we want to compare the same yield curve across different dates, the maturities must match. Hence, we need to interpolate and normalize all yield curves into a common set of maturities, e.g. 1m, 3m, 6m, 1y, 2y, 5y and etc.

On the other hand, for the purpose of bootstrapping, it would be convenient to interpolate all maturities into a  scale with annual intervals, such as from 1 year to 40 years. Therefore, we will go this way.

The interpolation is performed in Python automatically using SciPy's interp1d command. Apart of the linear interpolation, it provides few others, such as nearest, cubic or quadratic. Unfortunately this algorithm is not able to interpolate outside the initial range - this could be problematic especially for short-term maturities, such as 1 month:

t = list(for i in range(1,41)) # interpolating in range 1..40 years
y = []
interp = scipy.interpolate.interp1d(tr, yr, bounds_error=False, fill_value=scipy.nan)
for i in t:
    value = float(interp(i))
    if not scipy.isnan(value)# Don't include out-of-range values
        y.append(value)

The output of this step would be interpolated yield curve y with maturities t=1..40

TTM     YTM (Interpolated)
1.00    0.33%
2.00    0.30%
3.00    0.40%
4.00    0.60%
...     ...

Bootstrapping of spot rates

Before going into details regarding the bootstrapping algorithm, we should explain the difference between yield curve and spot rate curve.

By definition, the yield curve shows several bond yields to maturity (ytm) across different bond contract lengths, or times to maturity (ttm). Yield to maturity is an overall discount rate which equalizes principal and coupon payments to the initial investment value, assuming reinvestability of all cash flows.

In contrast to the yield curve, a spot rate curve represents spot rates used to discount individual cash flows of the bond. Hence, a whole range of different spot rates is typically used when equalizing bond's future cash flows to its present value.

To bootstrap the yield curve, we will be building upon a fact that all bonds priced at par have coupon rate equal to the yield-to-maturity, as denoted in the following equation:

$$ \frac{C}{\left ( 1+r \right )^1} + \frac{C}{\left ( 1+r \right )^2}+...+\frac{1+C}{\left ( 1+r \right )^n} = $100 $$
Given the par value is $100, coupon rate \(C\) is equal to \($100*r\)

Starting from the annual coupon bond which matures in one year, we will gradually derive all spot rates by forward substitution of the previously calculated ones. This can be best illustrated on a numerical example. To make the example more obvious, we will use exaggerated yields as opposed to the real yield curve calculated in previous step:


TTM     YTM (Interpolated)
1.00    3.00%
2.00    5.00%
3.00    7.00%
...     ...

Starting from the bond which pays both annual coupon and principal in one year, the yield-to-maturity is apparently the same as the first and single spot rate \(y_1 = s_1 = 3\%\). Hence, the following equation holds:
$$ \frac{$100+$3}{\left ( 1+0.03 \right )^1} = $100 $$
In the next step, we will put into the equation a bond maturing in two years. In this case, yield to maturity will be y_2 = 5\%. We will also employ the spot rate \(s_1=3\%\) "calculated" in the previous step. Please bear in mind that since this bond is also priced at par, the coupon rate will be this time 5%, or $5:
$$ \frac{$5}{\left ( 1+0.03 \right )^1} + \frac{$100+$5}{\left ( 1+s_2 \right )^1} = $100 $$
Solving this equation for the second spot rate will lead to the root \(s_2 = 5.0510\%\).

Similarly, solving all other rates iteratively in the same manner will lead to the following spot rate curve:

TTM     Spot Rate 
1.00    3.0000%
2.00    5.0510%
3.00    7.1979%
...     ...

The Python code implementing the calculation described above would look as follows. The spot rates will be populated in output list s, based on the same time scale t as the original YTM rates y

s = [] # output array for spot rates
for i in range(0len(t))#calculate i-th spot rate 
    sum = 0
    for j in range(0, i)#by iterating through 0..i
        sum += y[i] / (1 + s[j])**t[j]
    value = ((1+y[i]) / (1-sum))**(1/t[i]) - 1
    s.append(value)


Final result

We are of course interested mostly in final results based on the real market data. Previously in this article, we  gathered and interpolated on-the-run market prices of UK Gilts, as of 19th of September 2012. Based on these prices, the original and interpolated yield curves and spot rates curve would look as follows:

Please note that in this particular example, there is not a big difference between yield curve and spot rate curve, especially for short term maturities. This is due to low-yield nature of government bonds. If the coupon rates were bigger, the difference between spot and YTM rates would be more evident as well.