Fitting a Polynomial Discount Curve to Bond Prices
Summary
The document describes a least squares approach to infer discount factors from a set of benchmark bond prices. It represents each bond price as the sum of its cash flows multiplied by discount factors, then models each discount factor as a polynomial in time to payment. Substituting that polynomial into the pricing equation produces a linear model in the polynomial coefficients, which can be fitted by minimizing squared pricing errors.
The example uses R to build cash flow times and a design matrix, then passes the squared error objective to an optimizer. The author reports a large aggregate error across the sample bonds and asks whether the method or implementation is at fault. The document does not resolve that issue or validate the code, so it offers a modeling setup rather than evidence that the fit works. Practical considerations left open include cash flow date and coupon conventions, polynomial stability and discount factor constraints, and whether the fitted prices reproduce market quotes adequately.
Key ideas
- Bond prices can be expressed as discounted sums of their scheduled cash flows.
- A polynomial in payment time can parameterize discount factors using a small set of coefficients.
- Substitution yields a linear pricing model that can be estimated by minimizing squared price residuals.
- The example reports a large fitting error but does not diagnose its cause or establish model adequacy.
Tags
Full text
# Using Market Prices of Bonds to Model the Discount Curve with a Polynomial (Math + R)
# Using Market Prices of Bonds to Model the Discount Curve with a Polynomial (Math + R)
I have a small program I'm building to interpolate the discount curve from a portfolio of benchmark bonds. If anyone has any guesses as to whether it's my process, or my code that's messed up I would greatly appreciate it.
The math behind it is for the most part straight forward compared to most interest rate models:
The market price of a bond is the discounted value of its cash flows:
```
MPj = Cj1*D1 + Cj2*D2 +… + Cji*Di + Error
Where
MPj = Cash price of bond j
Cji = ith cash flow for bond j
Di = Discount factor for the ith cash flow
```
Use a polynomial to model the discount factor:
```
Di = B0 + B1*Ti + B2*Ti^2 + … + Bk*Ti^k
Where:
Di = Discount factor at time = i
k = The degree of the polynomial
Ti = Time until the ith cash flow, in years
Bk = The coefficients of the model which describe how the time to cash flow determines the discount factor
```
Substitute the second equation into the first:
```
MPj = Cj1 [B0 + B1*T1 + B2*T12 + … + Bk*T1k] +… + Cji [B0 + B1*Ti + B2*Ti2 + … + Bk*Tik ] + Error
```
and in matrix form: https://i.sstatic.net/2mLJ6.png
From there, take the squared error of this equation, and minimize it by changing the coefficients.
Does this process make sense?
When I try to implement it, I end up with very large squared errors (~73.5 over 15 bonds).
This is the data I'm using: https://www.dropbox.com/s/zeg5xyt5kq4xcpm/Sample%20Data.csv?dl=0
This is the code I'm using. Any comments are appreciated.
```
library(dplyr)
library(lubridate)
Polynomial_Degree = 3
Start_Date <- as.Date(ymd(19990122))
Face_Value <- 100
Default_Coeffs_Guess <- rep(0,Polynomial_Degree + 1)
Databank <- read.csv(paste(getwd(), "Sample Data.csv", sep="/"), stringsAsFactors = F)
Coupon_Values_List <- Databank %>%
select(COUPON) %>%
data.frame %>%
rename(.,Coupon_Values_List = COUPON)
Coupon_Values_List2 <- Databank %>%
select(COUPON) %>%
transmute(., Coupon_Values_List2 = COUPON +1) %>%
data.frame
Maturity_Date_List <- Databank %>%
select(MATURITY) %>%
lapply(mdy) %>%
lapply(as.Date) %>%
data.frame
Bond_Midpricing <- Databank %>%
transmute(., Bond_Midpricing = (BID.PRICE+ASK.PRICE)/2) %>%
as.matrix
#Functions 1:
Coupon_Count_Function <- function(Maturity_Date, Coupon_Payment_Frequency){
Coupon_Count <- 0
Coupon_Count <- as.double(ceiling(((as.Date(Maturity_Date)-as.Date(Start_Date))*Coupon_Payment_Frequency)/365)-1)
return(Coupon_Count)
}
#Function 2:
Coupon_Dates_Function <- function(Maturity_Date, Coupon_Payment_Frequency){
Coupon_Payment_Dates <- NULL
Coupon_Count=Coupon_Count_Function(Maturity_Date,2)
for (i in 0:Coupon_Count)
{x <- as.numeric(((as.Date(as.Date(Maturity_Date)-i*365/Coupon_Payment_Frequency))-Start_Date)/365)
Coupon_Payment_Dates <- rbind(Coupon_Payment_Dates, data.frame(x))}
return((Coupon_Payment_Dates))
}
#Function 3:
Coupon_Time_Func <- function(Maturity_Dates, Coupon_Payment_Frequency) {
All_Coupon_Time_List <- apply(Maturity_Dates, 1, Coupon_Dates_Function, Coupon_Payment_Frequency = 2)
return(All_Coupon_Time_List)
}
#Function 4:
Independent_Variable_Matrix_Func <- function(Maturity_Date_List, Coupon_Payment_Frequency, Coupon_Values_List,
Polynomial_Degree){
df <- data.frame(I(Coupon_Time_Func(Maturity_Date_List, Coupon_Payment_Frequency)),Coupon_Values_List)
names(df) <- c("All_Coupon_Time_List", "Coupon_Values_List")
Temp_List <- vector(mode = "list", Polynomial_Degree+1)
for (i in 1:(Polynomial_Degree+1)) {
Summation_Column_Coupons <- NULL
Summation_Column_Coupons <- mapply(FUN = function(x, y) {y* sum(x^(i-1))}, df$All_Coupon_Time_List,
df$Coupon_Values_List)
Summation_Column_With_Principle <- NULL
Summation_Column_With_Principle <- mapply(Summation_Column_Coupons, df$All_Coupon_Time_List, Face_Value,
FUN = function(x,y,z) {x+max(y^(i-1))*z})
Temp_List[[i]] <- Summation_Column_With_Principle
}
Independent_Variable_Matrix <- NULL
Independent_Variable_Matrix <- as.matrix(t(rbind(Independent_Variable_Matrix, do.call(rbind, Temp_List))))
return(Independent_Variable_Matrix)
}
Independent_Variable_Matrix <- Independent_Variable_Matrix_Func(Maturity_Date_List, Coupon_Payment_Frequency, Coupon_Values_List,
Polynomial_Degree)
#Function 5:
Sum_Error_Squared_Func <- function(Coeffs){
Estimated_Prices_Vector <- Independent_Variable_Matrix %*% Coeffs
Sum_Error_Squared <- sum((Bond_Midpricing - Estimated_Prices_Vector)^2)
return(Sum_Error_Squared)
}
optim (par = Default_Coeffs_Guess,
fn = Sum_Error_Squared_Func,
gr = NULL,
method = "L-BFGS-B",
lower = c(-1000,-1000,-1000,-1000),
upper = c(1000,1000,1000,1000))
```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.