diff --git a/docs/research/RESULTS-0822.md b/docs/research/RESULTS-0822.md index 34a02ea..d7ed9f8 100644 --- a/docs/research/RESULTS-0822.md +++ b/docs/research/RESULTS-0822.md @@ -698,6 +698,7 @@ stessi hanno nominato senza poterli eseguire**. Stesso contratto di consegna, st | 30 | LISTINO | `GATE PROP-01` gamba (a): la firm lista >=10 delle 13 gambe con short? | **PASS 13/13** — verificato sul venue, non sul sito | | 31 | GATE-RECON | cosa direbbero i 3 gate pre-registrati se le serie forward fossero riparate | **1 gate su 3 ribaltato** — e la premessa e' CONFERMATA | | 32 | SKH-ANCHOR-PROP | `GATE PROP-01` gamba (b), datata 31/10: girata oggi | **PASS 23/23** — e il numero scende ancora, a **4,4%** | +| 33 | WORST-DAY | il peggior giorno del libro, MISURATO: il gradino di leva e' autorizzabile? | **LEAD** — l'aritmetica passa, **il knob non esiste** | | 22 | MAKER | l'esecuzione passiva e' una fonte di ritorno, al netto del costo di non essere eseguiti? | **SCARTATO** — il segno dipende da `<` contro `<=` | | 23 | BOCPD | un rilevatore di cambio di regime vero batte il miglior lookback COSTANTE? | **SCARTATO** — filone chiuso definitivamente | | 24 | CRITICO | cosa NON e' stato misurato, e quale singola misura mancante vale di piu' | **3 correzioni all'ondata**, 1 regola mia ritirata | @@ -1488,3 +1489,70 @@ misurato proprio come mediana-ancore/canonica del drift). A ×1,00 il delta sale **Smentitore residuo, fuori dal perimetro della gamba (b):** l'ancora **giornaliera di TP01** (24 ore), che questa misura **non tocca** e che resta canonica 00:00 UTC = **la piu' fortunata delle 24** per l'hold-out di TP01. E' la **terza** dimensione, e l'agente la dichiara. + +--- + +## 33 — WORST-DAY (il peggior giorno del libro, misurato invece che assunto) + +`scripts/research/r0822c_worst_day.py` (1.112 righe, **12 s**). **847 valutazioni**, soglia EVT +**dichiarata a priori** (95° pctl) e criterio congelato prima delle sezioni decisive → *non c'e' un +massimo scelto su cui barare, quindi non c'e' un DSR da calcolare*. + +Il filone che il critico dell'ondata aveva indicato come **la misura di maggior valore mai fatta**: +il gradino di leva **1,00x → 1,25-1,50x** vale **14,7 → 11,6 anni** al capitale-rendita (~€300/mese di +versamenti equivalenti, a costo zero) ed era bloccato da **un solo parametro scelto a mano**: +*"un giorno a −10% ogni anno"*. + +📌 **Il peggior giorno ha DUE risposte e differiscono di ~2×:** **−3,94% in chiusura** (2020-03-13) e +**−7,33% al minimo intra-giorno** (mediana del rapporto sui giorni in perdita **1,89×**). + +⚠️ **E il critico va CORRETTO nel meccanismo, non solo confermato.** Sui 10 giorni peggiori del +sottostante il libro fa **−2,34%** contro **−182,1%** del mercato (guadagna in 4/10; il 12 marzo 2020, +−43,6%, fa **+3,80%**) — **ma sui 160 giorni di crash perde in media −0,505%**, e l'esposizione di +TP01 scende monotona col crollo (0,096 nella fascia −5/−2% → **0,042 sotto il −20%**). +**Il libro non e' immune ai crash: e' immune ai crash GROSSI, perche' li' il trend si e' gia' girato.** +E il suo peggior giorno **non e' un crash: e' uno short squeeze** — SKH01 short con stop 2% che gappa a +**−15,77%** di sleeve mentre TP01 e' flat (secondo peggiore −6,67% → **la coda di SKH01 e' un giorno +solo**). + +**La coda stimata** (GPD, IC95 bootstrap a blocchi): chiusura **−2,90% a 1/1.000 giorni**, **−4,55% a +1/10.000** [−6,34 / −3,08%]. ⚠️ **Hill dice −7,34%** (ξ 0,34 contro 0,09): **i due stimatori non +concordano → si cita la banda**, non il numero. **Il conto ARITMETICO** (cap + vol-target, che +converte un crash nel suo z-score rendendo il bound invariante alla vol, + gap-through di SKH01): +peggior giorno **possibile** a k=1,00x = **−14,32% in chiusura**. +📌 **Quindi il −10% sta al 70% del massimo strutturale: la TAGLIA e' plausibile, la FREQUENZA no** +(1 ogni 67-6.075 anni contro "ogni anno"). **La riserva e' ridimensionata di 2-4 ordini di grandezza, +non falsificata.** +📌 **E la forma della coda non e' la leva:** muove k* di 1-2 unita', **il drift di 6,7**. Anche +prendendo lo stress a mano **alla lettera**: k* 2,95x → mezzo-Kelly **1,48x** → *lo stress bocciava il +gradino DOPPIO, non quello singolo*. **C1-C5 passano tutti** (margine strutturale 2,8×, liquidazione +4×, guadagno a drift −2SE +2,01%/anno). + +🚨 **IL RISULTATO CHE NESSUNO CERCAVA, ed e' quello che decide: il cap di `config/live.json` e' un +CLAMP, NON un moltiplicatore.** Morde su **3 osservazioni-asset su 5.650 (0,053%)**: alzarlo da 0,50 a +0,625 lascia il libro a k=1,00x in **5.647 giorni-asset su 5.650**. ✅ **Verificato dal coordinatore:** +le chiavi di `live.json` sono `max_notional_per_asset_usd/_frac`, `min_order_usd`, `disaster_sl_pct`, +`max_data_age_days`, `skh_feed_max_age_min` — **non esiste una chiave di scala**; servirebbe toccare +`WEIGHT`/`W_TP01`/`W_SKH` in `src/live/book.py`, cioe' **codice** su un percorso con soldi veri. +**La regola del progetto — *"ogni cambio di SCALA passa dal cap di config, non da `target_vol`"* — non +e' implementabile come scritta.** *Due mesi di discussione su un knob inesistente.* + +🚨 **SOTTOPRODOTTO CHE TOCCA OGNI NUMERO DEL PROGETTO: il funding dei perpetual non e' modellato in +NESSUN backtest.** ✅ **Verificato dal coordinatore:** zero occorrenze di `funding` in +`src/backtest/harness.py`, `src/strategies/trend_portfolio.py`, `src/portfolio/{sleeves,portfolio}.py` +**e in tutto `src/live/`**. Proxy Hyperliquid 14,8%/anno × esposizione long media di TP01 0,103·E = +**~1,53%/anno che il libro paga davvero** = **il 9% del drift de-luckato**, cioe' **lo stesso ordine +della correzione d'ancora ×0,89** che il progetto ha misurato con cura. + +📌 **Sottoprodotto forense: la lente del critico si IDENTIFICA dai valori che ha pubblicato** (+0,97 e ++0,72 compaiono **solo** nella riga canonica) → aveva usato `book_series('canonical')`, e **su quella +lente il peggior giorno E' un giorno di ribasso** (−3,47%, 2025-10-10). *Aveva ragione, con la lente +sbagliata.* + +**VERDETTO: LEAD** (misura di politica, non strategia). +**`IL GRADINO DI LEVA A 1,25x: AUTORIZZABILE A CONDIZIONE DI` costruire prima una chiave di scala +esplicita in config, e di rifare `r0726_fee_sensitivity`** — la cui conclusione *"liquidation fee +irrilevante"* **vale solo a nozionale lordo ≤1x**, ed era gia' legata a un test di guardia sul cap. +⚠️ **Nota dell'agente, dovuta:** se si credono **entrambe** le riserve insieme (drift −2SE **e** −10% +annuo) il drift atteso e' **−0,61%/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.* diff --git a/scripts/research/r0822c_worst_day.py b/scripts/research/r0822c_worst_day.py new file mode 100644 index 0000000..b969731 --- /dev/null +++ b/scripts/research/r0822c_worst_day.py @@ -0,0 +1,1112 @@ +#!/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()