Files
PythagorasGoal/scripts/research/r0909_xrp_terza_gamba.py
T
Adriano Dal Pastro bb40e87d13 crolli: il libro perde nel giorno e chiude > 0 in 8/8 finestre dal 2022 (SKH01 short); crollo catturato a vol bassa, XRP diluisce come SOL
Quattro misure (r0909_*), nessun cambio a libro/pesi/config:
- libro nei crolli: −0,48%/g nei 160 giorni ≤ −5%, positivo per finestra dal 2022
  per la gamba short di SKH01 (108% dei guadagni); beta 0,0769 riprodotto (§46);
  il peso di SKH01 resta chiuso dal gate (in-sample).
- crollo catturato 1-5/06/2026 a quote vere: f_net 0,74 = rally; put δ−0,10
  1,92× il modello; a vol bassa e fuori dal gate di VRP01 → §3 non si riapre.
- XRP terza gamba (harness r0822_sol_leg): hold-out −0,169 in 0/24, un anno buono.
- universo Deribit: liquidi solo BTC/ETH/XRP/SOL; XS01 13/19; BTCDVOL future
  non negoziabile; PAXG non misurato.
Revisione fable: due conclusioni smontate (MTM 2,6× era un artefatto di quote;
«8/12 guadagna» era l'ordine delle classi). Debito §5.18: cblib.spot_series
+ asof guarda un'ora avanti (feed 1h etichettato all'apertura).
Test Opus: 92 nuovi, suite 1008/1008; due difetti del verdetto XRP corretti.
Docs: diario, RESULTS §74-77, CLAUDE.md, memoria 20/50, README; journal 07-08/09.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017zJjHUS7mf4pnGE6pq9RTt
2026-09-09 16:33:54 +00:00

243 lines
14 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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()