""" Hyperliquid BTC/ETH maker strategy with tight inventory limits. Uses Avellaneda-Stoikov optimal control from sim/maker.py, toxicity filter from live/filters/toxicity.py, and treasury from live/treasury.py for position/risk management. Designed for Phase 4 controlled deployment: post-only quotes, tight inventory caps, toxicity gating. """ from __future__ import annotations import time from typing import Optional from sim.maker import AvellanedaStoikovMaker, MakerConfig, Quote from sim.queue import QueueModel from live.filters.toxicity import ToxicityFilter from live.treasury import Treasury class HlMaker: """Hyperliquid market maker for a single coin. Lifecycle per tick: 1. observe(mid_price) — feed mid price for vol estimation 2. update_flow(buy_vol, sell_vol, obi) — feed trade flow for toxicity 3. quote() — get bid/ask quotes (or None if blocked) 4. record_fill(side, size, price, fee) — after exchange confirms fill Usage: maker = HlMaker("BTC", treasury=treasury, max_inventory=0.003) maker.observe(50000.0) maker.update_flow(buy_vol=0.1, sell_vol=0.05, obi=0.2) quote = maker.quote() if quote: # place bid at quote.bid, ask at quote.ask on exchange ... """ def __init__( self, coin: str, treasury: Treasury, max_inventory: float = 0.003, base_size: float = 0.0002, gamma: float = 0.1, k: float = 1.5, tau_hours: float = 1.0, min_spread_bps: float = 1.0, max_spread_bps: float = 15.0, vpin_threshold: float = 0.3, vpin_alarm: float = 0.5, skew_factor: float = 0.3, ): self.coin = coin.upper() self._treasury = treasury self._max_inventory = max_inventory self._base_size = base_size self._skew_factor = skew_factor self._maker = AvellanedaStoikovMaker( MakerConfig( gamma=gamma, k=k, tau=tau_hours, min_spread_bps=min_spread_bps, max_spread_bps=max_spread_bps, base_size=base_size, max_inventory=max_inventory, skew_factor=skew_factor, ) ) self._toxicity = ToxicityFilter( vpin_threshold=vpin_threshold, vpin_alarm=vpin_alarm, ) self._mid_price: float = 0.0 self._best_bid: float = 0.0 self._best_ask: float = 0.0 self._elapsed_hours: float = 0.0 self._start_time: float = time.time() self._last_quote: Optional[Quote] = None def observe(self, mid_price: float): """Feed a new mid price observation.""" self._mid_price = mid_price self._elapsed_hours = (time.time() - self._start_time) / 3600.0 self._maker.observe(mid_price) self._treasury.update_mark_price(self.coin, mid_price) def update_book(self, best_bid: float, best_ask: float): self._best_bid = best_bid self._best_ask = best_ask def update_flow(self, buy_vol: float, sell_vol: float, obi: float = 0.0): """Feed trade flow and order-book imbalance for toxicity tracking.""" self._toxicity.update_trade(buy_vol, sell_vol) self._toxicity.update_book_imbalance(obi) def quote(self) -> Optional[Quote]: """Generate the next set of quotes, or None if blocked.""" if self._treasury.is_halted(): return None if self._mid_price <= 0: return None tox = self._toxicity.check() if not tox["allow_quoting"]: return None size_mult = tox["size_multiplier"] position = self._treasury.position(self.coin) target_inv = 0.0 # neutral target q = self._maker.quote_with_skew( mid_price=self._mid_price, inventory=position, elapsed_hours=self._elapsed_hours, target_inventory=target_inv, ) # Scale sizes by toxicity multiplier q.bid_size *= size_mult q.ask_size *= size_mult # Never cross the market if self._best_bid > 0: q.bid = round(min(q.bid, self._best_bid * 0.999), 2) if self._best_ask > 0: q.ask = round(max(q.ask, self._best_ask * 1.001), 2) self._last_quote = q return q def record_fill(self, side: str, size: float, price: float, fee: float): """Record a fill after exchange confirmation.""" pnl = 0.0 position = self._treasury.position(self.coin) if (side == "sell" and position > 0) or (side == "buy" and position < 0): pnl = size * (price - (self._mid_price)) self._treasury.record_fill(self.coin, side, size, price, fee, pnl) def should_skip(self) -> bool: """Check if we should skip quoting this tick.""" if self._treasury.is_halted(): return True if self._treasury.position_size(self.coin) >= self._max_inventory: return True return False @property def last_quote(self) -> Optional[Quote]: return self._last_quote @property def current_vpin(self) -> float: return self._toxicity.vpin def summary(self) -> dict: return { "coin": self.coin, "mid": self._mid_price, "position": self._treasury.position(self.coin), "vpin": self._toxicity.vpin, "sigma": self._maker.sigma, "last_quote": { "bid": self._last_quote.bid, "ask": self._last_quote.ask, "spread_bps": self._last_quote.spread_bps, } if self._last_quote else None, } class HlMakerPool: """Manage multiple HlMaker instances across coins. Provides unified interface for multi-coin market making with shared treasury and coordinated quoting. """ def __init__(self, treasury: Treasury, maker_config: dict | None = None): self._treasury = treasury self._maker_config = maker_config or {} self._makers: dict[str, HlMaker] = {} def add_maker(self, coin: str, max_inventory: float = 0.003, **kwargs) -> HlMaker: cfg = dict(self._maker_config) cfg.update(kwargs) maker = HlMaker(coin=coin, treasury=self._treasury, max_inventory=max_inventory, **cfg) self._makers[coin.upper()] = maker return maker def get(self, coin: str) -> Optional[HlMaker]: return self._makers.get(coin.upper()) def observe_all(self, mid_prices: dict[str, float]): for coin, price in mid_prices.items(): maker = self._makers.get(coin.upper()) if maker: maker.observe(price) def quote_all(self) -> dict[str, Optional[Quote]]: return {coin: maker.quote() for coin, maker in self._makers.items()} def summary(self) -> dict: return { coin: maker.summary() for coin, maker in self._makers.items() } @property def makers(self) -> dict[str, HlMaker]: return self._makers