"""CC01 — CASH-AND-CARRY (basis trade) delta-neutral su Hyperliquid. Backtest onesto, STAT-MODE. DIVERSO da FC01 (funding cross-sectional, gia' SCARTATO 2026-06-22). Qui NON si pickano vincitori/perdenti cross-section: si HARVESTA il premio di funding AGGREGATO restando delta-neutral sullo STESSO asset. MECCANISMO. Long spot + short perp (stesso asset, stessa size): long spot -> +price_ret short perp -> -price_ret + funding (lo short INCASSA il funding quando f>0) netto -> +funding (il prezzo si cancella -> zero esposizione direzionale) Il ritorno della gamba delta-neutral = il funding realizzato, meno fee. Entrambe le gambe sono lineari nel prezzo => restano matchate in nozionale senza ribilanciare (delta ~neutro da solo); i costi reali sono entry/exit + spread, modellati come drag annuo fisso. VARIANTI: CC-static -> sempre long-spot/short-perp (basis trade classico): r = funding. Perde se f<0. CC-gated -> harvest solo quando il funding trailing CAUSALE > 0 (esci dai regimi a funding negativo invece di pagarli). r = funding se trail>0 else 0. UNIVERSI: BTC/ETH (sottoinsieme rilevante per l'esecuzione) e i 19 major (basket pieno). GIUDIZIO: standalone (Sharpe/DD/anni) + marginal_vs_tp01. CAVEAT ONESTI (pre-risultato): - NON eseguibile a $600: serve spot+perp per gamba (BTC/ETH = 4 posizioni; 19 = 38). Su Deribit lo storico funding e' bloccato e non operiamo spot HL -> STAT-MODE. - Il modello "r=funding" IGNORA il rischio di base (perp != spot), la liquidazione dello short in uno squeeze, e l'inversione brusca del funding. La vol modellata SOTTOSTIMA la coda. - Lo storico funding parte 2023-05 -> NON contiene il deleveraging 2022 (il regime peggiore per il carry). Edge potenzialmente sovrastimato. uv run python scripts/research/cash_carry_hl.py """ from __future__ import annotations import sys from pathlib import Path import numpy as np import pandas as pd ROOT = Path(__file__).resolve().parents[2] sys.path.insert(0, str(ROOT)) sys.path.insert(0, str(ROOT / "scripts" / "research" / "alt")) from src.portfolio.sleeves import XS_UNIVERSE, _HL_DIR from altlib import marginal_vs_tp01 # type: ignore SQ365 = np.sqrt(365.25) ANNUAL_COST = 0.02 # drag annuo fisso: entry/exit + spread + borrow (generoso per un hold continuo) def load_funding_panel(universe): """FUND, PREM [date x asset]: funding giornaliero (somma oraria) e premium/basis (ultimo del giorno) per gli asset con dati. PREM serve a iniettare il mark-to-market della BASE (perp!=spot).""" fund, prem = {}, {} for sym in universe: fp = _HL_DIR / f"hlfund_{sym.lower()}_1h.parquet" if not fp.exists(): continue df = pd.read_parquet(fp) fund[sym] = df["funding"].resample("1D").sum() prem[sym] = df["premium"].resample("1D").last() FUND = pd.concat(fund, axis=1).sort_index() PREM = pd.concat(prem, axis=1).sort_index().reindex(FUND.index) if FUND.index.tz is None: FUND.index = FUND.index.tz_localize("UTC"); PREM.index = PREM.index.tz_localize("UTC") return FUND, PREM def cc_returns(universe, mode="static", trail=14, cost=ANNUAL_COST, with_basis=False) -> pd.Series: """Ritorno giornaliero del basket cash-and-carry equal-weight, netto drag annuo. with_basis=True inietta il mark-to-market reale della base: r = funding - Δpremium (lo short perp marca contro l'allargamento del basis). E' il rischio che 'r=funding' nasconde.""" FUND, PREM = load_funding_panel(universe) daily_cost = cost / 365.25 leg_raw = FUND - PREM.diff() if with_basis else FUND # per-asset daily mark if mode == "gated": sig = FUND.rolling(trail, min_periods=trail // 2).mean().shift(1) # funding trailing causale active = (sig > 0).astype(float) n_active = active.sum(axis=1).replace(0, np.nan) gross = ((leg_raw * active).sum(axis=1) / n_active).fillna(0.0) drag = (active.sum(axis=1) > 0).astype(float) * daily_cost else: gross = leg_raw.mean(axis=1) drag = daily_cost return (gross - drag).dropna() def metrics(daily: pd.Series) -> dict: r = daily.values sh = float(np.mean(r) / np.std(r) * SQ365) if np.std(r) > 0 else 0.0 eq = np.cumprod(1.0 + np.clip(r, -0.99, None)) pk = np.maximum.accumulate(eq) dd = float(np.max((pk - eq) / pk)) if len(eq) else 0.0 yrs = (daily.index[-1] - daily.index[0]).days / 365.25 if len(daily) > 1 else 1.0 cagr = eq[-1] ** (1 / yrs) - 1 if yrs > 0 and len(eq) and eq[-1] > 0 else -1.0 s = pd.Series(eq, index=daily.index) yearly = {} for y, g in s.groupby(s.index.year): if len(g) > 1: v = g.values; p = np.maximum.accumulate(v) yearly[int(y)] = (float(g.iloc[-1] / g.iloc[0] - 1), float(np.max((p - v) / p))) return dict(sharpe=sh, dd=dd, cagr=cagr, yearly=yearly, gross_ann=float(np.mean(r) * 365.25)) def main(): print("=" * 92) print(" CC01 — CASH-AND-CARRY (basis trade) delta-neutral su Hyperliquid") print(" long spot + short perp -> r = funding (zero esposizione prezzo). Netto drag 2%/anno.") print("=" * 92) configs = [ ("BTC/ETH CC-static", ["BTC", "ETH"], "static"), ("BTC/ETH CC-gated", ["BTC", "ETH"], "gated"), ("19-major CC-static", XS_UNIVERSE, "static"), ("19-major CC-gated", XS_UNIVERSE, "gated"), ] print("\n [A] MODELLO INGENUO (r = funding) — IGNORA il rischio di base:") series = {} for label, uni, mode in configs: r = cc_returns(uni, mode=mode) series[label] = r m = metrics(r) ys = " ".join(f"{y}:{p*100:+.0f}%" for y, (p, d) in sorted(m['yearly'].items())) print(f"\n --- {label} --- ({r.index[0].date()} -> {r.index[-1].date()}, {len(r)}g)") print(f" Sharpe {m['sharpe']:+.2f} CAGR {m['cagr']*100:+.1f}% maxDD {m['dd']*100:.1f}% " f"carry lordo {m['gross_ann']*100:+.1f}%/anno | per-anno: {ys}") print("\n [B] CON MARK-TO-MARKET DELLA BASE (r = funding - Δpremium) — il rischio nascosto:") series_b = {} for label, uni, mode in configs: r = cc_returns(uni, mode=mode, with_basis=True) series_b[label] = r m = metrics(r) ys = " ".join(f"{y}:{p*100:+.0f}%" for y, (p, d) in sorted(m['yearly'].items())) print(f"\n --- {label} (basis) ---") print(f" Sharpe {m['sharpe']:+.2f} CAGR {m['cagr']*100:+.1f}% maxDD {m['dd']*100:.1f}% " f"| per-anno: {ys}") print("\n" + "=" * 92) print(" REALITY CHECK — perche' uno Sharpe 11-13 e' un ARTEFATTO, non un edge") print("=" * 92) FUND, _ = load_funding_panel(["BTC", "ETH"]) agg = FUND.mean(axis=1) neg = float((agg < 0).mean()) by_year = agg.groupby(agg.index.year).apply(lambda x: float(x.sum())) print(f" funding aggregato BTC/ETH: giorni a funding NEGATIVO {neg*100:.0f}% | " f"per-anno (somma): " + " ".join(f"{y}:{v*100:+.1f}%" for y, v in by_year.items())) print(" -> il carry e' PROCYCLICO: +23% nel toro 2024, ~+1% nel 2026 (si comprime nel bear).") print(" -> lo storico funding parte 2023-05: ASSENTE il deleveraging 2022 (funding -, basis blow-out),") print(" il regime che farebbe il vero drawdown. + assenti: liquidazione short in squeeze, slippage") print(" su spot+perp a $600. Sharpe reale di un basis-trade ~1-3 con code brusche, NON 13.") print("\n" + "=" * 92) print(" SCORING MARGINALE vs TP01 (sul modello ONESTO con rischio di base [B])") print(" NB: ADDS/robust_oos qui ESPONGONO un punto cieco dello scorer — si fida della vol") print(" riportata e non ha un gate 'Sharpe implausibile -> rischio nascosto'.") print("=" * 92) for label, r in series_b.items(): if r.std() == 0: print(f"\n[{label}] flat — skip"); continue m = marginal_vs_tp01(r) b = m.get("blends", {}).get("w25", {}) print(f"\n[{label}]") print(f" verdict={m.get('marginal_verdict')} corr->TP01 full={m.get('corr_full')} " f"hold={m.get('corr_hold')} is_hedge={m.get('is_hedge')} " f"has_insample_edge={m.get('has_insample_edge')} (cand IS Sharpe {m.get('cand_insample_sharpe')})") print(f" cand Sharpe full={m.get('cand_full_sharpe')} hold={m.get('cand_hold_sharpe')} | " f"blend25 full {b.get('full')} (upl {b.get('uplift_full')}) " f"hold {b.get('hold')} (upl {b.get('uplift_hold')}) DD {b.get('dd')}") print(f" multicut persistente={m.get('multicut_persistent')} robust_oos={m.get('robust_oos')} " f"hedge-check up/down {m.get('uplift_tp01_up')}/{m.get('uplift_tp01_down')}") if __name__ == "__main__": main()