Bond Price Calculator

by John | June 15, 2021

This article series is provided by guest author James. It aims to help build an understanding of fixed income markets, securities, and interest rates through Python code.

In the first article, we explored the determinants of a bond price using the open and closed forms of the bond price formula. Using the formulae, we were pricing an annual paying bond and there was a built-in assumption that we were settling the bond on a coupon date as we did not account for accrued interest. In this article, we are going to build a bond pricing calculator that can price any bullet bond. A bullet bond pays a single fixed coupon rate throughout the life of the bond and it will only redeem its principal/face amount at maturity. 

The calculator will be able to price a bond on any settlement date that falls between the issue and maturity date of the bond. Accounting for the time value of the bond's cash flows and adjust the price for accrued interest, both relative to the input settlement date .

 

Settlement Date

The settlement date is the actual date that the security is exchanged for cash, this typically happens one or two business days after the bond was traded, market convention is to refer to this as T+1 or T+2.

 

Accrued Interest

Accrued interest is interest earned while holding the bond but has not yet been paid to the bondholder. For example, if you buy an annual paying bond just after it paid a coupon and you then held and traded it a week before it paid the coupon, should you not be compensated for the time that you held the bond? When you go to settle the bond after trading it you will receive the traded price of the bond, known as the clean, flat or gross price of the bond, it will then be adjusted for the 'earned' interest, as the bond seller this will increase the amount of cash you will receive. This final amount is known as the dirty, full or net price. In some markets, bonds are quoted in terms of dirty prices but the majority of markets quote bonds using clean prices.

 

\(\text{accrued interest} = \text{coupon} \times\frac{\text{days accrued}}{\text{days between coupons}}\)

 

\(\text{where:}\)

\(\text{days accrued}= \text{settlement date}-\text{last coupon date}\\ \text{days between coupons}= \text{next coupon date}-\text{last coupon date}\)

 

Of course, this is fixed income so it not always as simple to subtract one date from another. For example, if we use an actual/actual (the ratio here is describing the number of days per month and year) date calendar then chronologically the distribution of coupons is correct but this will end up in varying \(\text{days accrued}\) and \(\text{days between coupons}\) which in turn will also result in varying coupon amounts. US treasuries that pay a coupon use Act/Act. Whilst corporate bonds typically use 30/360 which assumes that each month has thirty days and a year has 360 days.

In Excel, you can for example use the DAYS() and DAYS360() functions to calculate the number of days between dates for both conventions. There are actually three different 30/360 methods which I will not go into detail here, but ISDA has a very useful file on their page. Our Python code is going to use a module called yearfrac which has several functions that return the decimal year(s) between two given dates using different day count conventions.

 

Which Bond Formula Should We Use?

In the last article we introduced the open and closed form bond price formulae, so which one should we use for our bond calculator? Let's first set some objectives for our code. We want to be able to price a bond:

  • On any date, while the bond is active, in other words after the issue and before the maturity date
  • For any coupon frequency, annual, semi-annual, quarterly are the most common frequences.
  • Using different interest rates rather than a flat rate (in this article we are going to use a flat yield but the ability to have time-dependent rates will become very useful in later articles)

 

Let's revisit our formulas!

 

Open-Form

 

\(B=\sum_{t=1}^{T}\frac{C_{t}}{\left( 1+i_{t} \right)^{t}}+\frac{B_{T}}{\left( 1+i_{T} \right)^{T}}\)

 

Closed-Form

 

\(\frac{B}{B_{T}}=\frac{\frac{C}{B_{T}}}{i} \left[ 1-\frac{1}{\left( 1+ i \right)^{T}} \right]+\frac{1}{\left( 1+i\right)^{T}}\)

\(\text{where:}\)

\(B=\text{Bond Value}\\ t=\text{Time, years until receipt of cash flow}\\ T=\text{Maturity of the bond, years}\\ i_{t}=\text{Interest rate for time, t or T}\\ i=\text{Constant yield}\\ C_{t} = \text{Coupon amount at time t}=\frac{\text{coupon rate}}{\text{coupon frequency}} \times \text{face amount}\\ C=\text{Coupon amount}=\frac{\text{coupon rate}}{\text{coupon frequency}} \times \text{face amount}\\ B_{T}=\text{Bond face value}\)

 

We can see that both formulas meet our first requirement, \(T\) does not have to be a nice round number like your finance textbook it can instead be a decimal year value. The open form will have time-dependent values of \(t\) and \(T\) and the closed-form will have a single value for \(T\) making it a simpler formula to work with!

Next constraint, different payment frequencies. Now we introduce varying compounding frequencies given as \(m\) which adjusts the interest rate and \(t, T\)

 

\(B=\sum_{t=1}^{T}\frac{C}{\left( 1+\frac{i_{t}}{m} \right)^{t \times m}}+\frac{B_{T}}{\left(1+\frac{i_{t}}{m} \right)^{T \times m}}\)

 

\(\frac{B}{B_{T}}=\frac{\frac{C}{B_{T}}}{\frac{i}{m}} \left[ 1-\frac{1}{\left( 1+ \frac{i}{m} \right)^{T \times m}} \right]+\frac{1}{\left( 1+\frac{i}{m}\right)^{T \times {m}}}\)

 

If compounded interest is new to the reader please see this excellent video by Dr. Trefor Bazett.

Both formulas are still in the race and the closed-form still retains its advantage of being simpler without the summation of different terms. However, the closed-form falls short in that it cannot use time-dependent interest rates. I can not emphasize enough how useful the ability to have time-dependent interest rates will become later in several different calculations, models, and different types of securities/contracts/derivatives.

 

Our Bond

In our first lines of code, we are going to use a named tuple to store our bond data. Named tuples are great for simpler data structures instead of defining a custom class. The first bond that we are going to price is a corporate bond that matures in 2027, pays a 10% coupon, semi-annually. It last paid a coupon on March 1st, 2021. We traded this bond on the 14th of July, 2021 at a yield of 6.5%. It settles on a T+2 convention which in turn means that it settles on the 16th of July.

 

import yearfrac as yf
from collections import namedtuple

# a named tuple that will serve as the dataframe for our bond, the first argument is the name of our data structure,
# the second argument is number and names of our data entries.
Bond_Data = namedtuple("Bond_Data","name, coupon_schedule, coupon_rate, coupon_freq, face, bond_yld, prev_coupon")

# the dates at which the bond pays it's coupons, the final date is the maturity of the bond at which
# it pays it's final coupon and redeems it's face value
corp_10_2021_coupon_schedule = [(2021, 9, 1),
                              (2022, 3, 1),(2022, 9, 1),
                              (2023, 3, 1),(2023, 9, 1),
                              (2024, 3, 1),(2024, 9, 1),
                              (2025, 3, 1),(2025, 9, 1),
                              (2026, 3, 1),(2026, 9, 1),
                              (2027, 3, 1)]

# the bond that we are pricing, a corporate bond that matures on the 1st of Match 2027. It pays a 10% coupon, twice a year.
#  has a face value of 100 which we are going to price at a yield of 6.5%. It last paid a coupon on the 1st of march 2021.
corp_10_2021 = Bond_Data("Corporate Bond 10 01/03/2012", 
                         corp_10_2021_coupon_schedule,
                        0.10,
                        2,
                        100,
                        0.065,
                        (2021, 3, 1))

# the day which we will exchange the bond for cash. This typically occurs 1 or 2 business days after we trade the bond.
# The trade date is the day buy/sell the bond. Whilst the accrued interest and discounting is calculated
# relative to the settlement date.
settlement_date = (2021, 7, 16)

 

Recalling from the previous article.To value a bond we need to:

  1. Identify the cash flow amounts and when we will receive them
  2. Determine the required yield to compensate for the time and risk associated with the cash flows
  3. Discount and sum the future cash flows to find the present value of the bond.

 

To model our cash flows and discount factors we will create three seperate functions:

cash_flows, returns an iterable that contains a coupon for each date instance in our coupon schedule and the redemption of the face value on the final coupon/maturity date.

 

\(B=\sum_{t=1}^{T}\frac{{\color{Blue}C_{t}}}{\left( 1+\frac{i_{t}}{m} \right)^{t \times m}}+\frac{{\color{Blue}B_{T}}}{\left(1+\frac{i_{t}}{m} \right)^{T \times m}}\)

 

time_factors, returns an iterable that contains the decimal years for \(t_{i},T\) relative to the settlement date

 

\(B=\sum_{t=1}^{T}\frac{C_{t}}{\left( 1+\frac{i_{t}}{m} \right)^{\color{Red} {t \times m}}}+\frac{B_{T}}{\left(1+\frac{i_{t}}{m} \right)^{\color{Red} {T \times m}}}\)

 

discount_factors, returns an iterable that contains the discount factors for each payment date.

 

\(B=\sum_{t=1}^{T}\frac{C_{t}}{{\color{Green} {\left( 1+\frac{i_{t}}{m} \right)^{t \times m}}}}+\frac{B_{T}}{{\color{Green} {\left(1+\frac{i_{t}}{m} \right)^{T \times m}}}}\)

 

def cash_flows(coupon_schedule, coupon_rate, coupon_freq, face):
    """
    This a function which models the cash flows amounts of the bond and outputs a list of nominal cash flows
    INPUTS:
    #coupon_schedule = the dates of bonds cash flows where the first date is the next coupon date and the last is the maturity of the bond 
    #coupon_rate = the interest rate paid by the bond, expressed as a decimal value
    #coupon_freq =  the number of times per year that the bond pays interest
    #face =  the par amount of the bond
    OUTPUTS:
    #cf = a list of object containing nominal cash flows
    """
    cf = []
    for dates in coupon_schedule:
        cf.append(coupon_rate * face / coupon_freq)
    cf[-1] += face
    return cf

def time_factors(settlement_date, coupon_schedule, coupon_freq):
    """
    This is a function which outputs the time factors for a schedule of coupon payments relative to a single settlement date. Using 30/360 ISDA day count convention.
    INPUTS:
    #settlement_date = Single date value for the date at which the security is exchanged for cash, typically T+1 or T+2, iterable containing: YYYY, MM, DD
    #coupon_schedule = the dates of bonds cash flows where the first date is the next coupon date and the last is the maturity of the bond
    #coupon_freq = the number of times per year that the bond pays interest
    OUTPUTS:
    #time_factors = Iterable that contains the exponent value used in the computation of time 'distance' between coupon dates and settlement
    """
    time_factors = []
    for dates in coupon_schedule:
        time_factors.append(yf.d30360e(*settlement_date, *dates, matu = False) * coupon_freq)
    return time_factors

def discount_factors(settlement_date, coupon_schedule, bond_yld, coupon_freq):
    """
    This is a function which calcultes the discount factors which are used to bring nominal cash flows back to present value:
    INPUTS:
    #settlement_date = Single date value for the date at which the security is exchanged for cash, typically T+1 or T+2, iterable containing: YYYY, MM, DD
    #coupon_schedule = The dates of bonds cash flows where the first date is the next coupon date and the last is the maturity of the bond
    #bond_yld = The yield at which the bond is being priced
    #coupon_freq = the number of times per year that the bond pays interest
    #OUTPUTS:
    #df = An iterable that contains the discount factors
    """
    tf = time_factors(settlement_date, coupon_schedule, coupon_freq)
    df = []
    for factors in tf:
        df.append(1 / (1 + bond_yld / coupon_freq) ** factors )
    return df

 

The functions that we created above, complete steps one and two. Now we need to combine them to complete the third and final step (discount and sum the future cash flows to find the present value of the bond):

present_value, combines cash_flows and discount_factors to generate an iterable that contains the present value of each cash flow. Finally the function then sums the values in the iterable.

 

\(B=\sum_{t=1}^{T}\frac{{\color{Blue} C_{t}}}{{\color{Green} {\left( 1+\frac{i_{t}}{m} \right)^{t \times m}}}}+\frac{{{\color{Blue} B_{T}}}}{{\color{Green} {\left(1+\frac{i_{t}}{m} \right)^{T \times m}}}}\)

 

Before scrolling down! Is the sum of the present values the clean or dirty price of the bond?

 

def present_value(settlement_date, coupon_schedule, coupon_rate, coupon_freq, face, bond_yld):
    """
    This is function which caculates the sum product of the cash_flows and discount_factors function outputs
    INPUTS:
    #settlement_date = Single date value for the date at which the security is exchanged for cash, typically T+1 or T+2, iterable containing: YYYY, MM, DD
    #coupon_schedule = The dates of bonds cash flows where the first date is the next coupon date and the last is the maturity of the bond
    #coupon_rate = the interest rate paid by the bond, expressed as a decimal value
    #coupon_freq = the number of times per year that the bond pays interest
    #face =  the par amount of the bond
    #bond_yld = The yield at which the bond is being priced
    OUTPUTS:
    #pv_cf = an iterable that contains the discounted cash flows, the sum of which is the dirty price of the bond 
    """
    cf = cash_flows(coupon_schedule, coupon_rate, coupon_freq, face)
    df = discount_factors(settlement_date, coupon_schedule, bond_yld, coupon_freq)
    pv_cf = [cf[i] * df[i ]for i in range(len(cf))]
    return sum(pv_cf)

 

Our present_value function is calculating the dirty price of the bond, for the majority of markets, bonds are quoted using clean prices. In order for our bond calculator to output prices that are comparable with market prices, we will need to have the ability to adjust the bond value for the accrued interest.

The accrued interest calculation does not use decimal year time but instead needs the number of days using the applicable day count convention for the bond. To back out the number of days from the yearfrac functions, we will need to multiply the decimal output by assumed numbers of days in a year (360 for this bond)

 

\(\text{accrued interest} = \text{coupon} \times\frac{\text{days accrued}}{\text{days between coupons}}\)

 

 \(\text{where:}\)

\(\text{days accrued}= \text{settlement date}-\text{last coupon date}\\ \text{days between coupons}= \text{next coupon date}-\text{last coupon date}\)

 

def accrued_interest(settlement_date, coupon_schedule, coupon_rate, coupon_freq, face, prev_coupon):
    """
    This is a function which calculates the accrued interest of the bond. Accrued interest is the 'earned' but unpaid interest owed to the bond seller on
    the settlement date.
    INPUTS:
    #settlement_date = Single date value for the date at which the security is exchanged for cash, typically T+1 or T+2, iterable containing: YYYY, MM, DD
    #coupon_schedule = The dates of bonds cash flows where the first date is the next coupon date and the last is the maturity of the bond
    #coupon_rate = the interest rate paid by the bond, expressed as a decimal value
    #coupon_freq = the number of times per year that the bond pays interest
    #face =  the par amount of the bond
    #prev_coupon =  an iterable that contains the date value, YYYY, MM, DD of when the previous coupon was paid
    OUTPUTS:
    #accrued_interest =  a single numerical value which is used to adjust the bond price from dirty to clean (gross to net)
    """
    full_coupon =  coupon_rate * face / coupon_freq
    accrued_days = yf.d30360e(*prev_coupon, *settlement_date, matu = True) * 360
    days_between_coupon = yf.d30360e(*prev_coupon, *coupon_schedule[0], matu = True) * 360
    accrued_interest =  full_coupon * accrued_days / days_between_coupon
    return accrued_interest

 

We are now ready to combine the present value of the bond and the accrued interest to output the price of our bond. The function will return the clean price of the bond by default but there is also the option to output the dirty price if required.

 

def bond_price(settlement_date,coupon_schedule, coupon_rate, coupon_freq, face, bond_yld, prev_coupon, clean = True):
    """
    This is a function which combines the outputs from the functions: present_value and accrued_interest to return either the clean or dirty price of the bond.
    By default the function will return the clean price.
    
    #INPUTS:
    #settlement_date = Single date value for the date at which the security is exchanged for cash, typically T+1 or T+2, iterable containing: YYYY, MM, DD
    #coupon_schedule = The dates of bonds cash flows where the first date is the next coupon date and the last is the maturity of the bond
    #coupon_rate = the interest rate paid by the bond, expressed as a decimal value
    #coupon_freq = the number of times per year that the bond pays interest
    #face =  the par amount of the bond
    #bond_yld = The yield at which the bond is being priced
    #prev_coupon =  an iterable that contains the date value, YYYY, MM, DD of when the previous coupon was paid
    #clean = Boolean which defaults to True. When True the function returns the clean price of the bond, False it returns the dirty price of the bond.
    OUTPUTS:
    #The price of the bond, in clean or dirty terms dependent on the function inputs. The price is rounded to six decimals
    """
    dirty_px = present_value(settlement_date, coupon_schedule, coupon_rate, coupon_freq, face, bond_yld)
    ai = accrued_interest(settlement_date, coupon_schedule, coupon_rate, coupon_freq, face, prev_coupon)
    if clean:
        return round(dirty_px - ai, 6)
    else:
        return round(dirty_px, 6)


bond_price(settlement_date, 
           corp_10_2021.coupon_schedule, 
           corp_10_2021.coupon_rate, 
           corp_10_2021.coupon_freq, 
           corp_10_2021.face, 
           corp_10_2021.bond_yld,
           corp_10_2021.prev_coupon)

 

Let's create another bullet bond that matures this year, it pays a quarterly fixed coupon of 3.45% against a face value of 100. We traded this bond at a yield of 1% and it settled on the 16th of June 2021. It last paid a coupon on 15th of June 2021.

 

corp_345_2021_coupon_schedule = [(2021, 9, 15),(2021,12,15)]

corp_345_2021 = Bond_Data("Corporate Bond 3.45 15/12/2021", corp_345_2021_coupon_schedule, 0.0345, 4, 100, .01, (2021, 6, 15))

bond_price((2021,6,16), 
           corp_345_2021.coupon_schedule, 
           corp_345_2021.coupon_rate,
           corp_345_2021.coupon_freq,
           corp_345_2021.face, 
           corp_345_2021.bond_yld, 
           corp_345_2021.prev_coupon)

 

Up until now, yield has been input for pricing bonds. In the next article we will look at yield and other return measures and how we can back them out of prices. Let's take a 5% annual paying coupon that is priced at 112.673, that matures in exactly 5 years from the settlement date. I.e it is settles on a coupon date. How can we solve for the yield(\(y\))?

 

\(B_{a}=\frac{5}{\left( 1+y \right)}+\frac{5}{\left( 1+y \right)^{2}}+\frac{5}{\left( 1+y \right)^{3}}+\frac{5}{\left( 1+y \right)^{4}}+\frac{105}{\left( 1+y \right)^{5}}=112.673\)

 

When I was first taught this, we were not allowed to use financial calculators in exams and had to resort to guessing based on our intuition of the price/yield relationship. To calculate the yield in python we will also need to 'guess' or put more elegantly, create an algorithm.


Join the discussion

Share this post with your friends!