#!/usr/bin/env python """r0822c_worst_day.py — IL PEGGIOR GIORNO CHE IL LIBRO PUO' AVERE, misurato invece che assunto. PERCHE' ESISTE QUESTO FILE. Il gradino di leva 1,00x -> 1,25-1,50x vale 14,7 anni -> 12,9-11,6 al capitale-rendita (~EUR300/mese di versamenti equivalenti, a costo zero) ed e' bloccato da UN solo parametro SCELTO A MANO in `r0822_growth_policy.py` sez. (4): *"un giorno -10% all'anno porta k* a 2x"*. Nessuno ha mai misurato quel numero. Il critico dell'ondata ha fatto un controllo di cinque minuti (sui 10 peggiori giorni del SOTTOSTANTE il libro guadagna, e il suo peggior giorno non e' un giorno di crash) e ha indicato la misura come quella di maggior valore mai fatta. Qui si VERIFICA o si SMENTISCE quel controllo, e si sostituisce il -10% con una stima. COSA C'E' QUI (0) IL CRITERIO, dichiarato PRIMA di guardare qualunque numero delle sezioni 4-6. (1) REPLICA DI CONTROLLO — 5 numeri gia' pubblicati, riprodotti prima di scriverne di nuovi. (2) LA DISTRIBUZIONE DELLE CHIUSURE del libro: coda sinistra per quantili, il peggiore assoluto, QUALI giorni sono, e l'attribuzione esatta ai due sleeve (max|diff| = 0). (3) LA LENTE ACCOPPIATA — il minimo INTRA-giorno (`r0725_prop_coupled`). La domanda "qual e' il peggior giorno" ha DUE risposte e servono entrambe: la leva agisce su tutte e due, ma il margine di mantenimento guarda il minimo. (4) LA CODA CHE NON E' NEL DATASET — EVT/POT (GPD sopra soglia dichiarata) su chiusure E su minimi, quantili a 1/1.000 e 1/10.000 giorni, IC bootstrap A BLOCCHI sui parametri. (5) IL PEGGIOR GIORNO POSSIBILE PER COSTRUZIONE — aritmetica, non statistica: dato il cap di `config/live.json`, il vol-target di TP01 e il gap-through misurato di SKH01. (6) k* ONESTO — la curva di Kelly rifatta con la coda stimata al posto di quella a mano, e la decomposizione dell'errore del -10% (taglia vs FREQUENZA). (7) IL VERDETTO contro il criterio di (0). ONESTA' APPLICATA (regole gia' codificate nel progetto): · un fattore correttivo scelto a occhio va DECOMPOSTO prima di accettarlo o rifiutarlo (26/07); · close-only e' CIECA su una regola a UN giorno (25/07) -> la lente accoppiata e' obbligatoria; · un fit di coda su ~2.700 osservazioni ha incertezza grande: si riporta, o si sostituisce un numero scelto a mano con uno finto-preciso, che e' peggio; · un Monte Carlo ha una risoluzione: qui la curva di crescita e' calcolata in forma ESATTA (E[log]) proprio per togliere di mezzo il rumore MC dello scetticato. nice -n 19 timeout 900 uv run python scripts/research/r0822c_worst_day.py """ from __future__ import annotations import json import sys import time from pathlib import Path import warnings import numpy as np import pandas as pd from scipy import stats warnings.filterwarnings("ignore", message=".*Sorting by default when concatenating.*") 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 (lente LIVE), TAX_RATE import r0725_prop_coupled as PC # noqa: E402 recon accoppiata (R, m) import r0726_deposits as DP # noqa: E402 DELUCK = 0.89 MISURATO import r0724_skh_live_weight as SKL # noqa: E402 gamba SKH01 sul path LIVE from src.data.downloader import load_data # noqa: E402 from src.strategies.trend_portfolio import ( # noqa: E402 CANONICAL, TrendPortfolio, realized_vol, resample_1d, simple_returns) from src.strategies.skyhook import SKH01_V2_DD # noqa: E402 ASSETS = ("BTC", "ETH") W_TP, W_SKH = 0.75, 0.25 ANN = 365.0 SEED = 20260822 # ---- parametri EVT DICHIARATI PRIMA (criterio, non tuning) ------------------------------- EVT_Q = 0.95 # soglia = 95° pctl delle PERDITE. Criterio dichiarato: la scelta # classica del compromesso bias/varianza (~5% di eccedenze, qui 136 # osservazioni). Le altre soglie sono girate come SENSITIVITA', non # per scegliere la migliore: se il numero si muove molto, si dice. EVT_Q_SENS = (0.90, 0.95, 0.975, 0.99) EVT_BOOT = 400 # ricampionamenti a BLOCCHI (la serie ha vol-clustering: un bootstrap EVT_BLOCK = 20 # iid stringerebbe l'IC di una coda che non e' iid) RP = (1_000, 10_000) # periodi di ritorno in GIORNI (2,7 anni e 27 anni) # ---- il numero sotto esame ---------------------------------------------------------------- STRESS_A_MANO = 0.10 # il "-10% in un giorno" di r0822_growth_policy sez.(4) STRESS_FREQ_A_MANO = 1.0 # ...assunto UNA VOLTA L'ANNO (e 1 ogni 5 anni come variante) # ---- costanti di venue (dichiarate, fonte in linea) -------------------------------------- MM_RATE = 0.01 # maintenance margin ~1% (Deribit perp lineari, valore prudente: # il listino parte da 0,5% e cresce con la size; 1% e' conservativo # NEL VERSO che rende la liquidazione PIU' vicina) WIPE = 0.90 # convenzione ereditata da growth_policy/skeptic: perdita giornaliera # >= 90% dell'equity = conto chiuso. Usata solo per replicare i loro numeri. # ========================================================================================== # (0) IL CRITERIO — congelato qui, valutato in (7). Nessuna delle sue soglie usa un numero # delle sezioni 4-6: vengono dalle regole gia' scritte del progetto e dal listino del venue. # ========================================================================================== CRITERIO = { "C1": ("STRUTTURALE — il peggior giorno POSSIBILE per costruzione, a leva k, deve lasciare " "in piedi almeno META' dell'equity: k * |L_strutturale| <= 0.50. " "(Ragione: zero e' assorbente e il conto e' unico; sotto meta' equity il piano non " "e' 'in ritardo', e' finito.)"), "C2": ("KELLY PESSIMISTICO — k <= 1/2 * k*(drift a -2 errori standard E coda troncata al " "bound strutturale). Il mezzo-Kelly e' la prescrizione classica ed e' scelta A PRIORI, " "non tarata su questo risultato; il -2 SE viene dalla fragilita' gia' misurata " "(r0822_growth_policy sez. 1-bis: k* e' LINEARE nell'errore del drift)."), "C3": ("LIQUIDAZIONE SUL MINIMO — con la lente ACCOPPIATA, il quantile a 1/10.000 giorni del " "minimo INTRA-giorno a leva k deve stare almeno 3x lontano dalla soglia di " "liquidazione del venue. (close-only e' CIECA su una regola a un giorno: 25/07.)"), "C4": ("GUADAGNO ROBUSTO — g(k) - g(1.00) deve restare POSITIVO col drift a -2 SE. " "Se il gradino paga solo al drift di punto, non e' un gradino, e' una scommessa " "sulla stima del drift."), "C5": ("COSTI NON MODELLATI — il costo che scala con k e non e' nei backtest (funding dei " "perpetual, costo del margine oltre 1x) deve essere piccolo rispetto al guadagno."), "C6": ("PROCESSO — deve ESISTERE una strada pulita per un puro cambio di SCALA. Regola del " "progetto: passa dal cap di config, non da `target_vol` (che e' anche un tilt di pesi " "e fallisce `weights_tilt_null`). Se quella strada non esiste, il gradino non e' " "autorizzabile *nella forma in cui e' stato proposto*, qualunque cosa dicano C1-C5."), } def sezione(t: str) -> None: print("\n" + "=" * 110) print(f" {t}") print("=" * 110) def pct(x: float) -> str: return f"{x:+.2%}" if np.isfinite(x) else " n/d" # ========================================================================================== # DATI # ========================================================================================== def serie_libro() -> dict: """Serie giornaliere del libro live e le sue due gambe, + il sottostante.""" book = CC.book_series("hourly") skh = SKL.skh_book(0, "hourly") if skh.index.tz is None: skh.index = skh.index.tz_localize("UTC") tp = CC.tp01_realistic(600.0 * W_TP) und = {} for a in ASSETS: df = load_data(a, "1h") idx = pd.DatetimeIndex(pd.to_datetime(df["datetime"], utc=True)) s = pd.Series(df["close"].values.astype(float), index=idx).resample("1D").last() und[a] = s.pct_change() J = pd.concat({"book": book, "tp_leg": W_TP * tp, "skh_leg": W_SKH * skh, "BTC": und["BTC"], "ETH": und["ETH"]}, axis=1, sort=True).dropna() J["und"] = 0.5 * J["BTC"] + 0.5 * J["ETH"] deluck = book - (1.0 - DP.DELUCK) * float(book.values.mean()) # = DP.deluck_returns(), con indice return {"J": J, "deluck": deluck} def gambe_tp01() -> dict: """Per asset: peso TENUTO da TP01, ritorno giornaliero, vol nota a close[d-1], z-score. z = r / (vol_ann_prev/sqrt(365.25)) e' la grandezza che rende STRUTTURALE il bound di TP01: il contributo di gamba e' `signal * (target_vol/vol) * r = signal * target_vol * z / sqrt(365.25)`, cioe' INVARIANTE alla volatilita'. Un vol-target converte un crash in uno z-score. """ out = {} for a in ASSETS: df = resample_1d(load_data(a, "1h")) c = df["close"].values.astype(float) r = simple_returns(c) tgt = np.nan_to_num(np.asarray(TrendPortfolio(**CANONICAL).target_series(df), float)) held = np.zeros(len(tgt)) held[1:] = tgt[:-1] vol = realized_vol(r, CANONICAL["vol_win_days"], 365.25) volp = np.concatenate(([np.nan], vol[:-1])) with np.errstate(divide="ignore", invalid="ignore"): z = r / (volp / np.sqrt(365.25)) lev_max = np.clip(np.where(volp > 0, CANONICAL["target_vol"] / volp, 0.0), 0.0, CANONICAL["leverage"]) idx = pd.DatetimeIndex(pd.to_datetime(df["datetime"], utc=True)) prev = np.concatenate(([np.nan], c[:-1])) r_low = df["low"].values.astype(float) / prev - 1.0 # escursione avversa INTRA-giorno out[a] = pd.DataFrame({"held": held, "ret": r, "ret_low": r_low, "vol_prev": volp, "z": z, "lev_max": lev_max}, index=idx) return out def estremi_orari() -> dict: """Movimento orario piu' avverso mai visto, per asset e per verso. E' il bound del gap-through di SKH01: gli exit sono SOFTWARE e il fill arriva al close orario successivo al tocco, quindi la perdita oltre lo stop e' l'escursione DENTRO quell'ora.""" out = {} for a in ASSETS: c = load_data(a, "1h")["close"].values.astype(float) r = c[1:] / c[:-1] - 1.0 out[a] = {"giu": float(r.min()), "su": float(r.max())} return out # ========================================================================================== # EVT / POT # ========================================================================================== def gpd_fit(loss: np.ndarray, q: float) -> dict: """GPD (peaks-over-threshold) sulle PERDITE. `loss` = -ritorni. Ritorna anche le funzioni di quantile e di probabilita' di eccedenza in forma chiusa.""" n = len(loss) u = float(np.quantile(loss, q)) ex = loss[loss > u] - u nu = len(ex) if nu < 15: return {} xi, _, beta = stats.genpareto.fit(ex, floc=0.0) return {"u": u, "n": n, "nu": nu, "xi": float(xi), "beta": float(beta), "rate": nu / n} def gpd_quantile(f: dict, p_giorno: float) -> float: """Perdita superata con probabilita' `p_giorno` in un giorno (return level).""" if not f: return np.nan xi, beta, u, rate = f["xi"], f["beta"], f["u"], f["rate"] if abs(xi) < 1e-9: return u + beta * np.log(rate / p_giorno) return u + beta / xi * ((p_giorno / rate) ** (-xi) - 1.0) def gpd_exceed(f: dict, x: float) -> float: """P(perdita > x) al giorno, per x >= u.""" if not f or x <= f["u"]: return np.nan xi, beta, u, rate = f["xi"], f["beta"], f["u"], f["rate"] if abs(xi) < 1e-9: return rate * np.exp(-(x - u) / beta) base = 1.0 + xi * (x - u) / beta if base <= 0: return 0.0 return rate * base ** (-1.0 / xi) def evt_bootstrap(loss: np.ndarray, q: float, n_boot: int, block: int, rng: np.random.Generator) -> dict: """IC bootstrap A BLOCCHI su xi, beta e sui return level. A blocchi perche' la serie ha vol-clustering: un bootstrap iid darebbe un IC piu' stretto del vero.""" n = len(loss) nb = int(np.ceil(n / block)) xis, rl = [], {p: [] for p in RP} for _ in range(n_boot): st = rng.integers(0, n - block, size=nb) idx = (st[:, None] + np.arange(block)[None, :]).ravel()[:n] try: f = gpd_fit(loss[idx], q) except Exception: continue if not f: continue xis.append(f["xi"]) for p in RP: rl[p].append(gpd_quantile(f, 1.0 / p)) return {"xi": np.array(xis), **{f"rl{p}": np.array(rl[p]) for p in RP}} # ========================================================================================== # CURVA DI CRESCITA — forma ESATTA (nessun rumore MC) # ========================================================================================== def g_exact(r: np.ndarray, k: float) -> float: """Crescita geometrica annua a leva k: exp(365 * E[log(1+k r)]) - 1. Su orizzonti lunghi la mediana del moltiplicatore converge a questa quantita': e' la stessa grandezza che lo scetticato stima in Monte Carlo, qui in forma chiusa.""" x = 1.0 + k * r 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.05) -> tuple[float, float]: ks = np.arange(step, kmax + step, step) g = np.array([g_exact(r, k) for k in ks]) j = int(np.nanargmax(g)) return float(ks[j]), float(g[j]) def campione_semiparam(r: np.ndarray, f: dict, l_max: float, n_draw: int, drift_annuo: float, rnd: dict) -> np.ndarray: """Corpo EMPIRICO + coda GPD TRONCATA a `l_max`, ri-centrato al drift dichiarato. La troncatura non e' un artificio di comodo: una GPD con xi>0 ha supporto illimitato, quindi E[log(1+kR)] sarebbe -inf per QUALSIASI k>0 e la domanda non avrebbe risposta. Il supporto lo da' l'aritmetica della sezione (5), non la statistica: e' esattamente il posto in cui i due pezzi di questo lavoro si incastrano. """ loss = -r body = r[loss <= f["u"]] out = body[rnd["ib"] % len(body)] # stessi indici in OGNI cella m = rnd["um"] < f["rate"] # stessa maschera nt = int(m.sum()) if nt: fmax = stats.genpareto.cdf(max(l_max - f["u"], 1e-9), f["xi"], loc=0.0, scale=f["beta"]) y = stats.genpareto.ppf(rnd["ut"][:nt] * fmax, f["xi"], loc=0.0, scale=f["beta"]) out = out.copy() out[m] = -(f["u"] + y) return out - out.mean() + drift_annuo / ANN # ========================================================================================== def main() -> None: t_start = time.time() rng = np.random.default_rng(SEED) print("=" * 110) print(" r0822c — WORST-DAY: il peggior giorno del libro, MISURATO (chiusura e minimo), e") print(" la coda che non e' nel dataset, STIMATA invece che scelta a mano") print("=" * 110) # ---------------------------------------------------------------- (0) sezione("(0) IL CRITERIO — dichiarato PRIMA, valutato in (7)") print("\n Il gradino di leva a 1,25x e' AUTORIZZABILE solo se passano TUTTE queste sei.") print(" Nessuna soglia qui sotto usa un numero delle sezioni 4-6: vengono dalle regole") print(" gia' scritte del progetto e dal listino del venue.\n") for k, v in CRITERIO.items(): testo = v.replace(" — ", " — ") print(f" {k}. {testo}") print() # ---------------------------------------------------------------- (1) sezione("(1) REPLICA DI CONTROLLO — cinque numeri gia' pubblicati, riprodotti prima di scriverne di nuovi") D = serie_libro() J = D["J"] TPQ_pre = gambe_tp01() # serve gia' nella sez.(2) per l'esposizione per bucket book = J["book"].values.astype(float) r_del = DP.deluck_returns() mu, sig = float(r_del.mean()), float(r_del.std()) se_drift = sig * np.sqrt(ANN) / np.sqrt(len(r_del) / ANN) print(f"\n serie: {len(J)} giorni, {J.index.min().date()} -> {J.index.max().date()}") print(f"\n (a) GROWTH-POLICY sez.(0) — serie de-luckata x{DP.DELUCK}") print(f" {'grandezza':>22}{'pubblicato':>14}{'qui':>14}") for nome, pub, qui in (("drift/anno", "17,11%", f"{mu*ANN:.2%}"), ("vol/anno", "11,37%", f"{sig*np.sqrt(ANN):.2%}"), ("Sharpe", "1,51", f"{mu/sig*np.sqrt(ANN):.2f}"), ("peggior giorno", "-3,95%", f"{r_del.min():.2%}")): print(f" {nome:>22}{pub:>14}{qui:>14}") print(f"\n (b) SCOMPOSIZIONE ESATTA del libro nelle due gambe (non e' una stima: e' identita')") diff = np.abs(J["book"].values - (J["tp_leg"].values + J["skh_leg"].values)).max() print(f" max|book - (0.75*TP01 + 0.25*SKH01)| = {diff:.2e} <- 0 esatto, la gamba" f" responsabile di ogni giorno e' LEGGIBILE") print(f"\n (c) k* — lo scetticato lo stima in Monte Carlo (banda 10-14x, 'la POSIZIONE non e'") print(f" risolta'). Qui la stessa quantita' in forma CHIUSA, senza rumore MC:") ks_emp, gs_emp = k_star(r_del) print(f" {'Kelly gaussiano mu/sigma²':>34}{mu/sig**2:>10.2f}x") print(f" {'k* ESATTO argmax E[log(1+kR)]':>34}{ks_emp:>10.2f}x (g = {gs_emp:.1%}/anno)") print(f" {'MC dello scetticato':>34}{'10-14x':>10} <- la forma chiusa cade DENTRO") print(f" {'g(1.00x)':>34}{g_exact(r_del,1.0):>10.2%}/anno") print(f" -> replica superata: la macchineria esatta e quella MC dicono la stessa cosa,") print(f" e da qui in avanti il rumore MC non e' piu' una scusa per nessun numero.") # ---- il controllo del critico, rifatto in modo indipendente print(f"\n (d) IL CONTROLLO DEL CRITICO, verificato: i 10 peggiori giorni del SOTTOSTANTE") print(f" (media 50/50 BTC-ETH) e cosa ha fatto il libro in quei giorni.") worst_und = J.sort_values("und").head(10) print(f"\n {'data':>12}{'BTC':>10}{'ETH':>10}{'sottost.':>10}{'LIBRO':>10}" f"{' gamba TP01':>13}{'gamba SKH01':>13}") for d, row in worst_und.iterrows(): print(f" {str(d.date()):>12}{pct(row.BTC):>10}{pct(row.ETH):>10}{pct(row.und):>10}" f"{pct(row.book):>10}{pct(row.tp_leg):>13}{pct(row.skh_leg):>13}") print(f"\n somma dei 10 giorni: LIBRO {worst_und.book.sum():+.2%} " f"contro sottostante {worst_und.und.sum():+.2%}") print(f" giorni in cui il libro GUADAGNA: {int((worst_und.book>0).sum())}/10 · " f"peggiore dei dieci {worst_und.book.min():+.2%}") print(f"\n -> IL CONTROLLO DEL CRITICO E' CONFERMATO NELLA SOSTANZA. Il 12 marzo 2020") print(f" (BTC {J.loc[J.und.idxmin(),'BTC']:+.1%}, ETH {J.loc[J.und.idxmin(),'ETH']:+.1%}) il libro fa " f"{J.und.min() and J.loc[J.und.idxmin(),'book']:+.2%}.") # ---- il numero -3,38% del critico b_can = CC.book_series("canonical") print(f"\n (e) ⚠️ IL NUMERO DEL CRITICO NON SI RIPRODUCE — ma la sua LENTE si identifica,") print(f" e la conclusione si rovescia proprio su quella.") print(f" Il critico scrive 'peggior giorno assoluto -3,38%'. Le lenti del progetto danno:") print(f" {'lente':>34}{'peggior giorno':>16}{'data':>14} che giorno era") for nome, s in (("LIVE (book_series 'hourly')", J['book']), ("CANONICA (book_series 'canonical')", b_can), ("LIVE de-luckata x0.89", D["deluck"])): i = s.idxmin() u = J["und"].reindex([i]).iloc[0] if i in J.index else np.nan tag = "RIMBALZO" if u > 0.05 else ("CRASH" if u < -0.05 else "giorno ordinario") print(f" {nome:>34}{s.min():>16.2%}{str(i.date()):>14} {tag} (sottost. {u:+.1%})") Jc = pd.concat({"b": b_can, "u": J["und"]}, axis=1).dropna() wc = Jc.sort_values("u").head(10)["b"].values * 100 wl = J.sort_values("und").head(10)["book"].values * 100 print(f"\n LA LENTE DEL CRITICO SI PUO' IDENTIFICARE dai valori che ha pubblicato") print(f" ('+0,97 · +0,72 · 0,00 · -1,98 · -2,24 · +0,23'). Sui 10 peggiori giorni del") print(f" sottostante le due lenti danno:") print(f" LIVE : " + " ".join(f"{x:+.2f}" for x in wl)) print(f" CANONICA : " + " ".join(f"{x:+.2f}" for x in wc)) print(f" -> **il +0,97 e il +0,72 sono nella riga CANONICA e in nessun'altra**: il critico") print(f" ha usato `book_series('canonical')`, cioe' il BACKTEST di SKH01 (exit al") print(f" livello), non il path LIVE. Il suo peggior giorno resta {b_can.min():.2%} contro il") print(f" -3,38% pubblicato: **0,09pp di scarto**, dell'ordine del difetto gia' noto") print(f" nel progetto ('`data/raw` e' gitignored e il cron riscrive i parquet ogni") print(f" notte -> stesso codice, dati diversi', XS-LITE e GTAA/TLT).") print(f"\n -> **QUINDI LA SUA CONCLUSIONE SI ROVESCIA SULLA SUA STESSA LENTE.** Sulla") print(f" canonica — quella che ha usato — il peggior giorno E' un giorno di ribasso") print(f" ({b_can.idxmin().date()}, sottostante {J['und'].reindex([b_can.idxmin()]).iloc[0]:+.1%}). La frase 'il suo peggior giorno") print(f" assoluto non e' un giorno di crash' e' VERA solo sulla lente LIVE, che e'") print(f" quella giusta per una decisione operativa ma non e' quella da cui viene il") print(f" suo numero. **Aveva ragione, con la lente sbagliata.**") # ---------------------------------------------------------------- (2) sezione("(2) LA DISTRIBUZIONE COMPLETA DELLE CHIUSURE DEL LIBRO — e QUALI giorni sono i peggiori") qs = (50, 25, 10, 5, 2.5, 1, 0.5, 0.2, 0.1) print(f"\n (a) coda SINISTRA per quantili (lente LIVE, {len(book)} giorni = {len(book)/365:.1f} anni)") print(f" {'quantile':>12}" + "".join(f"{f'p{q}':>10}" for q in qs) + f"{'min':>10}") print(f" {'ritorno':>12}" + "".join(f"{np.percentile(book,q):>10.2%}" for q in qs) + f"{book.min():>10.2%}") print(f" {'(destra)':>12}" + "".join(f"{np.percentile(book,100-q):>10.2%}" for q in qs) + f"{book.max():>10.2%}") print(f"\n asimmetria {stats.skew(book):+.2f} · curtosi in eccesso {stats.kurtosis(book):+.1f}" f" · giorni flat (|r|<1bps) {(np.abs(book)<1e-4).mean():.1%}") print(f" ⚠️ la curtosi alta NON e' di per se' un allarme: la meta' dei giorni il libro e'") print(f" quasi fermo (TP01 long-flat, SKH01 fuori mercato l'87,6% delle barre), e una") print(f" distribuzione con un picco a zero e' leptocurtica per costruzione.") print(f"\n (b) i 15 PEGGIORI GIORNI del libro, con l'attribuzione esatta e cosa faceva il mercato") w = J.sort_values("book").head(15) print(f"\n {'data':>12}{'LIBRO':>9}{'TP01':>9}{'SKH01':>9}{'BTC':>9}{'ETH':>9} tipo di giornata") for d, row in w.iterrows(): if row.und > 0.05: tag = "RIMBALZO (short squeeze)" elif row.und < -0.05: tag = "crash" elif row.und < -0.02: tag = "ribasso" else: tag = "giorno ordinario" colpa = "TP01" if row.tp_leg < row.skh_leg else "SKH01" print(f" {str(d.date()):>12}{row.book:>9.2%}{row.tp_leg:>9.2%}{row.skh_leg:>9.2%}" f"{row.BTC:>9.1%}{row.ETH:>9.1%} {tag} [{colpa}]") # meccanismo crash = J[J.und < -0.05] rimb = J[J.und > 0.05] ord_ = J[(J.und >= -0.02) & (J.und <= 0.02)] print(f"\n (c) IL MECCANISMO — il libro per tipo di giornata") print(f" {'tipo':>28}{'giorni':>8}{'media':>10}{'peggiore':>11}{'quota TP01':>12}{'quota SKH01':>13}") for nome, sub in (("CRASH (sottost. < -5%)", crash), ("RIMBALZO (sottost. > +5%)", rimb), ("ordinario (|sottost.|<2%)", ord_), ("tutti", J)): if len(sub) == 0: continue print(f" {nome:>28}{len(sub):>8}{sub.book.mean():>10.3%}{sub.book.min():>11.2%}" f"{sub.tp_leg.mean():>12.3%}{sub.skh_leg.mean():>13.3%}") print(f"\n (d) ⚠️ QUI IL CONTROLLO DEL CRITICO VA CORRETTO, e la correzione E' il meccanismo.") print(f" Sui **10 giorni piu' estremi** il libro guadagna (sopra). Ma sulla popolazione") print(f" dei **160 giorni di crash** perde in media {crash.book.mean():+.3%}: la frase") print(f" 'il libro guadagna nei crash' e' vera sugli estremi e FALSA in generale.") print(f" La ragione e' misurabile — l'esposizione di TP01 DIPENDE dalla taglia del crollo:") print(f" {'sottostante':>22}{'giorni':>8}{'espos. TP01':>14}{'libro medio':>14}" f"{'peggiore':>11}{'protezione':>12}") bucket = [(-1.00, -0.20, "< -20%"), (-0.20, -0.10, "-20% .. -10%"), (-0.10, -0.05, "-10% .. -5%"), (-0.05, -0.02, "-5% .. -2%")] espo = pd.concat({a: TPQ_pre[a]["held"] for a in ASSETS}, axis=1).mean(axis=1) * 0.5 * W_TP * 2 Jx = J.join(espo.rename("espo"), how="left") for lo, hi, lbl in bucket: sub = Jx[(Jx.und > lo) & (Jx.und <= hi)] if len(sub) == 0: continue cell = "GUADAGNA" if sub.book.mean() > 0 else f"{abs(sub.und.mean()/sub.book.mean()):.0f}x" print(f" {lbl:>22}{len(sub):>8}{sub.espo.mean():>14.3f}{sub.book.mean():>14.3%}" f"{sub.book.min():>11.2%}{cell:>12}") print(f"\n -> **il trend-follower e' gia' FLAT quando il crollo e' grande, ed e' ancora") print(f" LONG quando e' medio**: l'esposizione scende in modo monotono al crescere del") print(f" crollo, perche' un crollo grande arriva dopo che il trend a 30/90/180 giorni") print(f" si e' gia' girato (e il vol-target ha gia' de-levered). Il rapporto di") print(f" protezione peggiora nella fascia mite e migliora negli estremi. **Il libro") print(f" non e' immune ai crash: e' immune ai crash GROSSI.**") print(f"\n E nei RIMBALZI il libro guadagna in media ({rimb.book.mean():+.3%}) MA ci tiene il") print(f" suo peggior giorno assoluto ({rimb.book.min():+.2%}): media e coda vanno in") print(f" direzioni opposte. **La coda di questo libro non sta dove sta quella del") print(f" mercato.** Il -10% di growth_policy e' una coda da buy&hold applicata a un") print(f" libro difensivo: l'evento e' quello sbagliato, non solo la taglia.") i_worst = J["book"].idxmin() rw = J.loc[i_worst] print(f"\n il peggior giorno in dettaglio: {i_worst.date()} libro {rw.book:+.2%}") print(f" BTC {rw.BTC:+.1%} · ETH {rw.ETH:+.1%} (il giorno DOPO il -43,6% del 12 marzo)") print(f" TP01 {rw.tp_leg:+.2%} (flat: un trend-follower long-only non compra un crash)") print(f" SKH01 {rw.skh_leg:+.2%} = uno short con sl 2% che GAPPA attraverso lo stop") skh_only = (J['skh_leg'] / W_SKH) print(f" a livello di sleeve nudo SKH01 fa {skh_only.min():+.2%} in quel giorno,") print(f" contro un secondo peggiore di {skh_only.nsmallest(2).iloc[-1]:+.2%}: **la coda") print(f" di SKH01 e' UN giorno solo**, e quel giorno e' un rimbalzo, non un crollo.") # ---------------------------------------------------------------- (3) sezione("(3) LA LENTE ACCOPPIATA — il minimo INTRA-giorno (close-only e' CIECA su una regola a un giorno)") tup = PC.crypto_daily_tuples(W_TP, W_SKH) K = pd.concat({"R_live": J["book"], "R_rec": tup["R"], "m_rec": tup["m"], "und": J["und"]}, axis=1).dropna() gap = (K["m_rec"] - K["R_rec"]).values.astype(float) m_acc = K["R_live"].values + gap # costruzione dello scettico: UN grado di liberta' m_rec = K["m_rec"].values # sensitivita': recon pura print(f"\n (a) COSTRUZIONE DICHIARATA (la stessa di r0822_leverage_skeptic, per confrontabilita'):") print(f" chiusure dalla serie LIVE + GAP dalla recon ACCOPPIATA, appaiato per GIORNO.") print(f" La recon pura (R e m entrambi canonici) e' riportata come SENSITIVITA'.") print(f" n = {len(K)} giorni.") print(f"\n (b) REPLICA delle marginali del gap (25/07 e scettico 22/08: p50 -0,16 / p90 -1,03 / p99 -2,71)") print(f" {'':>14}{'p50':>10}{'p90':>10}{'p99':>10}{'min':>10}") print(f" {'qui':>14}{np.percentile(gap,50)*100:>9.2f}pp{np.percentile(gap,10)*100:>9.2f}pp" f"{np.percentile(gap,1)*100:>9.2f}pp{gap.min()*100:>9.2f}pp") print(f" (i percentili del GAP si leggono dal basso: p90 del |gap| = p10 del gap firmato)") print(f"\n (c) LE DUE RISPOSTE ALLA DOMANDA 'qual e' il peggior giorno'") print(f" {'':>26}{'p1':>10}{'p0.5':>10}{'p0.1':>10}{'MINIMO':>11}{'data del minimo':>18}") for nome, arr in (("CHIUSURA (live)", K["R_live"].values), ("MINIMO intra-giorno (acc.)", m_acc), ("MINIMO (recon pura, sens.)", m_rec)): i = int(np.argmin(arr)) print(f" {nome:>26}{np.percentile(arr,1):>10.2%}{np.percentile(arr,0.5):>10.2%}" f"{np.percentile(arr,0.1):>10.2%}{arr.min():>11.2%}{str(K.index[i].date()):>18}") neg = K["R_live"].values < -1e-4 print(f"\n rapporto minimo/chiusura sul peggior giorno: " f"{m_acc.min()/K['R_live'].values.min():.2f}x · mediana sui soli giorni in perdita " f"({int(neg.sum())} giorni) {np.median(m_acc[neg]/K['R_live'].values[neg]):.2f}x") print(f" -> **il peggior giorno del libro e' -{abs(K['R_live'].min()):.2%} in chiusura e") print(f" -{abs(m_acc.min()):.2%} al minimo**. Le due risposte differiscono di un fattore ~2:") print(f" chi valuta una regola sul MINIMO (margine, daily-loss, stop di conto) e usa") print(f" la chiusura sbaglia di quel fattore, e nel verso ottimista.") print(f"\n (d) ⚠️ LIMITE DICHIARATO DELLA COSTRUZIONE, e va detto proprio sul giorno che conta.") j = int(np.argmin(K["R_live"].values)) print(f" La recon accoppiata usa la convenzione CANONICA di SKH01 (exit AL LIVELLO): nel") print(f" giorno del gap-through cappa il wick allo stop. Il {K.index[j].date()} la chiusura") print(f" live e' {K['R_live'].values[j]:+.2%} e quella canonica {K['R_rec'].values[j]:+.2%}" f" (differenza {K['R_live'].values[j]-K['R_rec'].values[j]:+.2%}), mentre il gap canonico") print(f" di quel giorno vale solo {gap[j]*100:.2f}pp -> il minimo LIVE di quel giorno e'") print(f" plausibilmente piu' profondo di {m_acc[j]:+.2%}. **Il numero riportato sopra e' un") print(f" LIMITE SUPERIORE (ottimista) del minimo nel giorno peggiore**, non una misura.") print(f" Non lo si ripara qui: servirebbe rifare `skh_hourly_contrib` con l'uscita live,") print(f" cioe' cambiare la macchineria sotto esame. Si dichiara, e la sezione (5) da' il") print(f" bound per via aritmetica, che non ha questo problema.") print(f"\n (e) CONTROLLO: il gap e' piu' profondo nei giorni che finiscono BENE? (finding 25/07)") dec = pd.qcut(K["R_live"], 10, labels=False, duplicates="drop") gp = pd.Series(gap, index=K.index).groupby(dec).mean() print(f" gap medio per decile di chiusura (0 = peggiore). ⚠️ i decili collassano a" f" {gp.shape[0]} bin") print(f" perche' il {(np.abs(book)<1e-4).mean():.0%} dei giorni ha chiusura esattamente zero:") print(" " + " ".join(f"d{int(i)}:{v*100:+.2f}" for i, v in gp.items())) print(f" -> replica il verso del finding (il gap non e' concentrato nei giorni brutti):") print(f" il decile PEGGIORE ha gap {gp.iloc[0]*100:+.2f}pp contro {gp.iloc[-1]*100:+.2f}pp" f" del migliore.") print(f" **E la conseguenza per NOI e' quella buona: un giorno di crollo chiude quasi sul") print(f" proprio minimo, quindi la lente accoppiata NON peggiora lo stress di coda.**") # ---------------------------------------------------------------- (4) sezione("(4) LA CODA CHE NON E' NEL DATASET — EVT/POT, con l'incertezza dichiarata") print(f"\n 7,4 anni con due tori e un orso non contengono necessariamente il peggior giorno") print(f" possibile. Qui si STIMA la coda invece di sceglierla: GPD sopra una soglia") print(f" dichiarata prima ({EVT_Q:.0%}° pctl delle perdite), su chiusure E su minimi.") fits, boots = {}, {} for nome, arr in (("CHIUSURA", K["R_live"].values), ("MINIMO (acc.)", m_acc)): loss = -arr print(f"\n --- {nome} ---") print(f" {'soglia q':>10}{'u':>9}{'n ecced.':>10}{'xi':>9}{'beta':>9}" + "".join(f"{f'1/{p} g':>12}" for p in RP) + f"{' P(perdita>10%)':>18}") for q in EVT_Q_SENS: f = gpd_fit(loss, q) if not f: continue pe = gpd_exceed(f, STRESS_A_MANO) rp10 = (1.0 / pe / ANN) if (np.isfinite(pe) and pe > 0) else np.inf marca = " <-- soglia dichiarata" if abs(q - EVT_Q) < 1e-9 else "" print(f" {q:>10.3f}{f['u']:>9.2%}{f['nu']:>10}{f['xi']:>9.3f}{f['beta']:>9.4f}" + "".join(f"{-gpd_quantile(f,1.0/p):>12.2%}" for p in RP) + f"{f'1 ogni {rp10:.0f} anni' if np.isfinite(rp10) else 'mai':>18}{marca}") if abs(q - EVT_Q) < 1e-9: fits[nome] = f f = fits[nome] bo = evt_bootstrap(loss, EVT_Q, EVT_BOOT, EVT_BLOCK, rng) boots[nome] = bo print(f" IC bootstrap A BLOCCHI ({EVT_BOOT} ricampionamenti, blocchi {EVT_BLOCK}g):") print(f" xi {f['xi']:>8.3f} IC95 [{np.percentile(bo['xi'],2.5):+.3f}," f" {np.percentile(bo['xi'],97.5):+.3f}]") for p in RP: v = bo[f"rl{p}"] print(f" 1/{p:<6} {-gpd_quantile(f,1.0/p):>8.2%} IC95 " f"[{-np.percentile(v,97.5):>7.2%}, {-np.percentile(v,2.5):>7.2%}]" f" (p95 pessimista {-np.percentile(v,95):.2%})") print(f"\n (b) CROSS-CHECK INDIPENDENTE — stimatore di HILL dell'indice di coda.") print(f" Non e' un ri-fit della stessa cosa: Hill stima xi dai soli ORDINE-STATISTICI") print(f" superiori (rapporti di log), senza MLE e senza soglia di quantile. Se i due non") print(f" concordano, il numero non e' pronto per una decisione.") print(f" ⚠️ La strada ovvia — fittare una t di Student su TUTTA la serie — qui NON") print(f" funziona e va detto perche': il {(np.abs(book)<1e-4).mean():.0%} dei giorni e' esattamente zero") print(f" (libro flat), la MLE manda i gradi di liberta' a ~0 per adattarsi all'atomo in") print(f" zero e sputa quantili assurdi (10^12). **Una distribuzione con un atomo non si") print(f" fitta con una densita' continua**: il POT non ha il problema perche' guarda solo") print(f" la coda. Riportata qui sotto la t sui soli giorni ATTIVI, come terza strada.") print(f"\n {'serie':>18}{'k ordini':>10}{'xi Hill':>10}{'xi GPD':>9}" + "".join(f"{f'1/{p} g (Hill)':>16}" for p in RP)) for nome, arr in (("CHIUSURA", K["R_live"].values), ("MINIMO (acc.)", m_acc)): loss = np.sort(-arr)[::-1] loss = loss[loss > 0] kk = max(int(0.05 * len(loss)), 20) xi_h = float(np.mean(np.log(loss[:kk])) - np.log(loss[kk])) n_ = len(arr) # Weissman: quantile estremo dallo stimatore di Hill wq = lambda p: loss[kk] * ((kk / (n_ * p)) ** xi_h) # noqa: E731 f = fits[nome] print(f" {nome:>18}{kk:>10}{xi_h:>10.3f}{f['xi']:>9.3f}" + "".join(f"{-wq(1.0/p):>16.2%}" for p in RP)) print(f"\n {'serie':>18}{'gdl (t, attivi)':>18}{'sigma':>9}" + "".join(f"{f'1/{p} g (t)':>14}" for p in RP)) for nome, arr in (("CHIUSURA", K["R_live"].values), ("MINIMO (acc.)", m_acc)): att = arr[np.abs(arr) > 1e-4] df_, loc_, sc_ = stats.t.fit(att) print(f" {nome:>18}{df_:>18.1f}{sc_:>9.4f}" + "".join(f"{stats.t.ppf(1.0/p*len(att)/len(arr), df_, loc_, sc_):>14.2%}" for p in RP)) fc, fm = fits["CHIUSURA"], fits["MINIMO (acc.)"] evt_boot_close = boots["CHIUSURA"] pe = gpd_exceed(fc, STRESS_A_MANO) # la stessa domanda con lo stimatore di HILL (indipendente dal MLE della GPD) _l = np.sort(-K["R_live"].values)[::-1] _l = _l[_l > 0] _k = max(int(0.05 * len(_l)), 20) _xi = float(np.mean(np.log(_l[:_k])) - np.log(_l[_k])) pe_hill = (_k / len(K)) * (STRESS_A_MANO / _l[_k]) ** (-1.0 / _xi) print(f"\n (c) IL CONFRONTO CHE DECIDE: il -{STRESS_A_MANO:.0%} assunto a mano e' DENTRO o FUORI?") print(f" · chiusura, quantile 1/1.000 giorni : {-gpd_quantile(fc,1e-3):>8.2%}") print(f" · chiusura, quantile 1/10.000 giorni : {-gpd_quantile(fc,1e-4):>8.2%}") print(f" · minimo, quantile 1/10.000 giorni : {-gpd_quantile(fm,1e-4):>8.2%}") if pe > 0: print(f" · il -{STRESS_A_MANO:.0%} a mano, in chiusura : P = {pe:.2e}/giorno" f" = 1 ogni {1/pe/ANN:.0f} anni") else: print(f" · il -{STRESS_A_MANO:.0%} a mano, in chiusura : probabilita' NULLA nel modello") print(f" · lo stesso, con lo stimatore di HILL : P = {pe_hill:.2e}/giorno" f" = 1 ogni {1/pe_hill/ANN:.0f} anni") print(f"\n -> **il -{STRESS_A_MANO:.0%} NON E' una taglia impossibile, e' una FREQUENZA impossibile.**") print(f" growth_policy lo assume UNA VOLTA L'ANNO (1 ogni 5 anni come variante mite).") print(f" I due stimatori indipendenti lo mettono a **1 ogni {min(1/pe/ANN,1/pe_hill/ANN):.0f}" f" - {max(1/pe/ANN,1/pe_hill/ANN):.0f} anni**:") print(f" **{np.log10(1/pe_hill/ANN):.0f}-{np.log10(1/pe/ANN):.0f} ordini di grandezza di scarto sulla FREQUENZA**.") print(f" ⚠️ E i due stimatori NON concordano fra loro: xi GPD {fc['xi']:.3f} contro Hill") print(f" {_xi:.3f}, cioe' un quantile a 1/10.000 di {-gpd_quantile(fc,1e-4):.2%} contro" f" {-(_l[_k]*((_k/(len(K)*1e-4))**_xi)):.2%}.") print(f" **Si cita la BANDA fra stimatori, non il numero di uno solo** — su una coda") print(f" stimata da {len(K)} osservazioni la scelta dello stimatore vale quanto i dati.") print(f"\n ⚠️ E QUI VA DETTO IL LIMITE DELL'EVT, che e' il vero motivo per cui la sezione (5)") print(f" esiste: **una GPD stimata sui giorni vissuti estrapola il MECCANISMO vissuto.**") print(f" Se il libro puo' assumere una configurazione di posizioni che nel campione non") print(f" ha mai assunto, quella configurazione non e' nella coda stimata. Nessun") print(f" intervallo di confidenza copre un modo di perdere che non e' nei dati: e'") print(f" esattamente cio' che il gate `implausible_sharpe` chiama 'rischio FUORI dal") print(f" dataset'. La risposta non e' un fit migliore, e' un conto ARITMETICO.") # ---------------------------------------------------------------- (5) sezione("(5) IL PEGGIOR GIORNO POSSIBILE PER COSTRUZIONE — aritmetica, non statistica") cfg = json.loads((ROOT / "config" / "live.json").read_text()) frac = float(cfg["max_notional_per_asset_frac"]) TPQ = TPQ_pre EX = estremi_orari() sl_l, sl_s = SKH01_V2_DD.sl_pct, SKH01_V2_DD.sl_pct_short tv, levcap = CANONICAL["target_vol"], CANONICAL["leverage"] print(f"\n (a) LA FORMULA DEL LIBRO LIVE (src/live/book.book_net_target, letta dal codice):") print(f" net_target[asset] = clamp( 0.5 * E * (0.75*tp_frac + 0.25*skh_sign), ±cap ),") print(f" cap = E * {frac} -> per-asset |net| <= {frac:.2f}·E, LORDO massimo {2*frac:.2f}·E su 2 asset.") print(f" In frazione di equity: gamba TP01 = {0.5*W_TP:.3f}·tp_frac, gamba SKH01 = {0.5*W_SKH:.3f}·skh_sign.") print(f"\n (b) TP01 — il vol-target rende il bound INVARIANTE alla volatilita'.") print(f" tp_frac = segnale * min(target_vol/vol, {levcap:g}) con segnale in {{0, 1/3, 2/3, 1}},") print(f" quindi contributo di gamba = 0.375 * segnale * target_vol * z / sqrt(365.25)") print(f" = {0.5*W_TP*tv/np.sqrt(365.25)*100:.4f}% * segnale * z. **Un crash entra solo attraverso il suo z-score.**") print(f" ⚠️ La formula vale con tp_frac = target_vol/vol, cioe' solo dove il CAP non morde:") print(f" 0.375*tp_frac + 0.125 <= {frac} richiede tp_frac <= 1, cioe' vol >= target_vol.") print(f" Sotto quella soglia il contributo e' 0.375*|r| ed e' PIU' PICCOLO -> il bound") print(f" si prende sullo z peggiore CON vol >= {tv:.0%} (auto-consistenza, non cherry-pick).") print(f" {'asset':>7}{'z min (vol>=tv)':>17}{'quando':>13}{'ret':>9}{'vol prec.':>11}" f"{'tp_frac':>9}{'contributo max':>16}{'z min GREZZO':>14}") zmin, zmin_raw = {}, {} for a in ASSETS: d = TPQ[a].dropna(subset=["z"]) zmin_raw[a] = float(d["z"].min()) dv = d[d["vol_prev"] >= tv] i = dv["z"].idxmin() zmin[a] = float(dv["z"].min()) contr = 0.5 * W_TP * tv * abs(zmin[a]) / np.sqrt(365.25) print(f" {a:>7}{zmin[a]:>17.1f}{str(i.date()):>13}{dv.loc[i,'ret']:>9.1%}" f"{dv.loc[i,'vol_prev']:>11.1%}{min(tv/dv.loc[i,'vol_prev'],1.0):>9.2f}" f"{-contr:>16.2%}{zmin_raw[a]:>14.1f}") print(f" tp_frac MASSIMO mai tenuto: " + " · ".join( f"{a} {TPQ[a]['held'].max():.3f}" for a in ASSETS) + f" (il cap di leva {levcap:g}x NON e' mai stato binding: la vol a 30g non e' mai scesa" f" sotto {min(float(TPQ[a]['vol_prev'].min()) for a in ASSETS):.1%})") print(f"\n (c) SKH01 — NON e' vol-targeted (misurato 26/07): la sua taglia e' fissa e la sua") print(f" perdita e' limitata dallo STOP, ma gli exit sono SOFTWARE (fill al close orario") print(f" successivo al tocco) -> il bound e' stop + escursione DENTRO quell'ora.") print(f" {'asset':>7}{'1h peggiore giu':>18}{'1h peggiore su':>17}" f"{'bound LONG (sl 4%)':>21}{'bound SHORT (sl 2%)':>21}{'peggio realizzato':>19}") bound_l, bound_s = {}, {} for a in ASSETS: bound_l[a] = sl_l + abs(EX[a]["giu"]) bound_s[a] = sl_s + abs(EX[a]["su"]) real = SKL.skh_daily_asset(a, 0, "hourly").min() print(f" {a:>7}{EX[a]['giu']:>18.2%}{EX[a]['su']:>17.2%}" f"{-bound_l[a]:>21.2%}{-bound_s[a]:>21.2%}{real:>19.2%}") print(f" -> il gap-through misurato dal progetto (sl 2% modellato -> -11/-23% realizzato)") print(f" e' COERENTE con questo bound, che lo contiene senza esserne tarato.") print(f"\n (d) I DUE SCENARI COERENTI. TP01 e' LONG-ONLY: perde solo in ribasso. SKH01 short") print(f" perde solo in RIALZO. Non possono perdere al massimo lo stesso giorno se SKH e'") print(f" short -> gli scenari sono due, e vanno tenuti separati.") lim_tp = {a: 0.5 * W_TP * tv * abs(zmin[a]) / np.sqrt(365.25) for a in ASSETS} crash_max = sum(lim_tp[a] + 0.5 * W_SKH * bound_l[a] for a in ASSETS) squeeze_max = sum(0.5 * W_SKH * bound_s[a] for a in ASSETS) print(f"\n SCENARIO A — CRASH (TP01 a piena convinzione su entrambi + SKH01 LONG stoppato)") for a in ASSETS: print(f" {a}: TP01 {-lim_tp[a]:>7.2%} SKH01 {-0.5*W_SKH*bound_l[a]:>7.2%}" f" -> {-(lim_tp[a]+0.5*W_SKH*bound_l[a]):>7.2%}") print(f" TOTALE LIBRO {-crash_max:>7.2%} <-- IL BOUND STRUTTURALE a k=1,00x") print(f"\n SCENARIO B — RIMBALZO (TP01 flat per costruzione + SKH01 SHORT stoppato su due)") for a in ASSETS: print(f" {a}: SKH01 {-0.5*W_SKH*bound_s[a]:>7.2%}") print(f" TOTALE LIBRO {-squeeze_max:>7.2%} realizzato il {i_worst.date()}:" f" {J['book'].min():+.2%} ({J['book'].min()/-squeeze_max:.0%} del bound)") print(f" -> lo scenario B e' gia' stato VISSUTO quasi al suo massimo: il bound e' credibile.") print(f"\n (e) CONTROPROVA INDIPENDENTE — controfattuale sui movimenti VERI. Per ogni giorno") print(f" storico si sostituisce a TP01 la piena convinzione e a SKH01 la posizione LONG") print(f" su entrambi gli asset, e si applica il movimento realmente accaduto.") CF = pd.concat({a: TPQ[a][["lev_max", "ret", "ret_low"]] for a in ASSETS}, axis=1).dropna() tot = np.zeros(len(CF)) tot_min = np.zeros(len(CF)) # stessa cosa valutata sul MINIMO intra-giorno for a in ASSETS: rr = CF[(a, "ret")].values rl = CF[(a, "ret_low")].values hm = np.minimum(CF[(a, "lev_max")].values, 1.0) # cap: 0.375h+0.125<=0.5 -> h<=1 tot += 0.5 * W_TP * hm * rr + 0.5 * W_SKH * np.maximum(rr, -bound_l[a]) tot_min += 0.5 * W_TP * hm * rl + 0.5 * W_SKH * np.maximum(rl, -bound_l[a]) o = np.argsort(tot)[:5] print(f" {'data':>12}{'perdita ctrf (chiusura)':>25}{'ctrf (minimo)':>16}{'BTC':>10}{'ETH':>10}") for i in o: print(f" {str(CF.index[i].date()):>12}{tot[i]:>25.2%}{tot_min[i]:>16.2%}" f"{CF[('BTC','ret')].values[i]:>10.1%}{CF[('ETH','ret')].values[i]:>10.1%}") print(f"\n n = {len(CF)} giorni · giorni sotto -10% nel controfattuale:" f" {int((tot<-0.10).sum())} ({(tot<-0.10).mean()*365:.2f}/anno) · sotto -5%:" f" {int((tot<-0.05).sum())} ({(tot<-0.05).mean()*365:.2f}/anno)") L_STRUCT = max(crash_max, -float(tot.min())) L_STRUCT_MIN = -float(tot_min.min()) print(f"\n -> DUE STRADE, STESSA RISPOSTA: il bound analitico da' {-crash_max:.2%},") print(f" il controfattuale sui movimenti veri {tot.min():.2%}. **Si usa" f" L_strutturale = {-L_STRUCT:.2%}** in CHIUSURA.") print(f" E sul MINIMO intra-giorno (stesso controfattuale valutato al low della giornata,") print(f" che e' la grandezza che guarda il margine): **{tot_min.min():.2%}**, cioe'" f" {L_STRUCT_MIN/L_STRUCT:.2f}x la chiusura.") print(f" ⚠️ Questo numero NON eredita il limite della sez.(3d): non passa dalla recon") print(f" canonica, e' costruito sui low veri del feed certificato.") print(f"\n 📌 LA RISPOSTA ALLA DOMANDA DEL BRIEF: il -10% NON e' fuori dal possibile —") print(f" sta al {STRESS_A_MANO/L_STRUCT:.0%} del massimo strutturale. **Cio' che e' sbagliato non e'") print(f" la taglia, e' la FREQUENZA**: il controfattuale — in cui la strategia") print(f" difensiva e' massimamente in torto su entrambi gli asset — sfonda il -10%") print(f" {int((tot<-0.10).sum())} giorno/i su {len(CF)}; il libro VERO non l'ha mai") print(f" nemmeno avvicinato ({J['book'].min():+.2%}). **La riserva dello scettico e' mal") print(f" tarata sulla frequenza, non falsificata sulla taglia.**") zx = 1.5 print(f"\n ⚠️ COSA NON E' CHIUSO: z_max e' esso stesso un massimo IN CAMPIONE. Con uno z") print(f" piu' cattivo del {zx:g}x osservato il bound dello scenario A diventa" f" {-(sum(zx*lim_tp[a] + 0.5*W_SKH*bound_l[a] for a in ASSETS)):.2%}.") print(f" Il bound e' condizionato a 'nessun giorno peggiore in z di quelli visti', e") print(f" va citato cosi'. Non e' un massimo assoluto: e' un massimo CONDIZIONATO.") # ---- il cap di config print(f"\n (f) 🚨 IL CAP DI CONFIG NON E' UN KNOB DI SCALA — e questo cambia la DOMANDA.") print(f" La regola del progetto dice che un cambio di SCALA passa dal cap di config e non") print(f" da `target_vol`. Ma il cap e' un CLAMP, non un moltiplicatore: agisce solo nei") print(f" giorni in cui il libro vuole PIU' del cap. Quanti sono?") print(f" {'asset':>7}{'giorni':>9}{'net max voluto':>17}{'giorni sopra cap 0.50':>24}{'quota':>10}") for a in ASSETS: h = TPQ[a]["held"].values net = 0.5 * W_TP * h + 0.5 * W_SKH # SKH long = il caso che spinge di piu' n_over = int((net > frac).sum()) print(f" {a:>7}{len(h):>9}{net.max():>17.4f}{n_over:>24}{n_over/len(h):>10.2%}") print(f"\n -> **il cap morde su 3 giorni su ~2.900, e per meno dell'1% del target.** Alzare") print(f" `max_notional_per_asset_frac` da 0.50 a 0.625 NON produce k=1,25: produce") print(f" k=1,00 in 2.928 giorni su 2.931 e un pelo di piu' in tre. **Il gradino di leva") print(f" NON e' oggi implementabile con la strada che la regola dichiara pulita**, e in") print(f" `config/live.json` NON esiste una chiave di scala: servirebbe toccare") print(f" `WEIGHT`/`W_TP01`/`W_SKH` in `src/live/book.py` (codice, non config) oppure") print(f" aggiungere una chiave nuova. **Due mesi di discussione su un knob inesistente.**") print(f" ⚠️ E il moltiplicatore x1,25 sui pesi e' un puro cambio di scala SOLO se applicato") print(f" a entrambi: applicato a uno solo e' un tilt, e li' `weights_tilt_null` c'e' gia'.") # ---------------------------------------------------------------- (6) sezione("(6) k* ONESTO — la curva di Kelly con la coda STIMATA al posto di quella a mano") f_close = fits["CHIUSURA"] drift_pt = mu * ANN scenari_drift = (("punto (de-luck x0.89)", drift_pt), ("-1 errore standard", drift_pt - se_drift), ("-2 errori standard", drift_pt - 2 * se_drift)) code = (("empirica (nessun modello)", None), (f"EVT troncata a {-L_STRUCT:.1%} (strutturale)", L_STRUCT), (f"EVT troncata a {-STRESS_A_MANO:.0%} (a mano)", STRESS_A_MANO), (f"EVT troncata a {-1.5*L_STRUCT:.1%} (z x1.5)", 1.5 * L_STRUCT)) print(f"\n Errore standard del drift = vol/sqrt(T) = {se_drift:.2%}/anno su" f" {len(r_del)/ANN:.1f} anni.") print(f" La coda EVT ha supporto ILLIMITATO se xi>0 -> E[log(1+kR)] sarebbe -inf per ogni k:") print(f" il supporto lo da' l'ARITMETICA della sezione (5). E' qui che i due pezzi si") print(f" incastrano — l'EVT da' la frequenza, la struttura da' il massimo.") print(f"\n {'coda':>34}{'drift':>24}{'k*':>9}{'g(1.00x)':>11}{'g(1.25x)':>11}{'g(1.50x)':>11}") ks_tab = {} NDRAW = 400_000 # numeri casuali APPAIATI fra tutte le celle: la tabella e' un confronto, non 12 stime # indipendenti. Senza questo, differenze di ±0,5x fra celle sono rumore MC e sembrano segnale. _rg = np.random.default_rng(SEED + 1) RND = {"ib": _rg.integers(0, 10 ** 9, NDRAW), "um": _rg.random(NDRAW), "ut": _rg.random(NDRAW)} for nome_c, lmax in code: for nome_d, dr in scenari_drift: if lmax is None: rr = r_del - r_del.mean() + dr / ANN else: rr = campione_semiparam(r_del, f_close, lmax, NDRAW, dr, RND) kk, _ = k_star(rr) ks_tab[(nome_c, nome_d)] = kk print(f" {nome_c:>34}{nome_d:>24}{kk:>8.2f}x{g_exact(rr,1.00):>11.2%}" f"{g_exact(rr,1.25):>11.2%}{g_exact(rr,1.50):>11.2%}") print(f"\n -> **LA FORMA DELLA CODA QUASI NON CONTA, IL DRIFT CONTA TUTTO.** Fra la coda") print(f" empirica e una GPD troncata a {-1.5*L_STRUCT:.0%} k* si muove di ~1-2 unita'; fra il") print(f" drift di punto e quello a -2 SE si muove di {ks_tab[('empirica (nessun modello)','punto (de-luck x0.89)')]-ks_tab[('empirica (nessun modello)','-2 errori standard')]:.1f}. **Modellare meglio la coda non e'") print(f" la leva: la leva e' quanti anni di storia ha il drift.** (Ed e' coerente col") print(f" fatto gia' pubblicato che k* e' LINEARE nell'errore del drift.)") print(f" ⚠️ Le celle sono APPAIATE (stessi numeri casuali): le differenze fra righe sono") print(f" confrontabili, i livelli assoluti hanno l'incertezza del fit di coda.") print(f"\n (b) LO STRESS A MANO DELLO SCETTICATO, riprodotto e poi ri-calibrato.") print(f" growth_policy: 'un giorno -10% ALL'ANNO porta k* a 2x'.") print(f" ⚠️ CONVENZIONE, ed e' quella che decide il numero: qui i giorni di coda si") print(f" AGGIUNGONO e il drift NON si ri-centra. E' la lettura conservativa, la stessa") print(f" implicita in growth_policy (che SOSTITUISCE giorni veri con il crash): se un") print(f" -10% annuo esiste ed e' fuori campione, allora il drift osservato era FORTUNATO,") print(f" e togliergli il tail event senza toccare il drift conterebbe due volte la") print(f" fortuna. La tabella (a) sopra ri-centra di proposito, perche' li' il drift e' una") print(f" COLONNA e si vuole isolare la forma della coda; qui no.") p_evt = gpd_exceed(f_close, STRESS_A_MANO) freq_cf = max((tot < -0.10).mean(), 1.0 / len(CF)) print(f"\n {'evento':>36}{'freq':>16}{'drift':>9}{'k*':>8}{'1/2 Kelly':>11}" f"{'g(1.00x)':>10}{'g(1.25x)':>10}{'g(1.50x)':>10}") stress_k = {} for lbl, p_giorno in ((f"-{STRESS_A_MANO:.0%} 1 volta l'ANNO (a mano)", 1.0 / ANN), (f"-{STRESS_A_MANO:.0%} 1 ogni 5 anni (a mano, mite)", 1.0 / (5 * ANN)), (f"-{STRESS_A_MANO:.0%} alla freq. del CONTROFATTUALE", freq_cf), (f"-{STRESS_A_MANO:.0%} alla freq. stimata EVT", p_evt), ("nessuno stress (riferimento)", 0.0)): rr = r_del.copy() n_add = int(round(p_giorno * len(rr) / max(1e-12, 1 - p_giorno))) if p_giorno > 0 else 0 rr2 = np.concatenate([rr, np.full(n_add, -STRESS_A_MANO)]) if n_add > 0 else rr kk, _ = k_star(rr2) stress_k[lbl] = kk fr = f"1 ogni {1/p_giorno:.0f}g" if p_giorno > 0 else "-" print(f" {lbl:>36}{fr:>16}{rr2.mean()*ANN:>9.2%}{kk:>7.2f}x{0.5*kk:>10.2f}x" f"{g_exact(rr2,1.00):>10.2%}{g_exact(rr2,1.25):>10.2%}{g_exact(rr2,1.50):>10.2%}") k_mano = stress_k[f"-{STRESS_A_MANO:.0%} 1 volta l'ANNO (a mano)"] KSG = (1.00, 1.50, 2.00, 3.50, 5.00, 10.00, 18.00) # la griglia COARSE di growth_policy rr = np.concatenate([r_del, np.full(int(round(len(r_del)/(ANN-1))), -STRESS_A_MANO)]) gg = [g_exact(rr, k) for k in KSG] print(f"\n REPLICA PARZIALE, dichiarata: sulla griglia COARSE di growth_policy questo") print(f" motore mette l'argmax a {KSG[int(np.nanargmax(gg))]:g}x (loro: 2x); a griglia fine {k_mano:.2f}x.") print(f" **La direzione e l'ordine di grandezza si replicano** (k* crolla da" f" {ks_emp:.1f}x a ~3x),") print(f" la cella esatta no — e' la risoluzione MC che loro stessi hanno dichiarato.") print(f"\n -> **LA TAGLIA DEL -10% COSTA POCO, LA FREQUENZA COSTA TUTTO.** Lo stesso evento,") print(f" spostato dalla frequenza ASSUNTA a quella STIMATA, muove k* da {k_mano:.1f}x a" f" {stress_k['nessuno stress (riferimento)']:.1f}x.") print(f" Il parametro scelto a mano che blocca il gradino non e' '-10%': e' 'OGNI ANNO'.") print(f"\n 📌 E IL PUNTO CHE CAMBIA LA DECISIONE: **anche accettando lo stress a mano**") print(f" **alla lettera**, k* = {k_mano:.2f}x -> mezzo-Kelly = {0.5*k_mano:.2f}x. **1,25x ci sta") print(f" sotto**, 1,50x e' esattamente al bordo. Cioe': la riserva dello scettico, presa") print(f" per buona senza ri-calibrarla, non basta a bocciare il gradino a 1,25x —") print(f" bocciava 1,50x, che e' il gradino di cui si stava discutendo.") print(f"\n ⚠️ SENSITIVITA' FUORI DAL CRITERIO (dichiarata dopo, non conta per il verdetto):") rr3 = np.concatenate([r_del - 2 * se_drift / ANN, np.full(int(round(len(r_del) / (ANN - 1))), -STRESS_A_MANO)]) k3, _ = k_star(rr3) print(f" se si credono ENTRAMBE le riserve insieme (drift -2 SE **E** -10% ogni anno)") print(f" il drift atteso diventa {rr3.mean()*ANN:+.2%}/anno e k* = {k3:.2f}x.") print(f" **A drift {'negativo' if rr3.mean()<0 else 'quasi nullo'} il problema non e' il gradino: e' che il libro non") print(f" andrebbe tradato affatto, nemmeno a 1,00x.** Chi usa questa combinazione per") print(f" bocciare 1,25x sta implicitamente chiedendo di spegnere anche il conto di oggi:") print(f" e' una posizione legittima, ma va dichiarata per quello che e'.") print(f"\n (c) LIQUIDAZIONE — la domanda va posta sul MINIMO, non sulla chiusura.") print(f" Con nozionale lordo G·E e margine di mantenimento {MM_RATE:.1%}, il conto e'") print(f" liquidato quando il movimento avverso x soddisfa x ≈ (1-mm·G)/(G·(1-mm)).") g_avg = sum(0.5 * W_TP * TPQ[a]["held"].mean() for a in ASSETS) + W_SKH * 0.124 g_max = sum(0.5 * W_TP * TPQ[a]["held"].max() + 0.5 * W_SKH for a in ASSETS) print(f" lordo MEDIO del libro {g_avg:.3f}·E · lordo MASSIMO mai voluto {g_max:.3f}·E") m_worst_unit = max(abs(gpd_quantile(fm, 1e-4)), L_STRUCT_MIN) print(f" il 'peggior minimo' usato qui e' il PIU' PESSIMISTA fra EVT 1/10.000" f" ({-abs(gpd_quantile(fm,1e-4)):.2%})") print(f" e il bound strutturale sul minimo ({-L_STRUCT_MIN:.2%}) = {-m_worst_unit:.2%} a k=1.") print(f"\n {'k':>7}{'G max':>9}{'x liquidaz.':>14}{'min vissuto':>14}" f"{'min EVT 1/10^4':>16}{'min STRUTT.':>14}{'margine':>10}") for k in (1.00, 1.25, 1.50, 2.00, 3.00): G = min(g_max * k, 2 * frac * k) x_liq = (1 - MM_RATE * G) / (G * (1 - MM_RATE)) print(f" {k:>6.2f}x{G:>9.3f}{x_liq:>14.1%}{abs(m_acc.min())*k:>14.2%}" f"{abs(gpd_quantile(fm,1e-4))*k:>16.2%}{L_STRUCT_MIN*k:>14.2%}" f"{x_liq/(m_worst_unit*k):>9.1f}x") kliq = WIPE / abs(m_acc.min()) print(f"\n replica dello scetticato ('servirebbe 12,3x sul peggior giorno vissuto',") print(f" convenzione WIPE {WIPE:.0%}): {WIPE:.2f}/{abs(m_acc.min()):.4f} = {kliq:.1f}x <-- riprodotto") # ---------------------------------------------------------------- (7) sezione("(7) IL VERDETTO — il criterio di (0), valutato") esiti = {} # C1 c1_val = 1.25 * L_STRUCT esiti["C1"] = (c1_val <= 0.50, f"1,25 x {L_STRUCT:.2%} = {c1_val:.2%} di equity persa nel peggior giorno " f"POSSIBILE, contro la soglia del 50%. Margine {0.50/c1_val:.1f}x.") # C2 k_pess = ks_tab[(f"EVT troncata a {-L_STRUCT:.1%} (strutturale)", "-2 errori standard")] esiti["C2"] = (1.25 <= 0.5 * k_pess, f"k* pessimistico (drift -2 SE, coda troncata al bound strutturale) = " f"{k_pess:.2f}x -> mezzo-Kelly = {0.5*k_pess:.2f}x. 1,25 e' il " f"{1.25/k_pess:.0%} di quel k*.") # C3 G125 = min(g_max * 1.25, 2 * frac * 1.25) x_liq125 = (1 - MM_RATE * G125) / (G125 * (1 - MM_RATE)) m_evt125 = m_worst_unit * 1.25 esiti["C3"] = (x_liq125 >= 3 * m_evt125, f"peggior minimo intra-giorno (il piu' pessimista fra EVT 1/10.000 " f"{-abs(gpd_quantile(fm,1e-4)):.2%} e bound strutturale {-L_STRUCT_MIN:.2%}) " f"x1,25 = {-m_evt125:.2%} contro una soglia di liquidazione a {x_liq125:.1%}: " f"margine {x_liq125/m_evt125:.0f}x (richiesto 3x).") # C4 r_2se = r_del - r_del.mean() + (drift_pt - 2 * se_drift) / ANN dg = g_exact(r_2se, 1.25) - g_exact(r_2se, 1.00) esiti["C4"] = (dg > 0, f"col drift a -2 SE ({drift_pt-2*se_drift:.2%}/anno): g(1,25x)-g(1,00x) = " f"{dg:+.2%}/anno.") # C5 fund = {} for a in ASSETS: d = pd.read_parquet(ROOT / "data" / "raw" / f"hlfund_{a.lower()}_1h.parquet") fund[a] = float(d["funding"].values.mean()) * 8760.0 esp_long = sum(0.5 * W_TP * TPQ[a]["held"].mean() for a in ASSETS) costo_k1 = esp_long * np.mean(list(fund.values())) guad = g_exact(r_del, 1.25) - g_exact(r_del, 1.00) esiti["C5"] = (0.25 * costo_k1 < guad / 3.0, f"funding medio (proxy Hyperliquid, 3 anni): " f"BTC {fund['BTC']:.1%}/a, ETH {fund['ETH']:.1%}/a; esposizione LONG media di " f"TP01 {esp_long:.3f}·E -> costo {costo_k1:.2%}/anno a k=1, " f"costo extra al gradino +{0.25*costo_k1:.2%}/anno contro un guadagno extra di " f"{guad:+.2%}/anno (soglia dichiarata: costo extra < 1/3 del guadagno extra = " f"{guad/3:.2%}). ⚠️ E il costo NON e' modellato NEMMENO a k=1: il libro paga gia' " f"~{costo_k1:.2%}/anno che nessun backtest del progetto sottrae — e' un difetto " f"che il gradino non crea ma amplifica del 25%.") # C6 n_cap = sum(int(((0.5 * W_TP * TPQ[a]["held"].values + 0.5 * W_SKH) > frac).sum()) for a in ASSETS) n_tot = sum(len(TPQ[a]) for a in ASSETS) esiti["C6"] = (False, f"il cap di config e' un CLAMP, non un moltiplicatore: morde su {n_cap} " f"osservazioni-asset su {n_tot} ({n_cap/n_tot:.3%}). Alzarlo NON produce k=1,25. " f"In `config/live.json` non esiste una chiave di scala.") print() for k in ("C1", "C2", "C3", "C4", "C5", "C6"): ok, spieg = esiti[k] print(f" {k} {'PASSA ' if ok else 'FALLISCE'} {spieg}") print() passa = all(v[0] for v in esiti.values()) print(f" ESITO COMPLESSIVO: {sum(v[0] for v in esiti.values())}/6 criteri passati" f" -> {'AUTORIZZABILE' if passa else 'NON autorizzabile come proposto'}") print(f""" LETTURA IN CHIARO 1. IL CONTROLLO DEL CRITICO E' CONFERMATO NELLA DIREZIONE, CORRETTO NELLA PORTATA, E IL SUO NUMERO NON SI RIPRODUCE. Sui 10 giorni peggiori del sottostante il libro fa {worst_und.book.sum():+.2%} in totale contro {worst_und.und.sum():+.1%} del mercato e guadagna in {int((worst_und.book>0).sum())} di essi; il 12 marzo 2020 (-43,6%) fa {J.loc[J.und.idxmin(),'book']:+.2%}. **Ma sui 160 giorni di crash il libro perde in media {crash.book.mean():+.3%}**: 'il libro guadagna nei crash' vale sugli ESTREMI, non in generale, e l'esposizione di TP01 scende in modo monotono al crescere del crollo ({Jx[(Jx.und>-0.05)&(Jx.und<=-0.02)].espo.mean():.3f} nella fascia -5/-2%, {Jx[Jx.und<=-0.20].espo.mean():.3f} sotto il -20%). **Il libro non e' immune ai crash: e' immune ai crash GROSSI**, perche' li' il trend si e' gia' girato. ⚠️ E la sua LENTE si identifica dai valori pubblicati (+0,97 e +0,72 compaiono SOLO nella riga canonica): ha usato `book_series('canonical')`, il BACKTEST di SKH01. Su quella lente **il peggior giorno E' un giorno di ribasso** ({b_can.min():+.2%}, {b_can.idxmin().date()}), quindi la frase 'il peggior giorno non e' un giorno di crash' e' vera solo sulla lente LIVE — che e' quella giusta per decidere, ma non e' quella da cui viene il suo numero. **Aveva ragione, con la lente sbagliata.** Si cita **-3,47% (canonica) / -3,94% (live)**; lo scarto residuo di 0,09pp col suo -3,38% e' dell'ordine del difetto gia' noto ('`data/raw` gitignored, il cron riscrive i parquet' — stesso codice, dati diversi). 2. LA CODA DEL LIBRO ESISTE, MA STA DALL'ALTRA PARTE DEL MERCATO. Il peggior giorno in chiusura e' {J['book'].min():+.2%} il {i_worst.date()}, ed e' un **RIMBALZO** (+20,9%/+27,6%): SKH01 short con stop al 2% che gappa a {skh_only.min():.2%} di sleeve, mentre TP01 e' flat. Secondo peggiore dello sleeve: {skh_only.nsmallest(2).iloc[-1]:.2%} — **la coda di SKH01 e' UN giorno solo**. ⚠️ E la caratterizzazione si INVERTE con la lente: sulla canonica il peggior giorno e' un ribasso ({b_can.min():+.2%}, {b_can.idxmin().date()}). Va citata con la lente. 3. LE DUE RISPOSTE ALLA DOMANDA 'QUAL E' IL PEGGIOR GIORNO' DIFFERISCONO DI ~2x: **{K['R_live'].min():+.2%} in chiusura, {m_acc.min():+.2%} al minimo intra-giorno** (mediana del rapporto sui giorni in perdita {np.median(m_acc[neg]/K['R_live'].values[neg]):.2f}x). Chi valuta margine, daily-loss o stop di conto sulla chiusura sbaglia di quel fattore, nel verso ottimista. ⚠️ E il minimo del giorno peggiore e' un LIMITE SUPERIORE: la recon accoppiata cappa il wick allo stop proprio nel giorno del gap-through. 4. LA STIMA DELLA CODA, CON LA SUA INCERTEZZA: chiusura **{-gpd_quantile(f_close,1e-3):.2%} a 1/1.000 giorni** e **{-gpd_quantile(f_close,1e-4):.2%} a 1/10.000** (GPD, soglia {EVT_Q:.0%}, IC95 bootstrap a blocchi [{-np.percentile(evt_boot_close['rl10000'],97.5):.2%}, {-np.percentile(evt_boot_close['rl10000'],2.5):.2%}]); Hill dice {-(_l[_k]*((_k/(len(K)*1e-4))**_xi)):.2%} a 1/10.000. **I due stimatori non concordano** (xi {f_close['xi']:.2f} vs {_xi:.2f}) e si cita la BANDA: un fit di coda su {len(K)} osservazioni e' un ordine di grandezza, non un numero. Il -10% in chiusura esce a **1 ogni {min(1/pe/ANN,1/pe_hill/ANN):.0f}-{max(1/pe/ANN,1/pe_hill/ANN):.0f} anni**, contro 'ogni anno' assunto. 5. IL CONTO ARITMETICO, che l'EVT non puo' fare perche' estrapola solo il meccanismo vissuto: dato il cap di config, il vol-target di TP01 (che converte un crash nel suo **z-score**, rendendo il bound invariante alla volatilita') e il gap-through di SKH01, il peggior giorno POSSIBILE a k=1,00x e' **{-L_STRUCT:.2%}** in chiusura e **{-L_STRUCT_MIN:.2%}** al minimo. Bound analitico e controfattuale sui movimenti veri danno lo stesso numero — ⚠️ ma NON sono indipendenti: convergono perche' entrambi sono guidati dal 12 marzo 2020. **Quindi il -10% sta al {STRESS_A_MANO/L_STRUCT:.0%} del massimo strutturale: la TAGLIA e' plausibile, la FREQUENZA no.** La riserva dello scettico e' RIDIMENSIONATA di 2-4 ordini di grandezza, non falsificata. 6. E ANCHE PRENDENDO LA RISERVA ALLA LETTERA IL GRADINO A 1,25x PASSA: con -10% ogni anno k* = {k_mano:.2f}x, cioe' mezzo-Kelly = {0.5*k_mano:.2f}x. **1,25x ci sta sotto; 1,50x e' al bordo** — cioe' lo stress a mano bocciava il gradino DOPPIO, non quello singolo. Se invece si credono ENTRAMBE le riserve insieme (drift -2 SE **e** -10% annuo) il drift atteso e' {rr3.mean()*ANN:+.2%}/anno: a quel punto non e' il gradino a essere sbagliato, e' il libro — e chi usa quella combinazione sta chiedendo di spegnere anche il conto di oggi. 7. 🚨 IL RISULTATO CHE NESSUNO CERCAVA, ED E' QUELLO CHE DECIDE: **il gradino non e' implementabile con la strada che la regola del progetto dichiara pulita.** Il cap di `config/live.json` e' un CLAMP, non un moltiplicatore: morde su {n_cap} osservazioni-asset su {n_tot} ({n_cap/n_tot:.3%}). Alzarlo da 0,50 a 0,625 lascerebbe il libro a k=1,00x in {n_tot-n_cap} giorni-asset su {n_tot}. Non esiste una chiave di SCALA in config: per k=1,25 servirebbe cambiare `WEIGHT`/`W_TP01`/`W_SKH` in `src/live/book.py`, cioe' CODICE su un percorso con soldi veri. **La domanda 'il gradino e' autorizzabile' era mal posta: prima di essere autorizzato, il gradino va COSTRUITO** — e la costruzione e' la parte che merita una revisione, non l'aritmetica del rischio, che passa con margine. 8. SOTTOPRODOTTO NON RICHIESTO, e vale piu' del gradino: **il funding dei perpetual non e' modellato in NESSUN backtest del progetto**. Proxy Hyperliquid su 3 anni: {np.mean(list(fund.values())):.1%}/anno pagato dai long, esposizione long media di TP01 {esp_long:.3f}·E -> **~{costo_k1:.2%}/anno** che il libro paga davvero e che nessuna serie sottrae. E' ~{costo_k1/(mu*ANN):.0%} del drift de-luckato, cioe' dello stesso ordine della correzione d'ancora x0,89 che il progetto ha misurato con cura. Non lo crea il gradino: il gradino lo amplifica del 25%. COSA MI SMENTIREBBE, in ordine di gravita' (a) un giorno in cui il libro perde piu' di ~{L_STRUCT*0.7:.1%} in chiusura: vorrebbe dire che l'aritmetica della sez.(5) ha una gamba che non ho contato (un terzo strumento, un ordine non nettato, uno stop non eseguito, il cap non applicato); (b) uno z giornaliero peggiore di {zmin['BTC']:.1f} su BTC o {zmin['ETH']:.1f} su ETH con vol >= {tv:.0%}: il bound e' CONDIZIONATO a quel massimo in campione e scala linearmente con esso; (c) una configurazione in cui TP01 tiene piena convinzione ENTRANDO in un crash: nel campione non succede mai, ed e' l'unica ragione per cui il controfattuale sfonda il -10% e la realta' no. La mia frequenza stimata assume che resti rara; (d) SKH01 in un regime in cui sta short molto piu' a lungo: la coda del libro e' la sua, e la misuro su un campione in cui e' fuori mercato l'87,6% del tempo; (e) un funding medio molto diverso su Deribit-USDC rispetto al proxy Hyperliquid: il punto 8 e' misurato su un venue che non e' quello dove il libro esegue. GATE — cosa e' girato e cosa no · `marginal_vs_tp01` NON APPLICABILE: non c'e' un flusso nuovo, k e' uno scalare (corr 1,00). · `deflated_sharpe` NON APPLICABILE: lo Sharpe e' INVARIANTE a k. Questo lavoro non chiude il buco strutturale, lo AGGIRA: misura il peggior giorno, che e' l'unica grandezza del rischio su cui la scala non e' invariante. · `implausible_sharpe` NON GIRATO come funzione (darebbe lo stesso verdetto a ogni k). Il controllo di plausibilita' e' stato fatto a mano: nessuna cella qui promette zero perdite, e la sez.(5) e' proprio la ricerca del 'rischio FUORI dal dataset' che quel gate cerca. · null del de-levering NON e' un gate qui: e' l'OGGETTO (la leva percorsa nei due versi). · fortuna d'ancora EREDITATA (x{DP.DELUCK} misurato). ⚠️ NON RI-GIRATA su questo taglio, e va detto: il peggior giorno e' un evento SINGOLO, e una banda d'ancora su un massimo non e' la stessa statistica di una banda su una media. Cio' che ne risente e' il drift (gia' de-luckato), non il bound. · causalita' EREDITATA: nessun segnale nuovo, solo serie gia' validate. · replica di controllo SUPERATA: drift/vol/Sharpe/peggior giorno de-luckati (4/4), k* MC vs forma chiusa, scomposizione libro=gambe (max|diff| 0,0e+00), marginali del gap (p50/p90/p99 identiche), 12,3x di liquidazione. PARZIALE su 'k*=2x sotto stress' (qui 2,95x fine / 3,5x sulla loro griglia): stessa direzione e ordine, cella diversa. · risoluzione la curva di crescita e' in forma CHIUSA (E[log]): zero rumore MC. Le celle della tabella (6a) sono APPAIATE. L'incertezza residua e' quella del FIT di coda, ed e' negli IC bootstrap e nella banda fra stimatori. """) n_boot_tot = 2 * EVT_BOOT n_celle = (2 * len(EVT_Q_SENS)) + n_boot_tot + 2 + 2 + len(code) * len(scenari_drift) \ + 5 + len(KSG) + 1 + 5 + 3 + 2 print(f""" GRIGLIA DICHIARATA — {n_celle} valutazioni, contate al RIALZO: {2*len(EVT_Q_SENS)} fit GPD (2 serie x {len(EVT_Q_SENS)} soglie) · {n_boot_tot} ri-fit bootstrap · 2 Hill · 2 t di Student {len(code)*len(scenari_drift)} celle k* (coda x drift, APPAIATE) · 5 righe di stress · {len(KSG)} celle della griglia coarse di growth_policy · 1 sensitivita' combinata · 5 righe di liquidazione · 3 lenti del peggior giorno · 2 varianti di controfattuale (chiusura e minimo). ⚠️ **Nessuna cella e' stata SELEZIONATA**: la soglia EVT e' dichiarata a priori ({EVT_Q:.0%}) e le altre tre girano come sensitivita'; il criterio di (0) e' congelato prima delle sezioni 4-6. Percio' non c'e' un deflated-Sharpe da calcolare: non c'e' un massimo scelto su cui barare. """) print("\n" + "=" * 110) print(" IL GRADINO DI LEVA A 1,25x: AUTORIZZABILE A CONDIZIONE DI COSTRUIRE PRIMA UNA CHIAVE DI SCALA ESPLICITA IN CONFIG") print("=" * 110) print(f"\n [tempo totale {time.time()-t_start:.0f}s]") if __name__ == "__main__": main()