1"""Deterministic ledger for Model C: investors, one bank, one risky asset in fixed supply, a safe deposit/loan rate.23Balance sheets4--------------5Investor i : assets = deposits c_i + p h_i ; liabilities = loan_i ; equity_i = c_i + p h_i - loan_i.6Bank : assets = sum loan_i + reserves ; liabilities = sum c_i + exchange account F + bank equity E_B.7Reserves are the bank's claim on the exogenous safe technology; they change only through *injections* (dividends paid8by the asset issuer into investors' deposits, interest earned on reserves). Every other operation is a transfer.910Invariants (asserted after every operation):11 (I1) sum h_i == S (no asset creation; no short sales in this version)12 (I2) sum c_i + F + E_B == sum loan_i + reserves (bank balance sheet)13 (I3) reserves == reserves_0 + cumulative injections (cash is only created by the outside sources)14Margin rule: loan_i <= (1 - m) p h_i. A breach triggers a forced sale at the current price (with optional price15impact); if the loan cannot be repaid the residual is written off against bank equity (default); if bank equity turns16negative, depositors are bailed in pro rata. Bank lending capacity: E_B >= k_B * (loans + reserves) (leverage ratio).17"""18from __future__ import annotations1920import numpy as np212223class Ledger:24 def __init__(self, n: int, cash0, holdings0, bank_equity0: float, margin: float, fee_rate: float = 0.0,25 price0: float = 1.0, rf: float = 0.0, bank_capital_ratio: float = 0.0):26 self.n = n27 self.cash = np.array(cash0, dtype=float)28 self.holdings = np.array(holdings0, dtype=float)29 self.loan = np.zeros(n)30 self.S = float(self.holdings.sum())31 self.margin = margin32 self.fee_rate = fee_rate33 self.rf = rf34 self.price = float(price0)35 self.bank_equity = float(bank_equity0)36 self.F = 0.0 # exchange account (fees)37 self.reserves = float(self.cash.sum() + bank_equity0)38 self._reserves0 = self.reserves39 self.injections = 0.040 self.k_B = bank_capital_ratio41 self.loan_losses = 0.042 self.deposit_haircuts = 0.043 self.defaults = 044 self.forced_sales = 0.045 self.alive = np.ones(n, dtype=bool)46 self.check()4748 # ---------------------------------------------------------------- identities49 def equity(self):50 return self.cash + self.price * self.holdings - self.loan5152 def check(self):53 tol = 1e-7 * max(1.0, abs(self.reserves))54 assert abs(self.holdings.sum() - self.S) < 1e-7 * max(1.0, self.S), 'I1 holdings'55 assert np.all(self.holdings >= -1e-9), 'I1 short'56 assert np.all(self.loan >= -1e-9), 'negative loan'57 lhs = self.cash.sum() + self.F + self.bank_equity58 rhs = self.loan.sum() + self.reserves59 assert abs(lhs - rhs) < tol, f'I2 bank balance sheet {lhs} vs {rhs}'60 assert abs(self.reserves - (self._reserves0 + self.injections)) < tol, 'I3 reserves'6162 # ---------------------------------------------------------------- outside flows (injections)63 def pay_dividend(self, d: float):64 amt = d * self.holdings65 self.cash += amt66 self.reserves += amt.sum()67 self.injections += amt.sum()68 self.check()6970 def accrue_interest(self):71 """Deposits earn rf, loans cost rf; reserves earn rf from the safe technology (injection)."""72 dep_int = self.rf * self.cash73 loan_int = self.rf * self.loan74 res_int = self.rf * self.reserves75 self.cash += dep_int - loan_int76 self.reserves += res_int77 self.injections += res_int78 self.bank_equity += res_int + loan_int.sum() - dep_int.sum()79 self.check()8081 # ---------------------------------------------------------------- credit82 def loan_capacity(self, i: int) -> float:83 cap_margin = max((1 - self.margin) * self.price * self.holdings[i] - self.loan[i], 0.0)84 if self.k_B > 0:85 # E_B >= k_B (loans + x + reserves) -> x <= E_B/k_B - loans - reserves86 cap_bank = max(self.bank_equity / self.k_B - self.loan.sum() - self.reserves, 0.0)87 cap_margin = min(cap_margin, cap_bank)88 return cap_margin8990 def borrow(self, i: int, amount: float) -> float:91 amt = float(min(max(amount, 0.0), self.loan_capacity(i)))92 self.loan[i] += amt93 self.cash[i] += amt94 self.check()95 return amt9697 def repay(self, i: int, amount: float) -> float:98 amt = float(min(max(amount, 0.0), self.loan[i], max(self.cash[i], 0.0)))99 self.loan[i] -= amt100 self.cash[i] -= amt101 self.check()102 return amt103104 # ---------------------------------------------------------------- trading105 def settle_trade(self, buyer: int, seller: int, qty: float, price: float) -> bool:106 if qty <= 0 or buyer == seller or price <= 0:107 return False108 cost = qty * price109 fee = self.fee_rate * cost110 if self.cash[buyer] + 1e-12 < cost + fee or self.holdings[seller] + 1e-12 < qty:111 return False112 self.cash[buyer] -= cost + fee113 self.cash[seller] += cost - fee114 self.F += 2 * fee115 self.holdings[buyer] += qty116 self.holdings[seller] -= qty117 self.holdings[seller] = max(self.holdings[seller], 0.0)118 self.price = price119 self.check()120 return True121122 def max_affordable(self, i: int, price: float) -> float:123 return max(self.cash[i], 0.0) / (price * (1 + self.fee_rate))124125 # ---------------------------------------------------------------- margin, default, bail-in126 def mark_to_market(self, price: float, liquidation_impact: float = 0.0) -> dict:127 self.price = float(price)128 forced = losses = 0.0129 nd = 0130 for i in range(self.n):131 breach = self.loan[i] - (1 - self.margin) * self.price * self.holdings[i]132 if breach <= 1e-10:133 continue134 x = min(self.holdings[i], breach / max(self.margin * self.price, 1e-12))135 exec_price = self.price * max(1 - liquidation_impact * x / max(self.S, 1e-12), 0.05)136 others = [j for j in range(self.n) if j != i and self.alive[j] and self.cash[j] > 0]137 if x > 0 and others:138 w = np.array([self.cash[j] for j in others])139 proceeds = x * exec_price140 if w.sum() < proceeds: # buyers' cash limits absorption141 x = w.sum() / exec_price142 proceeds = x * exec_price143 share = w / w.sum()144 self.holdings[i] -= x145 self.cash[i] += proceeds146 for j, s in zip(others, share):147 self.holdings[j] += x * s148 self.cash[j] -= proceeds * s149 self.price = exec_price150 forced += x151 self.forced_sales += x152 self.repay(i, self.loan[i])153 if self.loan[i] > 1e-10 and (self.cash[i] <= 1e-10 or self.equity()[i] < 0):154 loss = self.loan[i]155 self.loan[i] = 0.0156 self.bank_equity -= loss157 self.loan_losses += loss158 losses += loss159 self.defaults += 1160 nd += 1161 self.alive[i] = False162 if self.bank_equity < -1e-10:163 short = -self.bank_equity164 dep = np.maximum(self.cash, 0.0)165 if dep.sum() > 0:166 cut = min(short, dep.sum())167 self.cash -= cut * dep / dep.sum()168 self.deposit_haircuts += cut169 self.bank_equity += cut170 self.check()171 return dict(forced=forced, losses=losses, defaults=nd)172173 def snapshot(self):174 return dict(cash=self.cash.copy(), holdings=self.holdings.copy(), loan=self.loan.copy(), price=self.price,175 bank_equity=self.bank_equity, reserves=self.reserves, defaults=self.defaults,176 forced_sales=self.forced_sales, loan_losses=self.loan_losses, fees=self.F,177 deposit_haircuts=self.deposit_haircuts, leverage=float(self.loan.sum() / max(self.price * self.S, 1e-12)))178