Risky Bonds and CDS Valuation in Python
Risky Bonds and CDS Valuation in Python
Contents
Intro: ......................................................................................................................................................... 1
Bond valuation .............................................................................................................................................. 2
Price of bond without default time........................................................................................................... 2
Price of bond with default time ................................................................................................................ 2
Other notions ............................................................................................................................................ 2
Piecewise exponential distribution ........................................................................................................... 2
Python code .............................................................................................................................................. 2
CDS valuation ................................................................................................................................................ 6
Terms, price and spread ........................................................................................................................... 6
Python code .............................................................................................................................................. 6
Appendix ....................................................................................................................................................... 7
Exponential random variables .................................................................................................................. 7
Python code .......................................................................................................................................... 7
Indicator function of exponential times ................................................................................................... 7
Python code .......................................................................................................................................... 7
Piecewise exponential model: Monte Carlo simulation ........................................................................... 8
Python code .......................................................................................................................................... 8
Bond price with risk using Monte Carlo simulation .................................................................................. 8
Indicator function of piecewise exponential distribution..................................................................... 8
Premium leg and default leg (CDS) ........................................................................................................... 9
Price of the CDS (Python code) ............................................................................................................... 10
Intro:
My first mini-project is the implementation of pricing methods for bonds and CDS, as well as computing
other measures as yield and spread under the auspices of time-to-default random variables.
1) Full pricing;
The price with accrual will be 𝑷𝒕 + 𝑨𝑪𝒕 = ∑𝒕𝒎 ≥𝒕 𝑪(𝒕𝒎 )𝑩𝒕 (𝒕𝒎 )𝑺𝒕 (𝒕𝒎 ) + 𝑵𝑩𝒕 (𝑻)𝑺𝒕 (𝑻) + 𝑹𝑵 ⋅
𝑻
∫𝒕 𝑩𝒕 (𝒖)𝒇𝒕 (𝒖)𝒅𝒖(𝟐) where R = recovery rate, 𝑁 = nominal (face value) of the bond, 𝐴𝐶𝑡 =accrued
coupon at time t, 𝑆𝑡 (𝑇) = survival function of bond starting at time t and ending at time T.
Other notions
Yield = the value 𝑦 for which the price 𝑃𝑡 + 𝐴𝐶𝑡 = ∑𝑡𝑚≥𝑡 𝑒 −𝑦(𝑡𝑚−𝑡) 𝐶(𝑡𝑚 ) + 𝑒 −𝑦(𝑇−𝑡) 𝑁. (for non-risky
bonds).
For bonds with default time, instead of the term rate 𝑟𝑡 you put 𝑦, or instead of 𝐵𝑡 (𝑢) ≔ 𝑒 −𝑦(𝑡−𝑢) and
solve the equation (2).
The spread is given by 𝑠 = 𝑦 ∗ − 𝑦 where 𝑦 ∗ is the yield of the risky bond and 𝑦is the yield of the non-
risky bond.
Remark: 𝑦 ∗ > 𝑦 and 𝑃𝑡∗ < 𝑃𝑡 where 𝑃𝑡∗ is the price of the risky bond.
Python code
First I devise two functions for non-risky bond prices:
One will have a discrete term structure rate, namely for a division Δ = {0 < 𝑡1 < 𝑡2 < ⋯ < 𝑡𝑚 < ∞} we
have 𝑟𝑡 = 𝑟𝑖+1 , if 𝑡𝑖 < 𝑡 < 𝑡𝑖+1 , 𝑖 = 1, 𝑚 − 1 and 𝑟𝑡 = 𝑟1 , 𝑡 < 𝑡1 , 𝑟𝑡 = 𝑟𝑚+1 , 𝑡 > 𝑡𝑚 .
The second, which is mostly used will assume the term structure rate as a function of time (the first case
being a particular case of the 2nd one).
def bond_price_functions(num):
import numpy as np
import scipy.integrate as integ
def bp1(FV,c,zero_rates,times):
price=0
for i in range(len(times)):
if i==0:
price = price+c*times[0]*np.exp(-zero_rates[0]*times[0])*FV
else:
price = price+c*(times[i]-times[i-1])*np.exp(-zero_rates[i]*times[i])*FV
return price+np.exp(-zero_rates[-1]*times[-1])*FV
def bp2(FV,c,rate,times):
price = 0
for i in range(len(times)):
if i==0:
price = price+c*times[0]*np.exp(-integ.quad(rate,0,times[i])[0])*FV
else:
price = price+c*(times[i]-times[i-1])*np.exp(-integ.quad(rate,\
0,times[i])[0])*FV
return price+np.exp(-integ.quad(rate,0,times[-1])[0])*FV
dic = dict(zip([1,2],[bp1,bp2]))
return dic[num]
On the other hand, to approximate the yield we will need a pricing function given a
theoretical yield. That function will be used for numerical method (Newton/bisection/secant)
for approximation of the yield.
def bond_price_yield(FV,c,y,times):
"bond price when theoretical yield is given"
f = bond_price_functions(1)
rates = [y]*len(times);
return f(FV,c,rates,times)
def yield_bond(FV,c,rates,times):
f1 = bond_price_functions(1)
P0 = f1(FV,c,rates,times)
h = lambda y:bond_price_yield(FV,c,y,times)-P0
import scipy.optimize as scp
return scp.newton(h,0)
def yield_bond2(FV,c,rate,times):
f1 = bond_price_functions(2)
P0 = f1(FV,c,rate,times)
h = lambda y:bond_price_yield(FV,c,y,times)-P0
import scipy.optimize as scp
return scp.newton(h,0)
def test():
FV,c,times = 100,0.04,[0.5,1]
rate = lambda s:0.03 if s<0.5 else 0.04
print(yield_bond2(FV,c,rate,times))
This function devises a scale function for parameters:
Given the parameters {𝜆1 , … , 𝜆𝑘 }, times {𝑡1 , 𝑡2 , … , 𝑡𝑘−1 } and time 𝑡, we have:
𝜆1 , 𝑡 ≤ 𝑡1
𝜆2 , 𝑡1 < 𝑡 ≤ 𝑡2
𝜆= … .
𝜆𝑘−1 , 𝑡𝑘−2 < 𝑡 ≤ 𝑡𝑘−1
{ 𝜆𝑘 , 𝑡𝑘−1 < 𝑡
def scale_function(times,parameters,t):
"SUppose we have n-1 knots and n parameters, and a time"
"We localize the time t between the times provided and with the interval "
"position found we give the correct parameter"
if len(times)!=len(parameters)-1:
raise TypeError('parameters and times are not compatible')
if t<=times[0]:
return parameters[0],0
elif t>times[-1]:
return parameters[-1],len(times)
else:
for i in range(len(times)-1):
if times[i]<t and t<=times[i+1]:
return parameters[i+1],i+1
def localize(times,t):
if t<=times[0]:
return len(times)
elif t>times[-1]:
return 0
else:
for i in range(len(times)-1):
if times[i]<t and t<=times[i+1]:
return len(times)-i
#%%
"Survival and density functions of piecewise exponential distribution"
def survival_function(times,parameters,t):
def summ():
import scipy.integrate as integ
f = lambda s:scale_function(times,parameters,s)[0]
return integ.quad(f,0,t)[0]
import numpy as np
return np.exp(-summ())
def density_function(times,parameters,t):
"Works in case of exponential and piecewise exponential distributions"
param = scale_function(times,parameters,t)[0]
return param*survival_function(times,parameters,t)
#%%
def bond_price_closed(c,N,times,rate,lbd,R):
import numpy as np
import scipy.integrate as integ
s=0
for i in range(len(times)-1):
s=s+c*N*(times[i+1]-times[i])*np.exp(-integ.quad(rate,0,times[i])[0]-\
lbd*times[i])
s=s+N*np.exp(-integ.quad(rate,0,times[-1])[0])*np.exp(-lbd*times[-1])
f = lambda u:np.exp(-integ.quad(rate,0,u)[0]-lbd*u)
s = s+lbd*R*N*integ.quad(f,0,times[-1])[0]
return s
#%%
def bond_price_closed2(c,N,times,rate,params,R):
import numpy as np
import scipy.integrate as integ
s= 0
h1 = lambda t:survival_function(times,params,t)
s = s+c*N*times[0]*np.exp(-integ.quad(rate,0,times[0])[0])*h1(times[0])
for i in range(len(times)-1):
s = s+c*N*(times[i+1]-times[i])*np.exp(-integ.quad(rate,0,times[i])[0])\
*h1(times[i+1])
h2 = lambda u:np.exp(-integ.quad(rate,0,u)[0])
#h2 is the discount rate function depending on u
h3 = lambda u:density_function(times,params,u)
#h3 is the density function
h = lambda u:h2(u)*h3(u)
s = s + N*np.exp(-integ.quad(rate,0,times[-1])[0])*h1(times[-1])
s = s+R*N*integ.quad(h,0,times[-1])[0]
return s
#%%
def bond_price_yield2(c,N,times,y,params,R):
"compute the price of a bond when supposedly you have a yield"
rate = lambda s:y
return bond_price_closed2(c,N,times,rate,params,R)
def risky_bond_price_yield(c,N,times,rate,params,R):
P0 = bond_price_closed2(c,N,times,rate,params,R)
h = lambda y:bond_price_yield2(c,N,times,y,params,R)-P0
import scipy.optimize as scp
return scp.newton(h,0)
def risky_bond_spread(c,N,times,rate,params,R):
y1 = risky_bond_price_yield(c,N,times,rate,params,R)#risky yield
y2 = yield_bond2(N,c,rate,times)
return y1-y2
Stochastic bond price test
def sbp2_test():
c,N,times,params,R=0.04,100,[0.5,1],[0.2,0.1,0.4],0.4
rate = lambda s:0.04 if s<0.5 else 0.06
#print(bond_price_MC2(c,N,times,rate,params,R,10000))
print("Bond price with risk",bond_price_closed2(c,N,times,rate,params,R))
h = bond_price_functions(1)
print("Bond price without risk",h(N,c,[0.06,0.06],[0.5,1]))
print("Non-risky yield",yield_bond2(N,c,rate,times))
print("Single yield price",bond_price_yield2(c,N,times,0.07,params,R))
print("Risky yield",risky_bond_price_yield(c,N,times,rate,params,R))
print("Spread",risky_bond_spread(c,N,times,rate,params,R))
sbp2_test()
CDS valuation
Terms, price and spread
Suppose we have a Credit Default Swap with coupon 𝑐, nominal value 𝑁, recovery rate of the
defaultable entity is 𝑅, lifetime 𝑇.
Then the price of a CDS can be written as the Default Leg Value – Premium Leg Value.
𝜏
𝑆𝑉𝑡 (𝐷𝐿) = (1 − 𝑅) × 𝑁 × 1(𝜏 ≤ 𝑇) × 𝑒𝑥𝑝(− ∫𝑡 𝑟𝑠 𝑑𝑠) while the present value will be 𝑃𝑉𝑡 (𝐷𝐿) =
𝜏
𝐸[(1 − 𝑅)𝑁 × 1(𝜏 ≤ 𝑇) × exp(− ∫𝑡 𝑟𝑠 𝑑𝑠)|𝐹𝑡 ] = (1 − 𝑅) × 𝑁 × 𝐸[1(𝜏 ≤ 𝑇) × 𝐵𝑡 (𝜏)] =
𝑇
(1 − 𝑅) × 𝑁 × ∫𝑡 𝐵𝑡 (𝑢)𝑓𝑡 (𝑢)𝑑𝑢.
And the discounted value is 𝑃𝑉𝑡 (𝑃𝐿) = 𝑐 × 𝑁 × ∑𝑡𝑚≥𝑡 Δ𝑡𝑚 𝑆𝑡 (𝑡𝑚 )𝐵𝑡 (𝑡𝑚 ) where 𝜏 =default time,
𝑆𝑡 (𝑢) = 𝑃(𝜏 > 𝑢|𝜏 > 𝑡).
THE SPREAD of a CDS is the value of 𝑐 that makes the value of the CDS at time 𝑡 (beginning) = 0.
𝑇
𝑠 = ((1 − 𝑅) ∫𝑡 𝐵𝑡 (𝑢)𝑓𝑡 (𝑢)𝑑𝑢)/(∑𝑡𝑚≥𝑡 Δ𝑡𝑚 𝑆𝑡 (𝑡𝑚 )𝐵𝑡 (𝑡𝑚 )) .
Python code
def discounted_leg(N,times,rate,params,R):
import numpy as np
import scipy.integrate as integ
s=0
h1=lambda t:density_function(times,params,t)
h2 = lambda t:np.exp(-integ.quad(rate,0,t)[0])
h = lambda t:h1(t)*h2(t)
return (1-R)*N*integ.quad(h,0,times[-1])[0]
def premium_leg(c,N,times,rate,params,R):
import numpy as np
import scipy.integrate as integ
s= 0
h1 = lambda t:survival_function(times,params,t)
s = s+c*N*times[0]*np.exp(-integ.quad(rate,0,times[0])[0])*h1(times[0])
for i in range(len(times)-1):
s = s+c*N*(times[i+1]-times[i])*np.exp(-integ.quad(rate,0,times[i])[0])\
*h1(times[i+1])
return s
def price_CDS(c,N,times,rate,params,R):
return discounted_leg(N,times,rate,params,R)-premium_leg(c,N,times,rate,params,R)
def spread_CDS(times,rate,params,R):
import numpy as np
import scipy.integrate as integ
s= 0
h1 = lambda t:survival_function(times,params,t)
s1 = s+times[0]*np.exp(-integ.quad(rate,0,times[0])[0])*h1(times[0])
for i in range(len(times)-1):
s1=s1+(times[i+1]-times[i])*np.exp(-integ.quad(rate,0,times[i])[0])\
*h1(times[i+1])
h1 = lambda t:density_function(times,params,t)
h2 = lambda t:np.exp(-integ.quad(rate,0,t)[0])
h = lambda t:h1(t)*h2(t)
s2 = integ.quad(h,0,times[-1])[0]
return s2/s1
Appendix
Exponential random variables
The simulation of the exponential random samples is given by the Inverse Cdf method.
log(1−𝑢)
𝐹(𝑥) = 1 − 𝑒 −𝜆𝑥 = 𝑢 ⇔ 𝑥 = − . Therefore according to the inverse cdf sampling, if 𝑢 ∈ (0,1)is
𝜆
log(1−𝑈)
drawn from Uniform U(0,1) sample then 𝑋 ∼ − ∼ 𝑒𝜆 .
𝜆
Python code
def ind_expo(t,lbd):
import numyp as np
sample = np.random.uniform(0,1)
return -np.log(1-sample)/lbd
Then I take a sample 𝑒1 , 𝑒2 , … 𝑒𝑛 of the distribution 𝑒𝜆 and then check them if are greater than t or not.
Python code
def indicator_expo(t,lbd,size):
def exponential(lbd,size):
import numpy as np
sample = np.random.uniform(0,1,size)
return -np.log(1-sample)/lbd
return (exponential(lbd,size)>t)*1
def indicator_expo2(t1,t2,lbd,size):
def exponential(lbd,size):
import numpy as np
sample = np.random.uniform(0,1,size)
return -np.log(1-sample)/lbd
return ((exponential(lbd,size)>t1)*1)*((exponential(lbd,size)<t2)*1)
Python code
def piece_expo(times,params,u):
def survivals(times,parameters):
l = [survival_function(times,parameters,times[i]) for i in range(len(times))]
return l,l[::-1]
def localize_survivals(times,parameters,u):
"takes a number u between 0 and 1 and identifies the parameter associated"
"with the interval where u is located"
res = survivals(times,parameters)[1]
return localize(res,u)
"one sample of a piecewise exponential distribution"
survivs = survivals(times,params)[0]
import numpy as np
if u>survivs[0]:
return 1/params[0]*np.log(1/u)
elif u<=survivs[-1]:
return times[-1]+1/params[-1]*np.log(survivs[-1]/u)
else:
pos = localize_survivals(times,params,u)
return times[pos-1]+1/params[pos]*np.log(survivs[pos-1]/u)
def simulate_piece_expo(times,params,size):
import numpy as np
u=np.random.uniform(0,1,size)
return list([piece_expo(times,params,u[i]) for i in range(len(u))])
def ind_piece_expo(times,params,t):
import numpy as np
u = np.random.uniform(0,1)
return (piece_expo(times,params,u)<=t)*1
def ind_piece_expo2(times,params,t,size):
import numpy as np
u = np.random.uniform(0,1,size)
return list([(piece_expo(times,params,u[i])<=t)*1 for i in range(size)])
print(ind_piece_expo2([0.5,1],[0.2,0.3,0.4],0.75,100))
def bond_price_MC2(c,N,times,rate,params,R,size):
import numpy as np
def stochastic_bond_price2():
import numpy as np
import scipy.integrate as integ
s=0
s = s+c*N*times[0]*np.exp(-integ.quad(rate,0,times[0])[0])
for i in range(len(times)-1):
s = s+c*N*(times[i+1]-times[i])*np.exp(-integ.quad(rate,0,times[i])[0])\
*(1-ind_piece_expo(times,params,times[i]))
s=s+N*np.exp(-integ.quad(rate,0,times[-1])[0])\
*(1-ind_piece_expo(times,params,times[-1]))+\
R*N*np.exp(-integ.quad(rate,0,times[-1])[0])*\
ind_piece_expo(times,params,times[-1])
return s
prices = list([stochastic_bond_price2() for i in range(size)])
return np.mean(prices)
Premium leg and default leg (CDS)
𝜏
𝑆𝑉𝑡 (𝐷𝐿) = (1 − 𝑅) × 𝑁 × 1(𝜏 ≤ 𝑇) × 𝑒𝑥𝑝(− ∫𝑡 𝑟𝑠 𝑑𝑠)
𝑡
𝑆𝑉𝑡 (𝑃𝐿) = ∑𝑡𝑚≥𝑡 𝑐 × 𝑁 × (𝑡𝑚 − 𝑡𝑚−1 ) × 1(𝜏 > 𝑡𝑚 ) × exp(− ∫𝑡 𝑚 𝑟𝑠 𝑑𝑠)
def simulate_default_leg2(R,N,times,T,rate,params,size):
import numpy as np
import scipy.integrate as integ
def default_leg():
sample = ind_piece_expo(times,params,T)
return (1-R)*N*(sample<T)*np.exp(-integ.quad(rate,0,sample)[0])
def default_leg_CDS():
sample=[]
for i in range(size):
sample.append(default_leg())
return sample
return default_leg_CDS()
def simulate_prem_leg2(c,N,times,rate,params,size):
import numpy as np
import scipy.integrate as integ
def prem_leg():
s= 0
for i in range(len(times)-1):
s = s+c*N*(times[i+1]-times[i])*ind_piece_expo(times,params,times[i])*\
np.exp(-integ.quad(rate,0,times[i])[0])
return s
sample = []
for i in range(size):
sample.append(prem_leg())
return sample