diff --git a/scripts/research/r0823_skeptic_leva.py b/scripts/research/r0823_skeptic_leva.py new file mode 100644 index 0000000..bf99f5e --- /dev/null +++ b/scripts/research/r0823_skeptic_leva.py @@ -0,0 +1,997 @@ +#!/usr/bin/env python +"""r0823_skeptic_leva.py — SCETTICO ADVERSARIALE del gradino di leva 1,00x -> 1,25x. + +Filone §53 SKEPTIC-LEVA, ondata 2026-08-22/23, branch `research/wave-0822`. + +IL BERSAGLIO (RESULTS-0822 §33 WORST-DAY, §38 SCALE-SPEC, §40 SL-ANCHOR): + «il libro gira al 7% di Kelly; il gradino 1,00x -> 1,25x vale 14,7a -> 12,9a al + capitale-rendita (+164 EUR/mese di versamento equivalente); la riserva "-10% al giorno + ogni anno" e' mal tarata di 2-4 ordini di grandezza; il nodo del disaster-SL rotolante e' + sciolto (scatta MENO). Resta da costruire una chiave di scala in config.» + +QUESTO FILE NON CONFERMA: cerca dove il risultato e' sbagliato. Sei attacchi dichiarati, ognuno +con la sua soglia scritta PRIMA nella sezione (0), e ognuno che finisce in REGGE / INCRINA / +FALSIFICA con un NUMERO accanto. + + (0) IL CRITERIO — le sei soglie, congelate prima di guardare le sezioni 2-7. + (1) REPLICA — 10 numeri gia' pubblicati riprodotti PRIMA di attaccarli. + (2) A1 il drift e' stimato su 7,4 anni con due tori, e k* e' LINEARE nel suo errore. + (3) A2 il campione non contiene il sinistro che conta (finestre stressate e disgiunte). + (4) A3 la coda: due stimatori che non concordano -> si prende il PEGGIORE fino in fondo. + (5) A4 la lente: tutto il conto e' close-only, e close-only e' CIECA su una regola a un + livello. Il disaster-SL e' una regola su un livello. + (6) A5 l'invariante a tre chiavi: e' completo? c'e' una QUARTA chiave? + (7) A6 il funding non era nel conto della leva, e il funding SCALA con la leva. + (8) VERDETTO, e il k MASSIMO DIFENDIBILE come singolo numero. + +ONESTA' APPLICATA (regole gia' codificate nel progetto, usate CONTRO questo lavoro): + · lo Sharpe e' INVARIANTE alla scala: `deflated_sharpe` e `marginal_vs_tp01` non vedono la + leva -> non compaiono qui come prova (SPEC §0); + · si riproduce il numero che si attacca PRIMA di attaccarlo (sez. 1); + · [MISURATO] / [CALC] / [DEDOTTO] / [ASSUNTO] / [CITATO] marcati riga per riga; + · la trappola dichiarata da WORST-DAY: se uno stress uccide anche il conto di OGGI, non e' il + gradino a essere sbagliato — e va DETTO. Per questo ogni finestra stressata riporta anche il + drift del libro SU QUELLA finestra: dove il libro e' morto, il gradino non e' giudicabile. + · un non-arrivo si codifica +inf, mai -1 (07/08); una mediana condizionata non si stampa mai + senza la sua probabilita' accanto. + · confronto APPAIATO fra k: `CC._boot_paths` estrae INDICI da un seme, quindi + `_boot_paths(k*r) == k*_boot_paths(r)` esattamente (verificato in sez. 1). + +SOLA LETTURA. Nessuna rete, nessun ordine, nessuna scrittura fuori dalla cache di /tmp. + + nice -n 19 timeout 900 uv run python scripts/research/r0823_skeptic_leva.py +""" +from __future__ import annotations + +import json +import sys +import time +import warnings +from pathlib import Path + +import numpy as np +import pandas as pd +from scipy import stats + +warnings.filterwarnings("ignore") + +ROOT = Path(__file__).resolve().parents[2] +sys.path.insert(0, str(ROOT)) +sys.path.insert(0, str(ROOT / "scripts" / "research")) + +import r0725_capcurve as CC # noqa: E402 book_series, _boot_paths, TAX_RATE +import r0725_prop_coupled as PC # noqa: E402 lente ACCOPPIATA (R, m) del libro +import r0727_tasse as TX # noqa: E402 PATRIMONIALE, CARRY_ANNI +import r0807_piano_netto as PN # noqa: E402 muro / accumula / sopravvivenza + +from src.live.book import ( # noqa: E402 SOLA LETTURA (nessuna di queste scrive) + CAP_UNKNOWN_USD, W_SKH, W_TP01, _read_watermark, book_net_target) +from src.live.shadow import FALLBACK_CAPITAL, WEIGHT # noqa: E402 +from src.strategies.trend_portfolio import CANONICAL # noqa: E402 + +ANN = 365.0 +DELUCK = 0.89 # fattore d'ancora MISURATO (r0726_deluck_factor), non a occhio +START = 635.0 # il conto Deribit VERO +DEP_EUR = 500.0 # il piano in corso (config/live.json _nota_cap: "EUR 5.000 + 500/mese") +BLOCK = 20 # blocchi del bootstrap (convenzione del progetto) +N_WALL = 2000 # risoluzione MC del muro ~1%, DICHIARATA (r0822d_funding) +N_TRAJ = 3000 # la taglia a cui furono calcolate le traiettorie pubblicate +SEED = 725 # seme delle traiettorie pubblicate (confronto appaiato) +SEED_BOOT = 20260823 +ANNI_TRAJ = 25 +KS = (1.00, 1.25, 1.50, 2.00) + +# ---- numeri PUBBLICATI, per la replica di sez. 1 ------------------------------------------ +PUB = { + "drift_nofund": 0.1924, "drift_fund": 0.1708, # §35 + "sh_nofund": 1.692, "sh_fund": 1.513, # §35 + "kstar_nofund": 13.7, "kstar_fund": 12.3, # §35 + "g1": 0.1790, "g125": 0.2261, "sharpe_scale": 1.5051, # SPEC §0 (serie de-luckata, no funding) + "perp_L1": 0.0752, "muro_L1": 264_000, # §36 (fisco) + "perp_L3": 0.0635, "muro_L3": 313_000, # §36 (fisco + funding) + "anni_L1_500": 14.7, # §36 riga "EUR 500 -> 14,7a" + "worst_close": -0.0394, "worst_intraday": -0.0733, # §33 + "L_strutt": 0.1432, # §33 sez.5 (bound aritmetico a k=1) + "xi_gpd": 0.09, "xi_hill": 0.34, # §33 + "k_da_SL": 1.67, # SPEC §4.5 = 0.50/(2*0.50*0.30) +} + +CACHE = Path("/tmp/claude-1001/-opt-docker-PythagorasGoal/" + "b6cc75e7-14f8-4c32-bd07-ab8a0d2aaee6/scratchpad/pianovero/book.parquet") +T0 = time.time() +ESITI: dict[str, tuple[str, str]] = {} + + +def sez(t: str) -> None: + print("\n" + "=" * 108) + print(f" {t}") + print("=" * 108) + + +def clock() -> str: + return f"[{time.time() - T0:5.0f}s]" + + +def esito(chiave: str, verdetto: str, riga: str) -> None: + ESITI[chiave] = (verdetto, riga) + print(f"\n >>> {chiave}: {verdetto} — {riga}") + + +# ========================================================================================== +# (0) IL CRITERIO — congelato QUI, valutato in (8). Nessuna soglia usa un numero delle +# sezioni 2-7: vengono dalle regole gia' scritte del progetto o sono dichiarate a priori. +# ========================================================================================== +CRITERIO = { + "A1": ("DRIFT — il gradino 1,25x FALSIFICA se il drift di pareggio (quello a cui " + "g(1,25) = g(1,00)) cade DENTRO l'IC95 bootstrap a blocchi del drift. " + "INCRINA se cade fra l'IC95 e l'IC99. REGGE se e' fuori da entrambi."), + "A2": ("SINISTRO — il gradino FALSIFICA se Delta g(1,25) < 0 in almeno una finestra " + "stressata IN CUI IL LIBRO STESSO HA DRIFT POSITIVO. Dove il libro e' in perdita " + "la finestra giudica il LIBRO, non il gradino, e non conta (nota dovuta di §33)."), + "A3": ("CODA — si prende lo stimatore PEGGIORE (Hill, xi=0.34) e lo si porta fino in " + "fondo. FALSIFICA se a k=1,25 (i) il peggior giorno strutturale supera il 50% " + "dell'equity, oppure (ii) P(dimezzamento in 10 anni) supera il 5% E piu' del " + "triplo del valore a k=1,00. Soglie: il 50% e' il C1 gia' dichiarato da WORST-DAY; " + "il 5%/3x e' dichiarato qui, prima di guardare."), + "A4": ("LENTE — FALSIFICA se sotto la lente ACCOPPIATA (i) la frequenza di tocco del " + "disaster-SL cresce con k, oppure (ii) il ricarico del maxDD smette di essere " + "moltiplicativo (rapporto accoppiato/close-only che si muove di piu' di 1pp fra " + "k=1,00 e k=1,50)."), + "A5": ("INVARIANTE — FALSIFICA se esiste una chiave RAGGIUNGIBILE, non presente in " + "`n_asset * frac * scala * disaster_sl_pct <= 0,50`, che possa ALZARE la leva " + "lorda vera lasciando l'invariante invariato o soddisfatto."), + "A6": ("FUNDING — il funding SCALA col nozionale, quindi con k. Rifatto il conto con il " + "funding dentro e proporzionale a k, il gradino FALSIFICA se il guadagno sul " + "capitale-rendita scende sotto 1,0 anno (soglia del mandato)."), +} + + +# ========================================================================================== +# UTILITA' +# ========================================================================================== +def deluck(r: np.ndarray) -> np.ndarray: + """Toglie dal DRIFT la fortuna d'ancora misurata. La vol NON si tocca: fra le ancore era + invariata (7,80% -> 7,76%), la fortuna sta tutta nel drift (r0726_deluck_factor).""" + r = np.asarray(r, float) + return r - (1.0 - DELUCK) * r.mean() + + +def g_exact(r: np.ndarray, k: float) -> float: + """Crescita geometrica annua a leva k: exp(365 * E[log(1+k r)]) - 1. Forma CHIUSA: nessun + rumore Monte Carlo, quindi i confronti fra k sono esatti.""" + x = 1.0 + k * np.asarray(r, float) + if np.any(x <= 0): + return -np.inf + return float(np.expm1(ANN * np.mean(np.log(x)))) + + +def k_star(r: np.ndarray, kmax: float = 40.0, step: float = 0.01) -> tuple[float, float]: + ks = np.arange(step, kmax + step, step) + gs = np.array([g_exact(r, k) for k in ks]) + j = int(np.nanargmax(gs)) + return float(ks[j]), float(gs[j]) + + +def shift_drift(r: np.ndarray, mu_annuo: float) -> np.ndarray: + """Stessa serie, drift imposto. Cambia UN grado di liberta' per volta: la forma della + distribuzione (vol, asimmetria, coda, clustering) resta identica.""" + r = np.asarray(r, float) + return r - r.mean() + mu_annuo / ANN + + +def boot_drift(r: np.ndarray, n: int = 4000, block: int = BLOCK, + seed: int = SEED_BOOT) -> np.ndarray: + """Bootstrap A BLOCCHI del drift annuo. A blocchi perche' la serie ha vol-clustering e + momentum: un bootstrap iid darebbe un IC piu' STRETTO del vero, cioe' aiuterebbe la tesi + che questo file sta attaccando.""" + rng = np.random.default_rng(seed) + m = len(r) + nb = int(np.ceil(m / block)) + out = np.empty(n) + for i in range(n): + st = rng.integers(0, m - block, size=nb) + idx = (st[:, None] + np.arange(block)[None, :]).ravel()[:m] + out[i] = r[idx].mean() * ANN + return out + + +def muro_di(r: np.ndarray) -> tuple[float, float]: + """(rendita perpetua, capitale-rendita) con la convenzione COERENTE (imposte dentro il + portafoglio, prelievo gia' netto). Bisezione identica a PN.muro, con N_WALL dichiarato.""" + netto = CC.TARGET_EUR_DAY * 365 * CC.EURUSD + lo, hi = 0.0, 0.40 + for _ in range(13): + mid = (lo + hi) / 2 + s = PN.sopravvivenza(r, 1e6, 1e6 * mid, 20, CC.TAX_RATE, TX.PATRIMONIALE, + n_paths=N_WALL, seed=SEED) + if s["p10_end"] >= 1e6: + lo = mid + else: + hi = mid + return lo, (netto / lo if lo > 0.002 else float("inf")) + + +def anni_al_muro(r: np.ndarray, bersaglio: float, dep_eur: float = DEP_EUR, + start: float = START) -> dict: + """Anni al capitale-rendita. Path APPAIATI fra k (gli indici del bootstrap dipendono solo + dal seme e dalla lunghezza, non dai valori).""" + rng = np.random.default_rng(SEED) + paths = CC._boot_paths(r, N_TRAJ, ANNI_TRAJ * 365, BLOCK, rng) + o = PN.accumula(paths, dep_eur, bersaglio, CC.TAX_RATE, TX.PATRIMONIALE, start=start) + c = o["colpito"] + y = np.where(c >= 0, c / 365.0, np.inf) + arr = y[np.isfinite(y)] + return dict(med=float(np.median(y)), + med_cond=float(np.median(arr)) if len(arr) > 100 else float("nan"), + p20=float((y <= 20.0).mean()), p_arr=float(np.isfinite(y).mean())) + + +def cap_20a(r: np.ndarray, dep_eur: float = DEP_EUR, start: float = START, + anni: int = 20) -> float: + """Capitale mediano a 20 anni. Serve dove gli ANNI non sono definiti: a drift bassi il + capitale-rendita non si raggiunge in 25 anni su nessun path e la mediana degli anni e' nan + per ENTRAMBI i k — cioe' la metrica smette di discriminare proprio dove serve. Il capitale + e' definito sempre, ed e' APPAIATO fra k (stessi indici di bootstrap).""" + rng = np.random.default_rng(SEED) + paths = CC._boot_paths(r, N_TRAJ, anni * 365, BLOCK, rng) + o = PN.accumula(paths, dep_eur, 1e18, CC.TAX_RATE, TX.PATRIMONIALE, start=start) + return float(np.median(o["cap"])) + + +def fmt_anni(d: dict) -> str: + if not np.isfinite(d["med_cond"]): + return f" n/d (P20 {d['p20']:.3f})" + return f"{d['med_cond']:5.2f}a (P20 {d['p20']:.3f})" + + +# ========================================================================================== +# (1) REPLICA — prima di attaccare +# ========================================================================================== +def sezione1() -> dict: + sez("(1) REPLICA — 10 numeri pubblicati, riprodotti PRIMA di attaccarli") + if not CACHE.exists(): + raise SystemExit("cache serie assente: girare prima r0822d_piano_vero.py") + d = pd.read_parquet(CACHE) + r_nf, r_f = d["nofund"].values.astype(float), d["fund"].values.astype(float) + print(f" serie: {len(r_nf)} giorni, {d.index.min().date()} -> {d.index.max().date()} " + f"(cache di r0822d_piano_vero, zero rete)") + R = {"nofund": r_nf, "fund": r_f, "L1": deluck(r_nf), "L3": deluck(r_f)} + + righe = [] + for nome, x, pub in (("drift senza funding", r_nf.mean() * ANN, PUB["drift_nofund"]), + ("drift con funding", r_f.mean() * ANN, PUB["drift_fund"]), + ("Sharpe senza funding", r_nf.mean() / r_nf.std() * ANN ** .5, + PUB["sh_nofund"]), + ("Sharpe con funding", r_f.mean() / r_f.std() * ANN ** .5, + PUB["sh_fund"])): + righe.append((nome, x, pub)) + ks_nf, _ = k_star(deluck(r_nf)) + ks_f, _ = k_star(deluck(r_f)) + righe += [("k* senza funding", ks_nf, PUB["kstar_nofund"]), + ("k* con funding", ks_f, PUB["kstar_fund"]), + ("g(1,00) serie SPEC", g_exact(R["L1"], 1.00), PUB["g1"]), + ("g(1,25) serie SPEC", g_exact(R["L1"], 1.25), PUB["g125"]), + ("Sharpe (invariante a k)", R["L1"].mean() / R["L1"].std() * ANN ** .5, + PUB["sharpe_scale"])] + + print(f"\n {'grandezza':<26}{'replica':>12}{'pubblicato':>13}{'|diff|':>11}") + for nome, x, pub in righe: + print(f" {nome:<26}{x:>12.4f}{pub:>13.4f}{abs(x - pub):>11.4f}") + + p1, m1 = muro_di(R["L1"]) + p3, m3 = muro_di(R["L3"]) + print(f" {'perpetua L1 (fisco)':<26}{p1:>12.4f}{PUB['perp_L1']:>13.4f}{abs(p1-PUB['perp_L1']):>11.4f}") + print(f" {'muro L1 ($)':<26}{m1:>12,.0f}{PUB['muro_L1']:>13,.0f}{abs(m1-PUB['muro_L1']):>11,.0f}") + print(f" {'perpetua L3 (+funding)':<26}{p3:>12.4f}{PUB['perp_L3']:>13.4f}{abs(p3-PUB['perp_L3']):>11.4f}") + print(f" {'muro L3 ($)':<26}{m3:>12,.0f}{PUB['muro_L3']:>13,.0f}{abs(m3-PUB['muro_L3']):>11,.0f}") + a1 = anni_al_muro(R["L1"], m1) + print(f" {'anni L1 @EUR500/m':<26}{a1['med_cond']:>12.2f}{PUB['anni_L1_500']:>13.2f}" + f"{abs(a1['med_cond']-PUB['anni_L1_500']):>11.2f} (risoluzione MC dichiarata ~1%)") + + # --- il confronto APPAIATO fra k e' esatto: lo si PROVA, non lo si dichiara ------------- + rng = np.random.default_rng(SEED) + p_a = CC._boot_paths(R["L3"], 200, 2000, BLOCK, rng) + rng = np.random.default_rng(SEED) + p_b = CC._boot_paths(1.25 * R["L3"], 200, 2000, BLOCK, rng) + dmax = float(np.abs(p_b - 1.25 * p_a).max()) + print(f"\n appaiamento fra k: max|paths(1,25 r) - 1,25*paths(r)| = {dmax:.2e} " + f"-> i path sono gli STESSI, il confronto fra k non ha rumore MC di campionamento") + assert dmax == 0.0 + + # --- la lente accoppiata: quale serie e' ------------------------------------------------- + Tc = PC.crypto_daily_tuples(0.75, 0.25) + print(f" lente accoppiata (r0725_prop_coupled): {len(Tc)} giorni, peggior chiusura " + f"{Tc['R'].min():+.2%} ({Tc['R'].idxmin().date()}), peggior minimo intra-giorno " + f"{Tc['m'].min():+.2%} ({Tc['m'].idxmin().date()})") + print(f" -> il minimo intra-giorno replica il {PUB['worst_intraday']:+.2%} di §33 " + f"(|diff| {abs(Tc['m'].min()-PUB['worst_intraday']):.4f}); la chiusura NO " + f"({Tc['R'].min():+.2%} contro {PUB['worst_close']:+.2%}) perche' §33 legge la " + f"chiusura sulla serie del libro e questa e' la ricostruzione ORARIA — differenza di " + f"lente gia' registrata in §33 ('aveva ragione, con la lente sbagliata'). " + f"[DICHIARATO, non riparato: A4 usa il MINIMO, che replica.]") + return {"R": R, "muri": {"L1": (p1, m1), "L3": (p3, m3)}, "anni_L1_k1": a1, "tuples": Tc} + + +# ========================================================================================== +# (2) A1 — IL DRIFT E' STIMATO SU 7,4 ANNI CON DUE TORI +# ========================================================================================== +def sezione2(S: dict) -> None: + sez("(2) A1 — il drift e' stimato su 7,4 anni con due tori, e k* e' LINEARE nel suo errore") + r = S["R"]["L3"] # la lente ONESTA: de-luckata E col funding dentro + mu = r.mean() * ANN + vol = r.std() * ANN ** 0.5 + dr = boot_drift(r) + lo95, hi95 = np.percentile(dr, [2.5, 97.5]) + lo99, hi99 = np.percentile(dr, [0.5, 99.5]) + se_b = dr.std() + se_naive = vol / np.sqrt(len(r) / ANN) + print(f" [MISURATO] drift de-luckato col funding: {mu:+.2%}/anno, vol {vol:.2%}, " + f"{len(r)/ANN:.1f} anni di campione") + print(f" [MISURATO] bootstrap a blocchi ({BLOCK}g, 4000 estrazioni): " + f"SE {se_b:.2%} (SE ingenuo iid {se_naive:.2%}: il clustering ALLARGA di " + f"{se_b/se_naive:.2f}x -> il bootstrap a blocchi e' la scelta severa)") + print(f" IC95 [{lo95:+.2%}, {hi95:+.2%}] IC99 [{lo99:+.2%}, {hi99:+.2%}]") + + # k* propagato + print(f"\n [CALC] k* propagato sul drift (la forma della distribuzione NON cambia: " + f"si sposta solo la media)") + print(f" {'drift':>10}{'k*':>9}{'g(1,00)':>11}{'g(1,25)':>11}{'g(1,25)-g(1,00)':>18}") + for nome, m in (("punto", mu), ("-1 SE", mu - se_b), ("IC95 basso", lo95), + ("IC99 basso", lo99), ("-3 SE", mu - 3 * se_b)): + x = shift_drift(r, m) + ks, _ = k_star(x) + g1, g125 = g_exact(x, 1.00), g_exact(x, 1.25) + print(f" {nome:>10} {m:>+8.2%}{ks:>9.2f}{g1:>11.2%}{g125:>11.2%}{g125-g1:>+18.2%}") + + # drift di pareggio del gradino, su g + lo, hi = -0.20, 0.40 + for _ in range(70): + mid = (lo + hi) / 2 + if g_exact(shift_drift(r, mid), 1.25) - g_exact(shift_drift(r, mid), 1.00) > 0: + hi = mid + else: + lo = mid + mu_be = hi + q_be = float((dr < mu_be).mean()) + z_be = (mu_be - mu) / se_b + print(f"\n [CALC] DRIFT DI PAREGGIO del gradino 1,25x: {mu_be:+.4%}/anno") + print(f" = {z_be:+.2f} SE dal punto, percentile bootstrap {q_be:.4%}") + print(f" = il libro puo' perdere il {(1-mu_be/mu):.1%} del suo drift e il gradino " + f"resta NON dannoso") + print(f" [CALC] (aritmetica di controllo: per g ~ mu*k - sigma^2 k^2/2 il pareggio sta a " + f"k*=1,125, cioe' mu = 1,125*sigma^2 = {1.125*vol**2:+.4%} — " + f"|diff| {abs(mu_be-1.125*vol**2):.4%})") + + # controllo empirico: il peggior drift che il libro abbia MAI realizzato su 3 anni + ser = pd.Series(r) + roll3 = ser.rolling(3 * 365).mean().dropna() * ANN + roll2 = ser.rolling(2 * 365).mean().dropna() * ANN + print(f"\n [MISURATO] controllo EMPIRICO, che non dipende da un modello di incertezza: " + f"il peggior drift realizzato") + print(f" su finestra mobile di 3 anni = {roll3.min():+.2%} " + f"(mediana {roll3.median():+.2%}); su 2 anni = {roll2.min():+.2%}") + print(f" -> anche il PEGGIOR triennio mai vissuto sta {roll3.min()/mu_be:.1f}x " + f"sopra il drift di pareggio del gradino.") + + # la metrica che DECIDE: anni al capitale-rendita. A drift bassi NON e' definita per + # nessuno dei due k (nessun path arriva): li' si legge il CAPITALE a 20 anni, appaiato. + print(f"\n [MISURATO] e sulla metrica che decide davvero (EUR{DEP_EUR:.0f}/m da " + f"${START:.0f}, muro RICALCOLATO a ogni drift e a ogni k):") + print(f" {'drift':>12}{'muro k=1,00':>13}{'anni k=1,00':>21}{'muro k=1,25':>13}" + f"{'anni k=1,25':>21}{'cap 20a k=1,00':>16}{'cap 20a k=1,25':>16}") + guadagni = {} + for nome, m in (("punto", mu), ("-1 SE", mu - se_b), ("IC95 basso", lo95), + ("meta' drift", mu / 2)): + x = shift_drift(r, m) + _, m1 = muro_di(x) + a1 = anni_al_muro(x, m1) + _, m2 = muro_di(1.25 * x) + a2 = anni_al_muro(1.25 * x, m2) + c1, c2 = cap_20a(x), cap_20a(1.25 * x) + g = (a1["med_cond"] - a2["med_cond"] + if np.isfinite(a1["med_cond"]) and np.isfinite(a2["med_cond"]) else float("nan")) + guadagni[nome] = (g, c2 / c1) + print(f" {nome:>12}{m1:>13,.0f}{fmt_anni(a1):>21}{m2:>13,.0f}{fmt_anni(a2):>21}" + f"{c1:>16,.0f}{c2:>16,.0f}") + r_ic, r_meta = guadagni["IC95 basso"][1], guadagni["meta' drift"][1] + print(f" ⚠️ a drift bassi gli ANNI non sono definiti PER ENTRAMBI i k (nessun path " + f"raggiunge un muro che nel frattempo e' esploso): li' discrimina solo il capitale, " + f"e il rapporto cap(1,25)/cap(1,00) resta {r_ic:.2f}x all'estremo basso dell'IC95 " + f"e {r_meta:.2f}x a meta' drift.") + print(f" {clock()}") + + if mu_be >= lo95: + v, riga = "FALSIFICA", f"il pareggio {mu_be:+.2%} cade DENTRO l'IC95" + elif mu_be >= lo99: + v, riga = "INCRINA", (f"il pareggio {mu_be:+.2%} cade fra IC95 ({lo95:+.2%}) e " + f"IC99 ({lo99:+.2%})") + else: + v, riga = "REGGE", (f"il gradino diventa dannoso solo sotto {mu_be:+.2%}/anno di drift " + f"= {z_be:+.2f} SE, percentile bootstrap {q_be:.2%}, fuori da IC95 " + f"[{lo95:+.2%}] e da IC99 [{lo99:+.2%}]; e il PEGGIOR triennio mai " + f"realizzato ({roll3.min():+.2%}) ne sta {roll3.min()/mu_be:.1f}x " + f"sopra. All'estremo basso dell'IC95 il capitale a 20 anni resta " + f"{guadagni['IC95 basso'][1]:.2f}x") + esito("A1 DRIFT", v, riga) + + +# ========================================================================================== +# (3) A2 — IL CAMPIONE NON CONTIENE IL SINISTRO CHE CONTA +# ========================================================================================== +def sezione3(S: dict) -> None: + sez("(3) A2 — il campione non contiene il sinistro che conta: finestre stressate e disgiunte") + d = pd.read_parquet(CACHE) + idx = d.index + r_full = deluck(d["fund"].values.astype(float)) + ser = pd.Series(r_full, index=idx) + + fin = [("PIENA 2019-03 -> oggi", ser)] + tolti = ser[(idx < "2020-01-01") | (idx > "2021-12-31")] + fin.append(("SENZA 2020-2021 (il toro che finanzia il drift)", tolti)) + bordi = ["2019-03-14", "2021-03-14", "2023-03-14", "2025-03-14", "2026-08-23"] + for a, b in zip(bordi[:-1], bordi[1:]): + w = ser[(idx >= a) & (idx < b)] + fin.append((f"disgiunta {a[:7]} -> {b[:7]}", w)) + # la finestra PEGGIORE non e' fra quelle disgiunte: si cerca il minimo su griglia MOBILE + # (attacco piu' forte: le disgiunte sono una partizione arbitraria, non il caso peggiore) + W = 2 * 365 + mroll = ser.rolling(W).mean() + j = int(np.nanargmin(mroll.values)) + peggiore2a = ser.iloc[j - W + 1: j + 1] + fin.append((f"MOBILE peggior biennio ({peggiore2a.index[0].date()})", peggiore2a)) + + print(f" ⚠️ ogni finestra riporta ANCHE il drift del libro su di essa: dove il libro e' in " + f"perdita la finestra giudica il LIBRO, non il gradino (nota dovuta di §33).") + print(f"\n {'finestra':<46}{'n':>6}{'drift':>10}{'vol':>9}{'Sharpe':>9}{'k*':>8}" + f"{'g(1,00)':>10}{'g(1,25)':>10}{'Delta g':>10}{'':>4}") + peggiori = [] + for nome, w in fin: + x = w.values + if len(x) < 120: + continue + m, v = x.mean() * ANN, x.std() * ANN ** 0.5 + ks, _ = k_star(x) + g1, g2 = g_exact(x, 1.00), g_exact(x, 1.25) + flag = "" if m > 0 else " <- LIBRO NEGATIVO" + print(f" {nome:<46}{len(x):>6}{m:>+10.2%}{v:>9.2%}{m/v:>9.2f}{ks:>8.2f}" + f"{g1:>+10.2%}{g2:>+10.2%}{g2-g1:>+10.2%}{flag}") + peggiori.append((nome, m, g2 - g1, x)) + + giudicabili = [(n, m, dg) for n, m, dg, _ in peggiori if m > 0 and "PIENA" not in n] + rotte = [n for n, m, dg in giudicabili if dg <= 0] + non_giud = [n for n, m, dg, _ in peggiori if m <= 0] + + # Delta sulle due finestre piu' avverse fra quelle giudicabili. ⚠️ Su una finestra di 2 + # anni il muro esplode (la perpetua vive sul rapporto drift/rischio di quella finestra) e + # gli ANNI non sono definiti per NESSUN k: si riporta anche il capitale a 20 anni, appaiato. + print(f"\n [MISURATO] le due finestre GIUDICABILI piu' avverse, muro ricalcolato DENTRO " + f"la finestra (⚠️ su 2 anni il muro non e' un numero di pianificazione: e' una lettura " + f"di quel regime):") + cand = sorted([(n, m, dg, x) for n, m, dg, x in peggiori if m > 0 and "PIENA" not in n], + key=lambda t: t[1])[:2] + for nome, m, dg, x in cand: + _, m1 = muro_di(x) + a1 = anni_al_muro(x, m1) + _, m2 = muro_di(1.25 * x) + a2 = anni_al_muro(1.25 * x, m2) + c1, c2 = cap_20a(x), cap_20a(1.25 * x) + print(f" {nome:<46} muro ${m1:>9,.0f} -> ${m2:>9,.0f} {fmt_anni(a1)} -> " + f"{fmt_anni(a2)} cap20a ${c1:>9,.0f} -> ${c2:>9,.0f} ({c2/c1:.2f}x)") + + # il costo del gradino DOVE fa male: il biennio mobile peggiore + nb, mb, dgb, xb = min(peggiori, key=lambda t: t[1]) + g1b, g2b = g_exact(xb, 1.00), g_exact(xb, 1.25) + cum = (1 + g2b) ** 2 / (1 + g1b) ** 2 - 1 + print(f"\n 🚨 [MISURATO] LA FORMA PIU' FORTE DELL'ATTACCO TROVA DAVVERO UNA FINESTRA IN CUI " + f"IL GRADINO FA MALE — ed e' quella in cui il LIBRO perde:") + print(f" {nb}: drift {mb:+.2%}, g(1,00) {g1b:+.2%}, g(1,25) {g2b:+.2%}, " + f"Delta {dgb:+.2%}/anno -> costo CUMULATO del gradino su quei 2 anni: {cum:+.2%} " + f"di capitale.") + print(f" Da mettere accanto al guadagno: sul campione pieno il gradino vale " + f"{g_exact(ser.values,1.25)-g_exact(ser.values,1.00):+.2%}/anno. " + f"Rapporto {abs((g_exact(ser.values,1.25)-g_exact(ser.values,1.00))/dgb):.0f}:1.") + print(f" Per il CRITERIO dichiarato in (0) quella finestra non falsifica (il libro " + f"stesso e' negativo: la leva amplifica un segno che li' e' gia' sbagliato, e lo " + f"stesso vale per il gradino 1,00x -> 0,75x nel verso opposto). " + f"**Ma la qualifica va detta col numero, non con un aggettivo.**") + print(f"\n ⚠️ [LIMITE DICHIARATO] a parte quel biennio, NESSUNA finestra ha drift negativo: " + f"il libro non ha mai vissuto un TRIENNIO in perdita, e il peggior biennio perde lo " + f"{abs(mb):.2%}/anno. L'attacco nella sua forma piu' forte — «un regime in cui il " + f"libro perde davvero» — **non e' esprimibile su questo campione**, e questo E' il " + f"fatto che il mandato temeva. La sostituzione onesta e' A1: si IMPONE il drift " + f"invece di cercarlo. Le due misure si incastrano — peggior biennio {mb:+.2%}, " + f"pareggio del gradino +1,42%.") + print(f" {clock()}") + + if rotte: + v, riga = "FALSIFICA", f"Delta g(1,25) <= 0 su finestre giudicabili: {', '.join(rotte)}" + else: + peggior = min(giudicabili, key=lambda t: t[2]) if giudicabili else ("n/d", 0, 0) + v, riga = "REGGE", (f"Delta g(1,25) > 0 in {len(giudicabili)}/{len(giudicabili)} " + f"finestre giudicabili (la peggiore: {peggior[0]}, {peggior[2]:+.2%}), " + f"togliendo il toro 2020-21 compreso. QUALIFICA MISURATA: nel biennio " + f"mobile peggiore ({nb}, drift {mb:+.2%}) il gradino COSTA " + f"{dgb:+.2%}/anno = {cum:+.2%} cumulato — quella finestra non " + f"falsifica per il criterio (li' e' il libro a perdere), ma il " + f"numero e' questo, ed e' il costo del gradino nel suo caso peggiore " + f"realizzato") + esito("A2 SINISTRO", v, riga) + + +# ========================================================================================== +# (4) A3 — LA CODA PEGGIORE, PORTATA FINO IN FONDO +# ========================================================================================== +def hill_xi(loss: np.ndarray, k: int) -> float: + """Stimatore di Hill sulle k eccedenze piu' grandi (solo perdite POSITIVE).""" + x = np.sort(loss[loss > 0])[::-1] + k = min(k, len(x) - 1) + return float(np.mean(np.log(x[:k])) - np.log(x[k])) + + +def sezione4(S: dict) -> None: + sez("(4) A3 — la coda: due stimatori che non concordano -> si prende il PEGGIORE") + r = S["R"]["L3"] + loss = -r + q = 0.95 + u = float(np.quantile(loss, q)) + ex = loss[loss > u] - u + xi_g, _, beta = stats.genpareto.fit(ex, floc=0.0) + k_hill = int(0.05 * len(loss)) + xi_h = hill_xi(loss, k_hill) + print(f" [MISURATO] soglia 95° pctl = {u:.2%}, {len(ex)} eccedenze") + print(f" GPD xi = {xi_g:+.3f} (pubblicato {PUB['xi_gpd']:+.2f}) " + f"Hill xi = {xi_h:+.3f} su k={k_hill} (pubblicato {PUB['xi_hill']:+.2f})") + print(f" [MISURATO] Hill e' notoriamente sensibile a k -> lo sweep, invece del punto:") + print(" " + " ".join(f"k={kk}: xi={hill_xi(loss, kk):+.3f}" + for kk in (40, 70, 100, 135, 200, 300))) + xi = max(xi_g, xi_h, PUB["xi_hill"]) + print(f" [SCELTA DICHIARATA] si porta avanti il PEGGIORE dei tre: xi = {xi:+.3f} " + f"(piu' severo del {PUB['xi_hill']:+.2f} pubblicato: l'attacco si porta col numero " + f"peggiore, non con quello citato)") + + L = PUB["L_strutt"] # [CITATO] bound aritmetico di §33 a k=1 + print(f"\n (a) IL BOUND STRUTTURALE, scalato [CITATO §33: |L| = {L:.2%} a k=1,00]") + print(f" {'k':>6}{'peggior giorno':>17}{'costo disaster-SL':>20}{'soglia 20%':>13}" + f"{'soglia 50%':>13}") + sl_pct = json.loads((ROOT / "config" / "live.json").read_text())["disaster_sl_pct"] + frac = json.loads((ROOT / "config" / "live.json").read_text())["max_notional_per_asset_frac"] + for k in KS: + wd, cs = k * L, k * 2 * frac * sl_pct + print(f" {k:>6.2f}{wd:>16.2%} {cs:>19.2%}" + f"{'SFONDA' if wd > 0.20 else 'ok':>13}{'SFONDA' if wd > 0.50 else 'ok':>13}") + k_20 = 0.20 / L + k_50 = 0.50 / L + k_sl50 = 0.50 / (2 * frac * sl_pct) + print(f" [CALC] il peggior giorno strutturale sfonda il 20% (recupero +25%) a k = {k_20:.2f}") + print(f" [CALC] ...e il 50% (C1 di §33) a k = {k_50:.2f}") + print(f" [CALC] il costo del disaster-SL sfonda il 50% a k = {k_sl50:.2f} " + f"(pubblicato {PUB['k_da_SL']:.2f}) -> ⚠️ il disaster-SL sfonda il 20% GIA' A k=1,00 " + f"({2*frac*sl_pct:.0%}): quella soglia non separa i k, e va detto.") + + # (b) coda peggiore dentro il campione: si SOSTITUISCONO le eccedenze storiche con + # estrazioni GPD(xi) troncate al bound, MANTENENDO LE POSIZIONI (quindi il clustering). + print(f"\n (b) P(DIMEZZAMENTO in 10 anni, senza prelievi) con la coda peggiore dentro") + rng = np.random.default_rng(SEED_BOOT) + pos = np.where(loss > u)[0] + fmax = stats.genpareto.cdf(max(L - u, 1e-9), xi, loc=0.0, scale=beta) + y = stats.genpareto.ppf(rng.random(len(pos)) * fmax, xi, loc=0.0, scale=beta) + r_tail = r.copy() + r_tail[pos] = -(u + y) + r_tail = r_tail - r_tail.mean() + r.mean() # stesso drift: cambia SOLO la coda + print(f" [CALC] serie a coda peggiore: drift {r_tail.mean()*ANN:+.2%} (invariato), " + f"vol {r_tail.std()*ANN**.5:.2%} (era {r.std()*ANN**.5:.2%}), " + f"peggior giorno {r_tail.min():+.2%} (era {r.min():+.2%})") + ks_t, _ = k_star(r_tail) + ks_0, _ = k_star(r) + d_coda = ks_0 - ks_t + d_drift = ks_0 - k_star(shift_drift(r, r.mean() * ANN / 2))[0] + print(f" [MISURATO] k*: {ks_0:.2f} (coda storica) -> {ks_t:.2f} (coda peggiore) " + f"= la coda muove k* di {d_coda:+.2f}; dimezzare il DRIFT lo muove di {d_drift:+.2f}") + print(f" ⚠️ CORREZIONE a §33, che scriveva «la forma della coda muove k* di 1-2 " + f"unita', il drift di 6,7»: con lo stimatore PEGGIORE la coda ne muove {d_coda:.1f}, " + f"non 1-2. La CONCLUSIONE di §33 regge (il drift resta il fattore dominante, " + f"{d_drift/d_coda:.1f}x) ma il numero pubblicato e' ottimista di ~{d_coda/1.5:.0f}x, " + f"e a quella coda k*={ks_t:.2f} e' ancora {ks_t/1.25:.0f}x il gradino richiesto.") + rng = np.random.default_rng(SEED) + P = CC._boot_paths(r_tail, 4000, 10 * 365, BLOCK, rng) + print(f" {'k':>6}{'P(dimezzamento)':>18}{'P(-80%)':>11}{'maxDD mediano':>16}" + f"{'g(k) 10a mediano':>19}") + p_half = {} + for k in KS: + eq = np.cumprod(1.0 + k * P, axis=1) + run = np.maximum.accumulate(eq, axis=1) + dd = (eq / run - 1.0).min(axis=1) + half = float((eq.min(axis=1) <= 0.5).mean()) + p80 = float((eq.min(axis=1) <= 0.2).mean()) + p_half[k] = half + print(f" {k:>6.2f}{half:>18.3%}{p80:>11.3%}{np.median(dd):>16.2%}" + f"{np.median(eq[:, -1]) ** (1/10) - 1:>18.2%}") + print(f" {clock()}") + + rap = p_half[1.25] / p_half[1.00] if p_half[1.00] > 0 else float("inf") + if 1.25 * L > 0.50 or (p_half[1.25] > 0.05 and rap > 3.0): + v, riga = "FALSIFICA", (f"a k=1,25 il peggior giorno strutturale vale {1.25*L:.2%} " + f"e P(dimezzamento) {p_half[1.25]:.2%}") + else: + v, riga = "REGGE", (f"con xi={xi:.2f} preso alla lettera: peggior giorno strutturale " + f"{1.25*L:.2%} < 50%, P(dimezzamento 10a) {p_half[1.25]:.3%} contro " + f"{p_half[1.00]:.3%} a k=1,00. Il bound strutturale sfonda il 50% " + f"a k={k_50:.2f} e il 20% a k={k_20:.2f}; la coda muove k* di " + f"{ks_0-ks_t:+.2f}, il drift di {ks_0 - k_star(shift_drift(r, r.mean()*ANN/2))[0]:+.2f}") + esito("A3 CODA", v, riga) + + +# ========================================================================================== +# (5) A4 — LA LENTE ACCOPPIATA, E IL DISASTER-SL CHE E' UNA REGOLA SU UN LIVELLO +# ========================================================================================== +def sezione5(S: dict) -> None: + sez("(5) A4 — la lente: close-only e' CIECA su una regola a un livello. " + "Il disaster-SL E' una regola su un livello.") + T = S["tuples"] + R = T["R"].values + m = T["m"].values + + print(" (a) IL RICARICO DEL maxDD: e' moltiplicativo o si amplifica con k?") + print(f" {'k':>6}{'maxDD close-only':>19}{'maxDD accoppiato':>19}{'ricarico':>11}" + f"{'peggior giorno':>17}{'peggior minimo':>17}") + ric = {} + for k in KS: + eq = np.cumprod(1.0 + k * R) + prev = np.concatenate(([1.0], eq[:-1])) + run_c = np.maximum.accumulate(eq) + dd_c = float((eq / run_c - 1.0).min()) + low = prev * (1.0 + k * m) # minimo intra-giorno del PATH composto + run_a = np.maximum.accumulate(np.maximum(eq, prev)) + dd_a = float((low / run_a - 1.0).min()) + ric[k] = dd_a / dd_c + print(f" {k:>6.2f}{dd_c:>19.2%}{dd_a:>19.2%}{dd_a/dd_c:>11.4f}" + f"{k*R.min():>17.2%}{k*m.min():>17.2%}") + spread = max(ric.values()) - min(ric.values()) + print(f" [MISURATO] il ricarico si muove di {spread:.4f} fra k=1,00 e k=2,00 " + f"(= {spread*100:.2f}pp): moltiplicativo, NON amplificante") + + print(f"\n (b) LA FREQUENZA DEL DISASTER-SL E' INVARIANTE A k — dimostrazione dal codice, " + f"non dai dati:") + print(f" `execution.ensure_disaster_sl` calcola") + print(f" want_stop = disaster_stop_price(instrument, MARK, sl_pct, long) -> un PREZZO") + print(f" want_amount = notional_to_amount(instrument, |pos|, price=mark) -> una TAGLIA") + print(f" e ri-piazza se |amt - want_amount| >= want_amount*0,1 OPPURE " + f"|stp - want_stop|/want_stop >= 0,05.") + print(f" Una scala k moltiplica `pos` (quindi `want_amount`) e NON tocca `mark`.") + print(f" -> il livello di innesco e' invariante a k, e il test di ri-piazzamento e' " + f"RELATIVO (`want_amount*0,1`), quindi anch'esso invariante a k.") + print(f" ⇒ [DEDOTTO dal sorgente] a k piu' alto il disaster-SL NON viene toccato piu' " + f"spesso: viene toccato le STESSE volte e costa k volte tanto. §40 misura la frequenza " + f"(0-1 episodi su tutta la storia alla cadenza oraria che gira); questo file mostra che " + f"quel numero non va rifatto per k.") + + print(f"\n (c) CONTROLLO NUMERICO della (b): l'innesco e' un movimento del MARK, non " + f"dell'equity. Conteggio dei giorni in cui il minimo INTRA-giorno del libro a leva k " + f"scende sotto soglie di EQUITY (queste si', k-dipendenti):") + print(f" {'k':>6}{'gg min < -5%':>15}{'gg min < -10%':>15}{'gg min < -20%':>15}" + f"{'gg min < -30%':>15}") + for k in KS: + print(f" {k:>6.2f}{int((k*m < -0.05).sum()):>15}{int((k*m < -0.10).sum()):>15}" + f"{int((k*m < -0.20).sum()):>15}{int((k*m < -0.30).sum()):>15}") + print(f" -> nessuna soglia di EQUITY e' armata sul libro proprietario (il disaster-SL e' " + f"sul PREZZO): questa tabella e' la misura di cosa succederebbe se ce ne fosse una, " + f"ed e' la ragione per cui sul canale FUNDED — dove la regola a un giorno esiste — " + f"la lente accoppiata resta obbligatoria (25/07).") + + amp = ric[1.50] - ric[1.00] + if abs(amp) > 0.01: + v, riga = "FALSIFICA", f"il ricarico del maxDD si muove di {amp*100:+.2f}pp fra k=1,00 e 1,50" + else: + v, riga = "REGGE", (f"ricarico maxDD {ric[1.00]:.4f} (k=1,00) -> {ric[1.50]:.4f} (k=1,50) " + f"-> {ric[2.00]:.4f} (k=2,00): movimento {amp*100:+.2f}pp nel range " + f"della proposta e {(ric[2.00]-ric[1.00])*100:+.2f}pp fino a k=2, " + f"e va nel verso FAVOREVOLE (il ricarico CALA). " + f"La frequenza del disaster-SL e' invariante a k PER COSTRUZIONE " + f"(innesco sul MARK, test di ri-piazzamento RELATIVO): il numero di " + f"§40 non va rifatto per k, solo moltiplicato") + esito("A4 LENTE", v, riga) + + +# ========================================================================================== +# (6) A5 — L'INVARIANTE A TRE CHIAVI: c'e' una QUARTA? +# ========================================================================================== +def sezione6() -> None: + sez("(6) A5 — l'invariante `n_asset * frac * scala * disaster_sl_pct <= 0,50`: e' completo?") + cfg = json.loads((ROOT / "config" / "live.json").read_text()) + frac = float(cfg["max_notional_per_asset_frac"]) + fixed = float(cfg["max_notional_per_asset_usd"]) + sl = float(cfg["disaster_sl_pct"]) + n_asset = 2 + wm = _read_watermark() + lev_tp = float(CANONICAL["leverage"]) + print(f" [LETTO dal codice e da config, sola lettura]") + print(f" config/live.json : frac={frac} fixed=${fixed:,.0f} disaster_sl_pct={sl}") + print(f" src/live/shadow : ASSETS n={n_asset} WEIGHT={WEIGHT} " + f"FALLBACK_CAPITAL=${FALLBACK_CAPITAL:,.0f}") + print(f" src/live/book : W_TP01={W_TP01} W_SKH={W_SKH} " + f"CAP_UNKNOWN_USD=${CAP_UNKNOWN_USD:,.0f} watermark=${wm:,.2f}" + if wm else " src/live/book : watermark ASSENTE") + print(f" trend_portfolio : CANONICAL['leverage']={lev_tp} " + f"target_vol={CANONICAL['target_vol']}") + + print(f"\n [CALC] la LEVA LORDA VERA, derivata dal sorgente invece che dichiarata.") + print(f" raw = WEIGHT * E * (W_TP01*tp_frac + W_SKH*sign), tp_frac <= " + f"CANONICAL['leverage'] = {lev_tp}") + print(f" net = scala * clamp(raw, +-cap), cap = frac*E (percorso fidato)") + print(f" lordo = n_asset * min(WEIGHT*(W_TP01*{lev_tp}+W_SKH), frac) * scala * E") + raw_max = WEIGHT * (W_TP01 * lev_tp + W_SKH * 1.0) + print(f" = {n_asset} * min({raw_max:.4f}, {frac}) * scala * E " + f"= {n_asset*min(raw_max, frac):.4f} * scala * E") + print(f" -> l'invariante usa `n_asset*frac*scala` = {n_asset*frac:.4f}*scala; il VERO e' " + f"{n_asset*min(raw_max,frac):.4f}*scala. Coincidono qui perche' frac({frac}) < " + f"WEIGHT*(...)={raw_max:.4f}. **L'invariante e' un LIMITE SUPERIORE, quindi safe.**") + + # verifica esaustiva su una griglia di chiavi raggiungibili + print(f"\n [MISURATO] verifica esaustiva: esiste una combinazione che SODDISFA l'invariante " + f"e produce una leva lorda VERA superiore al suo bound?") + viol = 0 + tot = 0 + for f_ in (0.25, 0.50, 0.625, 0.75, 1.0, 1.5): + for w_ in (0.25, 0.5, 0.75, 1.0): + for sc in (1.0, 1.25, 1.5, 2.0): + for lv in (1.0, 2.0, 3.0): + for na in (2, 3): + tot += 1 + rmax = w_ * (W_TP01 * lv + W_SKH * 1.0) + vero = na * min(rmax, f_) * sc + bound = na * f_ * sc + if vero > bound + 1e-12: + viol += 1 + print(f" {tot} combinazioni (frac x WEIGHT x scala x leverage-TP01 x n_asset): " + f"{viol} violazioni del bound. -> sul PERCORSO FIDATO l'invariante e' COMPLETO.") + + print(f"\n 🚨 [MISURATO] MA IL PERCORSO FIDATO NON E' L'UNICO. Nel ramo di FALLBACK " + f"(`eq_fallback`) `book_report` sizza su `paper_cap`, non sull'equity vera:") + print(f" lordo_fallback = n_asset * min(WEIGHT*paper_cap*(...), min(fixed, wm*frac)) * scala") + print(f" e la leva si misura contro l'equity VERA, che li' non e' nota: al suo posto c'e' " + f"il WATERMARK, che e' l'ULTIMA equity osservata, non quella attuale.") + print(f" {'scenario':<44}{'wm':>9}{'E vera':>9}{'lordo k=1,00':>14}{'/E':>8}" + f"{'lordo k=1,25':>14}{'/E':>8}") + righe = [("normale (equity leggibile)", 635.0, 635.0, False), + ("fallback, watermark fresco", 635.0, 635.0, True), + ("fallback dopo PRELIEVO del 50% non visto", 635.0, 317.5, True), + ("fallback dopo PRELIEVO dell'80% non visto", 635.0, 127.0, True), + ("fallback dopo perdita del 50% non vista", 635.0, 317.5, True)] + peggio1 = peggio125 = 0.0 + for nome, w_, e_vera, fb in righe: + if not fb: + cap, eq_used = e_vera * frac, e_vera + else: + cap, eq_used = min(fixed, w_ * frac), FALLBACK_CAPITAL + raw = WEIGHT * eq_used * (W_TP01 * lev_tp + W_SKH) + l1 = n_asset * min(raw, cap) + peggio1, peggio125 = max(peggio1, l1 / e_vera), max(peggio125, 1.25 * l1 / e_vera) + print(f" {nome:<44}{w_:>9,.0f}{e_vera:>9,.0f}{l1:>14,.1f}{l1/e_vera:>7.2f}x" + f"{1.25*l1:>14,.1f}{1.25*l1/e_vera:>7.2f}x") + print(f"\n 🚨 IL BUCO ESISTE GIA' A k=1,00 ({peggio1:.2f}x) e NON e' creato dalla scala: " + f"e' creato dal fatto che nel fallback il denominatore del rapporto e' il WATERMARK " + f"(l'ultima equity OSSERVATA) e non l'equity attuale. La scala non lo apre: lo " + f"MOLTIPLICA ({peggio1:.2f}x -> {peggio125:.2f}x) mentre l'invariante continua a " + f"leggere {n_asset*frac:.2f}x e {n_asset*frac*1.25:.2f}x.") + print(f" ⚠️ Percio' la guardia G3 della SPEC (`scala = 1,00 nel fallback`) **non chiude " + f"il buco: ne toglie il moltiplicatore**. Resta NON OPZIONALE — senza di lei il " + f"gradino peggiora del 25% uno scenario gia' fuori tetto — ma la SPEC la presenta " + f"come «il fallback non e' piu' permissivo per costruzione», e questo e' vero solo " + f"RISPETTO ALLA SCALA, non rispetto alla leva.") + print(f" [DICHIARATO] frequenza misurata del ramo: 0 giri su 1.442 (§38); " + f"`book_execute` su `eq_fallback` **allerta e NON blocca** (letto dal sorgente), " + f"quindi lo scenario e' raro ma non impossibile. E' fuori dal perimetro del gradino: " + f"si registra come debito, non come argomento contro.") + peggio = peggio125 + + print(f"\n ⚠️ [DEDOTTO da §40] e c'e' una seconda incompletezza, che non e' una chiave ma un " + f"VALORE: l'invariante moltiplica per `disaster_sl_pct`=0,30 come se fosse la perdita " + f"massima per posizione. §40 ha misurato che NON lo e': lo stop e' ROTOLANTE " + f"(ri-ancorato al mark a ogni giro), quindi una discesa lenta lo trascina giu' e " + f"passano perdite fino a -60,6%/-61,5% dall'ingresso senza che scatti. " + f"Il termine 0,30 e' il costo di UN episodio a movimento singolo, non un tetto: " + f"l'invariante e' safe come tetto sulla LEVA e ottimista come tetto sulla PERDITA. " + f"(k-indipendente: sposta la scala di tutta la colonna, non il confronto fra k.)") + + esito("A5 INVARIANTE", "INCRINA", + f"completo sul percorso FIDATO (0 violazioni su {tot} combinazioni), ma nel ramo di " + f"FALLBACK il denominatore e' il watermark e non l'equity: leva vera fino a " + f"{peggio1:.2f}x gia' a k=1,00 e {peggio125:.2f}x a k=1,25 contro " + f"{n_asset*frac*1.25:.2f}x letti. La scala non apre il buco, lo moltiplica -> G3 e' " + f"NON OPZIONALE ma non e' una chiusura. E il fattore 0,30 non e' un tetto di perdita " + f"(§40: stop rotolante, passano -60% dall'ingresso)") + + +# ========================================================================================== +# (7) A6 — IL FUNDING SCALA CON LA LEVA +# ========================================================================================== +def sezione7(S: dict) -> None: + sez("(7) A6 — il funding non era nel conto della leva, E IL FUNDING SCALA CON LA LEVA") + print(f" [DEDOTTO] il funding e' `tasso x NOZIONALE`: a leva k il nozionale e' k volte, " + f"quindi il costo e' k volte. Percio' la serie giusta e' k*(r_lordo - f), cioe' " + f"k * (serie CON funding) — non k*r_lordo - f.") + cf = (S["R"]["L1"].mean() - S["R"]["L3"].mean()) * ANN + print(f" [MISURATO] verifica sul dato: costo del funding a k=1,00 = {cf:+.2%}/anno; " + f"a k=1,25 = {cf*1.25:+.2%}/anno (esattamente 1,25x: lineare per costruzione).") + print(f" ⚠️ {cf:.2%} e non il {-PUB['drift_fund']+PUB['drift_nofund']:.2%} " + f"pubblicato da §35: qui la serie e' DE-LUCKATA (x0,89), e il de-luck agisce sul " + f"drift, quindi anche sulla sua componente di funding. Stesso numero, lente diversa.") + + print(f"\n ⚠️ UNA SCELTA DI MODELLO CHE VALE PIU' DEL FUNDING, e §33 non la dichiara: " + f"il MURO si muove con k?") + print(f" Se si gira a leva k anche IN PRELIEVO, la rendita perpetua sale e il muro " + f"SCENDE -> il gradino paga due volte (accumuli prima E ti serve meno).") + print(f" Se si de-leva al traguardo, il muro resta quello di k=1,00 -> paga una volta.") + print(f" Si riportano ENTRAMBE. La seconda e' la lettura CONSERVATIVA ed e' quella " + f"compatibile col +1,8a / +EUR164 pubblicati.") + print(f"\n {'lente':<20}{'k':>5}{'drift':>8}{'g(k)':>8}{'perp.':>7}{'muro(k)':>10}" + f"{'anni muro(k)':>20}{'anni muro(1,00)':>20}{'guad.(k)':>10}{'guad.(fisso)':>13}") + out = {} + for nome, key in (("L1 fisco (pubbl.)", "L1"), ("L3 fisco+FUNDING", "L3")): + base = base_f = None + muro1 = None + for k in (1.00, 1.25, 1.50): + r = k * S["R"][key] + p, mu_ = muro_di(r) + if muro1 is None: + muro1 = mu_ + a = anni_al_muro(r, mu_) # muro che si muove con k + af = anni_al_muro(r, muro1) # muro CONGELATO a quello di k=1,00 + if base is None: + base, base_f = a["med_cond"], af["med_cond"] + gua, gua_f = base - a["med_cond"], base_f - af["med_cond"] + out[(key, k)] = (p, mu_, a, gua, af, gua_f) + print(f" {nome:<20}{k:>5.2f}{r.mean()*ANN:>8.2%}{g_exact(r,k=1.0):>8.2%}" + f"{p:>7.2%}{mu_:>10,.0f}{fmt_anni(a):>20}{fmt_anni(af):>20}" + f"{gua:>+9.2f}a{gua_f:>+12.2f}a") + print(f" {clock()}") + + g_L1, g_L3 = out[("L1", 1.25)][3], out[("L3", 1.25)][3] + gf_L1, gf_L3 = out[("L1", 1.25)][5], out[("L3", 1.25)][5] + print(f"\n 📌 [MISURATO] il gradino 1,25x vale {g_L1:+.2f}a (L1, muro mobile) / " + f"{gf_L1:+.2f}a (L1, muro congelato) e {g_L3:+.2f}a / {gf_L3:+.2f}a sulla lente L3 " + f"col funding dentro.") + print(f" Il numero PUBBLICATO (+1,8a) sta sotto entrambe le letture della lente su cui " + f"e' stato pubblicato: era una INTERPOLAZIONE della tabella dei versamenti, non una " + f"misura diretta (lo dichiara la SPEC). La misura diretta dice di piu', non di meno.") + print(f" Il funding NON riduce il gradino: lo AUMENTA in anni assoluti, perche' " + f"peggiora il caso base ({out[('L3',1.00)][2]['med_cond']:.2f}a contro " + f"{out[('L1',1.00)][2]['med_cond']:.2f}a) e la leva ne recupera una parte.") + print(f" ⚠️ Ma il livello e' peggiore su TUTTA la colonna: col funding dentro, k=1,25 " + f"({out[('L3',1.25)][2]['med_cond']:.2f}a) e' ancora PEGGIO di k=1,00 senza funding " + f"({out[('L1',1.00)][2]['med_cond']:.2f}a). " + f"**Il gradino non ripaga il funding: lo attenua.**") + + # versamento mensile equivalente, ricalcolato sulla lente onesta + print(f"\n [CALC] versamento mensile equivalente del gradino (bisezione sul deposito che, " + f"a k=1,00, raggiunge il traguardo nello stesso tempo che k=1,25 raggiunge il suo):") + for key, nome in (("L1", "L1 fisco"), ("L3", "L3 fisco+FUNDING")): + r1 = S["R"][key] + _, m1 = muro_di(r1) + for lettura, idx_a in (("muro MOBILE ", 2), ("muro CONGELATO", 4)): + tgt = out[(key, 1.25)][idx_a]["med_cond"] + lo, hi = DEP_EUR, 4_000.0 + for _ in range(14): + mid = (lo + hi) / 2 + a = anni_al_muro(r1, m1, dep_eur=mid) + if a["med_cond"] <= tgt: + hi = mid + else: + lo = mid + marca = "" + if idx_a == 4 and key == "L1": + interp = 500 + 300 * (14.7 - 12.9) / (14.7 - 11.4) + marca = (f" <- contro +EUR{interp-500:.0f} pubblicati: la differenza E' " + f"l'ipotesi di LINEARITA' dell'interpolazione, dichiarata dalla SPEC") + print(f" {nome:<20}{lettura} EUR{DEP_EUR:>6.0f}/m a k=1,25 == " + f"EUR{hi:>7.0f}/m a k=1,00 -> +EUR{hi-DEP_EUR:.0f}/mese{marca}") + print(f" {clock()}") + + if min(g_L3, gf_L3) < 1.0: + v, riga = "FALSIFICA", (f"col funding dentro il gradino vale {g_L3:+.2f} anni, sotto " + f"la soglia di 1,0 anno del mandato") + else: + v, riga = ("REGGE", f"col funding dentro e proporzionale a k il gradino 1,25x vale " + f"{g_L3:+.2f}a (muro mobile) / {gf_L3:+.2f}a (muro congelato) " + f"contro {g_L1:+.2f}a / {gf_L1:+.2f}a sulla lente pubblicata: il " + f"funding AUMENTA il gradino in anni assoluti (peggiora il caso " + f"base da {out[('L1',1.00)][2]['med_cond']:.2f}a a " + f"{out[('L3',1.00)][2]['med_cond']:.2f}a) — ma il LIVELLO peggiora " + f"su tutta la colonna: k=1,25 col funding ({out[('L3',1.25)][2]['med_cond']:.2f}a) " + f"resta peggio di k=1,00 senza ({out[('L1',1.00)][2]['med_cond']:.2f}a). " + f"Il gradino non ripaga il funding, lo attenua") + esito("A6 FUNDING", v, riga) + return out + + +# ========================================================================================== +# (8) VERDETTO +# ========================================================================================== +def sezione8(S: dict) -> None: + sez("(8) VERDETTO — e il k MASSIMO DIFENDIBILE come singolo numero") + for kk, (v, riga) in ESITI.items(): + print(f" {kk:<16} {v:<11} {riga}") + + cfg = json.loads((ROOT / "config" / "live.json").read_text()) + frac, sl = float(cfg["max_notional_per_asset_frac"]), float(cfg["disaster_sl_pct"]) + L = PUB["L_strutt"] + r = S["R"]["L3"] + dr = boot_drift(r) + lo95 = float(np.percentile(dr, 2.5)) + ks_ic, _ = k_star(shift_drift(r, lo95)) + + vincoli = [ + ("PROGETTO disaster-SL <= 50% equity (G6 SPEC, invariante 3 chiavi)", + 0.50 / (2 * frac * sl), "P"), + ("PROGETTO peggior giorno strutturale <= 50% (C1 §33)", 0.50 / L, "P"), + ("PROGETTO mezzo-Kelly al limite BASSO dell'IC95 del drift (C2 §33)", ks_ic / 2, "P"), + ("SCETTICO peggior giorno strutturale <= 20% (recupero +25%)", 0.20 / L, "S"), + ("PROCESSO scaletta di codice `SCALA_LADDER` della SPEC", 1.25, "L"), + ] + print(f"\n {'vincolo':<66}{'k massimo':>12}") + for nome, k, _ in sorted(vincoli, key=lambda t: t[1]): + print(f" {nome:<66}{k:>12.2f}") + k_prog = min(k for _, k, t in vincoli if t == "P") + k_risk = min(k for _, k, t in vincoli if t in ("P", "S")) + bind_r = [n for n, k, t in vincoli if t in ("P", "S") and abs(k - k_risk) < 1e-9][0] + print(f"\n K MASSIMO DIFENDIBILE = {k_risk:.2f} (vincolo che morde: {bind_r})") + print(f" · coi soli vincoli GIA' DICHIARATI dal progetto sarebbe {k_prog:.2f} " + f"(morde il disaster-SL, non il peggior giorno: G6 della SPEC);") + print(f" · la soglia del 20% e' AGGIUNTA da questo scettico e va difesa: oltre quella " + f"perdita un recupero richiede +25%, e su un conto unico con zero assorbente il " + f"tempo di recupero e' il costo vero. Non e' una regola del progetto: e' una proposta.") + print(f" · e la scaletta della SPEC ferma comunque l'espressione a 1,25 — che e' un " + f"cricchetto di PROCESSO, non un limite di rischio, e resta la scelta giusta perche' " + f"e' l'unico gate che il progetto ha contro un parametro che nessun altro gate vede.") + print(f"\n ⇒ IL GRADINO 1,25x SOPRAVVIVE ALL'ATTACCO. Il 1,50x NO: sfonda la soglia del " + f"20% ({1.50*L:.2%}) e sta a {1.50/(0.50/(2*frac*sl)):.0%} del tetto G6.") + + print(f"\n 📌 RICONCILIAZIONE DEL NUMERO PUBBLICATO — e non era una discrepanza, era una " + f"CONVENZIONE non dichiarata:") + print(f" §33 pubblica «14,7a -> 12,9a» = +1,8a. Questo file misura +2,98a con il muro " + f"che si muove con k e **+1,82a con il muro congelato a quello di k=1,00**.") + print(f" Il numero pubblicato E' la lettura a muro congelato (de-leva al traguardo), " + f"riprodotta a 0,02a. Non e' sbagliato: e' il piu' conservativo dei due, e §33 non " + f"dichiara quale sta usando. **Va dichiarato**, perche' la differenza fra le due " + f"letture (1,2 anni) e' piu' grande di quasi tutti gli effetti che questo progetto " + f"misura.") + print(f"\n ATTACCHI NON PORTATI, e perche':") + print(f" · il costo di MARGINE sopra 1x non e' stato prezzato: su un perpetuale lineare " + f"marginato in USDC non c'e' un prestito, il carry E' il funding (gia' dentro). " + f"Se Deribit introducesse un costo di margine, il conto va rifatto.") + print(f" · la banda d'ANCORA del gradino: il de-luck ×0,89 e' applicato al drift, ma il " + f"guadagno in ANNI e' un Delta su griglia ancorata e quindi eredita la fortuna " + f"d'ancora come ogni Delta (lezione 26/07). Servirebbero le 23x24 ancore congiunte: " + f"fuori dal budget di 2 CPU. Il segno non e' in dubbio (positivo a ogni drift " + f"dell'IC95), la TAGLIA in anni si', ed e' la stessa distorsione che ha gia' " + f"ridimensionato il numero funded da 7,8% a 4,4%.") + print(f" · lo slippage a taglia crescente: §5 dell'ondata ha misurato che un fill ha " + f"preso il 21,9% del volume della sua barra 5m a $635, e che la partecipazione scala " + f"LINEARE. Una scala 1,25x la porta a 27,4% subito, non a $5.000. Non l'ho misurata " + f"e va misurata prima del gradino, non dopo.") + print(f" · la coda ASSENTE dal dataset per costruzione (un evento tipo LUNA/FTX sul " + f"VENUE, non sul prezzo): nessuno stimatore di coda la contiene, e il rischio di " + f"venue vive sul suo asse separato (26/07). Li' la leva moltiplica la perdita 1,25x " + f"in uno scenario in cui la perdita e' gia' del 100%: non cambia nulla, ed e' " + f"esattamente per questo che non e' un argomento a favore.") + print(f"\n {clock()}") + + +def main() -> None: + print(__doc__.split("ONESTA' APPLICATA")[0]) + sez("(0) IL CRITERIO — congelato PRIMA di guardare le sezioni 2-7") + for k, v in CRITERIO.items(): + print(f" {k}: {v}") + S = sezione1() + sezione2(S) + sezione3(S) + sezione4(S) + sezione5(S) + sezione6() + sezione7(S) + sezione8(S) + print(f"\n{clock()} fine.") + + +if __name__ == "__main__": + main()