Skip to content

Commit

Permalink
Introduce the Fixed Income Class (WIP)
Browse files Browse the repository at this point in the history
  • Loading branch information
JerBouma committed Apr 21, 2024
1 parent 14261d8 commit f1b4667
Show file tree
Hide file tree
Showing 10 changed files with 1,687 additions and 456 deletions.
1 change: 1 addition & 0 deletions financetoolkit/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@
# flake8: noqa
from .toolkit_controller import Toolkit
from .economics.economics_controller import Economics
from .fixedincome.fixedincome_controller import FixedIncome
from .discovery.discovery_controller import Discovery
456 changes: 1 addition & 455 deletions financetoolkit/economics/economics_controller.py

Large diffs are not rendered by default.

Empty file.
355 changes: 355 additions & 0 deletions financetoolkit/fixedincome/bond_model.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,355 @@
"""Bond Model Module"""
import numpy as np


def get_bond_price(
par_value: float,
coupon_rate: float,
years_to_maturity: float,
yield_to_maturity: float,
frequency: int = 1,
):
"""
Calculate the price of a bond.
Args:
par_value (float): The face value of the bond.
coupon_rate (float): The annual coupon rate (in decimal).
years_to_maturity (int): The number of years until the bond matures.
yield_to_maturity (float): The yield to maturity of the bond (in decimal).
Returns:
float: The price of the bond.
"""
coupon_payment = (par_value * coupon_rate) / frequency
total_periods = int(years_to_maturity * frequency)
present_value: int | float = 0

# Calculate the present value of coupon payments
for t in range(1, total_periods + 1):
present_value += coupon_payment / ((1 + yield_to_maturity / frequency) ** t)

# Add the present value of the face value (at maturity)
present_value += par_value / ((1 + yield_to_maturity / frequency) ** total_periods)

return present_value


def get_current_yield(par_value: float, coupon_rate: float, bond_price: float):
"""
Calculate the current yield of a bond.
Args:
par_value (float): The face value of the bond.
coupon_rate (float): The annual coupon rate (in decimal).
bond_price (float): The current market price of the bond.
Returns:
float: The current yield of the bond.
"""
current_yield = (coupon_rate * par_value) / bond_price

return current_yield


def get_yield_to_maturity(
par_value: float,
coupon_rate: float,
years_to_maturity: float,
bond_price: float,
frequency: int,
guess: float = 0.05,
tolerance: float = 0.0001,
max_iterations: int = 100,
):
"""
Calculate the yield to maturity of a bond using the secant method.
Args:
par_value (float): The face value of the bond.
coupon_rate (float): The annual coupon rate (in decimal).
years_to_maturity (int): The number of years until the bond matures.
bond_price (float): The current market price of the bond.
frequency (int): The number of coupon payments per year.
guess (float): Initial guess for the yield to maturity.
tolerance (float): The desired level of accuracy.
max_iterations (int): Maximum number of iterations to perform.
Returns:
float: The yield to maturity of the bond.
"""

# Define the function to solve
def bond_value(ytm):
value = 0
total_periods = int(years_to_maturity * frequency)
for t in range(1, total_periods + 1):
value += coupon_rate * par_value / frequency / ((1 + ytm / frequency) ** t)
value += par_value / ((1 + ytm / frequency) ** total_periods)
return value - bond_price

# Initial values
ytm0 = guess
ytm1 = guess * 1.1 # Slightly higher guess for the secant method

# Iterative process using the secant method
for _ in range(max_iterations):
ytm_next = ytm1 - bond_value(ytm1) * (ytm1 - ytm0) / (
bond_value(ytm1) - bond_value(ytm0)
)
if abs(ytm_next - ytm1) < tolerance:
return ytm_next
ytm0 = ytm1
ytm1 = ytm_next

# If the method fails to converge
return np.nan


def get_macaulays_duration(
par_value, coupon_rate, years_to_maturity, yield_to_maturity, frequency
):
"""
Calculate Macaulay's duration of a bond.
Args:
par_value (float): The face value of the bond.
coupon_rate (float): The annual coupon rate (in decimal).
years_to_maturity (float): The number of years until the bond matures.
yield_to_maturity (float): The yield to maturity of the bond (in decimal).
frequency (int): The number of coupon payments per year.
Returns:
float: The Macaulay's duration of the bond.
"""
total_periods = int(years_to_maturity * frequency)
present_value_sum = 0
cash_flow_weighted_sum = 0

# Calculate present value of each cash flow and the sum of present values
for t in range(1, total_periods + 1):
coupon_payment = (par_value * coupon_rate) / frequency
present_value = coupon_payment / (
(1 + yield_to_maturity / frequency) ** (t / frequency)
)
present_value_sum += present_value
cash_flow_weighted_sum += (t / frequency) * present_value

# Add the present value of the face value (at maturity)
present_value_sum += par_value / (
(1 + yield_to_maturity / frequency) ** years_to_maturity
)
cash_flow_weighted_sum += years_to_maturity * (
par_value / ((1 + yield_to_maturity / frequency) ** years_to_maturity)
)

# Calculate Macaulay's duration
macaulays_duration = cash_flow_weighted_sum / present_value_sum

return macaulays_duration


def get_modified_duration(
par_value, coupon_rate, years_to_maturity, yield_to_maturity, frequency
):
"""
Calculate modified duration of a bond.
Args:
par_value (float): The face value of the bond.
coupon_rate (float): The annual coupon rate (in decimal).
years_to_maturity (float): The number of years until the bond matures.
yield_to_maturity (float): The yield to maturity of the bond (in decimal).
frequency (int): The number of coupon payments per year.
Returns:
float: The modified duration of the bond.
"""
macaulays_duration = get_macaulays_duration(
par_value, coupon_rate, years_to_maturity, yield_to_maturity, frequency
)

# Calculate modified duration
modified_duration = macaulays_duration / (1 + yield_to_maturity / frequency)

return modified_duration


def get_effective_duration(
par_value,
coupon_rate,
years_to_maturity,
yield_to_maturity,
frequency,
yield_change=0.01,
):
"""
Calculate the effective duration of a bond.
Args:
par_value (float): The face value of the bond.
coupon_rate (float): The annual coupon rate (in decimal).
years_to_maturity (float): The number of years until the bond matures.
yield_to_maturity (float): The initial yield to maturity of the bond (in decimal).
yield_change (float): The change in yield (in decimal).
frequency (int): The number of coupon payments per year.
Returns:
float: The effective duration of the bond.
"""
# Calculate bond price at initial yield
initial_price = get_bond_price(
par_value, coupon_rate, years_to_maturity, yield_to_maturity, frequency
)

# Calculate bond price at yield change
new_yield = yield_to_maturity + yield_change
new_price = get_bond_price(
par_value, coupon_rate, years_to_maturity, new_yield, frequency
)

# Calculate effective duration
effective_duration = -((new_price - initial_price) / (initial_price * yield_change))

return effective_duration


def get_dollar_duration(
par_value, coupon_rate, years_to_maturity, yield_to_maturity, frequency
):
"""
Calculate the bond's dollar duration.
Args:
par_value (float): The face value of the bond.
coupon_rate (float): The annual coupon rate (in decimal).
years_to_maturity (float): The number of years until the bond matures.
yield_to_maturity (float): The yield to maturity of the bond (in decimal).
frequency (int): The number of coupon payments per year.
Returns:
float: The dollar duration of the bond.
"""
# Calculate modified duration
modified_duration = get_modified_duration(
par_value, coupon_rate, years_to_maturity, yield_to_maturity, frequency
)

# Calculate bond price
bond_price = get_bond_price(
par_value, coupon_rate, years_to_maturity, yield_to_maturity, frequency
)

# Calculate dollar duration
dollar_duration = modified_duration * bond_price / 100

return dollar_duration


def get_dv01(par_value, coupon_rate, years_to_maturity, yield_to_maturity, frequency):
"""
Calculate DV01 (Dollar Value of 01) of a bond.
Args:
par_value (float): The face value of the bond.
coupon_rate (float): The annual coupon rate (in decimal).
years_to_maturity (float): The number of years until the bond matures.
yield_to_maturity (float): The yield to maturity of the bond (in decimal).
frequency (int): The number of coupon payments per year.
Returns:
float: The DV01 of the bond.
"""
# Calculate present value of each cash flow
total_periods = int(years_to_maturity * frequency)
present_value_sum = 0

for t in range(1, total_periods + 1):
coupon_payment = (par_value * coupon_rate) / frequency
present_value = coupon_payment / (
(1 + yield_to_maturity / frequency) ** (t / frequency)
)
present_value_sum += present_value

# Add the present value of the face value (at maturity)
present_value_sum += par_value / (
(1 + yield_to_maturity / frequency) ** years_to_maturity
)

# Calculate bond price when yield decreases by 1 basis point
yield_decreased = yield_to_maturity - 0.0001 # 1 basis point decrease
present_value_sum_decreased = 0

for t in range(1, total_periods + 1):
coupon_payment = (par_value * coupon_rate) / frequency
present_value = coupon_payment / (
(1 + yield_decreased / frequency) ** (t / frequency)
)
present_value_sum_decreased += present_value

# Add the present value of the face value (at maturity)
present_value_sum_decreased += par_value / (
(1 + yield_decreased / frequency) ** years_to_maturity
)

# Calculate bond price when yield increases by 1 basis point
yield_increased = yield_to_maturity + 0.0001 # 1 basis point increase
present_value_sum_increased = 0

for t in range(1, total_periods + 1):
coupon_payment = (par_value * coupon_rate) / frequency
present_value = coupon_payment / (
(1 + yield_increased / frequency) ** (t / frequency)
)
present_value_sum_increased += present_value

# Add the present value of the face value (at maturity)
present_value_sum_increased += par_value / (
(1 + yield_increased / frequency) ** years_to_maturity
)

# Calculate the change in bond price for a 1 basis point change in yield
dv01 = -0.01 * (present_value_sum_increased - present_value_sum_decreased) / 2

return dv01


def get_convexity(
par_value, coupon_rate, years_to_maturity, yield_to_maturity, frequency
):
"""
Calculate the convexity of a bond.
Args:
par_value (float): The face value of the bond.
coupon_rate (float): The annual coupon rate (in decimal).
years_to_maturity (float): The number of years until the bond matures.
yield_to_maturity (float): The yield to maturity of the bond (in decimal).
frequency (int): The number of coupon payments per year.
Returns:
float: The convexity of the bond.
"""
# Calculate bond price at current yield
bond_price = get_bond_price(
par_value, coupon_rate, years_to_maturity, yield_to_maturity, frequency
)

# Calculate bond price when yield decreases by 1 basis point
yield_decreased = yield_to_maturity - 0.0001 # 1 basis point decrease
bond_price_down = get_bond_price(
par_value, coupon_rate, years_to_maturity, yield_decreased, frequency
)

# Calculate bond price when yield increases by 1 basis point
yield_increased = yield_to_maturity + 0.0001 # 1 basis point increase
bond_price_up = get_bond_price(
par_value, coupon_rate, years_to_maturity, yield_increased, frequency
)

# Calculate convexity
convexity = (bond_price_up + bond_price_down - 2 * bond_price) / (
bond_price * 0.0001**2
)

return convexity
Loading

0 comments on commit f1b4667

Please sign in to comment.