"""r0909 — XRP COME TERZA GAMBA DEL BOOK DERIBIT (meccanismi CONGELATI): la misura che manca. PERCHE' ESISTE. L'operatore chiede "piu' monete, ma sempre in Deribit". Letto il venue OGGI (09/09/2026, `public/get_book_summary_by_currency`): dei 32 perpetual USDC-lineari, solo QUATTRO hanno volume a 8 cifre — BTC, ETH, **XRP ($38M/g, il PIU' scambiato del venue, spread 4 bps, listato 2022-03-16)** e SOL ($13M/g). Tutto il resto sta fra $0,01M e $7M al giorno. SOL come terza gamba e' stato misurato il 22/08 e ESCLUSO dall'operatore (hold-out −0,166 in 0/24 ancore, guadagno di UN anno, il 2023). XRP non e' mai stato misurato come gamba direzionale: e' l'unico candidato "piu' monete" del venue che sia liquido E non misurato. IPOTESI A PRIORI, dichiarata prima di misurare: DILUISCE, come SOL — il trend multi-asset del 19/06 fu scartato a corr 0,74 con TP01, e SOL ha confermato (corr 0,40-0,56, in salita col dato che migliora). Se XRP aggiunge, il primo sospetto e' l'anno del suo evento idiosincratico (la sentenza SEC dell'estate 2023 e la corsa di fine 2024): M9, "24/24 ancore positive possono essere un anno solo". METODO — identico a `r0822_sol_leg.py`, da cui IMPORTA il harness (non riscritto): TP01 e SKH01-V2-DD congelati, book a 2 vs 3 gambe, 24 ancore orarie, mediana delle differenze APPAIATE (M7), null del de-levering a iso-maxDD (M5), scomposizione per anno (M9), due lenti DICHIARATE PRIMA dalla certificazione del dato, non dal risultato: - L-FULL: dal listing (2022-03-16); - L-PULITA: dal primo anno in cui la quota di barre 1h a >1% da Coinbase USD scende sotto lo 0,5% (SOL 2023 aveva 0,6% e fu giudicata sporca: e' il precedente che fissa la soglia). Il dato XRP vive in `data/raw/alt_xrp_*.parquet` (namespace di ricerca, come SOL: fuori dal feed attivo, NON rinfrescato dal cron, `load_data("XRP")` continua a fallire). Nessun file di produzione toccato. Nessun ordine. Book/pesi/universo/cron INVARIATI. """ from __future__ import annotations import json import sys import time 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")) from r0822_sol_leg import ( # noqa: E402 — harness importato, non riscritto DUE, HOLDOUT, W_SKH, W_TP, book, cagr, dati, k_iso_dd, maxdd, sh, skh_leg, taglia, tp01_leg, ) ASSET = "XRP" TRE = ("BTC", "ETH", ASSET) LISTING = pd.Timestamp("2022-03-16", tz="UTC") SOGLIA_PULITA = 0.005 # quota di barre 1h a >1% da Coinbase: sotto = anno pulito CACHE = ROOT / "data" / "_cache" / "r0909_xrp_riferimento_1h.parquet" # ------------------------------------------------------------------ certificazione (D1: Coinbase = audit) def _fetch_1h(ex, symbol: str, start: pd.Timestamp, limit: int) -> pd.Series: """OHLCV 1h paginato in avanti; un batch vuoto NON ferma il ciclo (Coinbase ha un buco 2021-01 → 2023-07 su XRP: delistato dopo la causa SEC, rilistato dopo la sentenza).""" out, since = [], int(start.timestamp() * 1000) end = int(time.time() * 1000) while since < end: r = ex.fetch_ohlcv(symbol, "1h", since=since, limit=limit) r = [x for x in r if x[0] >= since] if not r: since += limit * 3_600_000 continue out.extend(r) nxt = r[-1][0] + 3_600_000 since = nxt if nxt > since else since + limit * 3_600_000 df = pd.DataFrame(out, columns=["ts", "o", "h", "l", "close", "v"]).drop_duplicates("ts") return pd.Series(df["close"].values.astype(float), index=pd.to_datetime(df["ts"], unit="ms", utc=True)).sort_index() def riferimento_1h(start: pd.Timestamp) -> pd.DataFrame: """Riferimento indipendente in USD (D1: audit, mai ancora): Coinbase dove c'e' (dal rilisting 2023-07), Bitstamp altrove — entrambi gia' usati dal progetto come venue di audit. Con cache.""" if CACHE.exists(): df = pd.read_parquet(CACHE); df.index = pd.to_datetime(df.index, utc=True) if len(df): return df import ccxt bs = _fetch_1h(ccxt.bitstamp({"enableRateLimit": True}), "XRP/USD", start, 1000) cb = _fetch_1h(ccxt.coinbase({"enableRateLimit": True}), "XRP/USD", start, 300) df = pd.concat({"bitstamp": bs, "coinbase": cb}, axis=1).sort_index() df["close"] = df["coinbase"].where(df["coinbase"].notna(), df["bitstamp"]) df["fonte"] = np.where(df["coinbase"].notna(), "CB", np.where(df["bitstamp"].notna(), "BS", "")) CACHE.parent.mkdir(parents=True, exist_ok=True) df.to_parquet(CACHE) return df def certifica() -> dict: """Per anno: quota di barre 1h a >1% da Coinbase, mediana in bps, quota di barre flat (1h e 5m).""" d1 = dati(ASSET, "1h") dz = pd.Series(d1["close"].values.astype(float), index=pd.to_datetime(d1["datetime"], utc=True)) ref = riferimento_1h(LISTING) J = pd.concat({"d": dz, "c": ref["close"], "f": ref["fonte"]}, axis=1, join="inner").dropna(subset=["d", "c"]) dev = (J["d"] - J["c"]).abs() / J["c"] d5 = dati(ASSET, "5m") f5 = pd.Series((d5["high"].values == d5["low"].values), index=pd.to_datetime(d5["datetime"], utc=True)) f1 = pd.Series((d1["high"].values == d1["low"].values), index=dz.index) per_anno = {} for y in sorted(set(J.index.year)): m = dev[dev.index.year == y] fy = J["f"][J.index.year == y] per_anno[int(y)] = dict(n=int(len(m)), q_1pct=float((m > 0.01).mean()), med_bps=float(m.median() * 1e4), fonte=f"CB {float((fy == 'CB').mean())*100:.0f}% / BS {float((fy == 'BS').mean())*100:.0f}%", flat_1h=float(f1[f1.index.year == y].mean()), flat_5m=float(f5[f5.index.year == y].mean())) puliti = [y for y, r in per_anno.items() if r["q_1pct"] <= SOGLIA_PULITA] # L-PULITA = dal primo anno pulito in poi SOLO se anche i successivi restano puliti (un anno pulito # isolato non e' un regime); altrimenti dal primo della coda pulita finale. anni = sorted(per_anno) inizio = None for y in anni: if all(per_anno[z]["q_1pct"] <= SOGLIA_PULITA for z in anni if z >= y): inizio = y; break return dict(per_anno=per_anno, puliti=puliti, pulita_da=pd.Timestamp(f"{inizio}-01-01", tz="UTC") if inizio else None) # ------------------------------------------------------------------ misura (stesso schema di r0822_sol_leg) def gamba(asset: str) -> pd.Series: return W_TP * tp01_leg(asset, 0).reindex(skh_leg(asset).index).fillna(0.0) + W_SKH * skh_leg(asset) def differenze(anchors, start) -> dict: d = {"sh": [], "hold": [], "dd": [], "cagr": []} for h in anchors: A, B = book(DUE, h), book(TRE, h) idx = A.index.intersection(B.index) A, B = taglia(A.loc[idx], start), taglia(B.loc[idx], start) if len(A) < 200: continue d["sh"].append(sh(B) - sh(A)); d["hold"].append(sh(taglia(B, HOLDOUT)) - sh(taglia(A, HOLDOUT))) d["dd"].append((maxdd(B) - maxdd(A)) * 100); d["cagr"].append((cagr(B) - cagr(A)) * 100) return {k: np.array(v, float) for k, v in d.items()} def misura(n_anc: int = 24) -> dict: anchors = list(range(0, 24, max(1, 24 // n_anc)))[:n_anc] cert = certifica() lenti = {"L-FULL": None} if cert["pulita_da"] is not None: lenti[f"L-PULITA ({cert['pulita_da'].year}+)"] = cert["pulita_da"] lenti["2024+ (lente di SOL)"] = pd.Timestamp("2024-01-01", tz="UTC") out = dict(cert=cert, lenti={}, gambe={}, anno={}, delev={}, corr={}) for a in TRE: out["gambe"][f"TP01 {a}"] = tp01_leg(a, 0); out["gambe"][f"SKH01 {a}"] = skh_leg(a) g = gamba(ASSET); b2 = book(DUE, 0) com = g.index.intersection(b2.index) for nome, start in lenti.items(): c = com if start is None else com[com >= start] out["corr"][nome] = float(np.corrcoef(g.loc[c], b2.loc[c])[0, 1]) if len(c) > 100 else np.nan out["lenti"][nome] = differenze(anchors, start) A, B = book(DUE, 0), book(TRE, 0); idx = A.index.intersection(B.index) A, B = taglia(A.loc[idx], start), taglia(B.loc[idx], start) k = k_iso_dd(A, B) out["delev"][nome] = dict(k=k, sh3=sh(B), sh2k=sh(k * A), cagr3=cagr(B), cagr2k=cagr(k * A)) A0, B0 = book(DUE, 0), book(TRE, 0); i0 = A0.index.intersection(B0.index); A0, B0 = A0.loc[i0], B0.loc[i0] for y in sorted(set(A0.index.year)): a_, b_ = A0[A0.index.year == y], B0[B0.index.year == y] if len(a_) >= 60: out["anno"][int(y)] = dict(sh2=sh(a_), sh3=sh(b_), dsh=sh(b_) - sh(a_), dcagr=(cagr(b_) - cagr(a_)) * 100, sh_gamba=sh(g[g.index.year == y])) out["gamba_xrp"] = g out["n_anchors"] = len(anchors) return out def verdetto(m: dict) -> str: """AGGIUNGE solo se, nella lente PULITA (o 2024+ se non c'e' una coda pulita): dSharpe hold-out mediano > 0 in ≥ 75% delle ancore, E il null del de-levering passa (Sh 3 gambe > Sh 2 gambe riscalate + 0,02), E il contributo per anno e' positivo in piu' anni di quanti sia negativo — contati sugli anni DENTRO la lente (derivati dal suo nome, non cablati: P1). Un campione vuoto e' "NON MISURABILE", non un verdetto (P5: "non vedo" non e' "va tutto bene").""" nomi = [n for n in m["lenti"] if n.startswith("L-PULITA")] or ["2024+ (lente di SOL)"] n = nomi[0]; d = m["lenti"][n]; dl = m["delev"][n] import re anno0 = int(re.search(r"(\d{4})\+", n).group(1)) # "L-PULITA (2024+)" / "2024+ (lente di SOL)" if not len(d["hold"]) or not np.isfinite(dl["sh3"]) or not np.isfinite(dl["sh2k"]): return f"XRP: NON MISURABILE — lente {n} senza ancore valide o de-levering non calcolabile" c_hold = np.median(d["hold"]) > 0 and np.mean(d["hold"] > 0) >= 0.75 c_delev = dl["sh3"] > dl["sh2k"] + 0.02 anni = [r["dsh"] for y, r in m["anno"].items() if y >= anno0] c_anni = sum(x > 0 for x in anni) > sum(x < 0 for x in anni) esito = "XRP AGGIUNGE" if (c_hold and c_delev and c_anni) else ("XRP DILUISCE" if (not c_hold and not c_delev) else "XRP: PARI / NON SELEZIONABILE") return (f"{esito} — lente {n}: dSharpe hold-out mediano {np.median(d['hold']):+.3f} " f"(>0 nel {np.mean(d['hold'] > 0) * 100:.0f}% di {len(d['hold'])} ancore); " f"de-levering: Sh 3 gambe {dl['sh3']:.2f} vs 2 gambe×{dl['k']:.3f} {dl['sh2k']:.2f}; " f"anni {anno0}+ con dSh>0: {sum(x > 0 for x in anni)}/{len(anni)}") def main() -> None: n_anc = int(sys.argv[sys.argv.index("--anchors") + 1]) if "--anchors" in sys.argv else 24 print("=" * 104) print(" r0909 — XRP COME TERZA GAMBA DEL BOOK DERIBIT (meccanismi CONGELATI; harness di r0822_sol_leg)") print("=" * 104) print(" ipotesi a priori dichiarata: DILUISCE (come SOL; trend multi-asset 19/06 corr 0,74)") m = misura(n_anc) c = m["cert"] print(f"\n [0/5] CERTIFICAZIONE XRP vs Coinbase USD (1h) — soglia anno pulito: quota >1% ≤ {SOGLIA_PULITA*100:.1f}%") print(f" {'anno':<6}{'barre':>7}{'>1%':>8}{'med bps':>9}{'flat 1h':>9}{'flat 5m':>9} riferimento") for y, r in c["per_anno"].items(): print(f" {y:<6}{r['n']:>7}{r['q_1pct']*100:>7.2f}%{r['med_bps']:>9.1f}{r['flat_1h']*100:>8.1f}%{r['flat_5m']*100:>8.1f}% {r['fonte']}") print(f" L-PULITA da: {c['pulita_da'].date() if c['pulita_da'] is not None else 'NESSUN anno pulito in coda'}") print(f"\n [1/5] LE GAMBE DA SOLE (ancora canonica h=0)") print(f"\n {'gamba':<26}{'barre':>7}{'Sharpe':>9}{'maxDD':>10}{'CAGR':>9}{'Sh hold':>10}") for k, s in m["gambe"].items(): print(f" {k:<26}{len(s):>7}{sh(s):>9.2f}{maxdd(s)*100:>9.1f}%{cagr(s)*100:>8.1f}%{sh(taglia(s, HOLDOUT)):>10.2f}") g = m["gamba_xrp"] print(f"\n [2/5] LA GAMBA XRP (75/25) — Sharpe {sh(g):.2f}, maxDD {maxdd(g)*100:.1f}%, CAGR {cagr(g)*100:.1f}%, hold-out {sh(taglia(g, HOLDOUT)):.2f}") for n, v in m["corr"].items(): print(f" corr(gamba XRP, book BTC/ETH) {n}: {v:+.3f} (SOL: +0,40 / +0,56; soglia del 19/06: 0,74)") print(f"\n [3/5] BOOK A 2 vs 3 GAMBE — differenze APPAIATE per ancora ({m['n_anchors']} ancore)") for n, d in m["lenti"].items(): if not len(d["sh"]): print(f"\n {n}: campione insufficiente"); continue print(f"\n {n} ({len(d['sh'])} ancore)") for lab, v, u in (("dSharpe FULL", d["sh"], ""), ("dSharpe hold-out", d["hold"], ""), ("dMaxDD", d["dd"], " pp"), ("dCAGR", d["cagr"], " pp")): print(f" {lab:<18} mediana {np.median(v):+7.3f}{u} banda [{np.percentile(v, 10):+.3f}, {np.percentile(v, 90):+.3f}] >0 nel {np.mean(v > 0)*100:.0f}% delle ancore") print(f"\n [3bis] PER ANNO (h=0): {'anno':<6}{'2g Sh':>7}{'3g Sh':>7}{'dSh':>7}{'dCAGR':>9}{'gamba XRP':>11}") for y, r in m["anno"].items(): print(f" {'':<19}{y:<6}{r['sh2']:>7.2f}{r['sh3']:>7.2f}{r['dsh']:>+7.2f}{r['dcagr']:>+8.1f}pp{r['sh_gamba']:>+11.2f}") print(f"\n [4/5] NULL DEL DE-LEVERING (iso-maxDD): {'lente':<24}{'k':>7}{'Sh 3g':>8}{'Sh 2g×k':>9}{'CAGR 3g':>9}{'CAGR 2g×k':>11}") for n, dl in m["delev"].items(): v = "AGGIUNGE" if dl["sh3"] > dl["sh2k"] + 0.02 else ("de-levering" if dl["sh2k"] > dl["sh3"] + 0.02 else "pari") print(f" {'':<39}{n:<24}{dl['k']:>7.3f}{dl['sh3']:>8.2f}{dl['sh2k']:>9.2f}{dl['cagr3']*100:>8.1f}%{dl['cagr2k']*100:>10.1f}% {v}") px = float(dati(ASSET, "1h")["close"].iloc[-1]) print(f"\n [5/5] ESEGUIBILITA': XRP_USDC-PERPETUAL min 1 XRP = ${px:.2f} < pavimento min_order $5 — non e' il vincolo (come SOL)") print("\n VERDETTO: " + verdetto(m)) print("=" * 104) if __name__ == "__main__": main()