Files
PythagorasGoal/scripts/research/cash_carry_hl.py
T
Adriano Dal Pastro 69e3517f1b research(carry): cash-and-carry CC01 — premio reale, Sharpe artefatto, non deployabile
Provato l'angolo basis/cash-and-carry (l'altro proposto; il funding cross-sectional
FC01 era gia' SCARTATO 2026-06-22). Delta-neutral long-spot/short-perp -> r=funding,
zero esposizione prezzo.

Premio di funding REALE (~+8-14%/anno aggregato, positivo ogni anno in-sample,
ortogonale a TP01). MA Sharpe modellato 11-13 = ARTEFATTO: rischi di coda fuori
dal dataset (manca il 2022; procyclico +23% 2024 -> +1.7% 2026; liquidazione/slippage
non modellati). Il mark-to-market della base (premium col) sgonfia solo 13->11 ->
il basis-from-data non e' il rischio vero. NON eseguibile a $600 (spot+perp, funding
HL non Deribit) -> STAT-MODE. LEAD da rivedere a scala, non uno sleeve.

Sottoprodotto: CC01 passa ogni gate del marginal scorer -> punto cieco (manca un
gate 'Sharpe implausibile -> rischio nascosto').

- scripts/research/cash_carry_hl.py (CC-static/gated, BTC-ETH + 19-major, basis MtM, reality-check)
- tests/test_cash_carry.py (lock del fatto economico, non dello Sharpe)
- docs/diary/2026-06-26-cash-carry-hl.md
- CLAUDE.md: riga nella sintesi di ricerca

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 18:46:14 +00:00

174 lines
8.6 KiB
Python

"""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()