Source code for tensorquantlib.finance.credit
"""Credit risk models: Merton structural model and reduced-form CDS pricing.
Merton (1974) structural model treats equity as a call option on firm assets.
Reduced-form models price CDS using hazard rates and survival probabilities.
"""
from __future__ import annotations
import numpy as np
from scipy.stats import norm
# ---------------------------------------------------------------------------
# Merton structural model
# ---------------------------------------------------------------------------
[docs]
def merton_default_prob(V: float, D: float, T: float, r: float, sigma_V: float) -> float:
"""Probability of default under Merton (1974) structural model.
Default occurs when firm value V_T < D at maturity.
Parameters
----------
V : float
Current firm asset value.
D : float
Face value of debt (default barrier).
T : float
Time to maturity in years.
r : float
Risk-free rate.
sigma_V : float
Volatility of firm assets.
Returns
-------
float
Risk-neutral probability of default.
"""
d2 = (np.log(V / D) + (r - 0.5 * sigma_V**2) * T) / (sigma_V * np.sqrt(T))
return float(norm.cdf(-d2))
[docs]
def merton_credit_spread(V: float, D: float, T: float, r: float, sigma_V: float) -> float:
"""Credit spread implied by the Merton model.
spread = -(1/T) * ln(B / (D * exp(-r*T)))
where B is the risky bond price = D*exp(-r*T) - Put(V, D, T).
Parameters
----------
V, D, T, r, sigma_V
Same as :func:`merton_default_prob`.
Returns
-------
float
Annual credit spread (continuously compounded).
"""
d1 = (np.log(V / D) + (r + 0.5 * sigma_V**2) * T) / (sigma_V * np.sqrt(T))
d2 = d1 - sigma_V * np.sqrt(T)
# Risky bond = D*exp(-rT) - put on V with strike D
put = D * np.exp(-r * T) * norm.cdf(-d2) - V * norm.cdf(-d1)
risky_bond = D * np.exp(-r * T) - put
# spread = yield_risky - r
yield_risky = -np.log(risky_bond / D) / T
return float(yield_risky - r)
# ---------------------------------------------------------------------------
# Survival / hazard rate utilities
# ---------------------------------------------------------------------------
[docs]
def survival_probability(hazard_rate: float, T: float) -> float:
"""Survival probability under constant hazard rate.
Q(T) = exp(-lambda * T)
"""
return float(np.exp(-hazard_rate * T))
[docs]
def hazard_rate_from_spread(spread: float, recovery: float = 0.4) -> float:
"""Implied constant hazard rate from CDS spread.
lambda ≈ spread / (1 - R)
Parameters
----------
spread : float
CDS spread (annualised, e.g. 0.01 = 100bp).
recovery : float
Recovery rate (default 0.4 = 40%).
Returns
-------
float
Implied hazard rate.
"""
return spread / (1.0 - recovery)
# ---------------------------------------------------------------------------
# CDS pricing
# ---------------------------------------------------------------------------
[docs]
def cds_spread(
hazard_rate: float, T: float, recovery: float = 0.4, r: float = 0.05, n_premium_dates: int = 4
) -> float:
"""Par CDS spread for constant hazard rate.
Premium leg = spread * sum( DF_i * Q_i * delta_i )
Protection leg = (1-R) * sum( DF_i * (Q_{i-1} - Q_i) )
We solve for spread = protection_leg / risky_annuity.
Parameters
----------
hazard_rate : float
Constant hazard rate (annualised).
T : float
CDS maturity in years.
recovery : float
Recovery rate.
r : float
Risk-free rate.
n_premium_dates : int
Number of premium payment dates per year.
Returns
-------
float
Par CDS spread (annualised).
"""
n_periods = int(T * n_premium_dates)
dt = T / n_periods if n_periods > 0 else T
risky_annuity = 0.0
protection_leg = 0.0
for i in range(1, n_periods + 1):
t_i = i * dt
df = np.exp(-r * t_i)
q_i = np.exp(-hazard_rate * t_i)
q_prev = np.exp(-hazard_rate * (t_i - dt))
risky_annuity += df * q_i * dt
protection_leg += df * (q_prev - q_i)
protection_leg *= 1.0 - recovery
if risky_annuity < 1e-15:
return 0.0
return float(protection_leg / risky_annuity)
[docs]
def cds_price(
hazard_rate: float,
T: float,
recovery: float = 0.4,
r: float = 0.05,
spread: float = 0.01,
notional: float = 1e6,
n_premium_dates: int = 4,
) -> float:
"""Mark-to-market value of a CDS position (protection buyer).
MTM = protection_leg - spread * risky_annuity
Parameters
----------
hazard_rate : float
Current constant hazard rate.
T : float
Remaining maturity.
recovery : float
Recovery rate.
r : float
Risk-free rate.
spread : float
Contracted CDS spread (annualised).
notional : float
Notional amount.
n_premium_dates : int
Premium payment frequency per year.
Returns
-------
float
MTM value of the CDS position for the protection buyer.
"""
n_periods = int(T * n_premium_dates)
dt = T / n_periods if n_periods > 0 else T
risky_annuity = 0.0
protection_leg = 0.0
for i in range(1, n_periods + 1):
t_i = i * dt
df = np.exp(-r * t_i)
q_i = np.exp(-hazard_rate * t_i)
q_prev = np.exp(-hazard_rate * (t_i - dt))
risky_annuity += df * q_i * dt
protection_leg += df * (q_prev - q_i)
protection_leg *= 1.0 - recovery
mtm = (protection_leg - spread * risky_annuity) * notional
return float(mtm)