5304534e38
6 new modules with 46 new tests (230 total): #21 HLP Vault Monitor (live/monitors/hlp_vault.py): Tracks Hyperliquid's native protocol market maker at address 0xfefefe... Queries clearinghouseState + metaAndAssetCtxs. - Delta exposure per asset (notional + PnL) - Overextension detection (notional exceeds M threshold) - Rebalancing signals: fade_short when HLP too short, fade_long when HLP too long (front-run forced rebalancing) - Toxicity score: HLP losing money = absorbing informed flow - Historical delta tracking #29 Hawkes Processes (microstructure/hawkes.py): Multivariate Hawkes calibrator for limit order book dynamics. - MLE calibration via SGD gradient descent on log-likelihood - Branching ratio enforcement (alpha/beta < 0.99 for stationarity) - Intensity computation λ_i(t) with cross-excitation - Activity forecasting (expected event count in horizon) - Synthetic event generator (Ogata thinning) - Pure functions: hawkes_intensity, hawkes_log_likelihood, generate_hawkes_events #23 Funding Whipsaw Trader (live/strategies/funding_whipsaw.py): Premium index decay trading in final 60s of funding epoch. - Detects deterministic convergence of premium→0 at settlement - Time-scaled position sizing (larger closer to settlement) - Auto-close after funding epoch completes - Confidence scoring based on premium magnitude #32 Term Structure Monitor (live/monitors/term_structure.py): Perp/quarterly/bi-quarterly futures basis curve trading. - Quarterly-perp basis with z-score anomaly detection - BiQ-quarterly curve steepness monitoring - Fair quarterly price via interest rate parity + funding carry - Calendar spread signals: buy_basis, sell_basis, curve_steepener, curve_flattener #24 Liquidation Waterfall (live/monitors/liq_waterfall.py): Cross-margin liquidation order prediction. - Margin ratio tracking (equity / maintenance margin) - Danger/critical level classification - Asset liquidation priority: maintenance / book_liquidity ratio (least liquid asset relative to margin = dumped first) - Strategy output: widen_spreads on target, tighten on rest #31 Spoof Detector (microstructure/spoof_detector.py): Adversarial ML-style spoofing pattern recognition. - Rule 1: Large order far from mid, cancelled immediately - Rule 2: Cancel right before trade approaches price level - Rule 3: Oversized order with no fill within short lifetime - Spoof probability (rolling window ratio) - Cancel-to-fill ratio monitoring
270 lines
11 KiB
Python
270 lines
11 KiB
Python
"""
|
|
HLP (Hyperliquidity Provider) Vault monitoring.
|
|
|
|
Tracks Hyperliquid's native protocol-level market-making vault at
|
|
address 0xfefefefefefefefefefefefefefefefefefefefe.
|
|
|
|
HLP acts as counterparty to all user trades. When it absorbs toxic flow
|
|
or becomes directionally overextended, it must rebalance — creating
|
|
predictable market impact that can be traded.
|
|
|
|
Signals:
|
|
- fade_short: HLP is too short → expect buying rebalance → go long
|
|
- fade_long: HLP is too long → expect selling rebalance → go short
|
|
- neutral: HLP delta is balanced, safe to provide liquidity alongside
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
import time
|
|
from collections import deque
|
|
from typing import Optional
|
|
|
|
import requests
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
HLP_ADDRESS = "0xfefefefefefefefefefefefefefefefefefefefe"
|
|
TESTNET_API = "https://api.hyperliquid-testnet.xyz/info"
|
|
MAINNET_API = "https://api.hyperliquid.xyz/info"
|
|
|
|
|
|
class HlpVaultMonitor:
|
|
"""Monitor HLP vault state — delta, PnL, rebalancing pressure, toxicity."""
|
|
|
|
def __init__(
|
|
self,
|
|
testnet: bool = True,
|
|
overextended_threshold: float = 5.0, # notional in $M before overextended
|
|
history_window: int = 1000,
|
|
):
|
|
self._api_url = TESTNET_API if testnet else MAINNET_API
|
|
self._overextended_threshold = overextended_threshold * 1_000_000 # convert to USD
|
|
self._testnet = testnet
|
|
|
|
# Per-coin state
|
|
self._positions: dict[str, dict] = {} # coin → {side, szi, entry_px, upnl}
|
|
self._mark_prices: dict[str, float] = {} # coin → mark price
|
|
self._oracle_prices: dict[str, float] = {} # coin → oracle price
|
|
self._funding_rates: dict[str, float] = {} # coin → funding rate
|
|
self._open_interest: dict[str, float] = {} # coin → OI
|
|
|
|
# History
|
|
self._delta_history: dict[str, deque] = {} # coin → deque of (time, notional)
|
|
self._pnl_history: list[dict] = []
|
|
self._history_window = history_window
|
|
|
|
self._last_update: float = 0
|
|
self._update_count: int = 0
|
|
|
|
# ── Core update ──────────────────────────────────────────
|
|
|
|
def _api_post(self, payload: dict) -> dict:
|
|
"""Make a POST request to HL info API (testable via mock)."""
|
|
resp = requests.post(self._api_url, json=payload, timeout=10)
|
|
resp.raise_for_status()
|
|
return resp.json()
|
|
|
|
def update(self):
|
|
"""Fetch latest HLP state from Hyperliquid API.
|
|
|
|
Makes two calls:
|
|
1. metaAndAssetCtxs → mark prices, funding, OI
|
|
2. clearinghouseState(HLP_ADDRESS) → positions, PnL
|
|
"""
|
|
now = time.time()
|
|
|
|
# Fetch market data
|
|
try:
|
|
meta_and_ctx = self._api_post({"type": "metaAndAssetCtxs"})
|
|
if isinstance(meta_and_ctx, list) and len(meta_and_ctx) >= 2:
|
|
universe = meta_and_ctx[0].get("universe", [])
|
|
ctxs = meta_and_ctx[1]
|
|
for i, asset in enumerate(universe):
|
|
name = asset.get("name", "")
|
|
if name and i < len(ctxs):
|
|
self._mark_prices[name] = float(ctxs[i].get("markPx", 0))
|
|
self._oracle_prices[name] = float(ctxs[i].get("oraclePx", 0))
|
|
self._funding_rates[name] = float(ctxs[i].get("funding", 0))
|
|
self._open_interest[name] = float(ctxs[i].get("openInterest", 0))
|
|
except Exception as e:
|
|
logger.warning("HLP meta fetch error: %s", e)
|
|
|
|
# Fetch HLP positions
|
|
try:
|
|
ch_state = self._api_post({
|
|
"type": "clearinghouseState",
|
|
"user": HLP_ADDRESS,
|
|
})
|
|
self._parse_positions(ch_state, now)
|
|
except Exception as e:
|
|
logger.warning("HLP clearinghouse fetch error: %s", e)
|
|
|
|
self._last_update = now
|
|
self._update_count += 1
|
|
|
|
def _parse_positions(self, data: dict, now: float):
|
|
"""Parse clearinghouseState response into per-coin positions."""
|
|
asset_positions = data.get("assetPositions", [])
|
|
new_positions: dict[str, dict] = {}
|
|
|
|
for ap in asset_positions:
|
|
pos = ap.get("position", {})
|
|
if not pos:
|
|
continue
|
|
coin = pos.get("coin", "")
|
|
if not coin:
|
|
continue
|
|
szi = float(pos.get("szi", 0))
|
|
entry_px = float(pos.get("entryPx", 0))
|
|
upnl = float(pos.get("unrealizedPnl", 0))
|
|
side = pos.get("side", "") # "A" = short, "B" = long
|
|
|
|
# HLP short is side="A", long is side="B"
|
|
signed_szi = -szi if side == "A" else szi
|
|
|
|
new_positions[coin] = {
|
|
"side": side,
|
|
"szi": szi,
|
|
"signed_szi": signed_szi,
|
|
"entry_px": entry_px,
|
|
"unrealized_pnl": upnl,
|
|
"mark_px": self._mark_prices.get(coin, entry_px),
|
|
"notional_usd": abs(szi) * self._mark_prices.get(coin, entry_px),
|
|
}
|
|
|
|
# Update delta history
|
|
if coin not in self._delta_history:
|
|
self._delta_history[coin] = deque(maxlen=self._history_window)
|
|
self._delta_history[coin].append({
|
|
"t": now,
|
|
"signed_szi": signed_szi,
|
|
"notional_usd": new_positions[coin]["notional_usd"],
|
|
"upnl": upnl,
|
|
})
|
|
|
|
self._positions = new_positions
|
|
self._pnl_history.append({
|
|
"t": now,
|
|
"total_upnl": sum(p["unrealized_pnl"] for p in new_positions.values()),
|
|
"asset_count": len(new_positions),
|
|
})
|
|
if len(self._pnl_history) > self._history_window:
|
|
self._pnl_history = self._pnl_history[-self._history_window:]
|
|
|
|
# ── Queries ──────────────────────────────────────────────
|
|
|
|
def position(self, coin: str) -> float:
|
|
"""Signed position for a coin (positive = long)."""
|
|
pos = self._positions.get(coin.upper(), {})
|
|
return pos.get("signed_szi", 0.0)
|
|
|
|
def delta_exposure(self) -> dict:
|
|
"""Delta exposure per coin with notional and PnL."""
|
|
result = {}
|
|
for coin, pos in self._positions.items():
|
|
result[coin] = {
|
|
"signed_size": pos["signed_szi"],
|
|
"notional_usd": round(pos["notional_usd"], 2),
|
|
"unrealized_pnl": round(pos["unrealized_pnl"], 2),
|
|
"side": "long" if pos["side"] == "B" else "short",
|
|
}
|
|
return result
|
|
|
|
def is_overextended(self, coin: str) -> bool:
|
|
"""Check if HLP delta on this coin exceeds the threshold."""
|
|
pos = self._positions.get(coin.upper(), {})
|
|
notional = pos.get("notional_usd", 0)
|
|
return notional > self._overextended_threshold
|
|
|
|
def overextended_assets(self) -> list[str]:
|
|
"""List of assets where HLP is overextended."""
|
|
return [c for c in self._positions if self.is_overextended(c)]
|
|
|
|
def rebalancing_signal(self, coin: str) -> dict:
|
|
"""Generate a trading signal based on HLP rebalancing pressure.
|
|
|
|
Returns:
|
|
signal: "fade_short" | "fade_long" | "neutral"
|
|
direction: -1 (short) | 0 | 1 (long) for the TRADE direction
|
|
overextended: whether HLP is over capacity
|
|
"""
|
|
pos = self._positions.get(coin.upper(), {})
|
|
if not pos:
|
|
return {"signal": "neutral", "direction": 0, "overextended": False, "reason": "no_position"}
|
|
|
|
signed = pos["signed_szi"]
|
|
is_over = self.is_overextended(coin)
|
|
|
|
if not is_over:
|
|
return {"signal": "neutral", "direction": 0, "overextended": False, "reason": "balanced"}
|
|
|
|
# HLP is short (side=A, signed_szi negative) → it will need to buy to rebalance
|
|
# We should fade the short (go long)
|
|
if signed < -0.001:
|
|
return {
|
|
"signal": "fade_short",
|
|
"direction": 1,
|
|
"overextended": True,
|
|
"reason": f"HLP short {abs(signed):.2f} units, expect buying rebalance",
|
|
"notional_usd": round(pos["notional_usd"], 2),
|
|
}
|
|
# HLP is long (side=B, signed_szi positive) → it will need to sell to rebalance
|
|
# We should fade the long (go short)
|
|
elif signed > 0.001:
|
|
return {
|
|
"signal": "fade_long",
|
|
"direction": -1,
|
|
"overextended": True,
|
|
"reason": f"HLP long {signed:.2f} units, expect selling rebalance",
|
|
"notional_usd": round(pos["notional_usd"], 2),
|
|
}
|
|
else:
|
|
return {"signal": "neutral", "direction": 0, "overextended": False, "reason": "flat"}
|
|
|
|
def toxicity_score(self) -> float:
|
|
"""Estimate how much toxic flow HLP is absorbing.
|
|
|
|
Higher score = HLP is losing money = informed traders are beating it.
|
|
Range: 0 (healthy) to 1 (toxic).
|
|
|
|
Uses: unrealized PnL / total notional as a proxy.
|
|
"""
|
|
total_notional = sum(p["notional_usd"] for p in self._positions.values())
|
|
total_upnl = sum(p["unrealized_pnl"] for p in self._positions.values())
|
|
|
|
if total_notional <= 0:
|
|
return 0.0
|
|
|
|
# Negative PnL → toxic score > 0
|
|
# Positive PnL → toxic score 0 (healthy)
|
|
loss_ratio = max(0.0, -total_upnl / total_notional)
|
|
toxicity = min(1.0, loss_ratio * 10) # scale: 10% loss = 1.0 toxicity
|
|
return round(toxicity, 4)
|
|
|
|
def delta_history(self, coin: str) -> list[dict]:
|
|
"""Historical delta trace for a coin."""
|
|
return list(self._delta_history.get(coin.upper(), []))
|
|
|
|
# ── Summary ──────────────────────────────────────────────
|
|
|
|
def summary(self) -> dict:
|
|
"""One-shot summary of HLP state for dashboard/monitoring."""
|
|
assets = list(self._positions.keys())
|
|
total_delta = sum(p["notional_usd"] for p in self._positions.values())
|
|
overextended = self.overextended_assets()
|
|
signals = {coin: self.rebalancing_signal(coin) for coin in assets}
|
|
|
|
return {
|
|
"assets_tracked": len(assets),
|
|
"total_delta_usd": round(total_delta, 2),
|
|
"total_delta_m": round(total_delta / 1_000_000, 2),
|
|
"toxicity_score": self.toxicity_score(),
|
|
"overextended_assets": overextended,
|
|
"signals": signals,
|
|
"positions": self.delta_exposure(),
|
|
"last_update": self._last_update,
|
|
"update_count": self._update_count,
|
|
}
|