Artefacts

Reproducible artefacts

A model is only as trustworthy as the evidence behind it — and I’d rather reproduce a result than take it on trust. The figure on the front page is generated from the code below; here is the maths, so you can check it yourself.

Pluto–Tasche PD upper bound

For a low-default portfolio — a rating grade with very few or zero observed defaults — the naïve PD estimate is zero, which is useless for capital. Pluto and Tasche (2005) instead take the most-prudent PD still consistent with the data at a chosen confidence level. With n obligors and no defaults, the independent-obligor bound has the closed form PD = 1 − (1 − γ)^(1/n); the single-factor version adds asset correlation. Fewer names, or a higher required confidence, push the conservative PD sharply up — exactly what the front-page chart shows.

\"\"\"
Pluto-Tasche most-prudent PD upper bound for low-default portfolios.

Reference: Pluto, K. and Tasche, D. (2005),
\"Estimating Probabilities of Default for Low Default Portfolios\".

For a rating grade with n obligors and ZERO observed defaults, the most-prudent
PD estimate at confidence level gamma is the largest PD still consistent with
observing no defaults at that confidence:

    independent obligors:
        (1 - PD)^n = 1 - gamma          =>   PD = 1 - (1 - gamma)^(1 / n)

    single systematic factor (asset correlation rho):
        E_Y[ (1 - N( (N^-1(PD) - sqrt(rho) Y) / sqrt(1 - rho) ))^n ] = 1 - gamma
        solved for PD, with Y ~ N(0, 1).
\"\"\"
import numpy as np
from scipy.stats import norm
from scipy.optimize import brentq
from scipy.integrate import quad
import matplotlib.pyplot as plt


def pd_bound_independent(gamma, n):
    \"\"\"Closed-form PD upper bound assuming independent obligors, zero defaults.\"\"\"
    return 1.0 - (1.0 - gamma) ** (1.0 / n)


def _prob_zero_defaults(pd, n, rho):
    \"\"\"P(0 defaults) for n obligors under a single Gaussian factor.\"\"\"
    def integrand(y):
        cond_pd = norm.cdf((norm.ppf(pd) - np.sqrt(rho) * y) / np.sqrt(1.0 - rho))
        return (1.0 - cond_pd) ** n * norm.pdf(y)
    return quad(integrand, -8.0, 8.0)[0]


def pd_bound_factor(gamma, n, rho):
    \"\"\"PD upper bound with asset correlation rho (root of the zero-default equation).\"\"\"
    return brentq(lambda pd: _prob_zero_defaults(pd, n, rho) - (1.0 - gamma),
                  1e-9, 1.0 - 1e-9)


if __name__ == \"__main__\":
    gammas = np.linspace(0.50, 0.999, 400)
    fig, ax = plt.subplots(figsize=(7, 6))
    for n in (50, 200, 1000, 5000):
        ax.plot(gammas, [pd_bound_independent(g, n) * 100 for g in gammas],
                label=f\"n = {n}\")
    ax.set_xlabel(\"Confidence level\")
    ax.set_ylabel(\"PD upper bound (%)\")
    ax.set_title(\"Pluto-Tasche PD upper bound - zero-default portfolio\")
    ax.legend()
    fig.tight_layout()
    fig.savefig(\"pluto_tasche.png\", dpi=200)

The structural (Merton) and portfolio (Vasicek / Basel IRB) figures are built the same way — from the model, in code. Back to home