From 82ccff2553e8086adfacba768387b176d7bd7c33 Mon Sep 17 00:00:00 2001 From: Adriano Dal Pastro Date: Sat, 22 Aug 2026 21:07:19 +0000 Subject: [PATCH] =?UTF-8?q?research(wave-0822):=20TP01-SINISTRO=20?= =?UTF-8?q?=E2=80=94=20l'allarme=20e'=20falsificato=20nel=20verso;=20il=20?= =?UTF-8?q?rischio=20dell'ottimo=20funded=20e'=20XS01,=20non=20TP01?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/research/r0822b_bin_freq.py | 136 +++-- scripts/research/r0822b_depeg.py | 640 +++++++++++++++++++++++ scripts/research/r0822b_prop_recal.py | 637 ++++++++++++++++++++++ scripts/research/r0822b_surface_rv.py | 43 +- scripts/research/r0822b_tp01_sinistro.py | 41 +- 5 files changed, 1430 insertions(+), 67 deletions(-) create mode 100644 scripts/research/r0822b_depeg.py create mode 100644 scripts/research/r0822b_prop_recal.py diff --git a/scripts/research/r0822b_bin_freq.py b/scripts/research/r0822b_bin_freq.py index bef27eb..a33d18b 100644 --- a/scripts/research/r0822b_bin_freq.py +++ b/scripts/research/r0822b_bin_freq.py @@ -62,6 +62,7 @@ USO: nice -n 19 timeout 900 uv run python scripts/research/r0822b_bin_freq.py [ from __future__ import annotations import argparse +import gc import sys import time from pathlib import Path @@ -132,6 +133,10 @@ class KeepCache: self.rv: dict = {} self.q: dict = {} + def clear(self) -> None: + self.rv.clear() + self.q.clear() + def keep(self, ex: dict, key, thr, w: int, h: float, sg: int) -> pd.Series: base = ex["base_daily"] k = (key, w) @@ -152,6 +157,18 @@ class KeepCache: KC = KeepCache() +def rss_mb() -> float: + """RSS in MB, per non far morire il lavoro degli altri agenti (brief §8: 2 core, ~3 GB).""" + try: + with open("/proc/self/status") as f: + for ln in f: + if ln.startswith("VmRSS:"): + return float(ln.split()[1]) / 1024.0 + except Exception: + pass + return float("nan") + + def size_bin(thr, w, h, sg): def f(ex, key): return V.sizes_from_L(ex, KC.keep(ex, key, thr, w, h, sg)) @@ -175,7 +192,7 @@ def iso_pair(TP: pd.Series, leg: pd.Series, leg_base: pd.Series): def main() -> None: ap = argparse.ArgumentParser() ap.add_argument("--every", type=int, default=1) - ap.add_argument("--ndraw", type=int, default=200) + ap.add_argument("--ndraw", type=int, default=120) args = ap.parse_args() offs = OFFSETS_FULL[:: max(1, args.every)] t0 = time.time() @@ -208,8 +225,10 @@ def main() -> None: print("\n estrazione tabelle trade (V2-DD) ...", flush=True) EX = {o: {a: V.extract(a, o) for a in ASSETS} for o in offs} ntr = {a: int(np.mean([len(EX[o][a]["i_ent"]) for o in offs])) for a in ASSETS} + R.get5m.cache_clear() # ~90 MB di 5m non servono durante la §4 (la VPS ha 2 core + gc.collect() # e ~3 GB liberi condivisi); si ricaricano in §9b in ~3 s print(f" fatto in {time.time()-t0:.0f}s — trade medi/ancora: " + - ", ".join(f"{a} {ntr[a]}" for a in ASSETS)) + ", ".join(f"{a} {ntr[a]}" for a in ASSETS) + f" (RSS {rss_mb():.0f} MB)") TP = A.tp01_baseline_daily() can = offs[0] @@ -299,52 +318,71 @@ def main() -> None: print(" 4. LA FAMIGLIA INTERA — ISO dShFULL, mediana delle differenze appaiate su " f"{len(offs)} ancore") print("-" * L) - # DUE PASSATE, per la RAM (7 GB sulla VPS, ~3 liberi, e siamo in tanti): la 1a calcola le - # metriche per-ancora di tutte le 144 celle SENZA conservare le serie (144 x 23 x 3 serie da - # 2717 punti sarebbero ~500 MB); la 2a ricalcola e CONSERVA le serie delle sole 3-4 celle che - # servono dopo (canonica / scelta al buio / argmax): 4 x 23 gambe, ~2 s. + # RAM (brief §8: 7 GB totali, ~3 liberi, e siamo in tanti). Tre accorgimenti, tutti + # necessari — la prima stesura di questo script e' stata UCCISA a meta' della §4: + # (a) DUE PASSATE: la 1a calcola le metriche per-ancora di tutte le 144 celle SENZA + # conservare le serie (144 x 23 x 3 serie da 2717 punti = ~500 MB); la 2a ricalcola e + # conserva le serie delle sole 3-4 celle che servono dopo (~2 s); + # (b) ciclo ANCORA-ESTERNO con la cache di rv/quantili SVUOTATA a ogni ancora: viva una sola + # ancora alla volta (72 serie invece di 1.656); + # (c) RSS stampata, cosi' se qualcuno la rilancia sa cosa aspettarsi. BK, CT, LEG, MS = {}, {}, {}, {} + ACC: dict = {} - def eval_cell(thr, w, h, sg, store: bool = False) -> dict: + def _one(o, thr, w, h, sg, store: bool): + """UNA cella a UNA ancora. Le size si calcolano una volta sola e si riusano (la prima + stesura le ricalcolava per il conteggio dei tenuti); la scomposizione per anno e il maxDD + si fanno SOLO nella seconda passata, perche' servono a 3 celle e costavano il 60% del + tempo su tutte e 144.""" nm = cname(thr, w, h, sg) fn = size_bin(thr, w, h, sg) - dF, dH, dI, lF, lI, lH, ms, kp, ddv, ddc, dC_ = ([] for _ in range(11)) - yr: dict = {} - for o in offs: - lg = leg_of(EX[o], o, fn) - b, c, m = iso_pair(TP, lg, base_leg[o]) - dF.append(sh(b) - sh(c)); dH.append(sh(hold(b)) - sh(hold(c))) - dI.append(sh(ins(b)) - sh(ins(c))); dC_.append(cagr(b) - cagr(c)) - lF.append(sh(lg)); lI.append(sh(ins(lg))); lH.append(sh(hold(lg))) - ms.append(m); ddv.append(maxdd(b)); ddc.append(maxdd(c)) - kp.append(float(np.mean(np.concatenate([fn(EX[o][a], (o, a)) for a in ASSETS])))) + szs = {a: fn(EX[o][a], (o, a)) for a in ASSETS} + lg = V.leg_daily(EX[o], szs) + b, c, m = iso_pair(TP, lg, base_leg[o]) + d = ACC.setdefault(nm, dict(thr=thr, w=w, h=h, sg=sg, dF=[], dH=[], dI=[], dC=[], + lF=[], lI=[], lH=[], ms=[], kp=[], ddv=[], ddc=[], yr={})) + d["dF"].append(sh(b) - sh(c)); d["dH"].append(sh(hold(b)) - sh(hold(c))) + d["dI"].append(sh(ins(b)) - sh(ins(c))) + d["lF"].append(sh(lg)); d["lI"].append(sh(ins(lg))); d["lH"].append(sh(hold(lg))) + d["ms"].append(m) + d["kp"].append(float(np.mean(np.concatenate(list(szs.values()))))) + if store: + d["dC"].append(cagr(b) - cagr(c)) + d["ddv"].append(maxdd(b)); d["ddc"].append(maxdd(c)) for y in {int(t) for t in b.index.year}: by, cy = b[b.index.year == y], c[c.index.year == y] if len(by) > 20: - yr.setdefault(y, []).append(sh(by) - sh(cy)) - if store: - BK.setdefault(nm, {})[o] = b - CT.setdefault(nm, {})[o] = c - LEG.setdefault(nm, {})[o] = lg - MS.setdefault(nm, {})[o] = m - a_ = np.asarray(dF, float) - return dict(nm=nm, thr=thr, w=w, h=h, sg=sg, - dF=float(np.median(a_)), nF=int((a_ > 0).sum()), - lo=float(a_.min()), hi=float(a_.max()), arrF=a_, - dH=float(np.median(dH)), nH=int((np.asarray(dH) > 0).sum()), - isoI=float(np.median(dI)), dC=float(np.median(dC_)), - legF=float(np.median(lF)), legI=float(np.median(lI)), - legH=float(np.median(lH)), kept=float(np.median(kp)), - weff=float(np.median([W_SKH * x for x in ms])), - ddv=float(np.median(ddv)), ddc=float(np.median(ddc)), - yr={y: float(np.median(v)) for y, v in yr.items()}) + d["yr"].setdefault(y, []).append(sh(by) - sh(cy)) + BK.setdefault(nm, {})[o] = b + CT.setdefault(nm, {})[o] = c + LEG.setdefault(nm, {})[o] = lg + MS.setdefault(nm, {})[o] = m - RES = {} - for (thr, w, h, sg) in cells: - r = eval_cell(thr, w, h, sg) - RES[r["nm"]] = r + for o in offs: + KC.clear() + for (thr, w, h, sg) in cells: + _one(o, thr, w, h, sg, store=False) + KC.clear() + + def _med(v, dflt=float("nan")): + return float(np.median(v)) if len(v) else dflt + + def _fin(d: dict) -> dict: + a_ = np.asarray(d["dF"], float) + return dict(nm=cname(d["thr"], d["w"], d["h"], d["sg"]), thr=d["thr"], w=d["w"], + h=d["h"], sg=d["sg"], dF=float(np.median(a_)), nF=int((a_ > 0).sum()), + lo=float(a_.min()), hi=float(a_.max()), arrF=a_, + dH=float(np.median(d["dH"])), nH=int((np.asarray(d["dH"]) > 0).sum()), + isoI=float(np.median(d["dI"])), dC=_med(d["dC"]), + legF=float(np.median(d["lF"])), legI=float(np.median(d["lI"])), + legH=float(np.median(d["lH"])), kept=float(np.median(d["kp"])), + weff=float(np.median([W_SKH * x for x in d["ms"]])), + ddv=_med(d["ddv"]), ddc=_med(d["ddc"]), + yr={y: float(np.median(v)) for y, v in d["yr"].items()}) + + RES = {nm: _fin(d) for nm, d in ACC.items()} print(f" ({len(cells)} celle x {len(offs)} ancore valutate in {time.time()-t0:.0f}s " - f"dall'avvio, prima passata senza conservare le serie)") + f"dall'avvio, prima passata senza conservare le serie; RSS {rss_mb():.0f} MB)") def matrix(sg: int, h: float) -> None: print(f"\n ISO dShFULL — segno {'+1 (tieni vol BASSA)' if sg>0 else '-1 (CONTROLLO)'}" @@ -383,9 +421,19 @@ def main() -> None: f"positive {sum(r['dF']>0 for r in NEG)}/{len(NEG)} <- controllo di segno") # seconda passata: conserva le serie delle sole celle che servono dopo sel_iso0 = max(ALLC, key=lambda r: r["isoI"]) + ACC2 = ACC for r in dict((x["nm"], x) for x in (RES[nmcan], argmax, sel_iso0, max(ALLC, key=lambda z: z["legI"]))).values(): - eval_cell(r["thr"], r["w"], r["h"], r["sg"], store=True) + ACC = {} + for o in offs: + _one(o, r["thr"], r["w"], r["h"], r["sg"], store=True) + KC.clear() + RES[r["nm"]] = _fin(ACC[r["nm"]]) # ora con dC / maxDD / per-anno + ACC = ACC2 + POS = [RES[cname(t, w, h, +1)] for t in THRS for w in WS for h in HS] + NEG = [RES[cname(t, w, h, -1)] for t in THRS for w in WS for h in HS] + ALLC = POS + NEG + argmax = RES[argmax["nm"]] dchk = A.anchor_luck_delta(lambda o: BK[nmcan][o], lambda o: CT[nmcan][o], offs, metric=sh) assert abs(dchk["median_paired"] - RES[nmcan]["dF"]) < 1e-12 and \ dchk["n_positive"] == RES[nmcan]["nF"], "la statistica appaiata di §4 non coincide con A.anchor_luck_delta" @@ -718,8 +766,10 @@ def main() -> None: print(f" {tag:<40}{row}") if fee != keep_fee: del EXf, basef + gc.collect() V.FEE_RT = keep_fee assert V.FEE_RT == 0.001 + print(f" (RSS {rss_mb():.0f} MB)") f_mod = FEEDEC[(f"MODELLATA {keep_fee*1e4:.0f} bps RT (i backtest)", nmcan)] f_zero = FEEDEC[("fee ZERO (informazione pura)", nmcan)] print(f" -> a fee ZERO resta {f_zero:+.3f} di {f_mod:+.3f} = " @@ -735,6 +785,8 @@ def main() -> None: print(" temporale, ma e' l'unico campione indipendente disponibile a questo costo.") print("-" * L) EX1 = {o: {a: V.extract(a, o, SKH01_V1) for a in ASSETS} for o in offs} + R.get5m.cache_clear() # i 5m non servono piu': ~80 MB restituiti + gc.collect() base1 = {o: V.leg_daily(EX1[o], {a: V.size_flat(EX1[o][a]) for a in ASSETS}) for o in offs} print(f" {'cella':<26}{'ISO dShFULL':>13}{'pos/n':>8}{'ISO dShHOLD':>14}{'pos/n':>8}" f"{'tenuti':>9}") @@ -751,6 +803,10 @@ def main() -> None: print(f" {cname(r['thr'], r['w'], r['h'], r['sg']):<26}{dF['median_paired']:>+13.3f}" f"{dF['n_positive']:>5}/{len(offs):<3}{dH['median_paired']:>+14.3f}" f"{dH['n_positive']:>5}/{len(offs):<3}{float(np.median(kp))*100:>8.0f}%") + del EX1, base1 + KC.clear() + gc.collect() + print(f" (RSS {rss_mb():.0f} MB)") # ---------------------------------------------------------- §11 per anno print("\n" + "-" * L) diff --git a/scripts/research/r0822b_depeg.py b/scripts/research/r0822b_depeg.py new file mode 100644 index 0000000..0c4a413 --- /dev/null +++ b/scripts/research/r0822b_depeg.py @@ -0,0 +1,640 @@ +"""r0822b_depeg.py — IL FALSIFICATORE DI VENUE PER XS01-OOS (2026-08-22, ondata wave-0822). + +IL PUNTO FRAGILE, DICHIARATO DALL'AUTORE DEL RISULTATO +----------------------------------------------------- +`r0822b_xs01_oos.py` (RESULTS-0822 §21) ha misurato che il meccanismo congelato di XS01 ha un edge +FUORI dalla sua finestra di scoperta — 2021-2023 su **Binance spot USDT** — e che li' e' PIU' GRANDE +(mediana di fase +1,12 contro +0,37, differenza appaiata +0,668 in 10/10 fasi). Quel risultato +riscrive il gate del canale funded, quindi merita il suo falsificatore. + +Lo stesso autore lo ha nominato: + > "La validazione di venue Binance-vs-Hyperliquid gira SOLO sulla finestra comune 2024+, cioe' + > proprio il periodo SENZA depeg USDT. Il fuori campione contiene il depeg di maggio 2022. Se i + > close Binance/USDT 2021-2023 divergessero da un venue USD in modo NON COMUNE fra gli asset, il + > ranking cambierebbe e il risultato con esso. Test che lo deciderebbe: rifare il meccanismo sui + > close Coinbase USD per il sottoinsieme quotato li', 2021-2023, e chiedere corr fra i due sleeve + > >= 0.99 come sul 2024+." + +Questo script esegue esattamente quel test. + +PERCHE' LA DOMANDA NON E' OZIOSA (il meccanismo del danno, prima dei numeri) +--------------------------------------------------------------------------- +La regola del progetto — "Binance non e' la verita', e' USDT, ~10 bps fuori, e sotto depeg fino al +3% off" — e' stata **correttamente** giudicata non-binding da §21: uno z-score cross-sezionale +sottrae la media di sezione, quindi e' invariante a un fattore **COMUNE**. Ma un depeg non e' +garantito comune: nei giorni di stress la liquidita' si sposta in modo diverso per asset (i major +restano scambiati contro USDT, i minori no; i flussi di fuga passano per coppie diverse), e una +divergenza **IDIOSINCRATICA** cambia il RANKING — che e' esattamente cio' che la strategia legge. +Maggio 2022 (UST/LUNA, con USDT sceso a ~0,95) e' il caso peggiore possibile. +Percio' la divergenza va misurata in DUE pezzi, e sono due domande diverse: + (a) COMUNE = mediana per giorno dello scarto fra i venue -> quella che lo z-score annulla; + (b) IDIOSINCRATICA = scarto per asset DOPO aver tolto la mediana del giorno -> quella che uccide. +Riportare solo (a) darebbe la risposta rassicurante e sbagliata. + +IL VINCOLO DEL DATO, E COME E' STATO RISOLTO (dichiarato PRIMA di misurare) +-------------------------------------------------------------------------- +Su Coinbase USD, dei 13 asset di U13: + BNB -> quotato solo dal 2025-10 = FUORI (nessun venue USD ce l'ha nel 2021-23: e' l'asset + nativo del venue che stiamo mettendo in discussione) + XRP -> SOSPESO su Coinbase dal 2021-01-19 al 2023-07 (causa SEC) = buco proprio sulla finestra + NEAR/INJ -> quotati su Coinbase solo dal 2022-09 + AVAX -> dal 2021-09-30 (il vincolo che decide l'inizio della finestra) +XRP e' recuperato da **Bitstamp USD** (l'altra referenza USD del `venue_watch` del progetto, che +non ha mai sospeso la coppia). Restano fuori BNB, NEAR, INJ. +Ne escono DUE test, e il secondo copre cio' che il primo non copre: + + TEST 1 "U10-USD" = universo FISSO di 10 gambe (BTC ETH LTC LINK AAVE ADA DOGE SOL AVAX + XRP), + 2021-09-30 -> 2023-12-31, meccanismo **CONGELATO** (k=5, min_assets=10: + dieci gambe sono ESATTAMENTE il minimo, nessun parametro toccato). + Contiene il depeg di maggio 2022 e FTX. NON contiene il 2021. + TEST 2 "U13-IBRIDO" = i 13 asset e la finestra INTERA 2021-01 -> 2023-12, con il close USD + sostituito a quello USDT **dove esiste** (9-12 gambe su 13 a seconda della + data) e Binance dove non esiste. Non e' un confronto puro di venue, e' + la domanda operativa: *se sostituisco tutto il prezzo USD che esiste, il + risultato di §21 si muove?* +In entrambi i test il confronto e' **APPAIATO**: stesso sottoinsieme, stessa finestra, stesse 10 +fasi, stesso codice del motore. Confrontare "19 gambe su Binance" con "10 su Coinbase" misurerebbe +l'UNIVERSO, non il venue (regola del progetto: un confronto appaiato eredita TUTTI i parametri +della riga in cui compare) — e per questo il TEST 1 riporta anche cosa fa U10 **su Binance** +rispetto a U13 su Binance, cioe' quanto del titolo di §21 questo sottoinsieme riproduce. + +MECCANISMO: CONGELATO. Il motore e' importato da `r0822b_xs01_oos.py` (che a sua volta replica +`sleeves._xsec_returns` a max|diff| = 0.0). Nessuna riga di strategia riscritta qui: se il motore +fosse riscritto, una differenza di venue e una differenza di implementazione sarebbero +indistinguibili. + +Uso: nice -n 19 timeout 900 uv run python scripts/research/r0822b_depeg.py +Rete: sola lettura, <= 2 req/s, MAI nella finestra :25-:29 (rate limit per-IP condiviso col +collettore della catena opzioni; guasto del 29/07). Download in cache nello scratchpad: la +seconda corsa e' offline. +""" +from __future__ import annotations + +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")) +sys.path.insert(0, str(ROOT / "scripts" / "research" / "alt")) + +import altlib as A # noqa: E402 +import r0822b_xs01_oos as X # noqa: E402 (motore CONGELATO + cache Binance) +from src.portfolio.sleeves import XS_CFG # noqa: E402 + +CACHE = X.CACHE.parent / "r0822b_depeg" +CACHE.mkdir(parents=True, exist_ok=True) + +# --------------------------------------------------------------------------- +# UNIVERSI E FINESTRE — dichiarati PRIMA di guardare i numeri +# --------------------------------------------------------------------------- +U13 = X.U13 # l'universo di §21 +# venue USD per asset. `None` = nessun venue USD con storia sulla finestra. +USD_SRC: dict[str, tuple[str, str] | None] = { + "BTC": ("coinbase", "BTC/USD"), "ETH": ("coinbase", "ETH/USD"), + "LTC": ("coinbase", "LTC/USD"), "LINK": ("coinbase", "LINK/USD"), + "AAVE": ("coinbase", "AAVE/USD"), "ADA": ("coinbase", "ADA/USD"), + "DOGE": ("coinbase", "DOGE/USD"), "SOL": ("coinbase", "SOL/USD"), + "AVAX": ("coinbase", "AVAX/USD"), "NEAR": ("coinbase", "NEAR/USD"), + "INJ": ("coinbase", "INJ/USD"), + "XRP": ("bitstamp", "XRP/USD"), # Coinbase l'ha sospeso 2021-01 -> 2023-07 + "BNB": None, # nessun venue USD prima del 2025-10 +} +U10 = ["BTC", "ETH", "LTC", "LINK", "AAVE", "ADA", "DOGE", "SOL", "AVAX", "XRP"] +T1_START, T1_END = "2021-09-30", "2024-01-01" # AVAX su Coinbase e' il vincolo +T2_START, T2_END = "2021-01-01", "2024-01-01" # la finestra fuori campione di §21 +DEPEG = ("2022-05-07", "2022-05-21") # UST/LUNA + USDT a ~0.95 (estremo incluso) +PHASES = list(range(XS_CFG["H"])) # 10 fasi: la stima onesta e' la MEDIANA +CORR_SOGLIA = 0.99 # soglia DICHIARATA DALL'AUTORE, non da me + + +# =========================================================================== +# 1. FETCH USD (Coinbase / Bitstamp) + certificazione locale +# =========================================================================== +def _fetch_paged(ex, market: str, tf: str, start_ms: int, end_ms: int, + step_bars: int, bar_ms: int) -> list: + """Paginazione DETERMINISTICA sulla finestra: avanza anche quando una pagina torna vuota. + + ⚠️ Non e' un dettaglio: XRP su Coinbase ha un buco di 2,5 anni (sospensione SEC) e un loop che + si ferma alla prima pagina vuota — il modo ovvio di scrivere questo ciclo — troncherebbe la + serie li', producendo un 'asset che non esiste' invece di un 'asset con un buco'. Sono due + fatti diversi e portano a due decisioni diverse. + """ + rows, cur, span = [], start_ms, step_bars * bar_ms + while cur < end_ms: + X._outside_forbidden_window() + batch = [] + for attempt in range(3): + try: + batch = ex.fetch_ohlcv(market, tf, since=cur, limit=step_bars) + break + except Exception as e: # noqa: BLE001 + if attempt == 2: + print(f" [rete] {market} @ {cur}: {type(e).__name__} {str(e)[:70]}") + time.sleep(2 ** attempt) + time.sleep(0.55) # <= 2 req/s + rows += [b for b in (batch or []) if cur <= b[0] < cur + span] + cur += span + return rows + + +def fetch_usd_daily(asset: str) -> pd.DataFrame: + """Close giornalieri USD dal venue dichiarato in USD_SRC. Cache su disco.""" + src = USD_SRC.get(asset) + if src is None: + return pd.DataFrame(columns=["timestamp", "close", "volume"]) + venue, market = src + f = CACHE / f"{venue[:2]}_{asset.lower()}_1d.parquet" + if f.exists(): + return pd.read_parquet(f) + import ccxt + ex = getattr(ccxt, venue)({"enableRateLimit": True}) + ex.load_markets() + if market not in ex.markets: + d = pd.DataFrame(columns=["timestamp", "open", "high", "low", "close", "volume"]) + d.to_parquet(f) + return d + start = ex.parse8601("2019-01-01T00:00:00Z") + end = int(time.time() * 1000) + step = 300 if venue == "coinbase" else 1000 + rows = _fetch_paged(ex, market, "1d", start, end, step, 86_400_000) + d = (pd.DataFrame(rows, columns=["timestamp", "open", "high", "low", "close", "volume"]) + .drop_duplicates("timestamp").sort_values("timestamp").reset_index(drop=True)) + d.to_parquet(f) + return d + + +def series_of(d: pd.DataFrame) -> pd.Series: + if not len(d): + # indice tz-aware anche da vuoto: un asset senza venue USD (BNB) deve poter essere + # confrontato/reindicizzato come gli altri, non far cadere lo script + return pd.Series(dtype=float, index=pd.DatetimeIndex([], tz="UTC")) + return pd.Series(d["close"].values.astype(float), + index=pd.to_datetime(d["timestamp"], unit="ms", utc=True)) + + +def certify_usd(asset: str, d: pd.DataFrame, a: str, b: str) -> dict: + """Stessa forma della certificazione di §21, ma valutata SULLA FINESTRA DEL TEST. + + Certificare "tutta la serie" direbbe la cosa sbagliata: XRP su Coinbase ha 900 giorni di buco + che sulla finestra 2021-09/2023-12 sono TUTTI dentro. Il numero utile e' la copertura della + finestra in cui il meccanismo gira, non della serie. + """ + s = series_of(d) + s = s[(s.index >= pd.Timestamp(a, tz="UTC")) & (s.index < pd.Timestamp(b, tz="UTC"))] + if not len(s): + return dict(sym=asset, n=0, ok=False, start="-", end="-", gap=-1, + flat=float("nan"), zero_vol=float("nan")) + v = series_of(d.assign(close=d["volume"])) + v = v[(v.index >= pd.Timestamp(a, tz="UTC")) & (v.index < pd.Timestamp(b, tz="UTC"))] + attesi = (pd.Timestamp(b, tz="UTC") - s.index.min()).days + c = s.values + flat = float(np.mean(c[1:] == c[:-1])) if len(c) > 1 else float("nan") + zv = float(np.mean(v.values == 0.0)) if len(v) else float("nan") + gap = int(attesi - len(s)) + return dict(sym=asset, n=len(s), start=str(s.index.min().date()), + end=str(s.index.max().date()), gap=gap, flat=flat, zero_vol=zv, + ok=bool(gap == 0 and flat < 0.02 and (not np.isfinite(zv) or zv < 0.01))) + + +# =========================================================================== +# 2. pannelli +# =========================================================================== +def bin_panel(syms: list[str]) -> pd.DataFrame: + """Pannello Binance USDT — dalla cache di §21, cosi' il lato USDT del confronto e' BIT-IDENTICO + a quello che ha prodotto il risultato che sto falsificando.""" + return pd.concat({s: series_of(X.fetch_binance_daily(s)) for s in syms}, axis=1).sort_index() + + +def usd_panel(syms: list[str]) -> pd.DataFrame: + return pd.concat({s: series_of(fetch_usd_daily(s)) for s in syms}, axis=1).sort_index() + + +def splice(usd: pd.Series, usdt: pd.Series) -> pd.Series: + """Serie IBRIDA: USD dove esiste, USDT prima — RI-SCALANDO il tratto USDT al livello USD nel + giorno del giunto. + + ⚠️ Senza la ri-scalatura il giunto crea un rendimento fantasma pari allo scarto fra i venue + (pochi bps, ma esattamente il tipo di artefatto che questo script deve misurare, non fabbricare). + Il meccanismo legge RAPPORTI di prezzo a 30 e 90 giorni: dopo la ri-scalatura la serie e' + continua e i rapporti che attraversano il giunto sono ben definiti. + """ + u = usd.dropna() + if not len(u): + return usdt + t0 = u.index[0] + if t0 not in usdt.index or not np.isfinite(usdt.get(t0, np.nan)): + return usd.reindex(usdt.index) + k = float(u.iloc[0]) / float(usdt.loc[t0]) + pre = usdt[usdt.index < t0] * k + return pd.concat([pre, u]).sort_index().reindex(usdt.index) + + +def hybrid_panel(syms: list[str]) -> tuple[pd.DataFrame, dict]: + B, U = bin_panel(syms), usd_panel(syms) + cols, meta = {}, {} + for s in syms: + u = U[s] if s in U.columns else pd.Series(dtype=float) + u = u.reindex(B.index) + cols[s] = splice(u, B[s]) + cov = u.dropna() + meta[s] = (str(cov.index[0].date()) if len(cov) else "mai") + return pd.concat(cols, axis=1).sort_index(), meta + + +# =========================================================================== +# 3. metriche e allineamento appaiato +# =========================================================================== +sh, dd, cagr, win, alive, line = X.sh, X.dd, X.cagr, X.win, X.alive, X.line + + +def pair(a: pd.Series, b: pd.Series) -> tuple[pd.Series, pd.Series]: + """Allinea due sleeve sullo stesso indice: un confronto di venue non puo' confrontare finestre.""" + j = pd.concat({"a": a, "b": b}, axis=1, join="inner").dropna() + return j["a"], j["b"] + + +def ranking_scan(PB: pd.DataFrame, PU: pd.DataFrame, phases: list[int], + label: str) -> pd.DataFrame: + """IL CANALE DEL DANNO, misurato dove passa: il venue puo' far male SOLO cambiando le gambe + scelte. Scansione di TUTTE le fasi (non solo la canonica: un ribaltamento su una fase sola + sarebbe invisibile all'ancora 0) con self-check dello specchio contro il motore.""" + from scipy.stats import spearmanr + lb = XS_CFG["lookbacks"] + mlb, H, k = max(lb), XS_CFG["H"], XS_CFG["k"] + rows, sp, checked, tot = [], [], 0, 0 + for ph in phases: + det: dict = {} + X.xsec_engine(PB, phase=ph, detail=det) + WB = det["W"] + for i in range(mlb, len(PB)): + if i % H != ph: + continue + iB, sB = score_at(PB, i, lb) + iU, sU = score_at(PU, i, lb) + if len(iB) < 10 or len(iU) < 10: + continue + tot += 1 + oB = np.argsort(sB) + loB, hiB = set(iB[oB[:k]]), set(iB[oB[-k:]]) + mh, ml = set(np.flatnonzero(WB[i] > 0)), set(np.flatnonzero(WB[i] < 0)) + if mh or ml: # self-check solo dove il motore ha una posizione + checked += 1 + if (mh, ml) != (hiB, loB): + raise AssertionError(f"specchio != motore ({label}, fase {ph}, i={i}): " + "misurerei la mia riscrittura invece del venue") + oU = np.argsort(sU) + loU, hiU = set(iU[oU[:k]]), set(iU[oU[-k:]]) + ov = (len(hiB & hiU) + len(loB & loU)) / (2 * k) + sp.append(float(spearmanr(sB, sU).statistic)) + if ov < 1.0: + diff = sorted(set(PB.columns[list(hiB ^ hiU)]) | set(PB.columns[list(loB ^ loU)])) + rows.append(dict(fase=ph, data=PB.index[i], overlap=ov, gambe=",".join(diff))) + D = pd.DataFrame(rows) + print(f" [{label}] {tot} ribilanciamenti su {len(phases)} fasi; specchio verificato " + f"contro il motore in {checked}/{checked} con posizione") + print(f" [{label}] Spearman dello score fra i venue: mediana {np.median(sp):.4f} " + f"min {np.min(sp):.4f}") + print(f" [{label}] ribilanciamenti con gambe DIVERSE: {len(D)}/{tot} " + f"({len(D)/max(tot,1)*100:.2f}%)") + for _, r in D.iterrows(): + print(f" fase {int(r['fase'])} {r['data'].date()} " + f"sovrapposizione {r['overlap']*100:3.0f}% gambe scambiate: {r['gambe']}") + return D + + +def score_at(P: pd.DataFrame, i: int, lookbacks) -> tuple[np.ndarray, np.ndarray]: + """Specchio della riga di score del motore (z-score medio sui lookback). Serve per confrontare + i RANKING fra venue; la sezione [4] verifica che i top-k/bottom-k che ne derivano coincidano + ESATTAMENTE con quelli scelti dal motore — se lo specchio divergesse, ogni numero di ranking + misurerebbe la mia riscrittura invece del venue.""" + px = P.values.astype(float) + elig = np.isfinite(px[i]) + for L in lookbacks: + elig = elig & np.isfinite(px[i - L]) + idx = np.flatnonzero(elig) + sc, cnt = np.zeros(len(idx)), 0 + for L in lookbacks: + r = px[i, idx] / px[i - L, idx] - 1.0 + if r.std() > 0: + sc += (r - r.mean()) / r.std() + cnt += 1 + return idx, (sc / cnt if cnt else sc) + + +# =========================================================================== +def main() -> None: + t0 = time.time() + pd.set_option("display.width", 200) + print("=" * 100) + print("DEPEG — il close USDT 2021-2023 puo' aver cambiato il RANKING di XS01-OOS?") + print(f"XS_CFG (CONGELATO) = {XS_CFG} soglia dichiarata dall'autore: corr >= {CORR_SOGLIA}") + print("=" * 100) + + # ------------------------------------------------------------- [0] sanity + print("\n[0] SANITY — il motore importato e' quello che ha prodotto §21?") + from src.portfolio.sleeves import _xsec_returns + off = _xsec_returns() + rep = X.xsec_engine(X.hl_panel(), phase=0) + j = pd.concat({"o": off, "r": rep}, axis=1, join="inner").dropna() + md = float(np.max(np.abs(j["o"] - j["r"]))) + print(f" motore vs sleeve UFFICIALE su HL: n={len(j)} max|diff| = {md:.2e} " + f"-> {'IDENTICO' if md < 1e-12 else 'DIVERGE — STOP'}") + if md >= 1e-12: + return + + # ------------------------------------------------------- [1] dato USD + print("\n[1] DATO USD — quanti dei 13 asset di §21 hanno un prezzo in DOLLARI nel 2021-2023?") + print(f" {'sym':<6}{'venue USD':>11}{'n':>6}{'start':>12}{'end':>12}{'gap':>5}" + f"{'flat%':>8}{'vol0%':>8} (finestra {T1_START} .. {T1_END})") + certs = {} + for s in U13: + d = fetch_usd_daily(s) + c = certify_usd(s, d, T1_START, T1_END) + certs[s] = c + ven = USD_SRC[s][0] if USD_SRC[s] else "—" + flag = "" if c["ok"] else " <-- non usabile su questa finestra" + print(f" {s:<6}{ven:>11}{c['n']:>6}{c['start']:>12}{c['end']:>12}{c['gap']:>5}" + f"{c['flat']*100:>8.2f}{c['zero_vol']*100:>8.2f}{flag}") + usabili = [s for s in U13 if certs[s]["ok"]] + print(f" -> usabili sulla finestra del TEST 1: {len(usabili)}/13 {usabili}") + print(f" -> ESCLUSI: BNB (nessun venue USD prima del 2025-10 — e' l'asset nativo del venue") + print(f" sotto esame), NEAR e INJ (Coinbase solo dal 2022-09).") + print(f" ⚠️ LIMITE DEL TEST, da dire subito: 10 gambe su 13 e finestra che parte dal") + print(f" {T1_START} (AVAX su Coinbase) -> il TEST 1 NON copre il 2021, che in §21 e'") + print(f" l'anno piu' forte (+2,14). Copre pero' ESATTAMENTE cio' che l'obiezione nomina:") + print(f" il depeg di maggio 2022 e FTX. Il 2021 lo copre il TEST 2 (ibrido).") + + # controllo di ALLINEAMENTO: le barre USD sono sulla stessa griglia di quelle USDT? + print("\n Controllo di ALLINEAMENTO — se le barre USD e USDT fossero su griglie sfasate,") + print(" ogni numero sotto sarebbe uno sfasamento di calendario travestito da divergenza") + print(" di venue. corr dei RITORNI giornalieri, 2024+ (finestra dove esistono entrambi):") + B24 = bin_panel(U13) + U24 = usd_panel(U13) + al, shifted = [], [] + for s in U13: + if s not in U24.columns or not U24[s].notna().any(): + continue + k = pd.concat({"u": U24[s], "b": B24[s]}, axis=1, join="inner").dropna() + k = k[k.index >= pd.Timestamp("2024-01-01", tz="UTC")].pct_change().dropna() + if len(k) > 200: + al.append((s, float(k["u"].corr(k["b"])))) + shifted.append(float(k["u"].shift(1).corr(k["b"]))) # controllo POSITIVO + print(" " + " ".join(f"{s}:{c:.4f}" for s, c in al)) + lo_al = min(c for _, c in al) + # ⚠️ Un controllo che non fallisce mai non e' un controllo: il modo di fallire e' uno + # sfasamento di UNA barra, e va mostrato che il numero se ne accorge. Con la serie USD + # ritardata di un giorno la corr collassa: e' quella la scala del guasto che sto escludendo. + print(f" corr minima {lo_al:.4f} | controllo POSITIVO (USD ritardato di 1 giorno): " + f"corr mediana {np.median(shifted):+.4f}, massima {np.max(shifted):+.4f}") + if lo_al < 0.95: + print(" -> SFASAMENTO: i due venue non sono sulla stessa griglia. STOP.") + return + print(" -> allineati (uno sfasamento darebbe ~0, non 0,99). Gli asset sotto 0,999") + print(f" ({', '.join(s for s, c in al if c < 0.999)}) sono solo piu' RUMOROSI in USD") + print(" (book piu' sottile sul venue USD), non sfasati.") + + # --------------------------------------------- [2] la divergenza, in DUE pezzi + print("\n[2] LA DIVERGENZA USD-vs-USDT, scomposta — e il pezzo che conta e' il SECONDO") + Bp = bin_panel(U10).loc[T1_START:T1_END] + Up = usd_panel(U10).reindex(Bp.index) + both = Bp.notna() & Up.notna() + D = 1e4 * np.log(Up / Bp) # scarto di livello, bps + D = D.where(both) + common = D.median(axis=1) # (a) componente COMUNE (il peg + il venue) + idio = D.sub(common, axis=0) # (b) componente IDIOSINCRATICA (cambia il rango) + print(f" {len(Bp)} giorni, {len(U10)} asset, {int(both.values.sum())} osservazioni appaiate") + print(f" (a) COMUNE |mediana per giorno|: med {common.abs().median():7.1f} bps " + f"p95 {common.abs().quantile(.95):7.1f} max {common.abs().max():7.1f}") + print(f" (b) IDIOSINCRATICO |scarto de-medianato|: med {idio.abs().stack().median():7.1f} bps " + f"p95 {idio.abs().stack().quantile(.95):7.1f} max {idio.abs().stack().max():7.1f}") + print(" -> (a) e' cio' che lo z-score cross-sezionale annulla per costruzione; (b) no.") + + print(f"\n LA FINESTRA DEL DEPEG {DEPEG[0]} .. {DEPEG[1]} (UST/LUNA; USDT a ~0,95):") + dm = (D.index >= pd.Timestamp(DEPEG[0], tz="UTC")) & (D.index < pd.Timestamp(DEPEG[1], tz="UTC")) + print(f" {'giorno':<12}{'comune':>9}" + "".join(f"{s:>8}" for s in U10)) + for t in D.index[dm]: + print(f" {str(t.date()):<12}{common.loc[t]:>9.0f}" + + "".join(f"{idio.loc[t, s]:>8.0f}" if np.isfinite(idio.loc[t, s]) else f"{'—':>8}" + for s in U10)) + print(" (numeri in bps; colonna 'comune' = mediana del giorno, le altre = scarto DOPO") + print(" averla tolta, cioe' la parte che puo' spostare il ranking)") + idm = idio[dm].abs().stack() + print(f" idiosincratico NELLA finestra: med {idm.median():.1f} bps p95 " + f"{idm.quantile(.95):.1f} max {idm.max():.1f} " + f"(FUORI: med {idio[~dm].abs().stack().median():.1f}, " + f"p95 {idio[~dm].abs().stack().quantile(.95):.1f})") + + print("\n Ma il ranking non legge il LIVELLO: legge i rapporti a 30 e 90 giorni. Divergenza") + print(" idiosincratica degli STESSI rendimenti che entrano nello score (bps):") + for L in XS_CFG["lookbacks"]: + rB = (Bp / Bp.shift(L) - 1.0) * 1e4 + rU = (Up / Up.shift(L) - 1.0) * 1e4 + dR = (rU - rB).where(both) + dRi = dR.sub(dR.median(axis=1), axis=0) + w = dRi[dm].abs().stack() + print(f" r{L:<3d}: idiosincratico med {dRi.abs().stack().median():6.1f} bps " + f"p95 {dRi.abs().stack().quantile(.95):7.1f} max {dRi.abs().stack().max():8.1f}" + f" | nel depeg med {w.median():6.1f} max {w.max():8.1f}") + + # ------------------------------------------- [3] IL TEST 1 — U10, k=5 congelato + print(f"\n[3] TEST 1 — meccanismo CONGELATO su U10, {T1_START} .. {T1_END}, USD vs USDT") + print(f" 10 gambe = ESATTAMENTE il minimo del meccanismo (min_assets=10, k=5): nessun") + print(f" parametro toccato, e il confronto e' appaiato su TUTTO tranne il venue.") + PB = Bp[U10].dropna() + PU = Up[U10].dropna() + idxc = PB.index.intersection(PU.index) + PB, PU = PB.loc[idxc], PU.loc[idxc] + print(f" pannello comune: {len(idxc)} giorni, {PB.index[0].date()} .. {PB.index[-1].date()}") + + rows = [] + for ph in PHASES: + a = alive(X.xsec_engine(PU, phase=ph)) + b = alive(X.xsec_engine(PB, phase=ph)) + a, b = pair(a, b) + rows.append(dict(ph=ph, corr=float(a.corr(b)), sh_usd=sh(a), sh_usdt=sh(b), + d=sh(a) - sh(b), dd_usd=dd(a), dd_usdt=dd(b), n=len(a))) + R = pd.DataFrame(rows) + print(f" {'fase':>5}{'corr':>9}{'Sh USD':>9}{'Sh USDT':>9}{'dSharpe':>10}" + f"{'DD USD':>9}{'DD USDT':>9}") + for _, r in R.iterrows(): + print(f" {int(r['ph']):>5}{r['corr']:>9.4f}{r['sh_usd']:>9.2f}{r['sh_usdt']:>9.2f}" + f"{r['d']:>+10.3f}{r['dd_usd']*100:>8.1f}%{r['dd_usdt']*100:>8.1f}%") + print(f" {'MEDIANA':>5}{R['corr'].median():>9.4f}{R['sh_usd'].median():>9.2f}" + f"{R['sh_usdt'].median():>9.2f}{R['d'].median():>+10.3f}" + f"{R['dd_usd'].median()*100:>8.1f}%{R['dd_usdt'].median()*100:>8.1f}%") + print(f" corr MINIMA fra le 10 fasi: {R['corr'].min():.4f} (fase {int(R.loc[R['corr'].idxmin(),'ph'])})" + f" | soglia dichiarata {CORR_SOGLIA} -> " + f"{'PASS' if R['corr'].min() >= CORR_SOGLIA else 'FAIL'}") + print(f" dSharpe appaiato (USD - USDT): mediana {R['d'].median():+.3f} " + f"banda [{R['d'].min():+.3f},{R['d'].max():+.3f}] " + f"fasi in cui USD e' MEGLIO {(R['d'] > 0).sum()}/{len(R)}") + print(f" ⚠️ la statistica e' la MEDIANA DELLE DIFFERENZE APPAIATE, non la differenza delle") + print(f" mediane (lezione 26/07: quest'ultima confronta fasi diverse e puo' ribaltare).") + + # quanto di §21 riproduce questo sottoinsieme? (o si misura l'universo, non il venue) + p13 = bin_panel(U13)[U13].dropna() + s13_w = win(alive(X.xsec_engine(p13, phase=0)), T1_START, T1_END) + s10_w = win(alive(X.xsec_engine(PB, phase=0)), T1_START, T1_END) + print(f"\n Contesto obbligatorio — quanto di §21 vive in questo sottoinsieme (Binance, fase 0,") + print(f" stessa finestra): U13 Sh {sh(s13_w):+.2f} | U10 Sh {sh(s10_w):+.2f}") + print(f" -> il TEST 1 giudica il VENUE su U10; NON e' una replica del titolo di §21.") + + # ------------------------------------------------ [4] i RANKING, non i P&L + print("\n[4] IL CANALE DEL DANNO — cambia il RANKING? (e' cio' che la strategia legge)") + print(" Un P&L simile puo' nascere da due ranking diversi che si compensano: la domanda") + print(" diretta e' se le GAMBE SCELTE cambiano. Scansione su tutte e 10 le fasi.") + D1 = ranking_scan(PB, PU, PHASES, "TEST 1 / U10") + if len(D1): + ind = D1[(D1["data"] >= pd.Timestamp(DEPEG[0], tz="UTC")) & + (D1["data"] < pd.Timestamp(DEPEG[1], tz="UTC"))] + print(f" di cui dentro la finestra del depeg: {len(ind)}") + + # ------------------------------- [5] TEST 2 — U13 ibrido, finestra INTERA di §21 + print(f"\n[5] TEST 2 — U13 IBRIDO (USD dove esiste, USDT altrove), {T2_START} .. {T2_END}") + print(" Domanda operativa: sostituendo TUTTO il prezzo in dollari che esiste, il titolo") + print(" di §21 (mediana di fase +1,12 fuori campione) si muove?") + HB, meta = hybrid_panel(U13) + print(" prima data USD per asset: " + " ".join(f"{s}:{meta[s]}" for s in U13)) + PBh = p13 + PUh = HB[U13].dropna() + ic = PBh.index.intersection(PUh.index) + PBh, PUh = PBh.loc[ic], PUh.loc[ic] + rows2 = [] + for ph in PHASES: + a = win(alive(X.xsec_engine(PUh, phase=ph)), T2_START, T2_END) + b = win(alive(X.xsec_engine(PBh, phase=ph)), T2_START, T2_END) + a, b = pair(a, b) + rows2.append(dict(ph=ph, corr=float(a.corr(b)), sh_usd=sh(a), sh_usdt=sh(b), + d=sh(a) - sh(b), dd_usd=dd(a), dd_usdt=dd(b))) + R2 = pd.DataFrame(rows2) + print(f" {'fase':>5}{'corr':>9}{'Sh IBRIDO':>11}{'Sh USDT':>10}{'dSharpe':>10}" + f"{'DD IBR':>9}{'DD USDT':>9}") + for _, r in R2.iterrows(): + print(f" {int(r['ph']):>5}{r['corr']:>9.4f}{r['sh_usd']:>11.2f}{r['sh_usdt']:>10.2f}" + f"{r['d']:>+10.3f}{r['dd_usd']*100:>8.1f}%{r['dd_usdt']*100:>8.1f}%") + print(f" {'MEDIANA':>5}{R2['corr'].median():>9.4f}{R2['sh_usd'].median():>11.2f}" + f"{R2['sh_usdt'].median():>10.2f}{R2['d'].median():>+10.3f}" + f"{R2['dd_usd'].median()*100:>8.1f}%{R2['dd_usdt'].median()*100:>8.1f}%") + print(f" corr minima {R2['corr'].min():.4f} -> " + f"{'PASS' if R2['corr'].min() >= CORR_SOGLIA else 'FAIL'} (soglia {CORR_SOGLIA})") + print(f" 📌 IL NUMERO DI TESTA DI §21, RICALCOLATO IN DOLLARI: mediana di fase fuori") + print(f" campione {R2['sh_usdt'].median():+.2f} (USDT, replica) -> " + f"{R2['sh_usd'].median():+.2f} (IBRIDO USD).") + + # differenza appaiata con la macchineria del progetto (stesso gate di §21) + dlt = A.anchor_luck_delta( + lambda ph: win(alive(X.xsec_engine(PUh, phase=ph)), T2_START, T2_END), + lambda ph: win(alive(X.xsec_engine(PBh, phase=ph)), T2_START, T2_END), + PHASES, metric=sh) + print(f" anchor_luck_delta (IBRIDO - USDT): mediana appaiata {dlt['median_paired']:+.3f} " + f"[{dlt['lo']:+.3f},{dlt['hi']:+.3f}], fasi con IBRIDO>USDT " + f"{dlt['n_positive']}/{dlt['n_anchors']}") + print("\n E il ranking, sull'ibrido (la fase con la corr piu' bassa va GUARDATA, non") + print(" riassunta: una corr di 0,97 su una fase o e' un ribaltamento vero o e' un giunto):") + D2 = ranking_scan(PBh, PUh, PHASES, "TEST 2 / U13-IBRIDO") + + # ------------------------------- [5b] le gambe che NESSUN venue USD puo' controllare + print("\n[5b] LE TRE GAMBE NON CONTROLLABILI — quanto del risultato di §21 ci passa?") + print(" BNB e' il token nativo del venue sotto esame e NON esiste in USD nel 2021-23;") + print(" NEAR e INJ hanno un prezzo USD solo dal 2022-09. Se il titolo di §21 vivesse su") + print(" di loro, questo falsificatore non avrebbe potuto vedere niente, e andrebbe detto.") + base_b = A.anchor_luck_band( + lambda ph: win(alive(X.xsec_engine(PBh, phase=ph)), T2_START, T2_END), + PHASES, canonical=0, metric=sh) + print(f" base U13 (Binance, fuori campione, mediana di fase): {base_b['median']:+.2f}") + for a_ in ("BNB", "NEAR", "INJ"): + sub = p13[[c for c in U13 if c != a_]].dropna() + bb = A.anchor_luck_band( + lambda ph, sub=sub: win(alive(X.xsec_engine(sub, phase=ph, min_assets=10)), + T2_START, T2_END), + PHASES, canonical=0, metric=sh) + print(f" senza {a_:<5s}: mediana di fase {bb['median']:+.2f} " + f"({bb['median']-base_b['median']:+.2f}) banda [{bb['lo']:+.2f},{bb['hi']:+.2f}]") + print(" (leave-one-out a 12 gambe: il meccanismo congelato regge, min_assets=10)") + + # ---------------------------------------------------- [6] IL CASO PEGGIORE + print("\n[6] IL CASO PEGGIORE — non la media: dove i due venue divergono di piu'?") + print(f" {'anno':>6}" + "".join(f"{k:>12}" for k in + ("corr T1", "dSh T1", "corr T2", "dSh T2"))) + for y in (2021, 2022, 2023): + a1 = b1 = a2 = b2 = None + cs1, ds1, cs2, ds2 = [], [], [], [] + for ph in PHASES: + a1 = win(alive(X.xsec_engine(PU, phase=ph)), f"{y}-01-01", f"{y+1}-01-01") + b1 = win(alive(X.xsec_engine(PB, phase=ph)), f"{y}-01-01", f"{y+1}-01-01") + if len(a1) > 60 and len(b1) > 60: + a1, b1 = pair(a1, b1) + cs1.append(float(a1.corr(b1))); ds1.append(sh(a1) - sh(b1)) + a2 = win(alive(X.xsec_engine(PUh, phase=ph)), f"{y}-01-01", f"{y+1}-01-01") + b2 = win(alive(X.xsec_engine(PBh, phase=ph)), f"{y}-01-01", f"{y+1}-01-01") + if len(a2) > 60 and len(b2) > 60: + a2, b2 = pair(a2, b2) + cs2.append(float(a2.corr(b2))); ds2.append(sh(a2) - sh(b2)) + f1 = f"{np.median(cs1):.4f}" if cs1 else "—" + g1 = f"{np.median(ds1):+.3f}" if ds1 else "—" + f2 = f"{np.median(cs2):.4f}" if cs2 else "—" + g2 = f"{np.median(ds2):+.3f}" if ds2 else "—" + print(f" {y:>6}{f1:>12}{g1:>12}{f2:>12}{g2:>12}") + worst1 = R.loc[R["corr"].idxmin()] + worst2 = R2.loc[R2["corr"].idxmin()] + wd1 = R.loc[R["d"].abs().idxmax()] + wd2 = R2.loc[R2["d"].abs().idxmax()] + print(f" peggior FASE per corr: T1 fase {int(worst1['ph'])} corr {worst1['corr']:.4f} | " + f"T2 fase {int(worst2['ph'])} corr {worst2['corr']:.4f}") + print(f" peggior FASE per dSharpe: T1 fase {int(wd1['ph'])} {wd1['d']:+.3f} | " + f"T2 fase {int(wd2['ph'])} {wd2['d']:+.3f}") + + st = idio.abs().stack().sort_values(ascending=False) + print(" le 8 divergenze IDIOSINCRATICHE piu' grandi della finestra (bps di livello):") + for (t, s), v in st.head(8).items(): + indep = "DEPEG" if pd.Timestamp(DEPEG[0], tz="UTC") <= t < pd.Timestamp(DEPEG[1], tz="UTC") else "" + print(f" {str(t.date()):<12}{s:<6}{v:8.0f} bps (comune quel giorno " + f"{common.loc[t]:+.0f} bps) {indep}") + print(" -> se le peggiori NON cadono nel depeg, il rumore di venue e' book sottile,") + print(" non contaminazione da stablecoin: due cause diverse, due implicazioni diverse.") + + # il giorno peggiore: dove il P&L dei due sleeve si stacca di piu' + a0 = alive(X.xsec_engine(PU, phase=0)); b0 = alive(X.xsec_engine(PB, phase=0)) + a0, b0 = pair(a0, b0) + gap = (a0 - b0) + print(f" scarto GIORNALIERO fra i due sleeve (fase 0): med " + f"{gap.abs().median()*1e4:.1f} bps p99 {gap.abs().quantile(.99)*1e4:.1f} bps " + f"max {gap.abs().max()*1e4:.1f} bps il {gap.abs().idxmax().date()}") + cum = (1 + a0).cumprod().iloc[-1] / (1 + b0).cumprod().iloc[-1] - 1 + print(f" divario CUMULATO su tutta la finestra del TEST 1: {cum*100:+.2f}% " + f"(equity USD vs equity USDT)") + + # --------------------------------------------------------------- [7] gate + print("\n[7] GATE") + print(f" soglia di venue (dichiarata dall'autore PRIMA, non da me): corr >= {CORR_SOGLIA}") + print(f" TEST 1 U10 congelato : corr mediana {R['corr'].median():.4f}, minima " + f"{R['corr'].min():.4f} -> {'PASS' if R['corr'].min() >= CORR_SOGLIA else 'FAIL'}") + print(f" TEST 2 U13 ibrido : corr mediana {R2['corr'].median():.4f}, minima " + f"{R2['corr'].min():.4f} -> {'PASS' if R2['corr'].min() >= CORR_SOGLIA else 'FAIL'}") + print(f" riferimento 2024+ di §21 (Binance vs Hyperliquid): 0,9991 / dSharpe -0,025") + su = alive(X.xsec_engine(PUh, phase=0)) + mg = A.marginal_vs_tp01(win(su, T2_START, T2_END)) + print(f" marginal_vs_tp01 sullo sleeve IBRIDO fuori campione: {mg.get('marginal_verdict')} " + f"(corr {mg.get('corr_full')}, is_hedge {mg.get('is_hedge')}, " + f"insample_edge {mg.get('has_insample_edge')})") + imp = A.implausible_sharpe(win(su, T2_START, T2_END)) + print(f" implausible_sharpe (ibrido OOS): {imp['implausible']} Sh {imp.get('sharpe'):+.2f} " + f"perdite su barre attive {imp.get('loss_frac', float('nan'))*100:.1f}%") + print(" causalita': NON RI-GIRATA — il motore e' importato invariato da r0822b_xs01_oos.py,") + print(" che la verifica (prezzi futuri randomizzati -> max|diff| = 0). Cambiare il venue") + print(" cambia i dati, non l'ordine temporale in cui il motore li legge.") + print(" deflated_sharpe: NON PERTINENTE — qui non si SELEZIONA una cella, si confronta due") + print(" volte lo STESSO meccanismo su due fonti di prezzo. Nessun trial nuovo di strategia.") + print(" null de-levering: NON PERTINENTE (nessun claim di 'meno drawdown').") + print(" anchor: girato per costruzione — ogni numero sopra e' su tutte e 10 le fasi.") + + print(f"\n[fine] {time.time()-t0:.1f}s") + + +if __name__ == "__main__": + main() diff --git a/scripts/research/r0822b_prop_recal.py b/scripts/research/r0822b_prop_recal.py new file mode 100644 index 0000000..e39acd8 --- /dev/null +++ b/scripts/research/r0822b_prop_recal.py @@ -0,0 +1,637 @@ +"""r0822b_prop_recal — PROP-ALLOC RIFATTO con XS01 preso dalla STORIA LUNGA (2026-08-22, ondata 2). + +IL BUCO CHE CHIUDE +------------------ +Il risultato di testa dell'ondata (RESULTS-0822 §1, PROP-ALLOC) e': su un conto FUNDED da $100k +l'allocazione ottima sotto barriera di drawdown mette **XS01 al 50%** e porta +J = P(pass) x P(vivo 12m) da 0,335 (libro live 75/25) a **0,738**, con +**P(>=50 EUR/g) da 600 EUR in 36 mesi = 42%**. Quel filone ha dichiarato da solo il proprio limite: + + "tutto il vantaggio poggia sul drift di XS01 misurato sulla sua finestra di scoperta" + +e ha proposto un gate che era un'ATTESA: *non aprire un funded prima che XS01 abbia una finestra +fuori dal 2024-2026*. Il filone XS01-OOS (RESULTS-0822 §21) ha trovato quella finestra lo stesso +giorno — stessi 19 ticker su Binance spot USDT 1d, **2021-01 -> 2023-12, con LUNA e FTX dentro** — +e ha misurato che l'edge di RENDIMENTO regge (mediana di fase +1,12 fuori contro +0,37 dentro, +differenza appaiata +0,668 in 10/10 fasi). Ma ha anche misurato TRE correzioni nel verso opposto: + (i) il maxDD standalone di XS01 RADDOPPIA: 10,8% -> 20,9-22,0%; + (ii) il peso 0,50 non sopravvive: fuori campione a w=0,50 il maxDD di libro risale a 8,9% + contro 6,0% a w=0,25 — cioe' restituisce la protezione di DD che e' l'UNICA ragione per + mettere XS01 su un conto a barriera; + (iii) il 32% dell'universo non e' testabile (ARB/OP/SUI/APT/SEI/TIA non quotati nel 2021-23) e + su di esso vive gran parte del numero di scoperta (19 gambe 1,31 vs 13 gambe 0,54). + +Questo script rifa' il conto di PROP-ALLOC con la serie LUNGA al posto di quella di scoperta e +risponde a UNA domanda: **J regge?** + +LA PREVISIONE, REGISTRATA PRIMA DI MISURARE +------------------------------------------- +Mi aspetto che J SCENDA ma resti sopra il libro live, per tre ragioni che tirano in versi diversi: + (a) il drift di XS01 fuori campione e' PIU' ALTO -> spinge J su; + (b) il suo maxDD raddoppia e la barriera e' STATICA (una soglia assoluta non e' scale-invariante) + -> spinge J giu', e su un conto a barriera questa e' la forza che conta; + (c) la finestra lunga contiene il 2022, quindi TP01 (lo sleeve DIFENSIVO, sotto-pesato da + PROP-ALLOC su un campione senza sinistro) ci arriva con un curriculum diverso -> mi aspetto + che l'argmax RIALZI TP01 e ABBASSI XS01 rispetto al 25/25/50. +Previsione numerica dichiarata prima: argmax con w_XS in [0,25 ; 0,38], J fra 0,50 e 0,70, +P(>=50/g) fra il 10% e il 35%. Se J restasse >= 0,70 con w_XS = 0,50 la previsione e' sbagliata e +lo dico; se J crollasse sotto il livello del libro live, il fronte funded va ripensato ADESSO. + +DISCIPLINA DI RIPRODUZIONE (non negoziabile) +-------------------------------------------- +Prima di pubblicare un numero nuovo la macchina deve riprodurre quello vecchio. §0 rigira +PROP-ALLOC con la sua stessa macchineria (`r0822_prop_alloc` IMPORTATO, non riscritto), stessi +semi, stessa griglia, stesso de-luck, e confronta con i valori PUBBLICATI, cablati qui sotto in +`PUB`. Se la riproduzione fallisce, quello E' il risultato e viene detto. + +CONVENZIONI DICHIARATE +---------------------- +* Lente **ACCOPPIATA obbligatoria**. Su una regola di DD a UN GIORNO la lente close-only e' + esattamente CIECA (misurato oggi: 0,00 breach contro 0,40-1,21 veri, rapporto INF). La + macchineria del wick e' quella di `r0725_prop_coupled` (crypto: minimo ESATTO sul path orario + condiviso TP01+SKH01; XS01: 4 checkpoint O/estremo/estremo/C con ordinamento CONDIVISO e ordine + AVVERSO dagli OHLC giornalieri). Per la finestra 2021-23 gli OHLC sono quelli **Binance** della + cache di `r0822b_xs01_oos` — il costruttore di tuple e' lo STESSO codice e viene validato in §1 + contro `pc.xsec_daily_tuples()` sui dati HL, dove deve dare max|diff| = 0. +* **Universo dichiarato: U13** (i 13 dei 19 quotati prima del 2020-11) come serie PRIMARIA, perche' + e' l'unica testabile fuori campione e perche' e' la versione **PIU' DEBOLE** di quella che sta nel + libro (13 gambe invece di 19: XS01-OOS misura 0,54 contro 1,31 sulla stessa finestra e venue). + Scegliere la piu' debole e' conservativo per la decisione. U19-EXP (universo che si allarga da 10 + a 19 gambe) e' riportato come **altro capo della banda**, mai come numero di testa. +* Meccanismo **CONGELATO** (XS_CFG: blend [30,90], H=10, k=5, gate p30, vol-target 20%, 5 bps/lato). + Nessun parametro toccato, nessuna cella cercata: qui non si seleziona, si RI-MISURA. +* de-luck sul drift **x0,89** (misurato 26/07), identico a PROP-ALLOC. +* **Distorsioni portate avanti DICHIARATE, non rimosse in silenzio:** + (1) la banda d'ancora NON e' girata sotto questa lente (fuori budget il 22/08 e fuori budget + qui): l'ottimo gira sull'ancora canonica e si appoggia a SKH01, che il progetto misura come + lo sleeve con la fortuna d'ancora piu' grande da restituire -> la distorsione e' A FAVORE + del risultato. Lo stress SKH x0,50 / x0,33 e' li' apposta. + (2) l'intero risultato poggia sull'assunto — **MAI VERIFICATO CON UNA FIRM** — che una firm + crypto listi i 19 (qui 13) alt Hyperliquid **con short abilitato**. Il critico dell'ondata + lo chiama "la ripetizione esatta dell'errore GTAA01/PRIIPs". Non e' verificabile da qui + (serve leggere un listino), quindi §6 misura quanto del risultato MUORE se e' falso. +* Monte Carlo: 2500 percorsi per J (come PROP-ALLOC), 1500 per la scala di conti. **Un Monte Carlo + ha una risoluzione e va detta**: SE su un livello di J ~ +-0,010, sulle differenze APPAIATE + (indici di bootstrap condivisi) molto meno, ~+-0,007. + +Uso: `nice -n 19 timeout 900 uv run python scripts/research/r0822b_prop_recal.py` +Rete: NESSUNA (tutto da disco: parquet certificati + cache Binance di r0822b_xs01_oos). +""" +from __future__ import annotations + +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")) +sys.path.insert(0, str(ROOT / "scripts" / "research" / "alt")) + +import r0725_prop_coupled as pc # noqa: E402 macchineria del wick accoppiato (25/07) +import r0725_prop_ladder as pl # noqa: E402 regole delle firm + scala di conti +import r0822_prop_alloc as PA # noqa: E402 IL filone da riprodurre (22/08) +import r0822b_xs01_oos as X # noqa: E402 motore XS01 + cache Binance (22/08) +from src.portfolio.sleeves import XS_UNIVERSE, _HL_DIR # noqa: E402 + +RAW = ROOT / "data" / "raw" +FEE_SIDE = 0.001 / 2.0 +HYRO = "HYRO" + +# ---------------------------------------------------------------- numeri PUBBLICATI da riprodurre +# (RESULTS-0822 §1, tabella "1 — PROP-ALLOC"; log della corsa nello scratchpad) +PUB = dict( + argmaxJ=dict(w="25/25/50", lev=0.750, J=0.738, p_pass=0.822, p_alive=0.898, sharpe=1.69), + argmaxS=dict(w="38/25/38", lev=0.750, J=0.721, p_pass=0.773, p_alive=0.933, sharpe=1.73), + live=dict(w="75/25/0", lev=0.500, J=0.335, p_pass=0.386, p_alive=0.868, sharpe=1.09), + dJ_vs_sharpe=0.0164, dJ_vs_live=0.3996, + ladder_med=39.28, ladder_p50=0.417, ladder_zero=0.139, +) + +LEVS = (0.25, 0.375, 0.50, 0.625, 0.75, 1.00, 1.25, 1.50) +STEP = 8 +N_PATHS = PA.N_PATHS # 2500 +EV_H = FU_H = 365 +SEED_EV, SEED_FU = 8220, 8221 # identici a PROP-ALLOC +W3 = PA.CRYPTO_PROP # ("TP01","SKH01","XS01") + +TRIALS = 0 # contatore della griglia dichiarata (al RIALZO) + + +def bump(n: int) -> None: + global TRIALS + TRIALS += n + + +def hr(t: str = "") -> None: + print("\n" + "-" * 104) + if t: + print(f" {t}") + print("-" * 104) + + +# =============================================================== 1. XS01 ACCOPPIATO, QUALSIASI DATO + +def ohlc_panels(loader, syms: list[str]) -> tuple[pd.DataFrame, ...]: + """(C, O, H, L) allineati. `loader(sym) -> DataFrame con timestamp/open/high/low/close`.""" + o, h, lo, c = {}, {}, {}, {} + for s in syms: + d = loader(s) + i = pd.to_datetime(d["timestamp"], unit="ms", utc=True) + o[s] = pd.Series(d["open"].astype(float).values, index=i) + h[s] = pd.Series(d["high"].astype(float).values, index=i) + lo[s] = pd.Series(d["low"].astype(float).values, index=i) + c[s] = pd.Series(d["close"].astype(float).values, index=i) + C = pd.concat(c, axis=1).sort_index() + return C, pd.concat(o, axis=1).reindex(C.index)[C.columns], \ + pd.concat(h, axis=1).reindex(C.index)[C.columns], \ + pd.concat(lo, axis=1).reindex(C.index)[C.columns] + + +def xs_tuples(C: pd.DataFrame, O: pd.DataFrame, H: pd.DataFrame, L: pd.DataFrame, + phase: int = 0, wick_mode: str = "shared", **kw) -> pd.DataFrame: + """(R, m) giornalieri di XS01 su un pannello QUALSIASI, con la stessa convenzione di wick di + `r0725_prop_coupled.xsec_daily_tuples`. Le posizioni vengono dal motore CONGELATO di + `r0822b_xs01_oos.xsec_engine` (che regge i NaN = asset non ancora quotato). + + Validato in §1 contro `pc.xsec_daily_tuples()` sui dati HL: deve dare max|diff| = 0.""" + det: dict = {} + r = X.xsec_engine(C, phase=phase, detail=det, **kw) + W, scale, turn = det["W"], det["scale"], det["turn"] + px = C.values.astype(float) + n, A = px.shape + prev_c = np.vstack([np.full((1, A), np.nan), px[:-1]]) + held = np.vstack([np.zeros((1, A)), W[:-1]]) # gambe TENUTE nel giorno i + with np.errstate(invalid="ignore"): + rO = O.values / prev_c - 1.0 + rH = H.values / prev_c - 1.0 + rL = L.values / prev_c - 1.0 + rC = px / prev_c - 1.0 + vO = np.nansum(held * rO, axis=1) + vH = np.nansum(held * rH, axis=1) + vL = np.nansum(held * rL, axis=1) + vC = np.nansum(held * rC, axis=1) + if wick_mode == "perleg": # bound severo: ogni gamba al suo peggio + worst = np.where(held >= 0, rL, rH) + m_gross = np.minimum(np.nansum(held * worst, axis=1), vC) + else: # 'shared': ordine AVVERSO + m_gross = np.minimum(np.minimum.reduce([vO, vL, vH, vC]), + np.minimum.reduce([vO, vH, vL, vC])) + m_net = (m_gross - turn * FEE_SIDE) * scale + R_net = r.values + out = pd.DataFrame({"R": R_net, "m": np.minimum(m_net, R_net)}, index=C.index) + return out + + +def bin_loader(sym: str) -> pd.DataFrame: + """Legge dalla cache di r0822b (nessuna rete) e SCARTA la barra del giorno in corso: una barra + giornaliera parziale in coda e' esattamente il difetto trovato oggi nei monitor forward.""" + d = X.fetch_binance_daily(sym) + today = pd.Timestamp.utcnow().normalize().value // 10**6 + return d[d["timestamp"] < today].reset_index(drop=True) + + +def hl_loader(sym: str) -> pd.DataFrame: + return pd.read_parquet(_HL_DIR / f"hl_{sym.lower()}_1d.parquet") + + +# =============================================================== 2. LIBRO A 3 GAMBE, PANNELLO LIBERO + +def rg(w: dict, idx: pd.DatetimeIndex, xs: pd.DataFrame, drop_xs: bool = False, + dm: tuple = ()) -> tuple[np.ndarray, np.ndarray]: + """(R, gap) del libro TP01/SKH01/XS01 su un pannello arbitrario e con una serie XS01 + arbitraria. E' `PA.book_RG` con il pannello e la serie XS01 come argomenti invece che + cablati: crypto col minimo ESATTO sul path orario condiviso (`PA._crypto`, che per uno + stress taglia il drift sulle GAMBE ORARIE prima di comporre — il minimo giornaliero non e' + separabile per gamba, quindi non si puo' tagliare a valle); il minimo di XS01 si SOMMA + (worst simultaneo, convenzione severa del 25/07). gap = min_intraday - chiusura (<= 0). + Validato bit-exact contro `PA.book_RG` in §1. + + `dm` = (("XS01", 0.5), ...) taglia il DRIFT di quello sleeve lasciando la forma della coda. + ⚠ Il taglio crypto usa la media sull'INTERA storia oraria (2019+), non sulla finestra: e' + la convenzione di PROP-ALLOC, tenuta identica perche' le due tabelle vanno confrontate.""" + dmd = dict(dm) + w_tp, w_sk = float(w.get("TP01", 0.0)), float(w.get("SKH01", 0.0)) + w_xs = 0.0 if drop_xs else float(w.get("XS01", 0.0)) + R = np.zeros(len(idx)) + M = np.zeros(len(idx)) + if w_tp > 0 or w_sk > 0: + c = PA._crypto(w_tp, w_sk, dmd.get("TP01", 1.0), dmd.get("SKH01", 1.0)).reindex(idx) + R += np.nan_to_num(c["R"].values) + M += np.nan_to_num(c["m"].values) + if w_xs > 0: + f = xs.reindex(idx) + rv = np.nan_to_num(f["R"].values) + mv = np.nan_to_num(f["m"].values) + mult = dmd.get("XS01", 1.0) + if mult != 1.0: + cutv = (1.0 - mult) * rv.mean() # media PRIMA del taglio (come PA) + rv, mv = rv - cutv, mv - cutv + R += w_xs * rv + M += w_xs * mv + return R, np.minimum(M, R) - R + + +def obj_rg(R: np.ndarray, G: np.ndarray, lev: float, ev_idx, fu_idx, + firm: str = HYRO, factor: float = PA.DELUCK) -> dict: + """`PA.objective` senza la costruzione del libro (che qui e' su un pannello diverso).""" + Rd = PA.deluck(R, factor) + e = PA.eval_sim(Rd, G, ev_idx, lev, firm) + f = PA.funded_sim(Rd, G, fu_idx, lev, firm, 100_000.0) + ann = float(Rd.mean() * 365.0) + vol = float(Rd.std() * np.sqrt(365.0)) + return dict(J=e["p_pass"] * f["p_alive"], p_pass=e["p_pass"], p_alive=f["p_alive"], + e_payout=f["e_payout"], sharpe=(ann / vol if vol > 0 else 0.0), + vol=vol, drift=ann, jvec=(e["passed"] & f["alive"]).astype(float)) + + +def maxdd(R: np.ndarray) -> float: + eq = np.cumprod(1.0 + R) + return float((eq / np.maximum.accumulate(eq) - 1.0).min()) + + +def scan(grid: list[dict], idx, xs, ev_idx, fu_idx, drop_xs: bool = False, + dm: tuple = ()) -> pd.DataFrame: + rows = [] + for w in grid: + R, G = rg(w, idx, xs, drop_xs=drop_xs, dm=dm) + dd = maxdd(PA.deluck(R)) + for lev in LEVS: + o = obj_rg(R, G, lev, ev_idx, fu_idx) + rows.append(dict(w=PA.wkey(w), lev=lev, maxdd=dd, + **{k: v for k, v in o.items() if k != "jvec"})) + bump(len(grid) * len(LEVS)) + return pd.DataFrame(rows) + + +def best_row(df: pd.DataFrame, key: str = "J") -> pd.Series: + return df.loc[df[key].idxmax()] + + +def wdict(key: str) -> dict: + return dict(zip(W3, [int(x) / 100 for x in key.split("/")])) + + +# =============================================================== main +def main() -> None: + try: + sys.stdout.reconfigure(line_buffering=True) + except Exception: # pragma: no cover + pass + t0 = time.time() + + def tick(lab: str) -> None: + print(f" [{time.time()-t0:6.0f}s] {lab}") + + print("=" * 104) + print(" PROP-RECAL — J regge se XS01 viene dalla STORIA LUNGA invece che dalla sua finestra" + " di scoperta?") + print("=" * 104) + print(" PREVISIONE REGISTRATA PRIMA: argmax con w_XS in [0.25, 0.38], J in [0.50, 0.70],") + print(" P(>=50 EUR/g) in [10%, 35%]. Il verso atteso: drift XS piu' alto (spinge su) contro") + print(" maxDD raddoppiato + barriera STATICA + 2022 dentro (spingono giu'; su un conto a") + print(" barriera la seconda forza pesa di piu').") + + # ================================================== §0. RIPRODUZIONE + hr("§0. RIPRODUZIONE — la macchina deve dare i numeri PUBBLICATI prima di darne di nuovi") + idx_pub = PA._panel_index(PA.START_WIN) + ev_pub = PA.boot_idx(len(idx_pub), EV_H, N_PATHS, seed=SEED_EV) + fu_pub = PA.boot_idx(len(idx_pub), FU_H, N_PATHS, seed=SEED_FU) + print(f" pannello 5-sleeve di PROP-ALLOC: {len(idx_pub)} giorni " + f"{idx_pub[0].date()} -> {idx_pub[-1].date()} | de-luck x{PA.DELUCK} | " + f"{N_PATHS} percorsi | semi {SEED_EV}/{SEED_FU}") + grid = PA.simplex_grid(W3, STEP) + t = time.time() + Gpub = PA.grid_scan(grid, LEVS, HYRO, ev_pub, fu_pub) + bump(len(grid) * len(LEVS)) + print(f" griglia {len(grid)} pesi x {len(LEVS)} leve = {len(grid)*len(LEVS)} celle " + f"({time.time()-t:.0f}s)") + b_pub = best_row(Gpub) + ws = Gpub.loc[Gpub["sharpe"].idxmax(), "w"] + bs_pub = best_row(Gpub[Gpub["w"] == ws]) + bl_pub = best_row(Gpub[Gpub["w"] == "75/25/0"]) + print(f"\n {'':>18} {'pesi':>10} {'leva':>6} {'J':>7} {'PUB':>7} {'d':>7} | " + f"{'P(pass)':>8} {'PUB':>7} | {'P(vivo)':>8} {'PUB':>7} | {'Sharpe':>7} {'PUB':>6}") + ok = True + for lab, row, p in (("argmax J", b_pub, PUB["argmaxJ"]), ("argmax Sharpe", bs_pub, PUB["argmaxS"]), + ("libro LIVE 75/25", bl_pub, PUB["live"])): + same_w = (row["w"] == p["w"]) and abs(row["lev"] - p["lev"]) < 1e-9 + dj = row["J"] - p["J"] + ok &= same_w and abs(dj) < 0.02 + print(f" {lab:>18} {row['w']:>10} {row['lev']:>6.3f} {row['J']:>7.3f} {p['J']:>7.3f} " + f"{dj:>+7.3f} | {row['p_pass']:>8.1%} {p['p_pass']:>7.1%} | " + f"{row['p_alive']:>8.1%} {p['p_alive']:>7.1%} | {row['sharpe']:>7.2f} {p['sharpe']:>6.2f}" + f"{'' if same_w else ' <-- CELLA DIVERSA'}") + dA, seA = PA.paired_delta(wdict(b_pub["w"]), float(b_pub["lev"]), + wdict(bs_pub["w"]), float(bs_pub["lev"]), HYRO, ev_pub, fu_pub) + dB, seB = PA.paired_delta(wdict(b_pub["w"]), float(b_pub["lev"]), + {"TP01": 0.75, "SKH01": 0.25}, float(bl_pub["lev"]), + HYRO, ev_pub, fu_pub) + bump(4) + print(f"\n differenze APPAIATE argmaxJ - argmaxSharpe {dA:+.4f} +-{seA:.4f} " + f"(PUB {PUB['dJ_vs_sharpe']:+.4f})") + print(f" argmaxJ - LIVE {dB:+.4f} +-{seB:.4f} " + f"(PUB {PUB['dJ_vs_live']:+.4f})") + ok &= abs(dA - PUB["dJ_vs_sharpe"]) < 0.02 and abs(dB - PUB["dJ_vs_live"]) < 0.03 + pc.DELUCK = PA.DELUCK + r_lad = pc.simulate([tuple(wdict(b_pub["w"]).get(n, 0.0) for n in W3)], + [0] * pl.MAX_CONCURRENT, n_paths=1500, lev=0.75, seed=20260822, + lens="coupled", start=PA.START_WIN) + print(f" scala di conti (I-bis) EUR/g med {r_lad['med']:.2f} (PUB {PUB['ladder_med']:.2f}) | " + f"P(>=50/g) {r_lad['p_target']:.1%} (PUB {PUB['ladder_p50']:.1%}) | " + f"P(zero) {r_lad['p_zero']:.1%} (PUB {PUB['ladder_zero']:.1%})") + ok &= abs(r_lad["p_target"] - PUB["ladder_p50"]) < 0.03 + verdetto = "OK — si procede" if ok else "FALLITA — ogni numero sotto va letto con questa riserva" + print(f"\n >>> RIPRODUZIONE: {verdetto}") + tick("§0 riproduzione") + + # ================================================== §1. LA SERIE LUNGA, ACCOPPIATA + hr("§1. LA SERIE LUNGA DI XS01, CON IL WICK — e la validazione del costruttore di tuple") + Chl, Ohl, Hhl, Lhl = ohlc_panels(hl_loader, [s for s in XS_UNIVERSE + if (_HL_DIR / f"hl_{s.lower()}_1d.parquet").exists()]) + Chl = Chl.dropna() + Ohl, Hhl, Lhl = Ohl.reindex(Chl.index), Hhl.reindex(Chl.index), Lhl.reindex(Chl.index) + mine = xs_tuples(Chl, Ohl, Hhl, Lhl, phase=0) + ref = pc.xsec_daily_tuples("shared") + j = mine.join(ref, lsuffix="_m", rsuffix="_r", how="inner").dropna() + dR = float(np.abs(j["R_m"] - j["R_r"]).max()) + dM = float(np.abs(j["m_m"] - j["m_r"]).max()) + print(f" CONTROLLO: il mio costruttore di tuple, alimentato con gli OHLC HL, contro") + print(f" `pc.xsec_daily_tuples()` (la macchineria del 25/07): n={len(j)} " + f"max|dR| = {dR:.2e} max|dm| = {dM:.2e} -> " + f"{'IDENTICO' if max(dR, dM) < 1e-12 else 'DIVERGE — STOP'}") + if max(dR, dM) >= 1e-12: + print(" !! il costruttore non replica: i numeri sulla storia lunga sarebbero di un'altra") + print(" macchina. Mi fermo qui.") + return + + for wtest in ({"TP01": 0.25, "SKH01": 0.25, "XS01": 0.50}, {"TP01": 0.75, "SKH01": 0.25}): + Ra, Ga = rg(wtest, idx_pub, ref) + Rb_, Gb_ = PA.book_RG(wtest) + dd1 = float(np.abs(Ra - Rb_).max()); dd2 = float(np.abs(Ga - Gb_).max()) + print(f" CONTROLLO: il mio costruttore di LIBRO contro `PA.book_RG` " + f"({PA.wkey(wtest)}): max|dR| {dd1:.2e} max|dgap| {dd2:.2e} -> " + f"{'IDENTICO' if max(dd1, dd2) < 1e-12 else 'DIVERGE'}") + + Cb, Ob, Hb, Lb = ohlc_panels(bin_loader, XS_UNIVERSE) + p13 = Cb[X.U13].dropna() + o13, h13, l13 = Ob.reindex(p13.index)[X.U13], Hb.reindex(p13.index)[X.U13], Lb.reindex(p13.index)[X.U13] + xs13 = xs_tuples(p13, o13, h13, l13, phase=0) + xsEXP = xs_tuples(Cb, Ob, Hb, Lb, phase=0) + bump(2) + first13 = xs13.index[np.flatnonzero(xs13["R"].values != 0.0)[0]] + firstEX = xsEXP.index[np.flatnonzero(xsEXP["R"].values != 0.0)[0]] + print(f"\n XS01 U13 (13 gambe fisse, Binance) prima barra ATTIVA {first13.date()} " + f"-> {xs13.index[-1].date()}") + print(f" XS01 U19-EXP (10 -> 19 gambe, Binance) prima barra ATTIVA {firstEX.date()} " + f"-> {xsEXP.index[-1].date()}") + print(" (barra del giorno in corso SCARTATA: una giornaliera parziale in coda e' il difetto") + print(" trovato oggi nei monitor forward)") + + # finestre dichiarate + cry_idx = pc.crypto_daily_tuples(1.0, 0.0).index + base_idx = cry_idx.intersection(xs13.index) + S_LONG = max(first13, pd.Timestamp("2021-01-01", tz="UTC")) + S_DISC = pd.Timestamp(PA.START_WIN, tz="UTC") + I_LONG = base_idx[base_idx >= S_LONG] + I_OOS = I_LONG[I_LONG < S_DISC] + I_DISC = base_idx[base_idx >= S_DISC] + WINDOWS = (("W-LONG 2021+ (OOS + scoperta)", I_LONG), + ("W-OOS 2021-2023 (puro OOS)", I_OOS), + ("W-DISC 2024+ (scoperta)", I_DISC)) + print(f"\n {'finestra':>32} {'giorni':>7} {'da':>12} {'a':>12}") + for lab, ii in WINDOWS: + print(f" {lab:>32} {len(ii):>7d} {str(ii[0].date()):>12} {str(ii[-1].date()):>12}") + tick("§1 serie lunga") + + # ================================================== §2. PROFILO SOTTO BARRIERA + hr("§2. IL PROFILO CHE CAMBIA — cio' che una barriera STATICA vede: drift, vol, coda, gap") + print(" (de-luckato x0.89; `gap peggiore` = l'escursione intraday che la close-only NON vede)") + print(f" {'sleeve / finestra':>34} {'Sharpe':>7} {'vol a':>7} {'drift a':>8} {'maxDD':>7} " + f"{'g.peggiore':>11} {'gap p90':>9} {'gap peggiore':>13}") + prof_rows = [ + ("TP01 W-LONG", {"TP01": 1.0}, I_LONG, xs13), + ("TP01 W-DISC", {"TP01": 1.0}, I_DISC, xs13), + ("SKH01 W-LONG", {"SKH01": 1.0}, I_LONG, xs13), + ("SKH01 W-DISC", {"SKH01": 1.0}, I_DISC, xs13), + ("XS01 U13 Binance W-OOS", {"XS01": 1.0}, I_OOS, xs13), + ("XS01 U13 Binance W-DISC", {"XS01": 1.0}, I_DISC, xs13), + ("XS01 U19EXP Binance W-OOS", {"XS01": 1.0}, I_OOS, xsEXP), + ("XS01 UFFICIALE HL W-DISC", {"XS01": 1.0}, I_DISC, ref), + ] + for lab, w, ii, xf in prof_rows: + R, G = rg(w, ii, xf) + Rd = PA.deluck(R) + vol = Rd.std() * np.sqrt(365.0) + print(f" {lab:>34} {Rd.mean()*365/vol if vol > 0 else 0:>7.2f} {vol:>6.1%} " + f"{Rd.mean()*365:>8.1%} {maxdd(Rd)*100:>6.1f}% {Rd.min():>11.2%} " + f"{-np.percentile(-G, 90)*100:>8.2f}pp {G.min()*100:>12.2f}pp") + print("\n Le righe da leggere insieme sono XS01 W-OOS e XS01 UFFICIALE: il RENDIMENTO regge") + print(" (anzi sale), ma il maxDD e la coda peggiorano — e la barriera guarda la coda.") + tick("§2 profilo") + + # ================================================== §3. LA GRIGLIA RIFATTA + hr("§3. LA GRIGLIA RIFATTA SULLA STORIA LUNGA — stesso simplesso, stesse leve, stessa lente") + res = {} + for lab, ii in WINDOWS: + ev = PA.boot_idx(len(ii), EV_H, N_PATHS, seed=SEED_EV) + fu = PA.boot_idx(len(ii), FU_H, N_PATHS, seed=SEED_FU) + t = time.time() + Gx = scan(grid, ii, xs13, ev, fu) + res[lab] = dict(G=Gx, ev=ev, fu=fu, idx=ii) + b, bl = best_row(Gx), best_row(Gx[Gx["w"] == "75/25/0"]) + print(f"\n {lab} ({time.time()-t:.0f}s)") + print(f" {'':>18} {'pesi':>10} {'leva':>6} {'J':>7} {'P(pass)':>8} {'P(vivo)':>8} " + f"{'Sharpe':>7} {'maxDD':>7} {'E[pay/a]':>10}") + for l2, r2 in (("argmax J", b), ("libro LIVE 75/25", bl)): + print(f" {l2:>18} {r2['w']:>10} {r2['lev']:>6.3f} {r2['J']:>7.3f} " + f"{r2['p_pass']:>8.1%} {r2['p_alive']:>8.1%} {r2['sharpe']:>7.2f} " + f"{r2['maxdd']*100:>6.1f}% {r2['e_payout']:>9,.0f}$") + d, se = None, None + try: + Rb, Gb = rg(wdict(b["w"]), ii, xs13) + Rl, Gl = rg({"TP01": 0.75, "SKH01": 0.25}, ii, xs13) + a1 = obj_rg(Rb, Gb, float(b["lev"]), ev, fu)["jvec"] + b1 = obj_rg(Rl, Gl, float(bl["lev"]), ev, fu)["jvec"] + dd_ = a1 - b1 + d, se = float(dd_.mean()), float(dd_.std(ddof=1) / np.sqrt(len(dd_))) + bump(2) + except Exception as exc: # pragma: no cover + print(f" delta appaiato non calcolato: {exc}") + if d is not None: + print(f" delta APPAIATO argmax - LIVE : {d:+.4f} +-{se:.4f}" + f" (PROP-ALLOC pubblicava {PUB['dJ_vs_live']:+.4f} su W-DISC con XS01 HL)") + tick("§3 griglie") + + # ================================================== §4. LA REGIONE, NON IL PUNTO + hr("§4. IL PESO DI XS01 — c'e' una REGIONE robusta su ENTRAMBE le finestre?") + print(" PROP-ALLOC ha gia' misurato che il PUNTO non conta (il massimo di 200 pesi casuali") + print(" eguagliava l'argmax): la domanda giusta e' se esiste una regione comune. Per ogni") + print(" livello di w_XS si riporta il MEGLIO su TP/SKH e su tutte le leve.") + xs_levels = sorted({int(round(100 * (i / STEP))) for i in range(STEP + 1)}) + print(f"\n {'w_XS':>6} | " + " | ".join(f"{lab.split()[0]:>26}" for lab, _ in WINDOWS)) + print(f" {'':>6} | " + " | ".join(f"{'J':>7} {'maxDD':>7} {'pesi':>10}" for _ in WINDOWS)) + prof_by_w = {lab: {} for lab, _ in WINDOWS} + for wx in xs_levels: + cells = [] + for lab, _ in WINDOWS: + Gx = res[lab]["G"] + sub = Gx[Gx["w"].str.endswith(f"/{wx}")] + if len(sub) == 0: + cells.append(f"{'—':>7} {'—':>7} {'—':>10}") + continue + r2 = best_row(sub) + prof_by_w[lab][wx] = float(r2["J"]) + cells.append(f"{r2['J']:>7.3f} {r2['maxdd']*100:>6.1f}% {r2['w']:>10}") + print(f" {wx:>5}% | " + " | ".join(cells)) + print("\n Regione robusta = livelli di w_XS entro il 3% del meglio di finestra, su TUTTE e tre:") + robust = [] + for wx in xs_levels: + okall = True + for lab, _ in WINDOWS: + d = prof_by_w[lab] + if wx not in d or d[wx] < 0.97 * max(d.values()): + okall = False + if okall: + robust.append(wx) + print(f" {robust if robust else 'NESSUN livello comune'}" + f" (entro il 5%: " + f"{[wx for wx in xs_levels if all(wx in prof_by_w[l] and prof_by_w[l][wx] >= 0.95*max(prof_by_w[l].values()) for l, _ in WINDOWS)]})") + tick("§4 regione") + + # ================================================== §5. LA BANDA ANCORATA AL MISURATO + hr("§5. LA BANDA — con gli stress ANCORATI a cio' che e' misurato, non a cio' che era assunto") + print(" PROP-ALLOC pubblicava 42% / 7,7% / 0,7% dove il caso severo era 'XS x0,00' = il drift") + print(" di XS01 assunto NULLO perche' non misurabile fuori campione. Quel drift ORA E'") + print(" MISURATO, e fuori campione e' PIU' ALTO: lo scenario cambia stato, da assunzione a") + print(" misura. Cio' che resta assunto e' (a) la fortuna d'ancora di SKH01 (misurata altrove,") + print(" non sotto questa lente) e (b) il LISTINO della firm (§6). Gli stress qui sotto sono") + print(" quelli, non un pessimismo arbitrario.") + st_rows = [] + STRESS = ((), (("SKH01", 0.50),), (("SKH01", 0.33),)) + SLAB = {(): "nessuno (drift pieno)", + (("SKH01", 0.50),): "SKH x0.50 (ancora 26/07)", + (("SKH01", 0.33),): "SKH x0.33 (ancora severa)"} + for lab, ii in (WINDOWS[0], WINDOWS[1]): + Gw, ev, fu = res[lab]["G"], res[lab]["ev"], res[lab]["fu"] + top_keys = list(Gw.sort_values("J", ascending=False).drop_duplicates("w")["w"].head(10)) + print(f"\n {lab} (argmax ri-cercato sulle {len(top_keys)} celle-peso migliori: e' un") + print(f" risparmio di calcolo, ed e' CONSERVATIVO per la stabilita' dell'ottimo)") + print(f" {'stress':>28} {'argmax pesi':>12} {'leva':>6} {'J':>7} {'J del LIVE':>11} " + f"{'delta appaiato':>16}") + for dmt in STRESS: + best_j, best_k, best_l, best_v = -1.0, None, None, None + for k in top_keys: + R, G = rg(wdict(k), ii, xs13, dm=dmt) + for lv in LEVS: + o = obj_rg(R, G, lv, ev, fu) + if o["J"] > best_j: + best_j, best_k, best_l, best_v = o["J"], k, lv, o["jvec"] + bump(len(top_keys) * len(LEVS)) + RL, GL = rg({"TP01": 0.75, "SKH01": 0.25}, ii, xs13, dm=dmt) + oL, lvL = None, None + for lv in LEVS: + o = obj_rg(RL, GL, lv, ev, fu) + if oL is None or o["J"] > oL["J"]: + oL, lvL = o, lv + bump(len(LEVS)) + d_ = best_v - oL["jvec"] + # guardia contro l'errore del 22/08: un delta appaiato DEVE ereditare lo stress + # della riga in cui compare, o confronta due configurazioni non stressate. + assert abs(float(d_.mean()) - (best_j - oL["J"])) < 0.05, "delta appaiato incoerente" + print(f" {SLAB[dmt]:>28} {best_k:>12} {best_l:>6.3f} {best_j:>7.3f} " + f"{oL['J']:>11.3f} {d_.mean():>+11.4f} " + f"+-{d_.std(ddof=1)/np.sqrt(len(d_)):.4f}") + st_rows.append((lab, SLAB[dmt], best_k, best_l, best_j, oL["J"])) + tick("§5 banda") + + # ================================================== §6. XS01 ASSENTE + hr("§6. LO SCENARIO CHE UCCIDE TUTTO — la firm NON lista gli alt (assunto mai verificato)") + print(" Non e' uno stress sul drift: e' la RIMOZIONE dello sleeve. Il libro torna a due gambe") + print(" e il conto funded diventa il libro live a leva. E' l'unico scenario che non si misura") + print(" con un backtest — si verifica leggendo un listino — quindi qui se ne misura il DANNO.") + print(f"\n {'finestra':>28} {'con XS01: pesi':>15} {'lev':>5} {'J':>7} | " + f"{'SENZA XS01: pesi':>17} {'lev':>5} {'J':>7} | {'J perso':>8} {'quota':>7}") + absent = {} + for lab, ii in WINDOWS: + Gx, ev, fu = res[lab]["G"], res[lab]["ev"], res[lab]["fu"] + b = best_row(Gx) + sub2 = Gx[Gx["w"].str.endswith("/0")] + b2 = best_row(sub2) + absent[lab] = b2 + lost = float(b["J"]) - float(b2["J"]) + print(f" {lab:>28} {b['w']:>15} {b['lev']:>5.2f} {b['J']:>7.3f} | " + f"{b2['w']:>17} {b2['lev']:>5.2f} {b2['J']:>7.3f} | {lost:>+8.3f} " + f"{lost/max(b['J'],1e-9):>6.0%}") + print("\n NB: 'SENZA XS01' e' gia' ri-ottimizzato su TP/SKH e su tutte le leve — e' il MEGLIO") + print(" che il libro a due gambe puo' fare, non il 75/25 cablato.") + tick("§6 assenza") + + # ================================================== §7. LA SCALA DI CONTI + hr("§7. LA SCALA DI CONTI — P(>=50 EUR/g) da EUR 600 in 36 mesi, lente ACCOPPIATA") + print(f" macchineria del 25/07 (`pc.simulate`), de-luck x{PA.DELUCK}, {pl.MAX_CONCURRENT} conti" + f" max, morte-firm {pl.FIRM_DEATH_PER_YEAR:.0%}/anno, 1500 percorsi") + print(" (le tuple del libro vengono INIETTATE nella cache di `pc.book_tuples`, cosi' la") + print(" simulazione e' letteralmente la stessa, con un input diverso)") + print(f"\n {'finestra':>28} {'allocazione':>26} {'leva':>6} {'EUR/g med':>10} " + f"{'P(>=10/g)':>10} {'P(>=50/g)':>10} {'P(zero)':>8}") + + def ladder(ii, w: dict, lev: float, tag: str, wlab: str, drop_xs: bool = False) -> dict: + R, G = rg(w, ii, xs13, drop_xs=drop_xs) + cfg = (float(w.get("TP01", 0.0)), float(w.get("SKH01", 0.0)), + 0.0 if drop_xs else float(w.get("XS01", 0.0))) + key = (cfg[0], cfg[1], cfg[2], "shared") + saved = pc._TUP_CACHE.pop(key, None) + pc._TUP_CACHE[key] = (R, G, ii) + r = pc.simulate([cfg], [0] * pl.MAX_CONCURRENT, n_paths=1500, lev=lev, + seed=20260822, lens="coupled", start=str(ii[0].date())) + pc._TUP_CACHE.pop(key, None) + if saved is not None: + pc._TUP_CACHE[key] = saved + print(f" {tag:>28} {wlab:>26} {lev:>6.2f} {r['med']:>10.2f} {r['p_ge10']:>10.1%} " + f"{r['p_target']:>10.1%} {r['p_zero']:>8.1%}") + return r + + lad = {} + for lab, ii in WINDOWS: + Gx = res[lab]["G"] + b = best_row(Gx) + b2 = absent[lab] + short = lab.split()[0] + lad[(short, "argmax")] = ladder(ii, wdict(b["w"]), float(b["lev"]), short, + f"argmax {b['w']}") + lad[(short, "2550")] = ladder(ii, wdict("25/25/50"), 0.75, short, + "25/25/50 (PROP-ALLOC)") + lad[(short, "noxs")] = ladder(ii, wdict(b2["w"]), float(b2["lev"]), short, + f"SENZA XS01 {b2['w']}", drop_xs=True) + lad[(short, "live")] = ladder(ii, {"TP01": 0.75, "SKH01": 0.25}, 0.75, short, + "LIVE 75/25 @0.75x") + bump(12) + tick("§7 scala di conti") + + # ================================================== §8. SINTESI + GATE + hr("§8. SINTESI — la banda da citare, e il gate da RI-REGISTRARE") + bL = best_row(res[WINDOWS[0][0]]["G"]) + bO = best_row(res[WINDOWS[1][0]]["G"]) + bD = best_row(res[WINDOWS[2][0]]["G"]) + print(f" J e argmax per finestra (XS01 = U13 Binance in tutte e tre, quindi la SOLA cosa che") + print(f" cambia fra le righe e' la FINESTRA):") + print(f" W-DISC 2024+ argmax {bD['w']:>10} @{bD['lev']:.2f}x J {bD['J']:.3f}") + print(f" W-OOS 2021-2023 argmax {bO['w']:>10} @{bO['lev']:.2f}x J {bO['J']:.3f}") + print(f" W-LONG 2021+ argmax {bL['w']:>10} @{bL['lev']:.2f}x J {bL['J']:.3f}") + print(f" (PROP-ALLOC pubblicato, XS01 HL 19 gambe, W-DISC: 25/25/50 @0.75x J 0.738)") + print(f"\n P(>=50 EUR/g) in 36 mesi, banda onesta:") + for short in ("W-LONG", "W-OOS", "W-DISC"): + row = " | ".join(f"{k}: {lad[(short, k)]['p_target']:.1%}" + for k in ("argmax", "2550", "noxs", "live") if (short, k) in lad) + print(f" {short:>8} {row}") + print(f"\n TRIAL/CELLE DICHIARATI (al rialzo): {TRIALS}") + print(f"\n fatto in {time.time()-t0:.0f}s") + print("=" * 104) + + +if __name__ == "__main__": + main() diff --git a/scripts/research/r0822b_surface_rv.py b/scripts/research/r0822b_surface_rv.py index 65a8206..2d56bc6 100644 --- a/scripts/research/r0822b_surface_rv.py +++ b/scripts/research/r0822b_surface_rv.py @@ -635,9 +635,9 @@ def main() -> None: print(f" ESEGUIBILE (compra ali all'ask, vendi corpo al bid): " f"{bf['viol_exec'].sum():,} = {bf['viol_exec'].mean():.4%}") if dte_min == DTE_MIN: - bf_keep = bf + bf_keep, v_keep = bf, v - bf = bf_keep + bf, vert = bf_keep, v_keep print("\n" + "=" * 100) print("3. QUANTO E' GRANDE L'INCOERENZA, E QUANTO COSTA ATTRAVERSARLA") print("=" * 100) @@ -651,12 +651,17 @@ def main() -> None: f"p90 {r.quantile(.9):.3f} · p99 {r.quantile(.99):.3f}") print(f" mezzo spread denaro-lettera mediana {hs.median():.3f} pt-vol · " f"p25 {hs.quantile(.25):.3f} · p75 {hs.quantile(.75):.3f}") + sub = x.dropna(subset=["iv_resid"]) print(f" UN TICK della griglia di prezzo mediana {tk.median():.3f} pt-vol " - f"-> l'incoerenza e' sotto il tick nel {np.mean(r.reindex(x.index) < tk):.1%} dei casi") + f"-> l'incoerenza e' sotto il tick nel " + f"{np.mean(sub['iv_resid'].abs() < sub['tick_pt']):.1%} dei casi (n={len(sub):,})") both = x.dropna(subset=["iv_resid"]) rap = np.abs(both["iv_resid"]) / (2 * both["halfspread_pt"]) print(f" |residuo| / (spread PIENO delle gambe): mediana {rap.median():.3f} · " f"p99 {rap.quantile(.99):.3f} · quota > 1 = {np.mean(rap > 1):.3%}") + print(f" RELATIVE VALUE (non arbitraggio): per guadagnare serve |residuo| > 1x lo spread") + print(f" anche con reversione TOTALE e uscita gratis ({np.mean(rap > 1):.3%} dei casi), " + f"> 2x se si paga anche l'uscita ({np.mean(rap > 2):.3%})") print(f" spread relativo (ask-bid)/mid del venue: mediana {x['relspread'].median():.1%} " f"· p25 {x['relspread'].quantile(.25):.1%}") # valore in dollari dell'incoerenza al MID (il caso migliore possibile: fee 0, spread 0) @@ -695,17 +700,17 @@ def main() -> None: print("\n" + "=" * 100) print("4. TEST C — varianza totale nel tenore (l'unico che tocca un interpolatore)") print("=" * 100) - c = test_calendario(q, DTE_MIN) - print(f" (i) strike CONDIVISO, nessuna interpolazione: {c['i_viol']:,}/{c['i_n']:,} = {c['i_quota']:.3%}" - f" · ampiezza mediana {c['i_amp_med_pt']:.3f} pt-vol (p95 {c['i_amp_p95_pt']:.3f})") - print(f" (ii) log-moneyness FISSO, con interpolazione (1 snapshot ogni {c['ii_ogni']}): " - f"{c['ii_viol']:,}/{c['ii_n']:,} = {c['ii_quota']:.3%} · ampiezza mediana {c['ii_amp_med_pt']:.3f} pt-vol") - print(f" -> contributo dell'INTERPOLATORE = {c['ii_quota'] - c['i_quota']:+.3%} di quota di violazione") - if "i_val_usd_med" in c: - print(f" valore mediano della violazione (i) al lotto minimo ${c['i_val_usd_med']:.2f} " - f"contro un costo di 2 gambe (spread + fee) di ${c['i_costo_usd_med']:.2f}") - print(f" margine di monotonia sull'intera superficie: la iv lunga sta {-c['room_med_pt']:.2f} pt-vol " - f"SOPRA il minimo che la non-arbitraggio richiede (5% piu' stretto: {-c['room_p5_pt']:.2f}) " + cal = test_calendario(q, DTE_MIN) + print(f" (i) strike CONDIVISO, nessuna interpolazione: {cal['i_viol']:,}/{cal['i_n']:,} = {cal['i_quota']:.3%}" + f" · ampiezza mediana {cal['i_amp_med_pt']:.3f} pt-vol (p95 {cal['i_amp_p95_pt']:.3f})") + print(f" (ii) log-moneyness FISSO, con interpolazione (1 snapshot ogni {cal['ii_ogni']}): " + f"{cal['ii_viol']:,}/{cal['ii_n']:,} = {cal['ii_quota']:.3%} · ampiezza mediana {cal['ii_amp_med_pt']:.3f} pt-vol") + print(f" -> contributo dell'INTERPOLATORE = {cal['ii_quota'] - cal['i_quota']:+.3%} di quota di violazione") + if "i_val_usd_med" in cal: + print(f" valore mediano della violazione (i) al lotto minimo ${cal['i_val_usd_med']:.2f} " + f"contro un costo di 2 gambe (spread + fee) di ${cal['i_costo_usd_med']:.2f}") + print(f" margine di monotonia sull'intera superficie: la iv lunga sta {-cal['room_med_pt']:.2f} pt-vol " + f"SOPRA il minimo che la non-arbitraggio richiede (5% piu' stretto: {-cal['room_p5_pt']:.2f}) " f"-> la superficie non e' nemmeno VICINA al bordo") print("\n" + "=" * 100) @@ -794,11 +799,15 @@ def main() -> None: print("SINTESI") print("=" * 100) nv = int(bf["viol_exec"].sum()); nt = len(bf) - print(f" 1. La superficie e' ARBITRAGE-FREE sui prezzi eseguibili: monotonia 0/{1_111_585:,},") + print(f" 1. La superficie e' ARBITRAGE-FREE sui prezzi eseguibili: monotonia " + f"{vert['n_viol']}/{vert['n_confronti']:,},") print(f" convessita' {nv}/{nt:,} = {nv/nt:.6%} (e quell'una vale $0,00 e sparisce l'ora dopo),") - print(f" calendario 6/107.177 con margine mediano di 13 punti di vol dal bordo.") + print(f" calendario {cal['i_viol']}/{cal['i_n']:,} con margine mediano di {-cal['room_med_pt']:.0f} " + f"punti di vol dal bordo.") print(f" 2. L'incoerenza al MID esiste ed e' grande ({bf['viol_mid'].mean():.1%} delle triple)") - print(f" ma vale {np.abs(bf['iv_resid']).median():.3f} punti di vol contro un mezzo spread di 0,86-1,21,") + hs_b = bf[bf.asset == "BTC"]["halfspread_pt"]; hs_e = bf[bf.asset == "ETH"]["halfspread_pt"] + print(f" ma vale {np.abs(bf['iv_resid']).median():.3f} punti di vol contro un mezzo spread di " + f"{hs_b[np.isfinite(hs_b)].median():.2f}-{hs_e[np.isfinite(hs_e)].median():.2f},") print(f" ed e' SOTTO IL TICK del venue nel 95-97% dei casi: non e' esprimibile nella griglia.") print(f" 3. MECCANISMO: |residuo| cresce in modo monotono col LARGO del mercato") print(f" (BTC 0,080 pt a spread 2,6% -> 0,255 pt a spread 50%). L'incoerenza al mid E' la") diff --git a/scripts/research/r0822b_tp01_sinistro.py b/scripts/research/r0822b_tp01_sinistro.py index 2c158f4..ff0002d 100644 --- a/scripts/research/r0822b_tp01_sinistro.py +++ b/scripts/research/r0822b_tp01_sinistro.py @@ -495,6 +495,10 @@ def main() -> None: # noqa: C901 a_sin = max(tab, key=lambda w: tab[w]["sin"][1]["J"]) print(f"\n argmax: 2019+ -> TP01 {a_full:.3f} | 2024+ -> TP01 {a_rec:.3f} | " f"SOLO-SINISTRO -> TP01 {a_sin:.3f}") + print(f" ⚠️ RISOLUZIONE: un LIVELLO di J ha errore MC ~{np.sqrt(0.25/N_PATHS):.3f} " + f"(N={N_PATHS}) -> celle entro ~0.02 NON sono distinguibili, e infatti 0.375 e 0.500") + print(" si scambiano il posto fra semi diversi. La lettura e' 'la regione 0.25-0.50',") + print(" non 'il punto 0.375' (stessa lezione del null dei tilt casuali di PROP-ALLOC).") print(" ⚠️ La colonna SOLO-SINISTRO e' una lente ESTREMA e va letta come tale: l'anno intero") print(" e' fatto di soli blocchi di crash (4 episodi ricampionati). Non e' uno scenario") print(" plausibile, e' il **limite superiore del danno**: se il verso della risposta e' lo") @@ -748,8 +752,9 @@ def main() -> None: # noqa: C901 (0.125, 0.375, 0.50), (0.25, 0.25, 0.50), (0.375, 0.125, 0.50)] com = xs.index.intersection(IDX[i_rec:]) xs_al = xs.reindex(com) + LEV_COM = 0.625 # leva comune per il blocco a XS01=50% (la minima fra le tre righe) print(f"\n {'TP/SKH/XS':>16} {'ipotesi XS':>12} {'leva':>6} {'J normale':>10} " - f"{'P(viv|replay peggiore)':>24} {'shock di rottura':>18}") + f"{'P(viv|replay pegg.)':>21} {'idem @0.625x':>14} {'shock di rottura':>18}") for cfg in W3: wtp, wsk, wxs = cfg base = wtp + wsk @@ -767,7 +772,7 @@ def main() -> None: # noqa: C901 bfu = PA.boot_idx(nn, HORIZON, N_PATHS, seed=8302) lab = f"{int(round(wtp*100))}/{int(round(wsk*100))}/{int(round(wxs*100))}" - def build(hyp: str, shock: float = 0.0): + def build(hyp: str, shock: float = 0.0, lev_: float | None = None): segsR, segsG = [], [] for t in epis: rc = base * (crypto(wtp_n)[0][t:t + EPI_LEN] - offc) @@ -790,16 +795,16 @@ def main() -> None: # noqa: C901 worst = 1.0 for k, g2 in enumerate(sg): fu = splice_paths(bfu, np.arange(g2[0], g2[1]), seed=8320 + k) - worst = min(worst, J_of(Rp, Gp, evn, fu, lv)["p_alive"]) + worst = min(worst, J_of(Rp, Gp, evn, fu, lv if lev_ is None else lev_)["p_alive"]) return worst if wxs == 0: print(f" {lab:>16} {'n/a (no XS)':>12} {lv:>6.3f} {on['J']:>10.3f} " - f"{build('A-flat'):>24.1%} {'-':>18}") + f"{build('A-flat'):>21.1%} {build('A-flat', lev_=LEV_COM):>14.1%} {'-':>18}") continue for hyp in ("A-flat", "B-peggiore"): print(f" {lab:>16} {hyp:>12} {lv:>6.3f} {on['J']:>10.3f} " - f"{build(hyp):>24.1%} {'-':>18}") + f"{build(hyp):>21.1%} {build(hyp, lev_=LEV_COM):>14.1%} {'-':>18}") lo, hi = 0.0, 0.40 # shock di rottura: bisezione su P(vivo) = 50% for _ in range(9): mid = 0.5 * (lo + hi) @@ -807,11 +812,22 @@ def main() -> None: # noqa: C901 lo = mid else: hi = mid - print(f" {lab:>16} {'C-shock':>12} {lv:>6.3f} {on['J']:>10.3f} {'(50% per def.)':>24} " - f"{-lo:>17.1%}") + lo2, hi2 = 0.0, 0.40 + for _ in range(9): + mid = 0.5 * (lo2 + hi2) + if build("C-shock", mid, lev_=LEV_COM) >= 0.50: + lo2 = mid + else: + hi2 = mid + print(f" {lab:>16} {'C-shock':>12} {lv:>6.3f} {on['J']:>10.3f} {'(50% per def.)':>21} " + f"{'(50% per def.)':>14} {-lo:>17.1%} / {-lo2:.1%} @0.625x") print("\n Lettura: 'shock di rottura' = il crollo di UN giorno su XS01 che porta P(vivo) al") print(" 50% quando il crash arriva. Piu' e' piccolo, piu' l'allocazione XS01-pesante e' a un") print(" giorno di coda dalla morte. Non e' una previsione: e' la taglia della scommessa.") + print(" ⚠️ Le ultime tre righe hanno XS01 FISSO al 50% e differiscono SOLO per come si") + print(" divide il resto fra TP01 e SKH01: e' li' che si legge se TP01 fa da zavorra contro") + print(" l'ignoto di XS01. **La colonna @0.625x e' quella da leggere**, perche' a leva libera") + print(" la riga con meno TP01 sceglie una leva piu' bassa e vince per quello.") tick("§6 XS01") # ============================================================ §7. BANDA + VERDETTO @@ -917,9 +933,14 @@ def main() -> None: # noqa: C901 print(" de-levering**, in una veste nuova: 'protezione dal crash' invece di 'meno DD'.") print(" 4) Il rischio vero dell'ottimo di PROP-ALLOC NON e' TP01 sotto-pesato: e' XS01 al") print(" 50% con un comportamento nel crash che il campione non contiene. Un solo giorno") - print(" a -8% su XS01 dimezza la sopravvivenza; con TP01 al 50% ne serve uno a -12.3%.") - print(" **Se TP01 vale qualcosa su un conto funded, vale come zavorra contro l'ignoto di") - print(" XS01, non come assicurazione contro il crash di BTC/ETH — che SKH01 fa meglio.**") + print(" a ~-10% su XS01 dimezza la sopravvivenza quando il crash arriva.") + print(" ⚠️ IPOTESI MIA, NATA E REFUTATA IN SESSIONE: avevo scritto che TP01 fa da") + print(" ZAVORRA contro quell'ignoto (a XS01 25% serviva uno shock -12.3% contro -8.0% a") + print(" XS01 50%). Il controllo a **XS01 FISSO al 50% e leva comune** lo smonta: lo shock") + print(" di rottura e' -10.1 / -10.2 / -10.2% muovendo TP01 da 12% a 38%, cioe' INVARIANTE,") + print(" e la sopravvivenza sotto l'ipotesi severa CALA (74.1 -> 72.6 -> 64.7%). Cio' che") + print(" protegge dall'ignoto di XS01 e' **meno XS01**, non piu' TP01: il primo numero") + print(" confrontava due pesi di XS01 diversi e attribuiva a TP01 il merito dell'altro.") print("\n ⚠️ COSA MI SMENTIREBBE: un crash in cui SKH01 NON si gira short in tempo (gap") print(" notturno oltre lo stop, o venue fermo). Il campione ne ha quattro e in tutti e") print(" quattro SKH01 ha girato. Basterebbe un episodio in cui SKH01 perde quanto il")