quanttoolbox.credit¶
credit.structural¶
Python alternatives
Hybrid: black_scholes's generalized cost-of-carry b parameterization has no single-function equivalent in py_vollib/mibian (both split by asset class instead) -- keep for that convenience, or call scipy.stats.norm.cdf directly for a one-off. The Merton (1974/1976), Black-Cox (1976), Blasberg (2024) extended-Merton, and Reinders et al. structural credit models are niche enough (each a specific published model, not a general option-pricing primitive) that no general credit-risk or derivatives-pricing library surveyed implements them as public utilities -- keep. pd_merton_model's asset-value/volatility calibration uses scipy.optimize.minimize(method="BFGS") in place of MATLAB's fminunc, one line doing what a hand-rolled Newton loop would otherwise require.
quanttoolbox.credit.structural
¶
Structural (asset-value) credit models: Black-Scholes option pricing, the classic Merton (1974) firm-value default model (calibrated from observed equity value/volatility), Blasberg (2024)'s extended Merton model with a stochastic growth-adjustment factor, the Black-Cox (1976) first-passage model, Merton (1976) jump-diffusion option/credit pricing, and the Reinders et al. credit-transition-loss model.
Ported from HSF toolbox credit/{Black_Scholes_Model,PD_Merton_Model,
B0_Extended_Merton_Model,E0_Extended_Merton_Model,
PD_Extended_Merton_Model,PD_Black_Cox_Model,Merton_Jump_Model,
Merton_Jump_Climate_Model,Reinders_Credit_Model}.m.
Translation notes:
B0_Extended_Merton_Model.m/E0_Extended_Merton_Model.macceptmu_abut never use it in the formula body -- kept in the Python signature anyway, matching the original's own documented reason ("not used here, kept for consistent signature with merton_PD"): all three*_extended_merton_modelfunctions share one 10-argument call signature, andpd_extended_merton_modeldoes usemu_a.Reinders_Credit_Model.macceptsmu_Abut never uses it anywhere in the function body, with no such cross-function-consistency rationale documented (unlike the extended-Merton trio above) -- dropped fromreinders_credit_model's signature as genuinely vestigial, the same treatment given todice_temperature_simulation's unusedparametersargument insustainable_finance/climate.py.Merton_Jump_Model.msumsn_max = max(50, ceil(...))Poisson-weighted Black-Scholes terms with no early exit forlambda=0, while its siblingMerton_Jump_Climate_Model.mdoesbreakoncelambda=0is detected. Both are mathematically equivalent either way -- oncelambda=0, the Poisson weightp_nis exactly0.0for everyn >= 1(verified:p_nrecurses asp_n *= lambda*T/(n+1), which multiplies by0.0oncelambda=0), so the terms forn >= 1contribute nothing regardless of whether the loop keeps running. The same early exit is added to both functions here as a pure performance improvement (skips the redundantn_maxremaining iterations), not a behavior change.pd_merton_model(PD_Merton_Model.m) calibrates the unobserved asset value/volatility(A0, sigma_A)from observed equity value/volatility(E0, sigma_E)by minimizing a 2-equation least-squares objective -- MATLAB'sfminunc(unconstrained quasi-Newton) isscipy.optimize. minimize(method="BFGS")here. Positivity of(A0, sigma_A)is enforced viaabs()inside the objective and on the final result, exactly as in the original (no bounds are passed to the optimizer either way). The original's manual "replicate every scalar/array input to a common lengthn" broadcasting (e = ones(n,1); E0 = E0.*e; ...) is replaced withnumpy.broadcast_arrays, which does the same thing more directly; the per-scenario nonlinear solve itself still runs in a loop, since each scenario is an independent 2-parameter optimization.
BlackScholesResult(call, put)
dataclass
¶
European call/put prices under the generalized Black-Scholes
model with cost-of-carry b (b = r for equities with no dividend,
b = r - q for a dividend yield q, b = 0 for futures, b = r -
r_f for FX).
MertonJumpClimateResult(e0, b0, k)
dataclass
¶
Merton (1976) jump-diffusion firm-value equity/bond values (a
climate-risk application: sudden jumps represent transition-risk
shocks to asset value), plus k (the expected relative jump size).
MertonJumpResult(call, put, k)
dataclass
¶
Merton (1976) jump-diffusion European call/put prices, plus k
(the expected relative jump size, used in the risk-neutral drift
correction).
PdBlackCoxResult(pd_tau, s_tau, d1, d2, varphi)
dataclass
¶
Black-Cox (1976) first-passage default probability at horizon
tau, plus the intermediate d1/d2/varphi terms.
PdMertonModelResult(pd_tau, a0, sigma_a, s_tau, dd_tau)
dataclass
¶
Merton (1974) calibrated default probability: a0/sigma_a are
the calibrated (unobserved) asset value/volatility, dd_tau the
distance-to-default and s_tau/pd_tau the survival/default
probability at horizon tau.
ReindersCreditModelResult(loss, d_loss, d2_loss, mv_e_t0, mv_d_t0, mv_e_t, mv_d_t)
dataclass
¶
Reinders et al. credit-transition loss at asset-value shock xi:
loss (the equity+debt mark-to-market loss), its first (d_loss)
and second (d2_loss) derivatives with respect to xi, and the
pre-/post-shock equity/debt values.
b0_extended_merton_model(a0, d, r, mu_a, delta0, mu_delta, sigma_a, sigma_delta, rho, t)
¶
Bond (debt) value at t=0 in Blasberg (2024)'s extended Merton
model, where the firm's growth-adjustment factor delta(t) follows
its own Brownian motion correlated (rho) with the asset value.
mu_a is accepted but unused -- see module docstring.
Balance-sheet check: e0 + b0 == a0 * exp(-delta0_prime * t).
Original: credit/B0_Extended_Merton_Model.m
Source code in src/quanttoolbox/credit/structural.py
210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 | |
black_scholes(s0, k, sigma, t, b, r)
¶
Generalized Black-Scholes European call/put prices with
cost-of-carry b (spot s0, strike k, volatility sigma, maturity
t, risk-free rate r).
Original: credit/Black_Scholes_Model.m
Source code in src/quanttoolbox/credit/structural.py
73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 | |
e0_extended_merton_model(a0, d, r, mu_a, delta0, mu_delta, sigma_a, sigma_delta, rho, t)
¶
Equity value at t=0 in Blasberg (2024)'s extended Merton model --
see b0_extended_merton_model for the model description.
Original: credit/E0_Extended_Merton_Model.m
Source code in src/quanttoolbox/credit/structural.py
242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 | |
merton_jump_climate_model(a0, d, sigma_a, t, r, lambda_, mu_z, sigma_z)
¶
Merton (1976) jump-diffusion firm-value equity/bond values: the
firm's asset value follows a jump-diffusion (Poisson rate lambda_,
lognormal jump sizes mu_z/sigma_z) rather than plain geometric
Brownian motion, otherwise the classic Merton (1974) structural
setup (debt face value d, maturity t, risk-free rate r).
Original: credit/Merton_Jump_Climate_Model.m
Source code in src/quanttoolbox/credit/structural.py
398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 | |
merton_jump_model(s0, k, sigma, t, b, r, lambda_, mu_z, sigma_z)
¶
Merton (1976) jump-diffusion European option prices: a Poisson
(rate lambda_) mixture of Black-Scholes prices, one per possible
jump count n, with lognormal jump sizes (mu_z, sigma_z).
Original: credit/Merton_Jump_Model.m
Source code in src/quanttoolbox/credit/structural.py
349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 | |
pd_black_cox_model(a0, mu_a, sigma_a, b, tau)
¶
Black-Cox (1976) first-passage-time default probability: the firm
defaults as soon as its asset value A(t) (geometric Brownian motion
with drift mu_a, volatility sigma_a, starting at a0) first
crosses the constant barrier b, evaluated over horizon tau.
Original: credit/PD_Black_Cox_Model.m
Source code in src/quanttoolbox/credit/structural.py
310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 | |
pd_extended_merton_model(a0, d, r, mu_a, delta0, mu_delta, sigma_a, sigma_delta, rho, t)
¶
Physical (real-world, drift mu_a) probability of default at
horizon t in Blasberg (2024)'s extended Merton model. r is
accepted but unused (kept for signature consistency with
b0_extended_merton_model/e0_extended_merton_model).
Original: credit/PD_Extended_Merton_Model.m
Source code in src/quanttoolbox/credit/structural.py
270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 | |
pd_merton_model(e0, sigma_e, d, mu_a, r, t, tau, config=None)
¶
Calibrate the Merton (1974) model's unobserved asset value/
volatility (A0, sigma_A) from observed equity value/volatility
(E0, sigma_E) and debt face value D at maturity T (via a
2-equation least-squares fit), then compute the physical
(real-world, drift mu_a) probability of default at horizon tau.
Original: credit/PD_Merton_Model.m
Source code in src/quanttoolbox/credit/structural.py
121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 | |
reinders_credit_model(xi, a0, d, r, sigma_a, t, omega_e, omega_d)
¶
Reinders et al.'s credit-transition loss model: the equity+debt
mark-to-market loss from an instantaneous fractional shock xi to
the firm's asset value (A(t) = A0 * (1 - xi)), weighted by
omega_e/omega_d (e.g. the investor's equity/debt holdings), plus
the loss's first/second derivatives with respect to xi.
Original: credit/Reinders_Credit_Model.m (the mu_A parameter is
accepted but never used in the original's formula body, with no
documented reason -- dropped here, see module docstring)
Source code in src/quanttoolbox/credit/structural.py
459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 | |
credit.reduced_form¶
Python alternatives
Keep -- default-time survival/density/hazard functions implied by a continuous-time Markov generator matrix (via scipy.linalg.expm), and the (piecewise-)exponential default-time model (survival/CDF/PDF/quantile/simulation). No general-purpose equivalent found: lifelines and scikit-survival model estimation from observed survival data, not simulation/inversion from an assumed hazard specification given up front.
quanttoolbox.credit.reduced_form
¶
Reduced-form (intensity/hazard-based) credit models: default-time survival/density/hazard functions implied by a continuous-time Markov generator matrix (e.g. a credit-rating transition-intensity matrix, with default as the absorbing state), and the (piecewise-)exponential default model used elsewhere in the toolbox for simulating default times.
Ported from HSF toolbox credit/{Survival_Markov_Generator,
Density_Markov_Generator,Hazard_Markov_Generator,survivalExponential,
cdfExponential,pdfExponential,invExponential,rndExponential}.m.
Translation notes:
Hazard_Markov_Generator.m's function body is declared asfunction lambda = Density_Markov_Generator(t, Lambda)-- an apparent copy-paste error in the original (the internal function name doesn't match its own filename or its actual computation,f / S). MATLAB still dispatches by filename when callingHazard_Markov_Generator(...)from another script, so the bug is silently harmless in the original. Namedhazard_markov_generatorhere, matching the filename and the actual computation rather than the erroneous internal name.survivalExponential.m/pdfExponential.m/invExponential.mall branch onsize(lambda, 2) == 1to distinguish a homogeneous per-scenario hazard-rate vector from a piecewise-constant hazard matrix (knots in column 1, per-scenario rates in the remaining columns) -- but the homogeneous docstring also describes a "1 x C" row-vector case that, ifC > 1, would actually havesize(lambda, 2) == C != 1and fall through to the (wrong) piecewise branch. That row-vector case is unreachable under the code's own dispatch condition; only the column-vector ("C x 1") reading is actually exercised anywhere in the original. This ambiguity doesn't translate cleanly to numpy (which has no row/column distinction for 1-D arrays), so the Python API instead dispatches on array dimensionality: a 1-Dlambda_(shape(C,)) is always the homogeneous case, and a 2-Dlambda_(shape(M, 1+C)) is always the piecewise case -- unambiguous, and consistent with the cases the original code actually exercises.- MATLAB's
discretize(t, edges)(returns the 1-based bin index of each element oft, orNaNoutside all bins) is reimplemented as a private_discretize_binhelper vianumpy.searchsorted, returning a 0-based index and clamping out-of-range values to the first bin -- matching the originals' own "safeguard for t < 0"NaN-clamping (idx(isnan(idx)) = 1), since every bin sequence here already extends to+infon the right.
build_climate_stressed_generator(lambda_matrix, beta)
¶
Climate-stressed variant of a continuous-time Markov generator
lambda_matrix (K x K), scaling the default-column (K-th, i.e. the
last column) entries by a stress multiplier beta and re-balancing
the diagonal so every row still sums to zero (a valid generator
matrix): stress = beta * Lambda[:, K-1],
Lambda_climate[:, K-1] = Lambda[:, K-1] + stress,
Lambda_climate = Lambda_climate - diag(stress).
Not ported from the MATLAB HSF toolbox -- no .m file in
hfs-archive implements this. Promoted from HSF-Notebooks chapter
13f.
Source code in src/quanttoolbox/credit/reduced_form.py
112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 | |
cdf_exponential(t, lambda_)
¶
Cumulative distribution function F(t) = 1 - S(t) of the
(piecewise) exponential default model. See survival_exponential.
Original: credit/cdfExponential.m
Source code in src/quanttoolbox/credit/reduced_form.py
181 182 183 184 185 186 187 | |
density_markov_generator(t, lambda_matrix)
¶
Default-time density f(t) implied by a continuous-time Markov
generator lambda_matrix: f(t) = (Lambda @ expm(t * Lambda))[:,
K-1] (f(0) = Lambda[:, K-1]).
Returns an array of shape (len(t), K).
Original: credit/Density_Markov_Generator.m
Source code in src/quanttoolbox/credit/reduced_form.py
76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 | |
hazard_markov_generator(t, lambda_matrix)
¶
Hazard rate lambda(t) = f(t) / S(t) implied by a continuous-time
Markov generator lambda_matrix.
Original: credit/Hazard_Markov_Generator.m (see module docstring for the source function-name mismatch this resolves)
Source code in src/quanttoolbox/credit/reduced_form.py
100 101 102 103 104 105 106 107 108 109 | |
inv_exponential(p, lambda_)
¶
Quantile function (inverse CDF): t such that
Pr(tau <= t) = p, for the (piecewise) exponential default model.
p is an array of shape (N,) or (N, C) of probabilities in
(0, 1); values too close to 0 or 1 to invert safely return nan.
Original: credit/invExponential.m
Source code in src/quanttoolbox/credit/reduced_form.py
208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 | |
pdf_exponential(t, lambda_)
¶
Density function f(t) = lambda_m(t) * S(t) of the (piecewise)
exponential default model. See survival_exponential.
Original: credit/pdfExponential.m
Source code in src/quanttoolbox/credit/reduced_form.py
190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 | |
rnd_exponential(r, c, lambda_, random_state=None)
¶
Simulated default times for the (piecewise) exponential default
model. If c != 0, generates an r x c matrix of uniforms and
inverts them via inv_exponential; if c == 0, r is instead
treated as a pre-generated matrix of uniforms (mirrors the original
GAUSS calling convention).
Original: credit/rndExponential.m
Source code in src/quanttoolbox/credit/reduced_form.py
260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 | |
survival_exponential(t, lambda_)
¶
Survival function S(t) = Pr(tau > t) of the (piecewise)
exponential default model. lambda_ is either a 1-D array of shape
(C,) (homogeneous hazard rate per scenario) or a 2-D array of
shape (M, 1+C) (column 0 = knots t*_1 < ... < t*_M, columns
1: = piecewise hazard rates per scenario, extended to +inf beyond
the last knot).
Returns an array of shape (len(t), C).
Original: credit/survivalExponential.m
Source code in src/quanttoolbox/credit/reduced_form.py
157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 | |
survival_markov_generator(t, lambda_matrix)
¶
Survival probabilities S(t) = Pr(tau > t) implied by a
continuous-time Markov generator lambda_matrix (K x K), where state
K is the absorbing "default" state: S(t) = 1 - expm(t * Lambda)[:,
K-1].
Returns an array of shape (len(t), K).
Original: credit/Survival_Markov_Generator.m
Source code in src/quanttoolbox/credit/reduced_form.py
51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 | |