Source code for merton.core.default_point

"""Default-point formulas.

The *default point* is the liability threshold ``L`` at which the firm is
deemed to default. Several conventions exist:

- **KMV** (``ST + 0.5·LT``): Crosbie & Bohn (2003) empirically-tuned formula.
- **TOTAL** (``ST + LT``): the raw total debt — Merton's original 1974 model.
- **SHORT_ONLY** (``ST``): a stress-test convention.
- **CUSTOM**: a user-supplied callable.

Examples
--------
>>> compute_default_point(20_000_000, 30_000_000, kind="kmv")
35000000.0
>>> compute_default_point(20_000_000, 30_000_000, kind="total")
50000000.0
"""

from __future__ import annotations

from collections.abc import Callable
from enum import Enum
from typing import TypeAlias

import numpy as np

from .._typing import ArrayLike, FloatArray
from ..exceptions import MertonInputError


[docs] class DefaultPoint(str, Enum): """How to compute the default threshold from a firm's balance-sheet items."""
[docs] KMV = "kmv"
[docs] TOTAL = "total"
[docs] SHORT_ONLY = "short_only"
[docs] CUSTOM = "custom"
_KMV_LT_WEIGHT = 0.5
[docs] DefaultPointLike: TypeAlias = str | DefaultPoint
DefaultPointCallable: TypeAlias = Callable[[ArrayLike, ArrayLike], FloatArray] def _coerce(kind: DefaultPointLike) -> DefaultPoint: if isinstance(kind, DefaultPoint): return kind try: return DefaultPoint(kind.lower()) except ValueError as err: raise MertonInputError( f"Unknown default-point kind {kind!r}", suggested_fix="Use one of: 'kmv', 'total', 'short_only', 'custom'.", ) from err
[docs] def compute_default_point( debt_short: ArrayLike, debt_long: ArrayLike, *, kind: DefaultPointLike = DefaultPoint.KMV, custom: DefaultPointCallable | None = None, ) -> FloatArray: """Return the default threshold given short- and long-term debt. Parameters ---------- debt_short, debt_long Balance-sheet values (scalar or array). Must be ≥ 0. kind One of ``"kmv"``, ``"total"``, ``"short_only"``, ``"custom"``. custom Required when ``kind == "custom"``. Signature ``(debt_short, debt_long) -> FloatArray``. """ enum = _coerce(kind) st = np.asarray(debt_short, dtype=np.float64) lt = np.asarray(debt_long, dtype=np.float64) if np.any(st < 0) or np.any(lt < 0): raise MertonInputError( "debt_short and debt_long must be non-negative", suggested_fix="Check the input columns / signs.", ) if enum is DefaultPoint.KMV: return st + _KMV_LT_WEIGHT * lt if enum is DefaultPoint.TOTAL: return st + lt if enum is DefaultPoint.SHORT_ONLY: return st if enum is DefaultPoint.CUSTOM: if custom is None: raise MertonInputError( "kind='custom' requires the `custom` callable", suggested_fix="Pass `custom=lambda st, lt: ...`.", ) return np.asarray(custom(st, lt), dtype=np.float64) raise MertonInputError(f"Unhandled default-point kind: {enum}") # pragma: no cover
__all__ = ["DefaultPoint", "DefaultPointLike", "compute_default_point"]