Source code for merton.core.distance

"""Distance-to-default and probability-of-default primitives.

These are the most-called functions in the whole package. They accept any
combination of scalar and array inputs (NumPy-style broadcasting) and
dispatch to the fastest available backend.

Examples
--------
>>> from merton import distance_to_default, prob_of_default
>>> dd = distance_to_default(100, 0.25, 60, 0.04, 1.0)
>>> float(round(dd, 4))
2.0783
>>> float(round(prob_of_default(dd), 6))
0.018841
"""

from __future__ import annotations

import numpy as np

from .._backend import get_kernel, resolve
from .._typing import ArrayLike, FloatArray
from ..exceptions import MertonInputError, NonFiniteInputError


def _size(*arrays: object) -> int:
    sz = 1
    for a in arrays:
        sz = max(sz, int(np.size(np.asarray(a))))
    return sz


def _validate(
    asset_value: object,
    asset_vol: object,
    debt: object,
    rf: object,
    T: object,
) -> None:
    asset_value_arr = np.asarray(asset_value, dtype=np.float64)
    asset_vol_arr = np.asarray(asset_vol, dtype=np.float64)
    debt_arr = np.asarray(debt, dtype=np.float64)
    T_arr = np.asarray(T, dtype=np.float64)
    rf_arr = np.asarray(rf, dtype=np.float64)
    for name, arr in (
        ("asset_value", asset_value_arr),
        ("asset_vol", asset_vol_arr),
        ("debt", debt_arr),
        ("rf", rf_arr),
        ("T", T_arr),
    ):
        if not np.all(np.isfinite(arr)):
            raise NonFiniteInputError(
                f"{name} contains NaN or infinity",
                suggested_fix="Drop or impute the offending rows before calling.",
            )
    if np.any(asset_value_arr <= 0):
        raise MertonInputError("asset_value must be strictly positive")
    if np.any(asset_vol_arr <= 0):
        raise MertonInputError("asset_vol must be strictly positive")
    if np.any(debt_arr <= 0):
        raise MertonInputError("debt must be strictly positive")
    if np.any(T_arr <= 0):
        raise MertonInputError("T must be strictly positive")


[docs] def distance_to_default( asset_value: ArrayLike, asset_vol: ArrayLike, debt: ArrayLike, rf: ArrayLike, T: ArrayLike, *, dividend_yield: ArrayLike = 0.0, backend: str | None = None, validate: bool = True, ) -> FloatArray: r"""Compute Merton's distance-to-default ``DD = d_2``. .. math:: DD = \frac{\ln(A/D) + (r - q - \sigma_A^2 / 2)\, T}{\sigma_A \sqrt{T}} Parameters ---------- asset_value Firm asset value(s) ``A`` (strictly positive). asset_vol Annualised asset volatility ``σ_A`` (strictly positive). debt Default threshold ``D`` (strictly positive). For real firms this is typically computed via :func:`merton.core.compute_default_point`. rf Risk-free rate ``r`` (decimal). T Horizon in years (strictly positive). dividend_yield Continuous dividend yield ``q``. Defaults to 0. backend Force a particular backend. validate If True, check for non-finite and out-of-domain inputs. Returns ------- FloatArray ``DD`` — number of asset-volatility standard deviations between the log of assets and the log of the default threshold. References ---------- Merton (1974). Vassalou & Xing (2004). Crosbie & Bohn (2003). Examples -------- >>> import numpy as np >>> dd = distance_to_default( ... asset_value=np.array([100.0, 150.0]), ... asset_vol=0.25, ... debt=60.0, ... rf=0.04, ... T=1.0, ... ) >>> dd.shape (2,) """ if validate: _validate(asset_value, asset_vol, debt, rf, T) chosen = resolve( asset_value, asset_vol, debt, rf, T, backend=backend, size=_size(asset_value, asset_vol, debt, rf, T), ) kernel = get_kernel(chosen, "distance_to_default_kernel") return kernel(asset_value, asset_vol, debt, rf, T, dividend_yield)
[docs] def prob_of_default( dd: ArrayLike, *, backend: str | None = None, ) -> FloatArray: r"""Risk-neutral probability of default given the distance-to-default. .. math:: \text{PD} = \Phi(-DD) where :math:`\Phi` is the standard-normal CDF. Examples -------- >>> import numpy as np >>> float(round(prob_of_default(2.5), 5)) 0.00621 """ chosen = resolve(dd, backend=backend, size=_size(dd)) kernel = get_kernel(chosen, "prob_of_default_kernel") return kernel(dd)
__all__ = ["distance_to_default", "prob_of_default"]