1"""Balance sheets with exact integer accounting and explicit reservations.23Invariant enforced by `Ledger.check_conservation()` after every fill:4 sum(cash of all agents) + exchange_fees_collected == initial_cash_total + dividends_paid_total5 sum(positions of all agents per asset) == initial_shares_total per asset (the exchange never creates shares)6"""7from __future__ import annotations89from dataclasses import dataclass, field101112@dataclass(slots=True)13class Account:14 agent_id: int15 cash: int # cents16 positions: dict[int, int] # asset -> lots (negative if short)17 reserved_cash: int = 0 # cents locked by open bids18 reserved_qty: dict[int, int] = field(default_factory=dict) # asset -> lots locked by open asks19 frozen: bool = False # bankrupt / liquidated: no new orders20 realized_fees: int = 0 # cents paid in fees (cumulative)21 dividends_received: int = 022 n_orders: int = 023 n_fills: int = 024 n_rejects: int = 025 n_cancels: int = 026 traded_qty: int = 027 # risk settings (per agent)28 leverage_cap: float = 1.0 # max gross exposure / equity (1.0 = no borrowing)29 short_allowed: bool = False30 maintenance_margin: float = 0.25 # equity / gross exposure must stay above this if leveraged31 initial_cash: int = 032 initial_positions: dict[int, int] = field(default_factory=dict)3334 @property35 def free_cash(self) -> int:36 return self.cash - self.reserved_cash3738 def free_qty(self, asset: int) -> int:39 return self.positions.get(asset, 0) - self.reserved_qty.get(asset, 0)4041 def equity(self, marks: dict[int, float]) -> float:42 """Cash + marked positions (marks in cents per lot)."""43 return self.cash + sum(q * marks.get(a, 0.0) for a, q in self.positions.items())4445 def gross_exposure(self, marks: dict[int, float]) -> float:46 return sum(abs(q) * marks.get(a, 0.0) for a, q in self.positions.items())4748 def net_worth_units(self, marks: dict[int, float]) -> float:49 return self.equity(marks) / 100.0505152class Ledger:53 """All accounts plus the exchange's own fee account; owns the conservation checks."""5455 def __init__(self) -> None:56 self.accounts: dict[int, Account] = {}57 self.exchange_fees: int = 058 self.dividends_paid: int = 059 self.initial_cash_total: int = 060 self.initial_shares: dict[int, int] = {}6162 def add(self, acct: Account) -> None:63 acct.initial_cash = acct.cash64 acct.initial_positions = dict(acct.positions)65 self.accounts[acct.agent_id] = acct66 self.initial_cash_total += acct.cash67 for a, q in acct.positions.items():68 self.initial_shares[a] = self.initial_shares.get(a, 0) + q6970 def __getitem__(self, agent_id: int) -> Account:71 return self.accounts[agent_id]7273 def apply_fill(self, buyer: int, seller: int, asset: int, price_cents: int, qty: int,74 buyer_fee: int, seller_fee: int, buyer_reserved: int, seller_reserved_qty: int) -> None:75 """Move cash and lots; release the reservations attached to the two orders for this quantity."""76 notional = price_cents * qty77 b, s = self.accounts[buyer], self.accounts[seller]78 b.cash -= notional + buyer_fee79 s.cash += notional - seller_fee80 b.positions[asset] = b.positions.get(asset, 0) + qty81 s.positions[asset] = s.positions.get(asset, 0) - qty82 b.reserved_cash -= buyer_reserved83 s.reserved_qty[asset] = s.reserved_qty.get(asset, 0) - seller_reserved_qty84 b.realized_fees += buyer_fee85 s.realized_fees += seller_fee86 b.n_fills += 187 s.n_fills += 188 b.traded_qty += qty89 s.traded_qty += qty90 self.exchange_fees += buyer_fee + seller_fee91 assert b.reserved_cash >= 0, "negative cash reservation"92 assert s.reserved_qty[asset] >= 0 or s.short_allowed, "negative qty reservation"9394 def pay_dividend(self, asset: int, per_lot_cents: int) -> int:95 """Pays (or charges, for shorts) a cash dividend; returns the net cash injected."""96 total = 097 for acct in self.accounts.values():98 q = acct.positions.get(asset, 0)99 if q:100 d = per_lot_cents * q101 acct.cash += d102 acct.dividends_received += d103 total += d104 self.dividends_paid += total105 return total106107 def check_conservation(self) -> None:108 cash = sum(a.cash for a in self.accounts.values()) + self.exchange_fees109 assert cash == self.initial_cash_total + self.dividends_paid, (110 f"cash not conserved: {cash} != {self.initial_cash_total} + {self.dividends_paid}")111 for asset, n0 in self.initial_shares.items():112 n = sum(a.positions.get(asset, 0) for a in self.accounts.values())113 assert n == n0, f"shares not conserved for asset {asset}: {n} != {n0}"114 for a in self.accounts.values():115 assert a.reserved_cash >= 0 and a.reserved_cash <= max(a.cash, 0) or a.leverage_cap > 1.0, (116 f"agent {a.agent_id}: reserved_cash {a.reserved_cash} > cash {a.cash}")117 for asset, rq in a.reserved_qty.items():118 assert rq >= 0, f"agent {a.agent_id}: negative reserved qty"119 assert a.short_allowed or rq <= a.positions.get(asset, 0), (120 f"agent {a.agent_id}: reserved {rq} > held {a.positions.get(asset, 0)} on asset {asset}")121