""" Funding and basis behavior analytics. Analyses funding rate regimes, basis dynamics (spot vs perp), and carry trade profitability. """ from __future__ import annotations import math import numpy as np # ── Funding rate analytics ────────────────────────────────── def funding_regime( funding_rates: list[float], window_hours: int = 24, n_samples_per_hour: int = 60, # e.g., 1 sample/min → 60/hr ) -> dict: """Classify current funding regime. Returns regime classification and rolling stats. """ window = window_hours * n_samples_per_hour if len(funding_rates) < window: return {"regime": "insufficient_data", "mean_annual": 0, "volatility": 0} recent = funding_rates[-window:] mean_rate = float(np.mean(recent)) std_rate = float(np.std(recent)) # Funding is per-hour rate. Annualize: compounded 3x daily. # Hyperliquid funding: 8h rate * 3 = daily, * 365 = annual (approx) ann_rate = mean_rate * 3 * 365 * 100 # *100 to convert from fraction to % if ann_rate > 15: regime = "high_positive" elif ann_rate > 5: regime = "positive" elif ann_rate < -15: regime = "high_negative" elif ann_rate < -5: regime = "negative" else: regime = "neutral" return { "regime": regime, "mean_hourly": round(float(mean_rate), 8), "mean_annual_pct": round(ann_rate, 2), "volatility": round(float(std_rate), 8), "window_hours": window_hours, } def funding_predictability( funding_history: list[float], n_lags: int = 3, ) -> dict: """Measure funding rate autocorrelation — is funding momentum persistent?""" if len(funding_history) < n_lags + 2: return {"autocorr": [], "momentum_strength": 0} autocorr = [] for lag in range(1, n_lags + 1): x = funding_history[:-lag] y = funding_history[lag:] if len(x) < 2: autocorr.append(0) continue corr = np.corrcoef(x, y)[0, 1] autocorr.append(round(float(corr) if not np.isnan(corr) else 0, 4)) momentum = float(np.mean([abs(a) for a in autocorr])) return { "autocorr": autocorr, "momentum_strength": round(momentum, 4), "is_momentum": momentum > 0.3, } def funding_carry_pnl( funding_rates: list[float], position_size: float = 1.0, mark_prices: list[float] | None = None, n_samples_per_hour: int = 60, ) -> dict: """Estimate carry PnL from holding a position given funding rates. For positive funding → shorts earn, longs pay. """ if not funding_rates: return {"cumulative_pnl": 0, "hourly_pnl": []} hourly = [] cum = 0.0 for i, rate in enumerate(funding_rates): if i % n_samples_per_hour == 0: notional = position_size * (mark_prices[i] if mark_prices and i < len(mark_prices) else 1.0) pnl = rate * notional # funding rate * position notional cum += pnl hourly.append(round(pnl, 8)) return { "cumulative_pnl": round(cum, 6), "hourly_pnl": hourly[-72:], # last 72 hours "n_hours": len(hourly), } # ── Basis analytics ────────────────────────────────────────── def basis_spread( perp_prices: list[float], spot_prices: list[float], ) -> dict: """Compute basis (perp premium over spot) and its statistics. basis_bps = (perp - spot) / spot * 10000 """ min_len = min(len(perp_prices), len(spot_prices)) if min_len < 2: return {"current_basis_bps": 0, "mean_basis_bps": 0, "max_basis_bps": 0} perp = perp_prices[-min_len:] spot = spot_prices[-min_len:] basis_arr = [] for p, s in zip(perp, spot): if s > 0: basis_arr.append((p - s) / s * 10000) a = np.array(basis_arr) if basis_arr else np.array([0.0]) return { "current_basis_bps": round(float(a[-1]), 2) if len(a) > 0 else 0, "mean_basis_bps": round(float(np.mean(a)), 2), "std_basis_bps": round(float(np.std(a)), 2), "max_basis_bps": round(float(np.max(a)), 2), "min_basis_bps": round(float(np.min(a)), 2), "n_samples": len(a), } def basis_convergence_speed( basis_history: list[float], half_life_lookback: int = 1440, # 24h at 1min samples ) -> dict: """Estimate basis mean-reversion half-life via AR(1). Half-life = -log(2) / log(|rho|) """ if len(basis_history) < 10: return {"half_life_minutes": 0, "ar1_coef": 0, "mean_reverting": False} x = basis_history[-half_life_lookback:] if len(x) < 10: return {"half_life_minutes": 0, "ar1_coef": 0, "mean_reverting": False} x_t = x[:-1] x_t1 = x[1:] rho = np.corrcoef(x_t, x_t1)[0, 1] rho = max(min(rho, 0.999), -0.999) if abs(rho) < 0.01: half_life = 0 else: half_life = -math.log(2) / math.log(abs(rho)) return { "half_life_minutes": round(half_life, 1), "ar1_coef": round(float(rho), 4), "mean_reverting": abs(rho) > 0.1 and rho < 0.95, }