SPB Git

spb/wp27_uqo

Public
5.5 KB · 121 lines python
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
Files · main
Menu

Theme