Files
ftdt-quant-lab/live/makers/hl_btc_eth.py
T
ramseshk 4f66ef36a9 feat: Phase 4 — controlled strategy deployment module + 38 tests
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.
2026-08-07 14:47:08 +08:00

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