Skip to content

Eisenberg-Noe clearing of interbank payments

The clearing of interbank payments of Eisenberg and Noe, a fixed point of proportional payments.

  • Name: eisenberg_noe_clearing
  • Field: economics/finance
  • Formalism: agent-based system
  • References: Bardoscia et al. 2021; Eisenberg and Noe 2001

Select this system

from skcomplex.systems import create_simulation, create_system, describe

system = create_system('eisenberg_noe_clearing')
entry = describe('eisenberg_noe_clearing')
trajectory = create_simulation('eisenberg_noe_clearing').prepare().run().trajectory_

Factory

@register_system(
    field='economics/finance',
    tags=('systemic risk', 'network', 'clearing', 'payments', 'fixed point', 'agents'),
    references=('Bardoscia et al. 2021', 'Eisenberg and Noe 2001'),
    defaults={'until': 30},
)
def eisenberg_noe_clearing(
    liabilities: Sequence[Sequence[float]] | None = None,
    external: Sequence[float] | None = None,
    *,
    n_banks: int = 50,
    density: float = 0.1,
    random_state: int | np.random.Generator | None = 0,
) -> System:
    """Return the clearing of interbank payments of Eisenberg and Noe, a fixed point of proportional payments.

    Bank `i` owes `L_ij` to bank `j`, `p_bar_i` in all, and holds external assets `e_i`. A clearing vector pays
    `p_i = min(p_bar_i, e_i + sum_j Pi_ji p_j)`, every bank paying in full or all it has, its debtors paying in
    proportion `Pi_ij = L_ij / p_bar_i`, the liquidity contagion of Bardoscia et al. (2021, Section 3.1). Every step is
    one round of the iteration from full payments, which falls monotonically to the greatest clearing vector, the
    solution of the linear programme `max sum_i p_i` with `p <= p_bar` and `p <= e + Pi^T p`. Without a given network
    the liabilities are random and the external assets are small for a few banks.

    Args:
        liabilities:
            The interbank liabilities, or `None` for a random network.

        external:
            The external assets of every bank, or `None` for random ones.

        n_banks:
            The number of banks of a random network.

        density:
            The probability of a liability between two banks of a random network.

        random_state:
            The seed or generator of a random network.

    Returns:
        system:
            The system, with the group `banks` of the payments `banks.payment`, and the clearing `clearing`, whose
            reporters are the total unpaid liabilities `clearing.shortfall` and the number of defaulting banks
            `clearing.defaults`.

    Examples:
        >>> from skcomplex.systems.economics.finance import eisenberg_noe_clearing
        >>> eisenberg_noe_clearing([[0.0, 2.0], [1.0, 0.0]], [0.5, 1.0]).elements[0].prepare().size_
        2
    """
    if liabilities is None or external is None:
        rng = np.random.default_rng(random_state)
        links = rng.random((n_banks, n_banks)) < density
        np.fill_diagonal(links, val=False)
        matrix = links * rng.uniform(1.0, 10.0, (n_banks, n_banks))
        assets = rng.exponential(5.0, n_banks)
    else:
        matrix, assets = np.asarray(liabilities, dtype=float), np.asarray(external, dtype=float)
    banks = GroupElement(
        'banks',
        element=BalanceSheetElement,
        size=len(matrix),
        values={'external': assets, 'payment': matrix.sum(axis=1)},
    )
    clearing = PaymentClearingInteraction('clearing', banks='banks', liabilities=matrix.tolist())
    return System(elements=[banks], interactions=[clearing])

Components

Elements:

  • BalanceSheetElement, from Element

Interactions:

  • PaymentClearingInteraction, from FeedbackInteraction
class BalanceSheetElement(Element):
    """Bank of an interbank network, with its external assets, its equity, its payments, and its state of default.

    The bank is a passive agent that the contagion of the system updates, a solvency cascade through its equity or a
    clearing of payments through its payment.

    Args:
        label:
            The label of the agent or group.

        visual:
            How the element is drawn, or `None` for `default_visual_`.

        external:
            The external assets.

        equity:
            The equity, the assets minus the liabilities.

        payment:
            The payment the bank makes on its interbank liabilities.

        status:
            0 for solvent, 1 for defaulting in this step, and 2 for defaulted.

    Examples:
        >>> from skcomplex.core.components.elements import GroupElement
        >>> from skcomplex.systems.economics.finance import BalanceSheetElement
        >>> GroupElement('banks', element=BalanceSheetElement, size=3, values={'equity': 4.0}).size
        3
    """

    def __init__(
        self,
        label: str,
        visual: VisualSpec | None = VisualSpec('agents', {'color_by': 'status'}),
        *,
        external: Annotated[float, StateSpec(domain=RealSet.closed_open(0, oo))] = 0.0,
        equity: Annotated[float, StateSpec()] = 0.0,
        payment: Annotated[float, StateSpec(domain=RealSet.closed_open(0, oo))] = 0.0,
        status: Annotated[float, StateSpec(domain=NN)] = SOLVENT,
    ) -> None:
        super().__init__(label, visual=visual)
        self.external = external
        self.equity = equity
        self.payment = payment
        self.status = status

    @classmethod
    def _get_view(cls) -> type[BalanceSheetView]:
        """Return the view of the agents of the element."""
        return BalanceSheetView

    def _get_laws(self, s: SimpleNamespace) -> list[BaseLaw]:
        """Return no laws, since the contagion updates every bank at once.

        Args:
            s:
                The namespace of the symbols the laws can use.

        Returns:
            laws:
                The laws of the component.
        """
        return []

class PaymentClearingInteraction(FeedbackInteraction[SystemView]):
    """Clearing of interbank payments by the iteration `p <- min(p_bar, e + Pi^T p)` of Eisenberg and Noe.

    Every step every bank pays the smaller of what it owes, `p_bar_i = sum_j L_ij`, and what it has, its external assets
    plus the payments it receives, which its debtors make in proportion to their liabilities, `Pi_ij = L_ij / p_bar_i`.
    From full payments the iteration falls to the greatest clearing vector, and a bank that pays less than it owes
    defaults.

    Args:
        label:
            The label of the clearing.

        rule:
            Unused, since the clearing overrides `act`.

        phase:
            The phase the clearing acts in, or `None` for the default phase.

        banks:
            The path of the group of banks.

        liabilities:
            The interbank liabilities, `liabilities[i][j]` owed by bank `i` to bank `j`.

    Examples:
        >>> from skcomplex.systems.economics.finance import PaymentClearingInteraction
        >>> PaymentClearingInteraction('clearing', banks='banks').reports
        ('shortfall', 'defaults')
    """

    reports: ClassVar[tuple[str, ...]] = ('shortfall', 'defaults')

    def __init__(
        self,
        label: str,
        *,
        rule: None = None,
        phase: str | None = None,
        banks: Annotated[str, RoleSpec()],
        liabilities: Sequence[Sequence[float]] = (),
    ) -> None:
        super().__init__(label, rule=rule, phase=phase)
        self.banks = banks
        self.liabilities = liabilities

    def act(self, system: SystemView) -> None:
        """Make one round of payments and record the shortfall and the defaults.

        Args:
            system:
                The system view of the clearing.
        """
        banks = sorted(system.agents('banks', BalanceSheetView), key=lambda bank: bank.id)
        matrix = np.asarray(self.liabilities, dtype=float)
        owed = matrix.sum(axis=1)
        shares = np.divide(matrix, owed[:, None], out=np.zeros_like(matrix), where=owed[:, None] > 0)
        payments = np.array([bank.payment for bank in banks])
        external = np.array([bank.external for bank in banks])
        payments = np.minimum(owed, external + shares.T @ payments)
        equity = external + shares.T @ payments - payments
        defaulted = payments < owed * (1 - 1e-12)
        for bank, paid, value, flag in zip(banks, payments, equity, defaulted, strict=True):
            bank.payment, bank.equity, bank.status = float(paid), float(value), float(DEFAULTED * flag)
        system.report('shortfall', float(np.sum(owed - payments)))
        system.report('defaults', float(defaulted.sum()))