├── README.md ├── data └── MCF21_FIC_Final_Exam_Group_5.xlsx ├── notebooks ├── Assignment 1 (Bond Pricing).ipynb ├── Assignment 2 (Yield Curve Fitting).ipynb ├── Assignment 3 (Principal Component Analysis).ipynb ├── Assignment 4 (Bond Returns Predictability).ipynb └── Assignment 5 (Fixed Income Derivatives).ipynb └── scripts └── Bond_Utils.txt /README.md: -------------------------------------------------------------------------------- 1 | # Fixed Income and Credit 2 | 3 | Docs: https://mcf-long-short.github.io/fixed-income-and-credit/ 4 | 5 | This repository represents group project work for course in `Fixed Income and Credit` for advanced degree [Masters in Computational Finance, Union University](http://mcf.raf.edu.rs/). 6 | 7 | ## Introduction 8 | 9 | The objective of this project is to develop understanding of the concept of time value of money, fixed income securities and markets as well as interest rate derivatives. 10 | We study valuation and hedging using these instruments and discuss how these methods are used in practice. Furthermore, we demonstrate how to 11 | apply different models for construction of yield curves, valuation and hedging using interest rate derivatives and estimating expected bond returns. 12 | 13 | Starting point for analysis of all the previously mention things is the dataset that contains: 14 | - Yield_Curve sheet: `monthly yield data`, for period from `1/31/2005 - 5/31/2021`, containing yields, for bonds from `4m tenor up to 30y`. 15 | - PCA sheet: `monthly sport rates for ZBCs (zero-coupon bonds)`, for period from `1/31/2000 - 5/31/2021`, for bonds of `1y, 2y, 3y, 4, and 5y maturities`. 16 | - Fama_Bliss sheet: `monthly prices of coupon-bearing bonds`, for period from `1/31/1964 - 12/31/2020`, for bonds of `1y, 2y, 3y, 4, and 5y maturities`. 17 | 18 | 19 | ## Project phases 20 | 21 | Each of the project phases has been logically separated based on things we're analysing and modeling. Every phase has a detailed description of all the steps, 22 | implementation details, intuition for modeling, interpretation of data analysis, modeling and evaluation that was performed. 23 | 24 | For each project phase there is a separate Jupyter Notebook file. You can view the nodebook directhly here on the github (some cells might not be rendered correctly), under [/notebooks](https://github.com/mcf-long-short/fixed-income-and-credit/tree/main/notebooks) folder, or you can copy the notebook link from github to view them with [Jupyter nbviewer](https://nbviewer.jupyter.org/). 25 | 26 | `Note`: Jupyter's nbviewer will render output much better than the Github, but for both of them 3D plots and some other things won't be rendered. It's better to download the notebook files and open them with Google Collab, or use the following links to view them on a shared GDrive, after loggin in to your Google account: 27 | - [Bond Pricing](https://colab.research.google.com/drive/1g6bqabvDTBxhwSr688x2PICEoWXZgYlJ?usp=sharing) 28 | - [Yield Curve Fitting](https://colab.research.google.com/drive/1PxrPSo232JYWVHOPHN2cz21S1NRQFF1y?usp=sharing) 29 | - [Principal Component Analysis](https://colab.research.google.com/drive/1XcRkJK91YOhFtfczAEce1CL7VY3y5ebt?usp=sharing) 30 | - [Bond Returns Predictability](https://colab.research.google.com/drive/14nF3Dbs0LG5o2BmkfMz4_b9sr9AplyBg?usp=sharing) 31 | - [Fixed Income Derivatives](https://colab.research.google.com/drive/1k1h5Ea8zGgQqeE0kYHNaJSIsN_FXuR8E?usp=sharing) 32 | 33 | ## Running Jupyter Notebook 34 | We recommend viewing and running notebook files with [Google Collab](https://colab.research.google.com/notebooks/intro.ipynb?utm_source=scs-index), 35 | so you won't have to manage any of the python requirements compared with running them locally. 36 | -------------------------------------------------------------------------------- /data/MCF21_FIC_Final_Exam_Group_5.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mcf-long-short/fixed-income-and-credit/07542c9d3622d44afe26e3073537458cf1890799/data/MCF21_FIC_Final_Exam_Group_5.xlsx -------------------------------------------------------------------------------- /scripts/Bond_Utils.txt: -------------------------------------------------------------------------------- 1 | # Python standard library 2 | from dataclasses import dataclass 3 | from typing import List 4 | from typing import Union 5 | 6 | # Third party libraries 7 | import numpy as np 8 | 9 | 10 | def coerce_list(r: float, size: int = 1) -> List[float]: 11 | """Creates list of given size with elements of the same value""" 12 | if isinstance(r, list) or isinstance(r, np.ndarray): 13 | return r 14 | return [r] * size 15 | 16 | 17 | 18 | @dataclass 19 | class CashFlow: 20 | """CashFlow utility class for manipulating with cash flows""" 21 | value: float 22 | 23 | def __add__(self, value: float) -> "CashFlow": 24 | return CashFlow(self.value + value) 25 | 26 | def pv(self, r: float, n: float, m: int = -1) -> float: 27 | """Calcualtes present value of the cash flow 28 | 29 | Args: 30 | r (float): interest rate 31 | n (float): number of years 32 | m (int): number of compounding years. Default value if -1 for 33 | continous compounding 34 | 35 | Returns: present value 36 | """ 37 | 38 | if m == -1: 39 | # Continuous compounding 40 | return self.value * np.exp(-r*n) 41 | else: 42 | # Discrete compounding 43 | return self.value / (1 + r/m)**(n*m) 44 | 45 | 46 | @dataclass 47 | class Bond: 48 | """Bond class for common bond calculations 49 | 50 | Attributes: 51 | fv (float): Face value 52 | c (float): Coupon rate 53 | t (int): Maturity (in years) 54 | pf (int): Coupon payment frequency. Number of time per year coupon payment 55 | is made. Default value of 1 represents annual coupon payment 56 | cf (int): Cash flow compounding frequency. Nuber of time per year bond cash 57 | flows are discounted. Value of -1 represent continuous componding 58 | """ 59 | fv: float 60 | c: float 61 | t: int 62 | pf: int = 1 63 | name: str = "" 64 | cf: int = 0 65 | 66 | def __post_init__(self): 67 | """If name is not provided create generic bond name from template""" 68 | if not self.name: 69 | self.name = f'Bond {self.fv}/{self.c}/{self.t}' 70 | if self.cf == 0: 71 | self.cf = self.pf 72 | 73 | def cash_flows(self) -> List[CashFlow]: 74 | """Calculates bond's cash flows""" 75 | 76 | # Coupon payments 77 | cash_flows = [CashFlow((self.c * self.fv) / self.pf) for t in range(self.t * self.pf)] 78 | 79 | # Face value at maturity 80 | cash_flows[-1] += self.fv 81 | 82 | return cash_flows 83 | 84 | def price(self, r: Union[float, List[float]]) -> float: 85 | """Calculates bond's theoretical price""" 86 | 87 | # Interest rates 88 | r = coerce_list(r, len(self.cash_flows())) 89 | 90 | price = 0 91 | time = 0 92 | for i, cf in enumerate(self.cash_flows()): 93 | time += 1 / self.pf 94 | price += cf.pv(r=r[i], n=time, m=self.cf) 95 | 96 | return price 97 | 98 | def duration(self, y: float, duration_type: str = "macaulay") -> float: 99 | """Calculates bodn's duration - price sensitivity with the ragards to the yiels""" 100 | 101 | cfs = self.cash_flows() 102 | p = self.price(r=y) 103 | cf_sum = 0 104 | time = 0 105 | 106 | def macaulay(cfs, p, cf_sum, time): 107 | for cf in cfs: 108 | time += 1 / self.pf 109 | cf_sum += time * cf.pv(r=y, n=time, m=self.cf) / p 110 | return cf_sum 111 | 112 | def dollar(cfs, p, cf_sum, time): 113 | for cf in cfs: 114 | time += 1 / self.pf 115 | cf_sum += time * cf.pv(r=y, n=time, m=self.cf) 116 | if self.cf == -1: 117 | # Continuous compounding 118 | return -1 * cf_sum 119 | else: 120 | # Discrete comounding (self.cf times a year) 121 | return - 1/(1 + y/self.cf) * cf_sum 122 | 123 | def modified(cfs, p, cf_sum, time): 124 | for cf in cfs: 125 | time += 1 / self.pf 126 | cf_sum += time * cf.pv(r=y, n=time, m=self.cf) / p 127 | if self.cf == -1: 128 | # Continuous compounding: MD == D 129 | return cf_sum 130 | else: 131 | # Discrete comounding (self.cf times a year) 132 | return 1/(1 + y/self.cf) * cf_sum 133 | 134 | if duration_type == "dollar": 135 | return dollar(cfs, p, cf_sum, time) 136 | elif duration_type == "modified": 137 | return modified(cfs, p, cf_sum, time) 138 | elif duration_type == "macaulay": 139 | return macaulay(cfs, p, cf_sum, time) 140 | 141 | def convexity(self, y: float, convexity_type = "relative") -> float: 142 | """Calculated bond's convexity - duration sensitivity with the regards to the yields""" 143 | 144 | cfs = self.cash_flows() 145 | p = self.price(r=y) 146 | cf_sum = 0 147 | time = 0 148 | 149 | def relative(cfs, p, cf_sum, time): 150 | if self.cf == -1: 151 | # Continuous compounding 152 | for cf in cfs: 153 | time += 1 / self.pf 154 | cf_sum += (time **2) * cf.pv(r=y, n=time, m=self.cf) / p 155 | return cf_sum 156 | else: 157 | # Discrete comounding (self.cf times a year) 158 | for cf in cfs: 159 | time += 1 / self.pf 160 | cf_sum += time * (time + 1/self.cf) * cf.pv(r=y, n=time, m=self.cf) / p 161 | return 1/(1 + y/self.cf)**2 * cf_sum 162 | 163 | def dollar(cfs, p, cf_sum, time): 164 | if self.cf == -1: 165 | # Continuous compounding 166 | for cf in cfs: 167 | time += 1 / self.pf 168 | cf_sum += (time **2) * cf.pv(r=y, n=time, m=self.cf) 169 | else: 170 | # Discrete comounding (self.cf times a year) 171 | for cf in cfs: 172 | time += 1 / self.pf 173 | cf_sum += time * (time + 1/self.cf) * cf.pv(r=y, n=time, m=self.cf) 174 | return 1/(1 + y/self.cf)**2 * cf_sum 175 | 176 | if convexity_type == "relative": 177 | return relative(cfs, p, cf_sum, time) 178 | elif convexity_type == "dollar": 179 | return dollar(cfs, p, cf_sum, time) 180 | --------------------------------------------------------------------------------