Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 2

Bond Pricing in Python

Recommended resources

Welcome to your second lesson. In our previous session, we derived the fundamental formula for bond pricing from the principle of present value, establishing the theoretical link between a bond's price, its cash flows, and its yield to maturity (YTM).

Today, we will transition from theory to practice, precisely as you prefer. Our goal is to implement a robust Python function that can price a bond using the concepts we've discussed. This will be the first practical tool in our bond analytics toolkit. By the end of this lesson, you will have a working Python function to price a bond given its coupon, maturity, and YTM.

1. Implementation Strategy: Open-Form vs. Closed-Form

In the last lesson, we derived the closed-form bond pricing formula using the present value of an annuity:

While elegant and efficient for a constant yield , this formula has a limitation: it assumes the same discount rate is used for all cash flows. A more flexible approach, and the one we will implement, is the open-form or summation formula:

This approach, where we discount each cash flow individually, might seem more verbose but is far more powerful. It allows us to eventually replace the single YTM with a series of different discount rates for different maturities (a term structure of interest rates), a key topic we will cover in Module 2.

The following reading explains this choice and outlines a modular strategy for building our Python pricing function.

Bond Price Calculator

This article from CodeArmo justifies using the open-form formula and breaks down the implementation into logical, reusable functions.

First, read the section "Which Bond Formula Should We Use?" to understand why the open-form formula provides greater flexibility for future applications, particularly with time-dependent interest rates. Next, read the section that begins "To model our cash flows and discount factors we will create three seperate functions:". This outlines the modular structure we will follow: cash_flows, time_factors, and discount_factors.

As the article suggests, we will build our pricing engine by creating a set of distinct functions. This modular design is not only good programming practice but also makes the logic transparent and easy to adapt later.


2. Building the Bond Pricer in Python

We will now implement the functions outlined in the CodeArmo article step-by-step. Our implementation will require a library called yearfrac to handle day-count conventions accurately, a crucial detail in financial calculations. You can install it via pip:
pip install yearfrac

Step 2.1: Defining the Bond's Characteristics

First, we need a way to store the bond's data. A namedtuple is a memory-efficient and readable choice for this purpose, acting as a simple data container.

from collections import namedtuple
import yearfrac as yf




# Define the structure for our bond data
Bond = namedtuple("Bond", [
    "coupon_rate",
    "face_value",
    "coupon_freq",
    "coupon_schedule", # A list of future coupon payment dates
    "prev_coupon_date" # The last coupon payment date
])




# Example: A semi-annual 5% coupon bond maturing March 1, 2030
# We are pricing it on July 16, 2024.
settlement_date = (2024, 7, 16)
coupon_dates = [
    (2025, 3, 1), (2025, 9, 1),
    (2026, 3, 1), (2026, 9, 1),
    (2027, 3, 1), (2027, 9, 1),
    (2028, 3, 1), (2028, 9, 1),
    (2029, 3, 1), (2029, 9, 1),
    (2030, 3, 1)
]

example_bond = Bond(
    coupon_rate=0.05,
    face_value=1000,
    coupon_freq=2,
    coupon_schedule=coupon_dates,
    prev_coupon_date=(2024, 3, 1)
)




# The yield at which we want to price the bond
bond_yield = 0.04

Step 2.2: Generating Cash Flows

Our first function will generate the list of all future payments the bondholder will receive.

Sample Bond Cash Flow Diagram
This diagram illustrates the typical cash flow structure of a bond, with periodic coupon payments and a final principal repayment at maturity. Our `cash_flows` function will programmatically generate this stream.
def get_cash_flows(bond: Bond) -> list[float]:
    """Generates a list of a bond's future cash flows."""
    coupon_payment = bond.coupon_rate * bond.face_value / bond.coupon_freq
    



    # Create a list of coupon payments for each date in the schedule
    cash_flows = [coupon_payment] * len(bond.coupon_schedule)
    



    # Add the face value to the final cash flow
    cash_flows[-1] += bond.face_value
    
    return cash_flows




# Test the function
cf = get_cash_flows(example_bond)
print(f"Cash Flows: {cf}")



# Expected output: [25.0, 25.0, ..., 25.0, 1025.0]

This function correctly calculates the periodic coupon and adds the face value repayment to the final payment at maturity.

Step 2.3: Calculating Time and Discount Factors

This is the most nuanced part of the implementation. We need to calculate the exact time, in years, from our settlement_date to each future cash flow date. This is more complex than simply subtracting years, as we need to account for day-count conventions. The CodeArmo article uses the 30/360 convention, which assumes 30 days per month and 360 days per year. The yearfrac library handles this for us.

The time to each cash flow, , is then used to create a corresponding discount factor, . The formula for the discount factor, adjusted for payment frequency , is:

Notice that the exponent is the time in years () multiplied by the frequency (). This effectively converts the annual yield to a periodic yield and compounds it for the correct number of periods.

Let's implement the functions for time factors and discount factors as presented in the article.

Bond Price Calculator

This reading provides the Python code for calculating time factors and discount factors, which are essential for the present value calculation.

Study the time_factors function. Note how it uses yf.d30360e to get the fraction of a year between the settlement date and each coupon date, and then multiplies by coupon_freq. Next, study the discount_factors function. See how it first calls time_factors and then applies the discounting formula to each time factor.

Here is the implementation based on the resource:

def get_time_factors(settlement_date: tuple, coupon_schedule: list, coupon_freq: int) -> list[float]:
    """
    Calculates the time periods for discounting, adjusted for frequency.
    Uses the 30E/360 ISDA day-count convention.
    """
    time_factors = []
    for date in coupon_schedule:



        # yearfrac gives time in years. We multiply by frequency to get number of periods.
        time_factors.append(yf.d30360e(*settlement_date, *date, matu=False) * coupon_freq)
    return time_factors

def get_discount_factors(settlement_date: tuple, coupon_schedule: list, bond_yield: float, coupon_freq: int) -> list[float]:
    """Calculates the discount factors for each cash flow."""
    time_factors = get_time_factors(settlement_date, coupon_schedule, coupon_freq)
    periodic_yield = bond_yield / coupon_freq
    
    discount_factors = []
    for tf in time_factors:
        discount_factors.append(1 / (1 + periodic_yield) ** tf)
        
    return discount_factors




# Test the functions
df = get_discount_factors(settlement_date, example_bond.coupon_schedule, bond_yield, example_bond.coupon_freq)



# print(f"Discount Factors: {df}")

Step 2.4: Calculating Clean vs. Dirty Price

When we sum the present values of all future cash flows, we get the dirty price of the bond. This price includes any coupon interest that has accrued since the last coupon payment date but has not yet been paid.

However, bonds are typically quoted at their clean price, which excludes this accrued interest. The buyer pays the clean price to the seller, and then separately compensates the seller for the accrued interest.

Clean Price = Dirty Price - Accrued Interest

We need two more functions: one to calculate the dirty price and another to calculate the accrued interest.

Bond Price Calculator

This reading provides the functions for calculating the present value (dirty price) and the accrued interest.

Review the present_value function. It performs an element-wise product of cash flows and discount factors and sums the result. Examine the accrued_interest function. It calculates the fraction of the coupon period that has passed since the last coupon payment and multiplies it by the coupon amount.

Here is the implementation:

def calculate_dirty_price(bond: Bond, settlement_date: tuple, bond_yield: float) -> float:
    """Calculates the dirty price of a bond."""
    cash_flows = get_cash_flows(bond)
    discount_factors = get_discount_factors(settlement_date, bond.coupon_schedule, bond_yield, bond.coupon_freq)
    



    # Dirty price is the sum of discounted cash flows
    dirty_price = sum(cf * df for cf, df in zip(cash_flows, discount_factors))
    return dirty_price

def calculate_accrued_interest(bond: Bond, settlement_date: tuple) -> float:
    """Calculates the accrued interest for a bond."""
    coupon_payment = bond.coupon_rate * bond.face_value / bond.coupon_freq
    



    # Days from previous coupon to settlement date
    accrued_days = yf.d30360e(*bond.prev_coupon_date, *settlement_date, matu=True) * 360
    



    # Total days in the current coupon period
    days_in_period = yf.d30360e(*bond.prev_coupon_date, *bond.coupon_schedule[0], matu=True) * 360
    
    if days_in_period == 0:
        return 0.0
        
    accrued_interest = coupon_payment * (accrued_days / days_in_period)
    return accrued_interest

Note: The matu=True argument in yf.d30360e ensures the end date is included, which is typical for accrued interest calculations.

Step 2.5: The Final Pricing Function

Finally, we combine these components into a single, user-friendly function that calculates the bond's price.

def price_bond(bond: Bond, settlement_date: tuple, bond_yield: float, price_type: str = 'clean') -> float:
    """
    Calculates the clean or dirty price of a bond.

    Args:
        bond (Bond): The bond data structure.
        settlement_date (tuple): The date for which to price the bond (Y, M, D).
        bond_yield (float): The annual yield to maturity.
        price_type (str): 'clean' or 'dirty'. Defaults to 'clean'.

    Returns:
        float: The calculated bond price.
    """
    dirty_px = calculate_dirty_price(bond, settlement_date, bond_yield)
    
    if price_type.lower() == 'dirty':
        return round(dirty_px, 6)
    
    elif price_type.lower() == 'clean':
        accrued = calculate_accrued_interest(bond, settlement_date)
        clean_px = dirty_px - accrued
        return round(clean_px, 6)
    
    else:
        raise ValueError("price_type must be 'clean' or 'dirty'")




# Let's price our example bond!
clean_price = price_bond(example_bond, settlement_date, bond_yield, price_type='clean')
dirty_price = price_bond(example_bond, settlement_date, bond_yield, price_type='dirty')
accrued = calculate_accrued_interest(example_bond, settlement_date)

print(f"Yield: {bond_yield:.2%}")
print(f"Accrued Interest: {accrued:.6f}")
print(f"Dirty Price: {dirty_price:.6f}")
print(f"Clean Price: {clean_price:.6f}")




# Sanity check: Clean Price + Accrued Interest should equal Dirty Price
print(f"Check: {clean_price + accrued:.6f}")

With a 5% coupon and a 4% yield, we expect the price to be at a premium (above the face value of 1000), which our function should confirm.

For an alternative, more direct implementation that uses the closed-form annuity formula, you can look at the calculate_bondvalue function in the article "Bond Valuations Explained With Python". While simpler for a flat yield curve, it is less adaptable for the more complex pricing models we will explore later.


Conclusion

In this lesson, you have successfully translated bond pricing theory into a practical and modular Python implementation. We moved from the abstract formula to a concrete function capable of handling real-world details like coupon frequencies and day-count conventions.

Key takeaways from today's session:

  • The open-form (summation) pricing formula is more flexible for advanced applications than the closed-form annuity formula.
  • A robust pricing function can be built modularly by separating the logic for cash flows, time factors, and discount factors.
  • A bond's full price is its dirty price, which includes accrued interest. The quoted market price is typically the clean price.
  • Libraries like yearfrac are essential for handling financial date and time calculations with the necessary precision.

You now have a function that can calculate a bond's price from its yield. In our next lesson, we will use this function to explore the crucial inverse and non-linear relationship between a bond's price and its yield. We will visualize this relationship and understand its implications for risk management.