Fixed Income with Python - 1

Basics of fixed income instruments.
finance
python
fixed income
quantlib
Published

October 26, 2020

Features of Fixed Income Securities

Fixed-income instruments are debt instruments, such as loans and bonds. Loans are debt instruments formed and governed by a private agreement usually between an individual or company and a financial intermediary, such as a bank. Bonds or fixed-income securities are more standardized contractual agreements between larger issuers and investors. A bond issuer borrows money most often to fund operations or capital expenditures. Bond investors are lenders who provide funds to the issuer in exchange for interest payments and future repayment of principal. Not all liabilities are fixed-income instruments (or “debt”), but all fixed-income instruments are liabilities. In this work, from the perspective of a corporate issuer, we are focused only on loans and bonds: instruments that can be settled in cash and for which the counterparty is an investor or a bank.

Brief Summary of Terms of the BRWA Bond
Issuer Bright Wheels Automotive Corporation
Settlement Date T + 3 Business Days
Maturity Date Five Years from Settlement Date
Principal Amount US$ 300 million
Interest 3.2% fixed coupon
Interest Payment Commencing six months from Settlement Date to be paid semiannually with final payment on Maturity Date
Seniority The Notes are unsecured and unsubordinated obligations of BRWA Corporation and will rank pari passu with all other unsecured and unsubordinated indebtedness
Business Days New York

Issuer

A bond issuer can be any legal entity and is liable for all interest and principal payments.

Maturity

A bond’s maturity is the date of the final payment the issuer makes to investors, and the tenor refers to the remaining time to maturity. Fixed-income securities with a tenor one year or less at issuance are known as money market securities. Bonds with tenors longer than one year at issuance are called capital market securities.

Principal (Par or Face Value)

The principal, par value, or face value is the amount an issuer agrees to repay to investors at maturity. In the BRWA Corporation example, the principal amount of USD300 million is repaid at maturity, which is five years from issuance.

Coupon Rate and Frequency

A bond’s interest can be paid as
■ a fixed coupon paid on specified dates,
■ a variable coupon determined and paid on specified dates, or
■ part of a single payment with the principal at maturity.
Fixed-coupon bond payments usually involve uniform payments at monthly, quarterly, semi-annual, or annual intervals. Corporate bonds tend to pay semiannually. Bonds with variable interest payments are called floating-rate notes (FRNs). An FRN coupon is determined as a combination of a market reference rate (MRR) and an issuer-specific spread referred to as the credit spread. Bonds that do not pay periodic interest and instead pay interest as part of a single payment with principal at maturity are termed zero-coupon bonds or pure discount bonds. Zero-coupon bonds are typically issued at a discount to par; the difference between the issuance price and par value represents a cumulative interest payment at maturity.

Seniority

A debt issue’s seniority or priority of repayment among all issuer obligations is an important determinant of risk. Senior debt has priority over other debt claims in the case of bankruptcy or liquidation. Junior debt, or subordinated debt, claims have a lower priority than senior debt and are paid only once senior claims are satisfied.

Contingency Provisions

A contingency provision is a clause in a legal agreement that allows for an action if an event or circumstance occurs. The most common contingency provision for bonds involves embedded options—specifically, call, put, and conversion to equity options. These resemble option contracts but cannot be traded separately from the bond itself. However, the value of these embedded options may be established by comparing the value of a bond with a contingency provision with that of an otherwise similar standard bond from the same issuer.

At the time of issuance, investors purchase the bond and pay cash to BRWA Corporation equal to the USD300 million in par value. BRWA investors receive periodic interest payments as follows:

  • Annual interest expense = Bond par value \(\times\) Coupon rate = USD300,000,000 \(\times\) 3.2% = USD9,600,000.

  • Semiannual interest expense = USD9,600,000/2 = USD4,800,000.

At the end of the fifth year on the maturity date, BRWA Corporation pays investors the final semiannual coupon payment plus the par value of USD300 million for a total of USD304.8 million.

Yield Measures

Given a bond’s expected cash flows and its price, return or yield measures can be calculated. One simple measure is the current yield (CY), equal to the bond’s annual coupon divided by the bond’s price and expressed as a percentage. For example, if the five-year BRWA bond were trading at a price of USD101 per USD100 in face value at time \(t\), its current yield would be

\(CY_{t}\) = \(Annual\ coupon_{t}\) / \(Bond\ price_{t}\) = \(\frac{3.2\%}{1.01} = 3.168\%\).

The current yield is analogous to the dividend yield for an equity security. A more complex but far more common yield measure is the yield-to-maturity (YTM), which is the internal rate of return (IRR) calculated using the bond’s price and its expected cash flows to maturity. YTM is usually quoted as an annual rate. An investor’s rate of return on a bond will equal the bond’s YTM at the time of purchase as long as the investor

  1. receives all promised interest and principal payments as scheduled (i.e., no default),

  2. holds the bond until maturity, and

  3. reinvests all periodic cash flows at the YTM.

If any of these assumptions do not hold, the investor’s rate of return on the bond investment will differ from the YTM. If the five-year BRWA bond were trading at a price of USD101 per USD100 in face value immediately after issuance, its YTM is the rate, \(r\), in the following equation (recall that the 3.2% coupon is paid semiannually, or 1.6 in each of 10 periods per USD100 in face value): \(101 = 1.6/(1 + r)^1 + 1.6/(1 + r)^2 + ... + 101.6/(1 + r)^10.\) \(r = 1.49\%\) on a semiannual basis, or \(1.49\% \times 2 = 2.98\%\) annualized. The Quantlib code to calculate YTM is given below:

import QuantLib as ql

# Market conventions
calendar = ql.UnitedStates(ql.UnitedStates.NYSE)
settlement_days = 2
day_count = ql.Thirty360(ql.Thirty360.BondBasis)  # Using the Bond Basis variant
face_value = 100

# Bond details
issue_date = ql.Date(1, 1, 2020)
maturity_date = ql.Date(1, 1, 2025)
coupon_rate = 0.032
frequency = ql.Semiannual
price = 101  # Current bond price

# Create the bond
schedule = ql.Schedule(issue_date, maturity_date, ql.Period(frequency), calendar,
                       ql.Unadjusted, ql.Unadjusted, ql.DateGeneration.Backward, False)
bond = ql.FixedRateBond(settlement_days, face_value, schedule, [coupon_rate], day_count)


yield_rate = bond.bondYield(price, day_count, ql.Compounded, ql.Semiannual, issue_date) 
print(yield_rate)

Fixed Income Cash Flow Structures

The most common bond cash flow structure is that of a standard fixed-coupon bond, often referred to as a bullet bond. The bond issuer receives the principal at settlement, makes periodic, fixed coupon payments, and repays the principal at maturity.

The Quantlib code to calculate cashflows is given below:

import QuantLib as ql
import pandas as pd

# Define the bond and its attributes as before:
calendar = ql.UnitedStates(ql.UnitedStates.NYSE)
settlement_days = 2
day_count = ql.Thirty360(ql.Thirty360.BondBasis)
face_value = 300000000
issue_date = ql.Date(1, 1, 2020)
maturity_date = ql.Date(1, 1, 2025)
coupon_rate = 0.032
frequency = ql.Semiannual

schedule = ql.Schedule(issue_date, maturity_date, ql.Period(frequency), calendar,
                       ql.Unadjusted, ql.Unadjusted, ql.DateGeneration.Backward, False)
bond = ql.FixedRateBond(settlement_days, face_value, schedule, [coupon_rate], day_count)

# Extracting the cash flows:
cashflows = list(bond.cashflows())

dates = [cf.date() for cf in cashflows]
amounts = [cf.amount() for cf in cashflows]

# Convert the dates and amounts to a pandas DataFrame:
df = pd.DataFrame({
    'Date': dates,
    'Amount': amounts
})
print(df)

Amortizing Debt

Fixed-income instruments that periodically retire a portion of principal outstanding prior to maturity offer a borrower the ability to spread payments more evenly over the life of the instrument. Examples include commercial or residential real estate mortgage loans. Investors receive higher near-term cash flows on this amortizing debt relative to bullet bonds and face lower credit risk because the borrower’s liability is reduced over time. However, investors also face the risk of reinvesting the higher cash flows at prevailing market interest rates over the life of the instrument, which could decline.

import QuantLib as ql
import pandas as pd
import math

def calculate_periodic_payment(face_value, rate, frequency, n_periods, balloon_payment=0):
    if frequency == ql.Annual:
        adjusted_rate = rate
    elif frequency == ql.Semiannual:
        adjusted_rate = rate / 2
    elif frequency == ql.Quarterly:
        adjusted_rate = rate / 4
    else:
        raise ValueError("Unsupported frequency")
    
    if balloon_payment > 0:
        annuity = (face_value - (balloon_payment/math.pow(1 + adjusted_rate, n_periods)))*adjusted_rate/(1 - math.pow(1 + adjusted_rate, -n_periods))
    else:
        annuity = face_value * adjusted_rate / (1 - math.pow(1 + adjusted_rate, -n_periods))
    return annuity

def create_amortized_cashflows(face_value, coupon_rate, issue_date, maturity_date, frequency, calendar, day_count, settlement_days, balloon_payment=0):

    schedule = ql.Schedule(issue_date, maturity_date, ql.Period(frequency), calendar,
                           ql.Unadjusted, ql.Unadjusted, ql.DateGeneration.Backward, False)
    
    n_periods = len(set(schedule))-1
    periodic_payment = calculate_periodic_payment(face_value, coupon_rate, frequency, n_periods, balloon_payment)
    cashflows = []
    principal_remaining = face_value

    for i in range(n_periods):
        interest_payment = principal_remaining * (coupon_rate / (2 if frequency == ql.Semiannual else (1 if frequency == ql.Annual else 4)))
        principal_payment = periodic_payment - interest_payment
        cashflows.append(periodic_payment)
        principal_remaining -= principal_payment

    cashflows[-1] += balloon_payment
    df = pd.DataFrame({
    'Date': [date for date in schedule][1:],
    'Amount': cashflows     
    })
    return df

# Define the bond attributes:
calendar = ql.UnitedStates(ql.UnitedStates.NYSE)
settlement_days = 2
day_count = ql.Thirty360(ql.Thirty360.BondBasis)
face_value = 300000000
issue_date = ql.Date(1, 1, 2020)
maturity_date = ql.Date(1, 1, 2025)
coupon_rate = 0.032
frequency = ql.Semiannual
balloon_payment = 100000000  # Example value

cashflows = create_amortized_cashflows(face_value, coupon_rate, issue_date, maturity_date, frequency, calendar, day_count, settlement_days, balloon_payment=150000000)

print(cashflows)

The cash flows are as follows:

Cash Flow Summary
Date Bullet Bond Partially Amortizing Bond Fully Amortizing Bond
July 1st, 2020 4800000.0 1.875142e+07 3.270283e+07
January 4th, 2021 4800000.0 1.875142e+07 3.270283e+07
July 1st, 2021 4800000.0 1.875142e+07 3.270283e+07
January 3rd, 2022 4800000.0 1.875142e+07 3.270283e+07
July 1st, 2022 4800000.0 1.875142e+07 3.270283e+07
January 3rd, 2023 4800000.0 1.875142e+07 3.270283e+07
July 3rd, 2023 4800000.0 1.875142e+07 3.270283e+07
January 2nd, 2024 4800000.0 1.875142e+07 3.270283e+07
July 1st, 2024 4800000.0 1.875142e+07 3.270283e+07
January 2nd, 2025 4800000.0 1.875142e+07 3.270283e+07
January 2nd, 2025 300000000.0 1.687514e+08 3.270283e+07

Fixed Income Bond Valuation: Prices and Yields

Bond pricing is an application of discounted cash flow analysis that depends on a bond’s cash flow features and the rate (or rates) used for discounting. Recall from an earlier lesson that the price of the bond at issuance is the present value of the promised interest and principal cash flows. The market discount rate is used in the time-value-of-money calculation to obtain the present value. The market discount rate is the rate of return required by investors given the risk of the bond investment. It is also called the required yield, or the required rate of return.

The code for calculating the bond price using a fixed market rate of return (1.2% semi annual) is given below:

import QuantLib as ql

# Market conventions
calendar = ql.UnitedStates(ql.UnitedStates.NYSE)
settlement_days = 0
day_count = ql.Thirty360(ql.Thirty360.BondBasis)  # Using the Bond Basis variant
face_value = 100

# Bond details
issue_date = ql.Date(1, 1, 2020)
maturity_date = ql.Date(1, 1, 2025)
coupon_rate = 0.032
frequency = ql.Semiannual

# Create the bond
schedule = ql.Schedule(issue_date, maturity_date, ql.Period(frequency), calendar,
                       ql.Unadjusted, ql.Unadjusted, ql.DateGeneration.Backward, False)
bond = ql.FixedRateBond(settlement_days, face_value, schedule, [coupon_rate], day_count)
yield_curve = ql.FlatForward(ql.Date(1, 1, 2020), 0.024, day_count)
handle = ql.YieldTermStructureHandle(yield_curve)
bond.setPricingEngine(ql.DiscountingBondEngine(handle))
print(bond.NPV())

Flat Price, Accrued Interest, and the Full Price

When a bond is priced between coupon payment dates, its price has two components: the flat price (\(PV^{Flat}\)) and the accrued interest (AI). The sum of the parts is the full price (\(PV^{Full}\)), which also is known as the invoice or “dirty” price. \[PV^{Full} = PV^{Flat} + AI\] The flat price, which is the full price minus the accrued interest, is also called the quoted or “clean” price and is the type of price usually quoted by bond dealers. If a trade takes place, the accrued interest is added to the flat price to obtain the full price paid by the buyer and received by the seller on the trade settlement date. The trade settles when the bond buyer pays and the seller delivers the security. The flat price is used for quotations to avoid misleading investors about a bond’s market price trend. If full prices were quoted by dealers, investors would see price rises each day even if the YTM did not change due to the accrual of interest. The accrued interest is the portion of the next coupon payment owed to the seller of a bond, because although the seller held the bond for a partial coupon period, the full coupon will be received by the buyer. To calculate accrued interest, we determine the fractional amount by counting the days in the period. If the coupon period has \(T\) days between payment dates and t days have passed since the last payment, the accrued interest is calculated using the equation below: \[AI = \frac{t}{T} \times PMT\] where

\(t\) = number of days from the prior coupon payment to the settlement date

\(T\) = number of days in the coupon period

\(\frac{t}{T}\) = fraction of coupon period that has passed since the prior payment

\(PMT\) = coupon payment per period

Note that the accrued interest portion of the full price does not depend on the yield-to-maturity. Therefore, it is only the flat price that is affected by a change in interest rates. There are multiple approaches for day counting (or “day counts”). The day count is specified by two parameters: how days are counted within a given period and the number of days assumed per period. Two common day count conventions are 30/360 and actual/actual. The 30/360 day count assumes each month has exactly 30 days (although most don’t) and each year has exactly 360 days (although none do). In contrast, the actual/actual day count convention assumes the actual number of days in each month and the actual number of days in a year.

Consider two otherwise identical bonds that pay 4.375% semiannual interest on 15 May and 15 November of each year. One is a government bond with an actual/actual day count and the other is a corporate bond with a 30/360 day count. The actual/actual method uses the actual number of days, including weekends, holidays, and leap days. The 30/360 method assumes 30 days in a month and 360 days in each year. Calculate accrued interest for settlement on 27 June for both bonds.

The code to calculate the accrued interest in both the above scenarios is given below:

import QuantLib as ql

# Define parameters
face_value = 100  # Typically 100 or 1000 for bonds
coupon_rate = 0.04625  # Example coupon rate of 5%
start_date = ql.Date(15, 5, 2020)
end_date = ql.Date(15, 11, 2020)
settlement_date = ql.Date(27, 6, 2020)


def actual_actual_bond_year_fraction(start_date, end_date, frequency):
    # Getting the previous and next coupon dates relative to the start_date
    period = ql.Period(frequency)
    previous_coupon_date = start_date - period
    next_coupon_date = start_date + period
    
    # If end_date is within the current coupon period
    if end_date <= next_coupon_date:
        days_in_period = next_coupon_date - previous_coupon_date
        days_elapsed = end_date - start_date
        return days_elapsed/days_in_period
    
    # If end_date spans multiple coupon periods
    else:
        # Days in the first partial coupon period
        first_period_fraction = (next_coupon_date - start_date) / (next_coupon_date - previous_coupon_date)
        
        # Days in the last partial coupon period
        last_period_fraction = (end_date - next_coupon_date) / period.length()
        
        # Full periods between start_date and end_date
        full_periods = (end_date - next_coupon_date) // period.length()
        
        return first_period_fraction + full_periods + last_period_fraction


# Accrued Interest using Actual/Actual day count
actual_actual_day_count = ql.ActualActual(ql.ActualActual.ISDA)
t = actual_actual_day_count.yearFraction(start_date, settlement_date)
T = actual_actual_day_count.yearFraction(start_date, end_date)
actual_actual_accrued_interest = (t/T) * (coupon_rate/2) * face_value


t = actual_actual_bond_year_fraction(start_date, settlement_date, frequency)
T = actual_actual_bond_year_fraction(start_date, end_date, frequency)
actual_actual_accrued_interest2 = (t/T) * (coupon_rate/2) * face_value


# Accrued Interest using 30/360 day count
thirty_360_day_count = ql.Thirty360(ql.Thirty360.USA)
t = thirty_360_day_count.yearFraction(start_date, settlement_date)
T = thirty_360_day_count.yearFraction(start_date, end_date)
thirty_360_accrued_interest = (t/T)* (coupon_rate/2) * face_value

print(f"Accrued Interest (Actual/Actual): {actual_actual_accrued_interest:.6f}")
print(f"Accrued Interest (Actual/Actual)-2: {actual_actual_accrued_interest2:.6f}")
print(f"Accrued Interest (30/360): {thirty_360_accrued_interest:.6f}")
Back to top