Default cascades with thresholds and recovery
A cascade of defaults through interbank exposures, from a shock to the external assets of some banks.
- Name:
default_cascade - Field:
economics/finance - Formalism: agent-based system
- References: Thurner et al. 2018; Bardoscia et al. 2021; Gai and Kapadia 2010
Select this system
from skcomplex.systems import create_simulation, create_system, describe
system = create_system('default_cascade')
entry = describe('default_cascade')
trajectory = create_simulation('default_cascade').prepare().run().trajectory_
Factory
@register_system(
field='economics/finance',
tags=('systemic risk', 'network', 'contagion', 'default cascade', 'agents'),
references=('Thurner et al. 2018', 'Bardoscia et al. 2021', 'Gai and Kapadia 2010'),
defaults={'until': 30},
)
def default_cascade(
liabilities: Sequence[Sequence[float]] | None = None,
equity: Sequence[float] | None = None,
*,
shocked: Sequence[int] = (0,),
shock: float = 1.0,
threshold: float = 0.0,
recovery: float = 0.0,
n_banks: int = 250,
degree: float = 4.0,
interbank_share: float = 0.2,
capital_ratio: float = 0.04,
random_state: int | np.random.Generator | None = 0,
) -> System:
"""Return a cascade of defaults through interbank exposures, from a shock to the external assets of some banks.
Banks are linked by contracts, `liabilities[i][j]` the amount bank `i` owes bank `j`. A bank whose equity falls
below the threshold `h` defaults, and its creditors write off their claims on it, losing `(1 - R) L_ij` of their
equity at the next step, which may default them in turn. This is the cascade of Thurner et al. (2018, Section
5.5.3.2), and with `h = 0` and the recovery rate `R = 0` it is the contagion on default of Gai and Kapadia (2010) as
Bardoscia et al. (2021, Section 3.1) review it. The shocked banks lose the share `shock` of their external assets at
the start. Without a given network, every bank has assets of 100, a fifth of them interbank claims split evenly over
a random number of debtors of mean `degree`, and capital of 4 percent, the network of Gai and Kapadia, whose
cascades are large in a window of intermediate degrees and rare for sparse or dense networks.
Args:
liabilities:
The interbank liabilities, or `None` for the random network of Gai and Kapadia.
equity:
The equity of every bank before the shock, or `None` for the capital of the random network.
shocked:
The indices of the banks whose external assets the shock hits.
shock:
The share of their external assets the shocked banks lose.
threshold:
The equity below which a bank defaults.
recovery:
The share of a claim on a defaulted bank that its creditor recovers.
n_banks:
The number of banks of the random network.
degree:
The mean number of debtors of a bank of the random network.
interbank_share:
The share of the assets of a bank of the random network that are interbank claims.
capital_ratio:
The capital of a bank of the random network as a share of its assets.
random_state:
The seed or generator of the random network.
Returns:
system:
The system, with the group `banks` and the cascade `cascade`, whose reporters are the number of defaulted
banks `cascade.defaults` and the losses written off in the step `cascade.losses`.
Examples:
>>> from skcomplex.systems.economics.finance import default_cascade
>>> default_cascade([[0.0, 5.0], [0.0, 0.0]], [1.0, 2.0]).elements[0].prepare().size_
2
"""
if liabilities is None or equity is None:
rng = np.random.default_rng(random_state)
matrix, external, capital = _gai_kapadia_network(n_banks, degree, interbank_share, capital_ratio, rng)
else:
matrix, capital = np.asarray(liabilities, dtype=float), np.asarray(equity, dtype=float)
external = np.maximum(capital, 0.0)
size = len(matrix)
hit = np.zeros(size, dtype=bool)
hit[list(shocked)] = True
losses = np.where(hit, shock * external, 0.0)
after = capital - losses
status = np.where(after < threshold, DEFAULTING, SOLVENT).astype(float)
banks = GroupElement(
'banks',
element=BalanceSheetElement,
size=size,
values={'external': external - losses, 'equity': after, 'status': status},
)
cascade = DefaultCascadeInteraction(
'cascade',
banks='banks',
liabilities=matrix.tolist(),
threshold=threshold,
recovery=recovery,
)
return System(elements=[banks], interactions=[cascade])
Components
Elements:
BalanceSheetElement, fromElement
Interactions:
DefaultCascadeInteraction, 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 DefaultCascadeInteraction(FeedbackInteraction[SystemView]):
"""Synchronous cascade of defaults, in which the creditors of a defaulting bank write off their claims on it.
Every step the banks that defaulted in the last step pass their losses on: every creditor `j` of a defaulting bank
`i` loses `(1 - recovery) L_ij` of its equity, and a solvent bank whose equity falls below the threshold defaults.
Args:
label:
The label of the cascade.
rule:
Unused, since the cascade overrides `act`.
phase:
The phase the cascade 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`.
threshold:
The equity below which a bank defaults.
recovery:
The share of a claim on a defaulted bank that its creditor recovers.
Examples:
>>> from skcomplex.systems.economics.finance import DefaultCascadeInteraction
>>> DefaultCascadeInteraction('cascade', banks='banks').reports
('defaults', 'losses')
"""
reports: ClassVar[tuple[str, ...]] = ('defaults', 'losses')
def __init__(
self,
label: str,
*,
rule: None = None,
phase: str | None = None,
banks: Annotated[str, RoleSpec()],
liabilities: Sequence[Sequence[float]] = (),
threshold: Annotated[float, ParameterSpec()] = 0.0,
recovery: Annotated[float, ParameterSpec(domain=RealSet.closed(0, 1))] = 0.0,
) -> None:
super().__init__(label, rule=rule, phase=phase)
self.banks = banks
self.liabilities = liabilities
self.threshold = threshold
self.recovery = recovery
def act(self, system: SystemView) -> None:
"""Pass on the losses of the defaulting banks and default the banks below the threshold.
Args:
system:
The system view of the cascade.
"""
banks = sorted(system.agents('banks', BalanceSheetView), key=lambda bank: bank.id)
status = np.array([bank.status for bank in banks])
equity = np.array([bank.equity for bank in banks])
defaulting = status == DEFAULTING
losses = (1 - system.parameter('recovery')) * np.asarray(self.liabilities, dtype=float).T @ defaulting
equity = equity - losses
hit = (status == SOLVENT) & (equity < system.parameter('threshold'))
status = np.where(defaulting, DEFAULTED, np.where(hit, DEFAULTING, status))
for bank, value, code in zip(banks, equity, status, strict=True):
bank.equity, bank.status = float(value), float(code)
system.report('defaults', float(np.sum(status != SOLVENT)))
system.report('losses', float(losses.sum()))