0% found this document useful (0 votes)
235 views25 pages

Bond Pricing Guide Using Python

Uploaded by

xxwarxx
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
235 views25 pages

Bond Pricing Guide Using Python

Uploaded by

xxwarxx
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd

BOND PRICING WITH

PYTHON
Complete guide with formulas and programming code

Abstract
Understand and perform simple bond pricings by constructing a pricer on any python code editor.

[Link]@[Link]
November 2024

Bond pricing with Python | Page 0


TABLE OF CONTENTS
Introduction _______________________________________ 2

I. Bonds Principles _________________________________ 3


A) What is a bond and how does it work? ________________ 3
B) Bonds pricing components _________________________ 4

II. Bond pricer on Python ____________________________ 10


A) Pricer return explained ___________________________ 10
B) Code construction _______________________________ 11
C) Code output ____________________________________ 18

Appendix ________________________________________ 20

Bond pricing with Python | Page 1


INTRODUCTION

Bonds are the core product of the fixed income market, and a reliable source of income. Unlike
stocks which pay dividends that could fluctuate among time, bonds generally pay fixed interests and can
be predicted over time. Understanding bonds-related mechanisms can be very valuable in various finance
fields, as they are commonly used for portfolio diversification, capital preservation, corporate finance, debt
management, and a lot of other things. It can also give insights on macroeconomic and issuers situation,
as bonds maintain a close relationship with interest rates and requires analysing credit risks associated with
bonds issuers.

This guide will focus mainly interest rates relationship with bonds, as well as all calculations and data
required to construct a bond pricer on Python covering the followings:

- Payment dates
- Coupons payments and face value
- Present value of payments
- Dirty Price, accrued interests, and clean price
- Macauley duration and modified duration
- Convexity
- Bonds price fluctuation regarding interest rates

The Python code described in this guide will return all of the above from parameters computed beforehand,
which will be explained as well.

Bond pricing with Python | Page 2


I. BONDS PRINCIPLES

A) WHAT IS A BOND AND HOW DOES IT WORK?

A bond is the common representation of an issuer’s debt monetization. Indeed, in exchange for liquidities,
an entity (generally governments, corporations or municipalities) can issue bonds. These bonds are a loan
made to an investor (the bondholder) who receives periodic interest payments known as coupons plus the
guarantee to be repaid the principal amount invested in those bonds at the maturity of it.

In practice, when a bond is issued, investors purchase it to the issuer to lend them the money. This amount
will then be repaid by the issuer after periodic interests will have been paid to the investor. These interests
are calculated by a percentage of the face value of the bond, this is the coupon rate. When the bond reaches
maturity, the investor is paid by the issuer his last coupon plus the principal amount invested.

Depending on the issuer, bonds are rated by credit agencies (Moody’s, S&P, Fitch) based on its capacity
to pay back the loan to investors. Ratings starts from the top with AAA rated bonds, to the bottom with D
rated bonds (C for Moody’s ratings). The higher rated the bond, the lower its associated risk, and vice versa
for lower rated bonds. So, buying an AA rated bond instead of a BB- drastically reduces the credit risk of
the issuer. Typically, lower rated bonds offer higher yield to attract investors.

Bond pricing with Python | Page 3


Bond prices are sensitive to fluctuations in interest rates. Indeed, a yield of a bond is basically determined
by the coupon payments. Coupon rates associated to bonds are scaled on current interest rates at the bond
issuance. This high sensitivity is explained by the fact that if interest rates rise, newly issued bonds will
offer higher yield due to the new coupon rates associated. Bonds issued earlier then become less attractive
as their yield is lower than newly issued bonds. To balance this phenomenon, market prices drop to make
old bonds more attractive for investors. Economic policies and interest rates decisions driven by central
banks tend to influence a lot the bond market. This relationship is important to take into consideration as
such fluctuations could impact the performance of a bond-based portfolio.
To sum up:
- If interest rates rise, bond prices decrease and their yield rise
- If interest rates drop, bond prices rise and their yield drop

B) BONDS PRICING COMPONENTS

1- Face value (or par value):

The face value is the principal amount of money loaned by the investor to the issuer. This amount is repaid
to the investor at maturity. The face value of a bond is always calculated as multiples of 100 and is most
of the time 1000 for corporate bonds. This means that a bond issued will carry a principal value of 1000,
and buying multiple bonds will increase interests received.

Coupons and coupon rate:

Coupons are the interests perceived by an investor as they own a bond. The amount of interest paid is
determined by the coupon rate and is calculated on the base of the face value of the bond. Coupons of a
single bond are calculated by the following:

𝐶 =𝐹∗𝑟
with C = coupons
F = face value
r = coupon rate

The coupon rate “r” is an annual rate fixed at the bond issuance according to current interest rates. If it
fluctuates, the bond price will have to vary to meet sufficient yield to attract investors.

Bond pricing with Python | Page 4


2- Frequency:

The frequency refers to the number of times per year interests are earned by the investor. The more
frequently the coupons paid the more yield perceived by the investor. Frequency can either be annual,
semiannual, quarterly and monthly, but most of the time corporate bonds pay annual or semiannual
coupons.

3- Yield to maturity:

Yield to maturity is calculated as an annual rate and represents the total expected return of the bond if held
until maturity. YTM is a bond’s true annualized return and can be used to compare bonds with different
coupon payments and maturities.
YTM is calculated with the following formula:

𝐹 − 𝑃𝑉
𝐶+
𝑌𝑇𝑀 = 𝑡
𝐹 + 𝑃𝑉
2
whereas YTM = yield to maturity
C = coupons
F = face value
PV = present value of the bond

The present value of the bond also refers to the price of the security and is used in the formula as yield to
maturity is calculated at discount. The present value of a bond is calculated with the following formula:

𝑡
𝐶 𝐹
𝑃𝑉 = ∑ +
(1 + 𝑟)𝑡 (1 + 𝑟)𝑡
𝑡=1
where PV = present value of the bond
C = coupons
r = discount rate
t = number of periods
F = face value

To calculate the present value, we discount all the cash flows including coupons and face value paid at the
last cash flow. The sum of all this turns into the present value of the bond.
In our pricer on Excel and Python, the YTM is a parameter that will be computed in our functions and will
therefore be assumed and not calculated by our functions.

Bond pricing with Python | Page 5


4- Dirty price:

Dirty price is the price of the bond paid by investors when buying it in the middle of two coupon payments
date. Indeed, buying a bond at its issuance and after several coupon payments can’t be paid the same price.
Therefore, the dirty price of a bond will vary over time.
Dirty price is calculated by summing the discounted cash flows of a bond. Referring to the above page
formula, we can then state:

𝐷𝑃 = 𝑃𝑉
with DP = dirty price
PV = present value of the bond

The dirty price of a bond also carries the value of the spread between the purchase date of the bond and
the next payment date, assuming the bond hasn’t been bought at issuance. This spread value is known as
accrued interests.

5- Accrued interests:

Accrued interests represent the accumulated interests on the bond from its issuance date up to the purchase
date. This notion is crucial on the secondary bond market as the investor will pay those accrued interests
through the dirty price. Here is the formula of accrued interests:

𝑑𝑝 − 𝑑𝑙
𝐴𝐼 = 𝐹 ∗ 𝑟 ∗
365
with AI = accrued interests
F = face value
r = coupon rate
𝑑𝑝 = purchase date
𝑑𝑙 = last accrual period

Multiplying the face value to the coupon rate and the time between the purchase date and the last coupon
date gives us the accrued interests between the two periods used in the formulas. This way, we could also
calculate the accrued interests accumulated over a period. The most relevant thus is to calculate interests
accrued from the issuance of the bond.

Bond pricing with Python | Page 6


6- Clean price:

The clean price of a bond is the one generally quoted in bond markets. It reflects the pure value of a bond
without considering past interests. We can calculate the clean price of a bond by simply excluding accrued
interests to the dirty price:

𝐶𝑃 = 𝐷𝑃 − 𝐴𝐼
with CP = clean price
DP = dirty price
AI = accrued interests

The clean price of a bond tends to vary over time with interest rates and issuer’s creditworthiness.

7- Macaulay duration:

Macaulay duration (and duration in general) is used to assess a bond’s sensitivity to rate changes. By
calculating the duration of a bond, we will later on be able to calculate the new bond price after a variation
of its YTM. It is expressed in years and includes both coupon payments and principal amount repayment.
Macaulay duration is calculated as the following:

∑𝑛𝑡=1 𝑃𝑉𝑛 ∗ 𝑡𝑛
𝑀𝑐𝐷 =
𝐶𝑃
with McD = macaulay duration
n = number of cash flows
t = time to maturity
𝑃𝑉𝑛 = present value of a period
𝑡𝑛 = number of years between payments date and purchase date
CP = clean price

8- Modified duration:

To accurately calculate a bond price variation from a change in YTM, we have to adjust the Macaulay
duration and calculate the modified duration. This adjusted version measures the percentage change in a
bond price for each percentage change in interest rates. It provides a good estimate of the interest rate risk
associated to a bond. The modified duration is calculated this way:

𝑀𝑐𝐷
𝑀𝐷 =
1 + 𝑌𝑇𝑀
with MD = modified duration
McD = macaulay duration
YTM = yield to maturity

Bond pricing with Python | Page 7


With a modified duration of 5, a bond price would lose approximately 5% if interest rates increased by
1%.

9- Convexity:

A bond’s convexity provides a measure of its duration change as interest rates fluctuate. It could be seen
as the “curvature” of the relationship between a bond price and its yield. The higher the convexity the
lower the sensitivity of a bond to interest rates changes. Higher convexity bonds thus offer a better
protection against shifting interest rates compared to lower convexity ones. Generally, bonds with high
convexity are long-term bonds and bonds with lower coupons, as the time to maturity and yield have a
negative relationship with sensitivity.
The convexity of a bond can be calculated with the formula:

∑𝑛𝑡=1 𝑃𝑉𝑛 ∗ 𝑡²𝑛


𝐶𝑉𝑋 =
𝐶𝑃
with CVX = convexity
n = number of cash flows
t = time to maturity
𝑃𝑉𝑛 = present value of a period
𝑡²𝑛 = squared number of years between payments date and purchase date
CP = clean price

Convexity is a great complementary tool to calculate bond price changes. Indeed, when interest rates
fluctuate, the price of a bond doesn’t vary linearly. Calculating the convexity of a bond permits us to take
into account the variation of a bond’s duration as interest rates fluctuates.

10- Bond price variation:

Using duration and convexity, we can now calculate the percentage variation that will occur on a bond
price if we observe a change in interest rates. The formula is the following:

∆𝑌𝑇𝑀 2
∆𝐶𝑃 = (−𝑀𝐷 ∗ ∆𝑌𝑇𝑀) + 0,5 ∗ 𝐶𝑉𝑋 ∗ ( ) ∗ 100
100
with ∆𝐶𝑃 = bond price variation in %
MD = modified duration
∆𝑌𝑇𝑀 = yield to maturity variation (interest rates change) in %
CVX = convexity

Bond pricing with Python | Page 8


After using this formula, we get returned the percentage of variation in the bond’s clean price for the
computed change in its yield to maturity. Then, we just apply this percentage to calculate the new bon
price:
𝐶𝑃′ = 𝐶𝑃 ∗ (1 + ∆𝐶𝑃)
with 𝐶𝑃′ = new bond price
CP = bond clean price
∆𝐶𝑃 = bond price variation in %

The above formulas are the core of bonds pricing and are extremely valuable to understand the bonds
primary and secondary markets. It also reflects the relationship between its price and interest rates
fluctuation driven by central banks. Now that these calculations have been covered, let’s dive into the
pricer construction!

Bond pricing with Python | Page 9


II. BOND PRICER ON PYTHON

A) PRICER RETURN EXPLAINED

The purpose of this pricer is to provide a complete pricing of a bond thanks to initial computations done at
the beginning. Additionally, we want to analyze the bond price’s fluctuation in case of changes in interest
rates. Using the Pandas module in Python, we are going to aggregate all data returned by our code in three
different dataframes for cleaner printing and easier output. The first one will contain payment dates,
coupons associated by periods, and present values per year. The second one will cover dirty price, accrued
interests, clean price, Macaulay duration, modified duration and convexity. The last dataframe will be used
to have a sight on the change in interest rates, and the associated price fluctuation to calculate the new bond
price. To achieve this, we are going to define two functions, which will need the following initial
computations:
- Face value
- Coupon rate
- Frequency
- Yield to maturity
- Purchase date
- Next coupon date
- Maturity date

For this guide, we will be using a sample from the following bond:

EADS OCT 2029S 2.125


Face value = 1000
Coupon rate = 2.125%
Frequency = 1 (annual)
Yield to maturity = 4%
Purchase date: 10/15/2024
Next coupon date: 10/29/2024
Maturity date: 10/29/2029

Now let’s dive into the code construction step by step. You will be able to find the entire code at the end
of this document in the appendix section.

Bond pricing with Python | Page 10


B) CODE CONSTRUCTION

1- Modules importation:
import pandas as pd
import numpy as np
import datetime
from datetime import datetime, timedelta
from [Link] import relativedelta

At the beginning of our code, we must import modules that will help us to format and calculate bond-
related data. Pandas bring us dataframes to aggregate the data, Numpy will be used to round results for a
cleaner printing, Datetime will allow us to manipulate date objects and Dauteil is used to bring the
“relativedelta” function for quicker date objects calculation.

2- First function definition:


def bond_pricing(face_value, coupon_rate, freq, yield_to_maturity, pur-
chase_date, next_coupon_date, maturity_date):

return payments_df, pricing_df

Right after modules importation, we define our functions with the initial computation data. From now on,
code listed in the next pages will be computed in this function definition. If any confusion arises, remember
that the entire code is written at the end in the appendix section.
This function will return two of our three dataframes and will contain a third one that will not be printed
as it will just be used for calculations.

3- Date objects conversion and date rows creation:


purchase_date = [Link](purchase_date, "%Y-%m-%d")
next_coupon_date = [Link](next_coupon_date, "%Y-%m-%d")
maturity_date = [Link](maturity_date, "%Y-%m-%d")

These few lines are crucial to the way we want to return data. This is meant to convert the purchase, next
coupon, and maturity dates into date objects using the Datetime module. As it is computed as strings in the
function, we need to convert in order to calculate from these dates. The “strptime” function does this
conversion, as well as the date format we chose using the argument “%Y-%m-%d”.

starting_date = next_coupon_date
date_rows = [next_coupon_date]

Next up we create a clone of the “next_coupon_date” variable. We are doing so because we don’t want
“next_coupon_date” to change its value. Instead, we add it to a list as we need this date object for our
calculations. This list is meant to contain all the date objects of the life cycle of the bond.

Bond pricing with Python | Page 11


while starting_date < maturity_date:
if freq == 1:
starting_date = starting_date.replace(year=(starting_date.year + 1))
date_rows.append(starting_date)
elif freq == 2:
starting_date = starting_date + relativedelta(months=6)
date_rows.append(starting_date)
else:
raise ValueError("Compounding frequency should be either annual (1)
or semiannual (2).")

This loop is used to create as many date objects as the number of cash flows of the bond. It will create
another date object from the clone far from one year if the frequency of the bond is equal to one, and far
from six months if the frequency is equal to two. New date objects are then added to the “date_rows” list
and incremented to the “starting_date” variable until the next occurrence of the loop. In the case of a
semiannual compounding, we use the “relativedelta” function from Dauteil to avoid creating another
condition for the addition of six months to the current date object, as it would raise an error if the payment
dates were later than June (months>6).
Here, we cover only annual and semiannual compounding as it is the most common frequencies, but adding
quarterly, monthly and any other frequency is possible in this loop. If an incorrect frequency is computed,
we raise a value error.

t_list = []
for x in date_rows:
t_list.append((x - purchase_date).days / 365)

date_rows = [[Link]("%Y-%m-%d") for dt in date_rows]

Before finishing with date objects, we create a list called “t_list” which will contain the number of days
between the payment dates and the purchase date. This data will be contained in a dataframe that will not
be printed at the end. By doing so right after the above-described loop, we can now use the “strftime”
function to convert back date objects to string. We do this to avoid printing hours, minutes and seconds,
which are time objects included in date objects by default.

4- Coupons and present values rows creation:

c_rows = []
for x in t_list:
c_rows.append([Link](face_value * coupon_rate, 2))
c_rows[-1] += face_value

pv_rows = []
for x,y in zip(c_rows, t_list):
pv_rows.append([Link](x / (1 + yield_to_maturity) ** y, 2))

Bond pricing with Python | Page 12


By following the same “for in” loop as for the “t_list” list, we create the list called “c_rows” wich will
contain all the coupons per period, and the list called “pv_rows” with all the present values per period.
These two lists, as well as the “date_rows” list, will be a column of our “payments_df” dataframe.
So, for each value (x) in the list “t_list”, we calculate the coupon by multiplying the coupon rate to the face
value before adding the result to the “c_rows” list. Finally, we add the face value of the bond to the last
variable of the list by using the “-1” index.
Regarding the present value, we use the “zip” function to assign “x” to the coupons and “y” to the number
of days from the purchase date. We then applicate the formula to feed the “pv_rows” list with each period’s
present value. Using Numpy’s “.round()” function, we round coupons and present values to two digits for
better readability.

5- Payments dataframe creation:

payments_data = [date_rows, c_rows, pv_rows]


payments_columns = ["Payment Dates", "Coupons", "Present Values"]
payments_df = [Link](payments_data, payments_columns)
payments_df = payments_df.transpose()

Using the Pandas module, there is a bunch of different ways to create a dataframe. Here, we chose to use
the list of lists method to calculate each column separately. We set the data argument with our
“payments_data” list composed of our payment dates, coupons, and present values. We give our columns
names through the “payments_columns” list, and then create the dataframe using the “[Link]()”
function (capital D and F are mandatory for the function to work). Finally, we invert rows and columns
with the function “.transpose()”.

Now the first dataframe is done! Printing “payments_df” will output something like this on the code editor:

Payment Dates Coupons Present Values


0 2024-09-29 21.25 21.22
1 2025-09-29 21.25 20.4
2 2026-09-29 21.25 19.62
3 2027-09-29 21.25 18.86
4 2028-09-29 21.25 18.14
5 2029-09-29 1021.25 838.04

But this is only the first part of the “bond_pricing” function. In order for it to output the “pricing_df”
dataframe, we first need to construct another dataframe containing data we will use to calculate dirty and
clean prices, as well as durations, convexity and accrued interests.

Bond pricing with Python | Page 13


6- Calculations data dataframe creation:

pv_times_t = []
for x, y in zip(pv_rows, t_list):
pv_times_t.append(x * y)

tsquared = []
for x in t_list:
[Link](x ** 2)

pv_times_tsquared = []
for x, y in zip(pv_rows, tsquared):
pv_times_tsquared.append(x * y)

calculations_data = [t_list, pv_times_t, tsquared, pv_times_tsquared]


calculations_columns = ["t Values", "PV times t Values", "t squared Values",
"PV times t squared Values"]
calculations_df = [Link](calculations_data, calculations_columns)
calculations_df = calculations_df.transpose()

Using the same “for in” loops as for coupons and present values, we create a dataframe composed of the
previously calculated “t ratio”, the present values multiplied by this ratio, the “t ratio” squared, and the
present values multiplied by the “t ratio” squared.
As a reminder, this “calculations_df” dataframe will not be printed, so we don’t need to round values. We
still need to transpose using the “.transpose()” function for easier data retrieval later in the function.
If we decided to print it anyway, the output would look like this (still with sample’s initial computation):

t Values PV times t Values t squared Values PV times t squared Values


0 0.038356 0.813918 0.001471 0.031219
1 1.038356 21.182466 1.078184 21.994944
2 2.038356 39.992548 4.154896 81.519057
3 3.038356 57.303397 9.231608 174.108130
4 4.041096 73.305479 16.330456 296.234472
5 5.041096 4224.640000 25.412648 21296.815342

Bond pricing with Python | Page 14


7- Pricing dataframe creation:

if freq == 1:
last_coupon_date = next_coupon_date.replace(year=(next_coupon_date.year
- 1))
elif freq == 2:
last_coupon_date = next_coupon_date - relativedelta(months=6)

Before using formulas to calculate data that would be contained in our “pricing_df” dataframe, we need to
define the “last_coupon_date” variable, which will be used to calculate accrued interests of the bond. Here,
we simply need this variable to be equal to “next_coupon_date” minus one period. This loop sets
“last_coupon_date” one year before the next coupon date computed if “freq” is equal to one (annual
compounding), and six months before if “freq” is equal to two (semiannual compounding) using the
“relativedelta()” function.

accrued_interests = [Link]((purchase_date - last_coupon_date).days * cou-


pon_rate * face_value / 365, 2)
dirty_price = [Link](sum(payments_df["Present Values"]), 2)
clean_price = [Link](dirty_price - accrued_interests, 2)
mc_duration = [Link](sum(calculations_df["PV times t Values"]) /
clean_price, 2)
m_duration = [Link](mc_duration / (1 + yield_to_maturity), 2)
convexity = [Link](sum(calculations_df["PV times t squared Values"]) /
clean_price, 2)

pricing_data = [dirty_price, accrued_interests, clean_price, mc_duration,


m_duration, convexity]
pricing_columns = ["Dirty Price", "Accrued Interests", "Clean Price", "Ma-
caulay Duration", "Modified Duration", "Convexity"]
pricing_df = [Link](pricing_data, pricing_columns)

As the dataframe will return single values of accrued interests, dirty price, clean price, Macaulay duration,
modified duration and convexity, here we just translate mathematical formulas to Python language. Every
result is rounded by two digits using the “[Link]()” function from Numpy, and is a component of the
“pricing_data” list through their assigned variable. The “sum()” function allows us to simply sum all values
from our previous “calculations_df” dataframe, which drove us to construct dataframes regardless of the
number of values implied by our bond. We deliberately didn’t transpose the dataframe as single values
printed vertically is better looking than a horizontal table.

Bond pricing with Python | Page 15


The printed “pricing_df” dataframe outputs a table that looks like this:

0
Dirty Price 936.28
Accrued Interests 20.49
Clean Price 915.79
Macaulay Duration 4.82
Modified Duration 4.63
Convexity 23.88

This returned dataframe closes our first “bond_pricing” function. In the end, the function returns the
“payments_df” dataframe and the “pricing_df” dataframe.

8- Second function definition:

payments_df, pricing_df = bond_pricing(1000, 0.02125, 1, 0.04, "2024-09-15",


"2024-09-29", "2029-09-29")

Before defining the second function that will return the change in the bond price if interest rates fluctuate,
it is important to call the function (outside of it of course) as data contained in the “pricing_df” dataframe
will be used for calculations.

def bond_price_fluctuation(change_in_ytm):

return bond_price_fluctuation_df

We can now define the “bond_price_fluctuation” function. The only initial computation we need is the
change in yield to maturity which reflects the interest rates variation as other needed data will be defined
directly in the function from previous dataframes returned by the first function.

9- Data retrieval from the first function:

modified_duration = pricing_df.iloc[4, 0]
convexity = pricing_df.iloc[5, 0]
clean_price = pricing_df.iloc[2, 0]

In order to calculate the change in bond price, we need the modified duration, the convexity and the clean
price from the previous “pricing_df” dataframe. Here, we are using the “.iloc()” function from Pandas to
locate and extract the data corresponding to the given coordinates as arguments.

Bond pricing with Python | Page 16


10- Bond price fluctuation dataframe construction:

change_in_bond_price = [Link]((-modified_duration * change_in_ytm) + 0.5 *


convexity * (change_in_ytm / 100) ** 2 * 100, 4)
new_bond_price = [Link](clean_price * (1 + change_in_bond_price), 2)

fluctuation_data = [change_in_ytm, change_in_bond_price, new_bond_price]


fluctuation_columns = ["Change in YTM", "Change in bond price", "New bond
price"]

bond_price_fluctuation_df = [Link](fluctuation_data, fluctuation_col-


umns)

As for the “pricing_df” dataframe, the “bond_price_fluctuation_df” dataframe will return single values, so
we didn’t transpose as well. Percentage in such calculations sometimes contain three digits, so we chose
here to round by four digits as the “change_in_ytm” variable has to be computed in decimal.
The printed “bond_price_fluctuation_df” dataframe outputs a table that looks like this:

0
Change in YTM 0.0100
Change in bond price -0.0463
New bond price 873.3900

As we can see, the new bond price is not rounded by 2 digits as we stated in the code. This is because of
default Pandas dataframes formatting. It takes into account the first number of digits in a “.round()”
function and invalidates the following if any. This is not a problem in itself, but I found it important to
highlight it.

bond_price_fluctuation_df = bond_price_fluctuation(0.01)

To complete our code, we assign to a variable the called “bond_price_fluctuation” function outside of its
definition. Here, we assume that the yield to maturity of the bond rises by 1%. After running the code, the
code editor now contains three dataframes, “payments_df”, “pricing_df”, and
“bond_price_fluctuation_df”.

Bond pricing with Python | Page 17


C) CODE OUTPUT

As a reminder, used initial computations of an EADS bond which parameters are described in the “Pricer
return explained” section.

print(payments_df)
print(pricing_df)
print(bond_price_fluctuation_df)

When printing the three dataframes, the terminal returns this:

As the bond still holds six periods, the “payments_df” has six rows, each giving coupons and present
values per period.

payments_df, pricing_df = bond_pricing(1000, 0.02125, 2, 0.04, "2024-09-15",


"2024-09-29", "2029-09-29")

Now switching to semiannual compounding (“freq” variable is set to be equal to two), with every other
initial computation staying the same as our bond sample. As coupons are now paid semiannually, we expect
the number of rows of the “payments_df” dataframe to increase. The bond price should also rise as it is
paying more coupons, and the accrued interests should be lower as the last coupon date is closer to the
purchase date.

Bond pricing with Python | Page 18


When printing the three dataframes, the terminal returns this:

As expected, the number of rows in the first dataframe increases by the number of additional periods
implied by the semiannual compounding. The bond now holds eleven periods, resulting in a “payments_df”
dataframe containing eleven rows. Dirty and Clean price also increased, and the spread between the two
also shrinked because of the lower accrued interests.

payments_df, pricing_df = bond_pricing(1000, 0.02125, 3, 0.04, "2024-09-15",


"2024-09-29", "2029-09-29")

Setting the “freq” variable to three, the code raises a ValueError:

This witnesses that our “date_rows” list feeding loop works properly.

Bond pricing with Python | Page 19


APPENDIX

Entire code:
"""
Created on Wed Oct 23 [Link] 2024
@author: Theo Azema
"""

import pandas as pd
import numpy as np
import datetime
from datetime import datetime, timedelta
from [Link] import relativedelta

def bond_pricing(face_value, coupon_rate, freq, yield_to_maturity, pur-


chase_date, next_coupon_date, maturity_date):

purchase_date = [Link](purchase_date, "%Y-%m-%d")


next_coupon_date = [Link](next_coupon_date, "%Y-%m-%d")
maturity_date = [Link](maturity_date, "%Y-%m-%d")

starting_date = next_coupon_date
date_rows = [next_coupon_date]
while starting_date < maturity_date:
if freq == 1:
starting_date = starting_date.replace(year=(starting_date.year + 1))
date_rows.append(starting_date)
elif freq == 2:
starting_date = starting_date + relativedelta(months=6)
date_rows.append(starting_date)
else:
raise ValueError("Compounding frequency should be either annual (1)
or semiannual (2).")

t_list = []
for x in date_rows:
t_list.append((x - purchase_date).days / 365)

date_rows = [[Link]("%Y-%m-%d") for dt in date_rows]

c_rows = []
for x in t_list:
c_rows.append([Link](face_value * coupon_rate, 2))
c_rows[-1] += face_value

pv_rows = []
for x,y in zip(c_rows, t_list):
pv_rows.append([Link](x / (1 + yield_to_maturity) ** y, 2))

Bond pricing with Python | Page 20


payments_data = [date_rows, c_rows, pv_rows]
payments_columns = ["Payment Dates", "Coupons", "Present Values"]
payments_df = [Link](payments_data, payments_columns)
payments_df = payments_df.transpose()

pv_times_t = []
for x, y in zip(pv_rows, t_list):
pv_times_t.append(x * y)

tsquared = []
for x in t_list:
[Link](x ** 2)

pv_times_tsquared = []
for x, y in zip(pv_rows, tsquared):
pv_times_tsquared.append(x * y)

calculations_data = [t_list, pv_times_t, tsquared, pv_times_tsquared]


calculations_columns = ["t Values", "PV times t Values", "t squared Values",
"PV times t squared Values"]
calculations_df = [Link](calculations_data, calculations_columns)
calculations_df = calculations_df.transpose()

if freq == 1:
last_coupon_date = next_coupon_date.replace(year=(next_coupon_date.year
- 1))
elif freq == 2:
last_coupon_date = next_coupon_date - relativedelta(months=6)

accrued_interests = [Link]((purchase_date - last_coupon_date).days * cou-


pon_rate * face_value / 365, 2)
dirty_price = [Link](sum(payments_df["Present Values"]), 2)
clean_price = [Link](dirty_price - accrued_interests, 2)
mc_duration = [Link](sum(calculations_df["PV times t Values"]) /
clean_price, 2)
m_duration = [Link](mc_duration / (1 + yield_to_maturity), 2)
convexity = [Link](sum(calculations_df["PV times t squared Values"]) /
clean_price, 2)

pricing_data = [dirty_price, accrued_interests, clean_price, mc_duration,


m_duration, convexity]
pricing_columns = ["Dirty Price", "Accrued Interests", "Clean Price", "Ma-
caulay Duration", "Modified Duration", "Convexity"]
pricing_df = [Link](pricing_data, pricing_columns)

return payments_df, pricing_df

payments_df, pricing_df = bond_pricing(1000, 0.02125, 1, 0.04, "2024-09-15",


"2024-09-29", "2029-09-29")

Bond pricing with Python | Page 21


def bond_price_fluctuation(change_in_ytm):

modified_duration = pricing_df.iloc[4, 0]
convexity = pricing_df.iloc[5, 0]
clean_price = pricing_df.iloc[2, 0]

change_in_bond_price = [Link]((-modified_duration * change_in_ytm) + 0.5 *


convexity * (change_in_ytm / 100) ** 2 * 100, 4)
new_bond_price = [Link](clean_price * (1 + change_in_bond_price), 2)

fluctuation_data = [change_in_ytm, change_in_bond_price, new_bond_price]


fluctuation_columns = ["Change in YTM", "Change in bond price", "New bond
price"]

bond_price_fluctuation_df = [Link](fluctuation_data, fluctuation_col-


umns)

return bond_price_fluctuation_df

bond_price_fluctuation_df = bond_price_fluctuation(0.01)

print(payments_df)
print(pricing_df)
print(bond_price_fluctuation_df)

Be careful with indentations. Just in case, here is the code in screenshot from VSCode:

Bond pricing with Python | Page 22


Bond pricing with Python | Page 23
If you have any questions or remarks, don’t hesitate to to contact me by email: [Link]@[Link]

Thanks for reading

Bond pricing with Python | Page 24

You might also like