""" Cross-margin liquidation waterfall prediction. When a whale's cross-margin portfolio approaches liquidation, the Hyperliquid liquidation engine selects which asset to dump first based on maintenance margin requirements and order book liquidity. By monitoring large cross-margin accounts via clearinghouseState, we can predict which asset gets liquidated first and position accordingly: - Widen spreads on the predicted liquidation asset - Tighten spreads on non-liquidation assets - Pre-position for the post-liquidation bounce """ from __future__ import annotations import logging from collections import deque from typing import Optional logger = logging.getLogger(__name__) class LiquidationWaterfall: """Predict the order of cross-margin liquidations for large accounts.""" def __init__( self, danger_margin_ratio: float = 1.2, # margin_ratio < this = danger critical_margin_ratio: float = 1.05, # margin_ratio < this = imminent maintenance_margin_pct: float = 0.03, # 3% maintenance book_depth_window: int = 10, # levels to estimate liquidity ): self._danger = danger_margin_ratio self._critical = critical_margin_ratio self._mm_pct = maintenance_margin_pct self._depth_window = book_depth_window self._accounts: dict[str, dict] = {} # address → positions, equity self._book_depths: dict[str, dict] = {} # coin → {bid_depth, ask_depth} self._history: deque = deque(maxlen=1000) # ── Data feed ──────────────────────────────────────────── def update_account(self, address: str, positions: list[dict], margin_balance: float): """Update a tracked account's positions and margin.""" self._accounts[address] = { "positions": positions, "margin_balance": margin_balance, "margin_ratio": self._compute_margin_ratio(positions, margin_balance), } def update_book_depth(self, coin: str, bid_depth: float, ask_depth: float): """Update estimated order book depth for a coin.""" self._book_depths[coin.upper()] = { "bid_depth": bid_depth, "ask_depth": ask_depth, } # ── Risk assessment ───────────────────────────────────── def _compute_margin_ratio( self, positions: list[dict], margin_balance: float ) -> float: """Compute margin ratio = equity / maintenance_margin.""" if not positions or margin_balance <= 0: return float("inf") total_mm = 0.0 for pos in positions: size = abs(float(pos.get("szi", 0))) px = float(pos.get("entryPx", pos.get("markPx", 0))) total_mm += size * px * self._mm_pct return margin_balance / total_mm if total_mm > 0 else float("inf") def at_risk_accounts(self) -> list[dict]: """List accounts approaching liquidation.""" risky = [] for addr, acct in self._accounts.items(): ratio = acct["margin_ratio"] if ratio < self._danger: level = "critical" if ratio < self._critical else "danger" risky.append({ "address": addr[:10] + "...", "margin_ratio": round(ratio, 3), "level": level, "positions": len(acct["positions"]), }) return sorted(risky, key=lambda r: r["margin_ratio"]) def predict_liquidation_order(self, address: str) -> list[dict]: """Predict which assets get liquidated first for a given account. Returns assets ranked by liquidation priority (first to go = top). Uses: maintenance_margin_requirement / book_liquidity ratio. Higher ratio = less liquid relative to margin cost = dumped first. """ acct = self._accounts.get(address) if not acct: return [] positions = acct["positions"] ranked = [] for pos in positions: coin = pos.get("coin", "").upper() size = abs(float(pos.get("szi", 0))) px = float(pos.get("entryPx", pos.get("markPx", 0))) maintenance = size * px * self._mm_pct # Liquidity: how much the book can absorb before significant slippage book = self._book_depths.get(coin, {}) side = "buy" if float(pos.get("szi", 0)) < 0 else "sell" depth = book.get("bid_depth" if side == "buy" else "ask_depth", size * px * 0.1) # Liquidation priority score: higher = dumped first liquidity_ratio = maintenance / max(depth, 1e-8) ranked.append({ "coin": coin, "size": size, "notional_usd": round(size * px, 2), "maintenance_usd": round(maintenance, 2), "liquidity_ratio": round(liquidity_ratio, 4), "predicted_first": False, # set below }) # Sort by liquidity ratio (highest = least liquid = dumped first) ranked.sort(key=lambda r: r["liquidity_ratio"], reverse=True) if ranked: ranked[0]["predicted_first"] = True return ranked def signal(self, address: str) -> dict: """Generate trading signal based on predicted liquidation waterfall. If an account is in danger and we can predict the liquidation order: - Asset predicted to be dumped first → widen spreads, go short - Other assets in the portfolio → tighten spreads (safer to quote) """ acct = self._accounts.get(address) if not acct or acct["margin_ratio"] > self._danger: return {"action": "none", "reason": "account_safe"} order = self.predict_liquidation_order(address) if not order: return {"action": "none", "reason": "no_positions"} first_asset = order[0] return { "action": "position", "reason": f"liquidation_imminent_{acct['margin_ratio']:.2f}", "margin_ratio": round(acct["margin_ratio"], 3), "liquidation_target": first_asset["coin"], "strategy": { first_asset["coin"]: "widen_spreads_2x", **{r["coin"]: "tighten_spreads" for r in order[1:]}, }, "predicted_order": [r["coin"] for r in order], } def summary(self) -> dict: return { "at_risk_accounts": self.at_risk_accounts(), "tracked_accounts": len(self._accounts), }