Tuesday December 11th 2018

Modeling a Treasury Bond Fund in R

Chicago Board of Trade. Chicago, Sept-2013.
Photo by Standard Travel Fotos

I’ve written a lot about investing in Treasury bonds and CDs. These are examples of Type 0 investments that, when held to maturity, pay a certain amount on a certain date. With Type 0 investments, to calculate the exact ending balance1, we can use the compound interest formula that calculates Future Value PV from Present Value PV, interest rate i and number of periods n.

FV = PV * (1+i)^{n}

But many people invest in Treasury bonds through bond funds, often just for their convenience and liquidity. Bond funds are Type 1 investments, meaning that you are not guaranteed a certain amount on a certain date. Instead, there is a distribution of outcomes that depends on what happens in the future, specifically what future yield curves will be.2. With bond funds, the simple FV formula can not be used to determine the exact outcome, because we don’t know the bond fund’s future rate of return.

Here’s a statement of the problem: Given some future Treasury yield curves, what will be the return of a Treasury bond fund over some holding period?

Intermediate-Term Treasury Bond Fund
There are numerous Treasury Bond Funds offered by fund companies. Most fund companies offer a Short Term ST, Intermediate Term IT, and Long-Term LT Treasury. Since many investors use an IT Treasury bond fund, we’ll look at modeling the Vanguard Intermediate-Term Treasury Fund Investor Shares (VFITX).

If you visit the link to Vanguard’s fund, there is a wealth of information there. Go to the Portfolio and Management tab, and look at funds characteristics and holdings.

As of 8/31/2014, the average maturity was 5.6 years and the average Yield-to-Maturity YTM was 1.7%. VFITX held 74 bonds and 99.5% were U.S. Government bonds. The table shows how many bonds were of each maturity.

Distribution by effective maturity (% of fund) as of 08/31/2014
Under 1 Year 0.2%
1 - 3 Years 0.6%
3 - 5 Years 46.1%
5 - 7 Years 30.4%
7 - 10 Years 22.6%
10 - 20 Years 0.1%
20 - 30 Years 0.0%
Over 30 Years 0.0%
Total 100.0%

There were essentially no bonds with maturity greater than 10 years nor less than 3 years. The lack of bonds less than 3 years implies that, at the latest, bonds are sold at 3 years prior to maturity.

Bond Fund Models
We want to model this bond fund to see how changes in the yield curve effect the outcome of an investment in the fund. We could just use the portfolio of 74 bond holdings and run bond calculations on the entire portfolio for various yield curve scenarios. Maybe the bond fund managers might do that, but it’s way too complicated for us.

Since all of the bond are from 3 to 10 years, one way would be to model it as 8 individual bonds, with  terms of 3 to 10 years. We start out by purchasing, at par3, 8 bonds with maturities 3, 4, 5, 6, 7, 8, 9, 10 years, with coupon equal to the current YTM. During the year, we collect interest. After one year, we have 8 bonds with 2-9 year maturities. We sell all the bonds at the market price and calculate our total return from interest and price changes. Do this every year.

But I would prefer a simpler model that’s easier to understand. A simple model that was proposed by user ogd is to model a bond fund as a single bond that is rolled over each year. At the beginning of the first year, we purchase a bond at par with coupon = YTM. Collect interest during the year, and sell at the end of the year. Calculate Total Return = Capital Return + Income Return. Repeat every year.

Bond Price Formula
I decided to create the bond fund model in R. Since R doesn’t have a PRICE or PV function like Excel, the first thing is to write a function to return a bond price. From Wikipedia Bond Valuation, the formula for bond price is:

 P = \sum\limits_{i=1}^N \frac{C}{(1+i)^n} + \frac{M}{(1+i)^n}

The summation term is the present value of all the coupon payments. The second term is the present value of the principal payment at the maturity. This formula is an approximation which uses a single discount rate for all the payments, but that’s fine for now. Here’s a simple R function to return a bond price:

getbondprice <- function(Y, N, C, M) {
# Returns bond price as sum of PV of all payments.
# Y = Discount Rate, N = number of periods, C = coupon payment, M = Principle
# Approximation using a single discount rate for all payments.
PV.C <- 0
for (i in 1:N) {
PV.C <- PV.C + (C / (1 + Y)^i) # PV of coupon payments
PV.M <- M / (1 + Y)^N # PV of principle payment
PV <- PV.C + PV.M

This bond pricing model also assumes a single coupon payment per year. Treasury bonds actually pay interest every six months, but we’ll keep it simple for now. I tested getbondprice() against Excel’s PV function and it it gives about the same answer:

# Excel: 992.38 =-PV(2%, 4, 18, 1000, 0)
> bond.price <- getbondprice(Y=0.02, N=4, C=18, M=1000)
> bond.price
[1] 992.4
# Excel: 1039.42 =-PV(4%, 4, 37.60, 1056.29, 0)
> bond.price <- getbondprice(Y=0.04, N=4, C=37.60, M=1056.29)
> bond.price
[1] 1039

The next step is to write the R code to calculate the bond fund model.

Calculating the Model
I created an R function calcModel() that, for each year, calculates the ending balance and returns. It returns a data frame of the results like you would see with an Excel spreadsheet. The major steps are:

  1. Create an empty matrix for spreadsheet
  2. Calculate the first row of the matrix
  3. For each year, calculate remaining rows of matrix
  4. Return a data frame with the result

I won’t show all the code here4 but the calling arguments are:

> args(calcModel)
function (year.start, years, bal.start = 100, r.S, r.L, dr.S, dr.L)

The first three arguments are self explanatory.
r.S and r.L are the shorter and longer rates in percent.
dr.S and dr.L are the ratea of change of r.S and r.L in basis points per annum. 100 bps. = 1 per cent.

Verifying the R Code
Now we can test calcModel(). This bond fund model was first implemented in Excel by user #Cruncher. So we can test against known good result provided by #Cruncher. There is also a function called summarizeSS() that prints one line of summary data for the simulation. summarizeSS() also shows the result from the baseline scenario, which is simply to buy and hold the bond to maturity.

Test Case 1: Time-invariant Yield Curve
Case 1 simulates a yield curve that does not vary over time. Starting balance was $1,000 and the ending balance is $1,156.82. Total annualized return over 5 years was 2.956%.

Observe that the bond fund model had a greater return than the baseline scenario. Each year, the bond paid its 1.8% coupon. But there was an additional capital return of 1.156% due to the rise in bond price with an upwardly-sloping yield curve. Bond traders call this effect “riding the yield curve”, and bond funds can benefit from this.

> # Test Case 1 Bal.End = 1156.82, Annualized Return = 2.956
> ss.df <- calcModel(year.start=2014, years<-5, bal.start=1000, r.S=1.5, r.L=1.8, dr.S=0, dr.L=0)
> print(ss.df, years)
Year YTM.S YTM.L Bal.Start Interest Bond.Price Mkt.Val Bal.End Cap.Ret Inc.Ret Tot.Ret
1 2014 1.5 1.8 NA NA 100.000 1000.00 1000.00 NA NA NA
2 2015 1.5 1.8 1000.00 18.0000 101.156 1011.56 1029.56 1.15632 1.8 2.95632
3 2016 1.5 1.8 1029.56 18.5321 101.156 1041.47 1060.00 1.15632 1.8 2.95632
4 2017 1.5 1.8 1060.00 19.0800 101.156 1072.26 1091.34 1.15632 1.8 2.95632
5 2018 1.5 1.8 1091.34 19.6441 101.156 1103.96 1123.60 1.15632 1.8 2.95632
6 2019 1.5 1.8 1123.60 20.2248 101.156 1136.59 1156.82 1.15632 1.8 2.95632
> summarizeSS(ss.df, 5)
bal.start baseline bal.end gain tr tr,annualized tr.baseline
1000.000000 1093.298847 1156.817788 1.156818 0.156818 2.956315 1.800000

Test Case 2: Rising, Flattening Yield Curve
Case 2 simulates a rising and flattening yield curve. Starting balance was $1,000 and the ending balance is $1,077.82. Total annualized return over 5 years was 1.495%. The outcome was worse than the baseline hold-to-maturity scenario annualized return of 1.8%.

In this scenario, rising rates resulted in a negative capital return. Flattening of the yield curve reduces and ultimately eliminates gain from “riding the yield curve.”

> # Test Case 2 Bal.End = 1077.02, Annualized Return = 1.5
> ss.df <- calcModel(year.start=2014, years<-5, bal.start=1000, r.S=1.5, r.L=1.8, dr.S=50, dr.L=44)
> print(ss.df, years)
Year YTM.S YTM.L Bal.Start Interest Bond.Price Mkt.Val Bal.End Cap.Ret Inc.Ret Tot.Ret
1 2014 1.5 1.80 NA NA 100.0000 1000.000 1000.00 NA NA NA
2 2015 2.0 2.24 1000.00 18.0000 99.2385 992.385 1010.38 -0.761546 1.80 1.03845
3 2016 2.5 2.68 1010.38 22.6326 99.0219 1000.502 1023.13 -0.978113 2.24 1.26189
4 2017 3.0 3.12 1023.13 27.4200 98.8105 1010.965 1038.38 -1.189471 2.68 1.49053
5 2018 3.5 3.56 1038.38 32.3976 98.6042 1023.891 1056.29 -1.395770 3.12 1.72423
6 2019 4.0 4.00 1056.29 37.6039 98.4028 1039.418 1077.02 -1.597154 3.56 1.96285
> summarizeSS(ss.df, 5)
bal.start baseline bal.end gain tr tr,annualized tr.baseline
1000.000000 1093.298847 1077.022020 1.077022 0.077022 1.495063 1.800000

There are more test cases. I won’t show them all, but you can try it out yourself.4 For all the tests, the R model matches the results provided by #Cruncher.

In the next article, I’ll show how the simple bond fund model stands up in the real world.

Acknowledgement: Thanks to users ogd, who proposed the original idea for this model, and #Cruncher, who did the original implementation in Excel.

1 Sometimes your CD interest calculation may be off by a few pennies from the bank’s calculation because you don’t know how many days they are counting, whether a year is 360 or 365 days, etc. But if you know all that, you will get the same result as the bank.

2 The Treasury Yield Curve shows, either in a table or graph, interest rates for terms from 1 to 30 years. See Daily Treasury Yield Curve Rates

3 To simplify, assume that we purchase par bonds, i,e. price p = 100, with coupon  = ytm. In reality, there may be discount (p<100) and/or premium (p>100) bonds.

4 Download the bond fund model R script and run it yourself. SimpleBondFundModel.R

More from category

Investing Returns for 2017
Investing Returns for 2017

2017 was a banner year for stock investors. [Read More]

Investing Returns for 2016
Investing Returns for 2016

2016 was a decent year for most investors. [Read More]

Investing Returns for 2015
Investing Returns for 2015

2015 was a poor year for investors. [Read More]

Investing Returns for 2014
Investing Returns for 2014

U.S. stocks won in 2014. [Read More]

Stock or Bond for Ten Years?
Stock or Bond for Ten Years?

How much money can you end up with for various mixes of stocks and bonds [Read More]