Source code for merton.portfolio.concentration
"""Concentration metrics."""
from __future__ import annotations
import numpy as np
from .._typing import ArrayLike
from ..exceptions import MertonInputError
[docs]
def hhi(exposures: ArrayLike) -> float:
r"""Herfindahl-Hirschman Index of a portfolio.
.. math::
\mathrm{HHI} = \sum_i w_i^2,\quad w_i = \frac{E_i}{\sum_j E_j}.
Ranges in ``[1/n, 1]``. A perfectly diversified portfolio of ``n``
equal exposures has ``HHI = 1/n``; a portfolio concentrated in one
name has ``HHI = 1``.
"""
exp = np.asarray(exposures, dtype=np.float64)
if exp.ndim != 1:
raise MertonInputError("exposures must be 1-D")
if np.any(exp < 0):
raise MertonInputError("exposures must be non-negative")
total = exp.sum()
if total <= 0:
raise MertonInputError("exposures sum must be positive")
weights = exp / total
return float(np.sum(weights**2))
[docs]
def effective_n(exposures: ArrayLike) -> float:
"""Effective number of names ``1/HHI``."""
return 1.0 / hhi(exposures)
[docs]
def granularity_adjustment(
pd: ArrayLike,
lgd: ArrayLike,
rho: ArrayLike,
exposures: ArrayLike,
*,
alpha: float = 0.999,
) -> float:
r"""Pykhtin-Dev (2002) granularity adjustment for the IRB asymptotic VaR.
Computes the leading-order correction to the Vasicek IRB VaR for finite
portfolio size. Returns the *additive* adjustment in units of exposure
fraction (so the actual VaR upper bound is
``VaR_IRB(pd, lgd, rho) + granularity_adjustment(...)``).
Pykhtin & Dev (2002) *Credit Risk in Asset Securitizations*. Risk 15(5).
"""
from scipy.stats import norm
from .vasicek_factor import vasicek_var
exp = np.asarray(exposures, dtype=np.float64)
if exp.ndim != 1:
raise MertonInputError("exposures must be 1-D")
weights = exp / exp.sum()
pd_arr = np.asarray(pd, dtype=np.float64)
lgd_arr = np.asarray(lgd, dtype=np.float64)
rho_arr = np.asarray(rho, dtype=np.float64)
pd_arr = np.broadcast_to(pd_arr, weights.shape)
lgd_arr = np.broadcast_to(lgd_arr, weights.shape)
rho_arr = np.broadcast_to(rho_arr, weights.shape)
# First-order Pykhtin-Dev kernel for homogeneous-PD case.
var_inf = vasicek_var(pd_arr, rho_arr, alpha=alpha)
sqrt_rho = np.sqrt(rho_arr)
sqrt_1mrho = np.sqrt(1.0 - rho_arr)
z = norm.ppf(alpha)
# K_i = LGD_i * φ(Φ⁻¹(p_i) - √(1-ρ) Φ⁻¹(α)) etc. — keep it simple and
# use the leading-order density correction.
K = lgd_arr * norm.pdf((norm.ppf(pd_arr) - sqrt_1mrho * (-z)) / sqrt_rho)
ga = 0.5 * np.sum((weights**2) * K * (1.0 - var_inf))
return float(ga)
__all__ = ["effective_n", "granularity_adjustment", "hhi"]