bb40e87d13
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
243 lines
14 KiB
Python
243 lines
14 KiB
Python
"""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()
|