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, fromElement
Interactions:
PaymentClearingInteraction, fromFeedbackInteraction
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()))