Skip to content
All library documents

Fitting a Polynomial Discount Curve to Bond Prices

Article Quant Q&A · Author: user3338639

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.