"""Deterministic ledger for Model C: investors, one bank, one risky asset in fixed supply, a safe deposit/loan rate. Balance sheets -------------- Investor i : assets = deposits c_i + p h_i ; liabilities = loan_i ; equity_i = c_i + p h_i - loan_i. Bank : assets = sum loan_i + reserves ; liabilities = sum c_i + exchange account F + bank equity E_B. Reserves are the bank's claim on the exogenous safe technology; they change only through *injections* (dividends paid by the asset issuer into investors' deposits, interest earned on reserves). Every other operation is a transfer. Invariants (asserted after every operation): (I1) sum h_i == S (no asset creation; no short sales in this version) (I2) sum c_i + F + E_B == sum loan_i + reserves (bank balance sheet) (I3) reserves == reserves_0 + cumulative injections (cash is only created by the outside sources) Margin rule: loan_i <= (1 - m) p h_i. A breach triggers a forced sale at the current price (with optional price impact); if the loan cannot be repaid the residual is written off against bank equity (default); if bank equity turns negative, depositors are bailed in pro rata. Bank lending capacity: E_B >= k_B * (loans + reserves) (leverage ratio). """ from __future__ import annotations import numpy as np class Ledger: def __init__(self, n: int, cash0, holdings0, bank_equity0: float, margin: float, fee_rate: float = 0.0, price0: float = 1.0, rf: float = 0.0, bank_capital_ratio: float = 0.0): self.n = n self.cash = np.array(cash0, dtype=float) self.holdings = np.array(holdings0, dtype=float) self.loan = np.zeros(n) self.S = float(self.holdings.sum()) self.margin = margin self.fee_rate = fee_rate self.rf = rf self.price = float(price0) self.bank_equity = float(bank_equity0) self.F = 0.0 # exchange account (fees) self.reserves = float(self.cash.sum() + bank_equity0) self._reserves0 = self.reserves self.injections = 0.0 self.k_B = bank_capital_ratio self.loan_losses = 0.0 self.deposit_haircuts = 0.0 self.defaults = 0 self.forced_sales = 0.0 self.alive = np.ones(n, dtype=bool) self.check() # ---------------------------------------------------------------- identities def equity(self): return self.cash + self.price * self.holdings - self.loan def check(self): tol = 1e-7 * max(1.0, abs(self.reserves)) assert abs(self.holdings.sum() - self.S) < 1e-7 * max(1.0, self.S), 'I1 holdings' assert np.all(self.holdings >= -1e-9), 'I1 short' assert np.all(self.loan >= -1e-9), 'negative loan' lhs = self.cash.sum() + self.F + self.bank_equity rhs = self.loan.sum() + self.reserves assert abs(lhs - rhs) < tol, f'I2 bank balance sheet {lhs} vs {rhs}' assert abs(self.reserves - (self._reserves0 + self.injections)) < tol, 'I3 reserves' # ---------------------------------------------------------------- outside flows (injections) def pay_dividend(self, d: float): amt = d * self.holdings self.cash += amt self.reserves += amt.sum() self.injections += amt.sum() self.check() def accrue_interest(self): """Deposits earn rf, loans cost rf; reserves earn rf from the safe technology (injection).""" dep_int = self.rf * self.cash loan_int = self.rf * self.loan res_int = self.rf * self.reserves self.cash += dep_int - loan_int self.reserves += res_int self.injections += res_int self.bank_equity += res_int + loan_int.sum() - dep_int.sum() self.check() # ---------------------------------------------------------------- credit def loan_capacity(self, i: int) -> float: cap_margin = max((1 - self.margin) * self.price * self.holdings[i] - self.loan[i], 0.0) if self.k_B > 0: # E_B >= k_B (loans + x + reserves) -> x <= E_B/k_B - loans - reserves cap_bank = max(self.bank_equity / self.k_B - self.loan.sum() - self.reserves, 0.0) cap_margin = min(cap_margin, cap_bank) return cap_margin def borrow(self, i: int, amount: float) -> float: amt = float(min(max(amount, 0.0), self.loan_capacity(i))) self.loan[i] += amt self.cash[i] += amt self.check() return amt def repay(self, i: int, amount: float) -> float: amt = float(min(max(amount, 0.0), self.loan[i], max(self.cash[i], 0.0))) self.loan[i] -= amt self.cash[i] -= amt self.check() return amt # ---------------------------------------------------------------- trading def settle_trade(self, buyer: int, seller: int, qty: float, price: float) -> bool: if qty <= 0 or buyer == seller or price <= 0: return False cost = qty * price fee = self.fee_rate * cost if self.cash[buyer] + 1e-12 < cost + fee or self.holdings[seller] + 1e-12 < qty: return False self.cash[buyer] -= cost + fee self.cash[seller] += cost - fee self.F += 2 * fee self.holdings[buyer] += qty self.holdings[seller] -= qty self.holdings[seller] = max(self.holdings[seller], 0.0) self.price = price self.check() return True def max_affordable(self, i: int, price: float) -> float: return max(self.cash[i], 0.0) / (price * (1 + self.fee_rate)) # ---------------------------------------------------------------- margin, default, bail-in def mark_to_market(self, price: float, liquidation_impact: float = 0.0) -> dict: self.price = float(price) forced = losses = 0.0 nd = 0 for i in range(self.n): breach = self.loan[i] - (1 - self.margin) * self.price * self.holdings[i] if breach <= 1e-10: continue x = min(self.holdings[i], breach / max(self.margin * self.price, 1e-12)) exec_price = self.price * max(1 - liquidation_impact * x / max(self.S, 1e-12), 0.05) others = [j for j in range(self.n) if j != i and self.alive[j] and self.cash[j] > 0] if x > 0 and others: w = np.array([self.cash[j] for j in others]) proceeds = x * exec_price if w.sum() < proceeds: # buyers' cash limits absorption x = w.sum() / exec_price proceeds = x * exec_price share = w / w.sum() self.holdings[i] -= x self.cash[i] += proceeds for j, s in zip(others, share): self.holdings[j] += x * s self.cash[j] -= proceeds * s self.price = exec_price forced += x self.forced_sales += x self.repay(i, self.loan[i]) if self.loan[i] > 1e-10 and (self.cash[i] <= 1e-10 or self.equity()[i] < 0): loss = self.loan[i] self.loan[i] = 0.0 self.bank_equity -= loss self.loan_losses += loss losses += loss self.defaults += 1 nd += 1 self.alive[i] = False if self.bank_equity < -1e-10: short = -self.bank_equity dep = np.maximum(self.cash, 0.0) if dep.sum() > 0: cut = min(short, dep.sum()) self.cash -= cut * dep / dep.sum() self.deposit_haircuts += cut self.bank_equity += cut self.check() return dict(forced=forced, losses=losses, defaults=nd) def snapshot(self): return dict(cash=self.cash.copy(), holdings=self.holdings.copy(), loan=self.loan.copy(), price=self.price, bank_equity=self.bank_equity, reserves=self.reserves, defaults=self.defaults, forced_sales=self.forced_sales, loan_losses=self.loan_losses, fees=self.F, deposit_haircuts=self.deposit_haircuts, leverage=float(self.loan.sum() / max(self.price * self.S, 1e-12)))