4f66ef36a9
New live/ sub-modules for production-ready market making:
live/filters/toxicity.py (ToxicityFilter):
VPIN-based pre-trade filter. Accumulates buy/sell volume, computes
VPIN via microstructure module, produces quoting decision:
- allow_quoting: bool
- size_multiplier: 0.0–1.0 (graduated reduction approaching alarm)
- granular thresholds (threshold vs alarm) with smooth reduction
live/treasury.py (Treasury):
Central capital/risk management — single source of truth:
- Position tracking per coin (opening, closing, average entry)
- Realized + unrealized PnL computation
- Pre-trade constraint checks (inventory limits, fee estimates)
- Circuit breaker (drawdown, trade count, toxic fill rate, API errors)
- Liquidation distance monitoring
- Automatic cooldown reset after trip expiry
live/makers/hl_btc_eth.py:
HlMaker — per-coin market maker integrating:
- AvellanedaStoikovMaker (Phase 3) for optimal quotes
- ToxicityFilter for pre-trade gating
- Treasury for position/risk checks
HlMakerPool — manages multiple HlMaker instances with shared treasury
and coordinated observe_all()/quote_all()
live/monitors/cross_venue.py (CrossVenueMonitor):
Cross-exchange lead-lag detection via cross-correlation at multiple
lags. Spot premium (basis proxy) computation. Multi-venue summary.
live/monitors/funding_basis.py (FundingBasisMonitor):
Funding regime classification, momentum detection, carry PnL
estimation, basis spread analysis. Uses microstructure/funding.py.
live/monitors/liq_risk.py (LiquidationRiskOverlay):
Per-position liquidation distance monitoring with tiered warnings
(safe/warning/danger/critical). Recommended position reduction.
38 tests across 4 files (all pass):
test_live_filters.py (5)
test_live_maker.py (9)
test_live_monitors.py (11)
test_live_treasury.py (13)
Total test suite: 172 tests, all passing.
219 lines
7.0 KiB
Python
219 lines
7.0 KiB
Python
"""
|
|
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
|