"""r0822b_tp01_sinistro — quanto costa l'ASSICURAZIONE TP01 su un conto a barriera (2026-08-22). IL BUCO CHE CHIUDE ------------------ `r0822_prop_alloc.py` (stessa ondata) ha ottimizzato l'allocazione di un conto **funded** sotto barriera di drawdown e ha trovato l'ottimo a **TP01 25 / SKH01 25 / XS01 50**, cioe' TP01 pesato **tre volte meno** del libro live (75%). Ha anche dichiarato il proprio limite: "la sotto-pesatura di TP01, lo sleeve DIFENSIVO, e' misurata su un campione SENZA SINISTRO (2024-2026 non contiene un crash): su una barriera assorbente quella scommessa si paga una volta sola." E questo si scontra con due misure del progetto: (a) il **leave-one-out de-luckato** del 26/07: TP01 e' il maggior contributore al Sharpe FULL (+0.390, positivo nel 100% delle estrazioni) e il suo hold-out negativo (-0.200, negativo nel 99.1%) e' *la firma dell'assicurazione, non della morte* — "uno sleeve difensivo si giudica sul SINISTRO, non sul premio"; (b) `edge_watch` criterio B: negli **8 anni su 8** con drawdown buy&hold > 10% TP01 ha protetto da **1.8x a 34.4x**. DOMANDA: qual e' il peso giusto di TP01 se l'obiettivo include sopravvivere a un crash che nel campione di ottimizzazione NON c'e'? IPOTESI REGISTRATA PRIMA DI MISURARE ------------------------------------ Mi aspettavo che (1) la sotto-pesatura di TP01 costasse molto **nel sinistro** e poco fuori, e che (2) esistesse un peso di compromesso (~50%) dentro la regione robusta del funded e non catastrofico al crash. In altre parole mi aspettavo di **confermare** il caveat di PROP-ALLOC. Se la misura dice il contrario, e' un risultato e va scritto come tale. CIO' CHE E' UN'ASSUNZIONE E CIO' CHE E' UN DATO ----------------------------------------------- * Lo **scenario di stress e' uno SCENARIO, non una previsione.** Due costruzioni, entrambe dichiarate: (S1) bootstrap MISTO — ogni blocco ha probabilita' `p` di essere estratto dal pool dei sinistri 2019-2023 invece che dalla finestra normale; (S2) **replay** — un episodio di crash VERO innestato per intero, in ordine, a una data casuale dentro l'anno. In piu' il replay deterministico del **2022 intero** (aprile-dicembre), che non e' un modello: e' successo. * Il vincolo binding di TUTTO il filone e' che i sinistri osservati sono **QUATTRO**. Nessun bootstrap crea informazione: l'ampiezza effettiva della coda resta 4 episodi, e ogni numero qui eredita quell'errore. Detto una volta, vale per ogni tabella. * Lente **ACCOPPIATA** (`r0725_prop_coupled`): il minimo intraday si compone ESATTO sul path orario condiviso TP01/SKH01. Non e' un dettaglio: su una regola a UN giorno (daily-loss) la lente close-only e' misurata **esattamente cieca** (0.00 breach contro 0.40-1.21 veri), mentre su una statistica multi-giorno l'errore vale il 3.5%. Il vincolo funded E' una regola a un giorno. * de-luck sul drift **x0.89** (fattore MISURATO il 26/07). ⚠️ L'offset di de-luck e' calcolato UNA VOLTA sulla finestra 2019+ e sottratto ovunque in valore ASSOLUTO: de-luckare un pool di crash sulla sua PROPRIA media (negativa) *aggiungerebbe* drift proprio dove non deve. * macchineria RIUSATA, non riscritta: `r0725_prop_coupled` (tuple accoppiate) e `r0822_prop_alloc` (`eval_sim`, `funded_sim`, `boot_idx`, `FIRMS`). Qui si aggiungono solo i pool e gli innesti. Uso: `nice -n 19 timeout 900 uv run python scripts/research/r0822b_tp01_sinistro.py` """ from __future__ import annotations import sys import time from pathlib import Path import numpy as np import pandas as pd ROOT = Path(__file__).resolve().parents[2] sys.path.insert(0, str(ROOT)) sys.path.insert(0, str(ROOT / "scripts" / "research")) from src.data.downloader import load_data # noqa: E402 from src.portfolio.sleeves import _tp01_returns, _skyhook_returns # noqa: E402 import r0725_prop_coupled as pc # noqa: E402 import r0822_prop_alloc as PA # noqa: E402 # ------------------------------------------------------------------ costanti DICHIARATE DELUCK = PA.DELUCK # 0.89, misurato il 26/07 FULL_START = "2019-03-14" # inizio del recon crypto (TP01+SKH01) RECENT_START = "2024-01-01" # finestra di ottimizzazione di PROP-ALLOC (nasce XS01) BLOCK = 20 # blocco del bootstrap, in giorni N_PATHS = 2500 EPI_LEN = 30 # lunghezza di un episodio di sinistro, in giorni HORIZON = 365 LEVS = (0.25, 0.375, 0.50, 0.625, 0.75, 1.00, 1.25, 1.50) W_GRID = tuple(i / 8 for i in range(9)) # peso di TP01 nella coppia crypto, 0 -> 1 SURV_TARGET = 0.90 # livello di sopravvivenza della frontiera ISO-SOPRAVVIVENZA N_EVAL = 0 # contatore onesto delle valutazioni (griglia dichiarata) def hr(t: str = "") -> None: print("\n" + "-" * 106) if t: print(f" {t}") print("-" * 106) # ================================================================== §A. PANNELLO _TUP: dict = {} def crypto(w_tp: float) -> tuple[np.ndarray, np.ndarray, pd.DatetimeIndex]: """(R, gap) giornalieri della coppia crypto a peso TP01 = w_tp, SKH01 = 1-w_tp. gap = minimo_intraday - chiusura (<=0). Storia piena dal 2019-03.""" k = round(float(w_tp), 6) if k not in _TUP: c = pc.crypto_daily_tuples(k, 1.0 - k) R = c["R"].values.astype(float) G = np.minimum(c["m"].values, R) - R _TUP[k] = (R, G, c.index) R, G, I = _TUP[k] return R.copy(), G.copy(), I _OFF: dict = {} def deluck_offset(w_tp: float, factor: float = DELUCK) -> float: """Offset ASSOLUTO di de-luck, calcolato UNA VOLTA sulla finestra 2019+ e riusato ovunque. Calcolarlo sulla media di un sotto-campione di crash (negativa) *aggiungerebbe* drift.""" key = (round(float(w_tp), 6), round(float(factor), 4)) if key not in _OFF: R, _, I = crypto(w_tp) _OFF[key] = float((1.0 - factor) * R[I >= pd.Timestamp(FULL_START, tz="UTC")].mean()) return _OFF[key] def buyhold_daily(idx: pd.DatetimeIndex) -> pd.Series: """Buy&hold 50/50 BTC+ETH sulla stessa griglia giornaliera del libro.""" bh = None for a in ("BTC", "ETH"): d = load_data(a, "1h") i = pd.DatetimeIndex(pd.to_datetime(d["datetime"], utc=True)) s = pd.Series(d["close"].astype(float).values, index=i).resample("1D").last() r = s.pct_change().fillna(0.0) * 0.5 bh = r if bh is None else bh.add(r, fill_value=0.0) return bh.reindex(idx).fillna(0.0) def worst_windows(lbh: np.ndarray, idx: pd.DatetimeIndex, K: int, L: int, hi: int | None = None) -> list[int]: """Le K finestre di L giorni NON sovrapposte col peggior rendimento buy&hold, greedy sul minimo. `hi` limita i possibili inizi (per restare dentro 2019-2023).""" cs = np.concatenate(([0.0], np.cumsum(lbh))) win = np.expm1(cs[L:] - cs[:-L]) cand = np.arange(len(win)) if hi is None else np.arange(min(hi, len(win))) order = cand[np.argsort(win[cand])] out: list[int] = [] for t in order: if all(abs(int(t) - q) >= L for q in out): out.append(int(t)) if len(out) == K: break return sorted(out) def binom_two_sided(k: int, n: int) -> float: """p-value esatto a due code di un test dei segni con p0=0.5 (n piccolo: si conta).""" from math import comb tot = 2 ** n kk = max(k, n - k) tail = sum(comb(n, j) for j in range(kk, n + 1)) return min(1.0, 2.0 * tail / tot) def maxdd(r: np.ndarray) -> float: eq = np.cumprod(1.0 + r) return float((eq / np.maximum.accumulate(eq) - 1.0).min()) # ================================================================== §B. SIMULAZIONE def J_of(R: np.ndarray, G: np.ndarray, ev_idx: np.ndarray, fu_idx: np.ndarray, lev: float, firm: str = "HYRO") -> dict: """J = P(passare l'eval) x P(essere vivo a 12 mesi da funded), lente ACCOPPIATA.""" global N_EVAL N_EVAL += 1 e = PA.eval_sim(R, G, ev_idx, lev, firm, "coupled") f = PA.funded_sim(R, G, fu_idx, lev, firm, 100_000.0, "coupled") return dict(J=e["p_pass"] * f["p_alive"], p_pass=e["p_pass"], p_alive=f["p_alive"], e_payout=f["e_payout"], jvec=(e["passed"] & f["alive"]).astype(float), alive=f["alive"]) def best_lev(R: np.ndarray, G: np.ndarray, ev_idx, fu_idx, firm: str = "HYRO") -> tuple: rows = [(lv, J_of(R, G, ev_idx, fu_idx, lv, firm)) for lv in LEVS] return max(rows, key=lambda x: x[1]["J"]) def pool_paths(seg_bounds: list[tuple[int, int]], n_days: int, n_paths: int, seed: int, block: int = BLOCK) -> np.ndarray: """Indici di bootstrap a blocchi ristretti a segmenti CONTIGUI (ogni blocco vive dentro un solo segmento -> non si incollano giorni di episodi diversi).""" rng = np.random.default_rng(seed) starts = [] for a, b in seg_bounds: if b - a >= block: starts.extend(range(a, b - block + 1)) starts = np.array(starts) nb = int(np.ceil(n_days / block)) st = starts[rng.integers(0, len(starts), size=(n_paths, nb))] return (st[:, :, None] + np.arange(block)[None, None, :]).reshape(n_paths, -1)[:, :n_days] def mix_paths(base_idx: np.ndarray, crash_starts: np.ndarray, p: float, seed: int, block: int = BLOCK) -> np.ndarray: """S1 — bootstrap MISTO: ogni blocco della path normale ha probabilita' `p` di essere sostituito da un blocco estratto dal pool dei SINISTRI.""" rng = np.random.default_rng(seed) out = base_idx.copy() P, D = out.shape nb = int(np.ceil(D / block)) hit = rng.random((P, nb)) < p for b in range(nb): sl = slice(b * block, min((b + 1) * block, D)) L = sl.stop - sl.start rows = np.where(hit[:, b])[0] if len(rows) == 0: continue st = crash_starts[rng.integers(0, len(crash_starts), size=len(rows))] out[rows, sl] = st[:, None] + np.arange(L)[None, :] return out def splice_paths(base_idx: np.ndarray, epi_idx: np.ndarray, seed: int) -> np.ndarray: """S2 — REPLAY: l'episodio VERO (contiguo, in ordine) innestato una volta sola a una data casuale dentro l'orizzonte. E' la domanda operativa: *se il sinistro arriva*.""" rng = np.random.default_rng(seed) out = base_idx.copy() P, D = out.shape L = len(epi_idx) st = rng.integers(0, max(D - L, 1), size=P) for i in range(P): a = int(st[i]) out[i, a:a + L] = epi_idx[:min(L, D - a)] return out # ================================================================== main def main() -> None: # noqa: C901 try: sys.stdout.reconfigure(line_buffering=True) except Exception: # pragma: no cover pass t0 = time.time() def tick(lab: str) -> None: print(f" [{time.time()-t0:6.0f}s] {lab} (valutazioni finora: {N_EVAL})") print("=" * 106) print(" TP01-SINISTRO — quanto costa l'assicurazione su un conto a barriera, e quanto rende") print(" quando il sinistro arriva. Lente ACCOPPIATA, firm HYRO.") print("=" * 106) R75, G75, IDX = crypto(0.75) n_full = len(IDX) i_rec = int(np.searchsorted(IDX, pd.Timestamp(RECENT_START, tz="UTC"))) print(f"\n finestra crypto piena: {n_full} giorni {IDX[0].date()} -> {IDX[-1].date()}") print(f" finestra di ottimizzazione di PROP-ALLOC: {n_full - i_rec} giorni dal " f"{IDX[i_rec].date()} (e' anche la finestra di SCOPERTA di XS01)") # --------------------------------------------------------- sanity: recon vs sleeve ufficiale print("\n SANITY — il recon orario contro gli sleeve ufficiali. TP01/SKH01 NON coincidono") print(" barra per barra per costruzione (il recon e' un mark-to-market ORARIO, lo sleeve") print(" marca a fine-trade): il controllo giusto e' il log-totale.") for nm, off_s, w in (("TP01", _tp01_returns(), 1.0), ("SKH01", _skyhook_returns(), 0.0)): o = off_s.dropna().sort_index() if o.index.tz is None: o.index = o.index.tz_localize("UTC") Rn, _, In = crypto(w) com = o.index.intersection(In) a = o.reindex(com).values b = pd.Series(Rn, index=In).reindex(com).values print(f" {nm:>6} log-tot sleeve {float(np.log1p(a).sum()):+8.4f} " f"recon {float(np.log1p(b).sum()):+8.4f} scarto " f"{float(np.log1p(a).sum()-np.log1p(b).sum()):+7.4f}") # ============================================================ §1. CHE COS'E' UN SINISTRO hr("§1. CHE COS'E' UN SINISTRO — e la frase '2024-2026 non contiene un crash' e' FALSA") bh = buyhold_daily(IDX) lbh = np.log1p(bh.values) print(" Peggior rendimento buy&hold 50/50 su finestra mobile, per periodo:") print(f" {'W':>4} {'2019-2023 min':>15} {'2024+ min':>12} {'rapporto':>10} " f"{'2019-23 p1':>11} {'2024+ p1':>10} {'data del min 2024+':>20}") ratios = [] for W in (1, 5, 10, 20, 60): cs = np.concatenate(([0.0], np.cumsum(lbh))) roll = np.expm1(cs[W:] - cs[:-W]) # finestra che FINISCE in t=W-1+k tt = IDX[W - 1:] pre = roll[tt < pd.Timestamp(RECENT_START, tz="UTC")] post = roll[tt >= pd.Timestamp(RECENT_START, tz="UTC")] tpost = tt[tt >= pd.Timestamp(RECENT_START, tz="UTC")] rr = pre.min() / post.min() ratios.append(rr) print(f" {W:>4} {pre.min():>15.1%} {post.min():>12.1%} {rr:>10.2f}x " f"{np.percentile(pre, 1):>11.1%} {np.percentile(post, 1):>10.1%} " f"{str(tpost[int(np.argmin(post))].date()):>20}") print(f"\n -> il 2024+ **contiene** un crash: peggior giorno -14.5%, peggiori 20g -39.6%,") print(f" drawdown buy&hold fino a -60%. Cio' che NON contiene e' un crash della TAGLIA") print(f" dei precedenti: il rapporto e' {np.mean(ratios[:3]):.1f}x sulla coda veloce (1-10g)") print(f" e {ratios[-1]:.1f}x a 60 giorni. **La frase giusta non e' 'senza sinistro' ma") print(f" 'con un sinistro di taglia dimezzata'** — e cambia la domanda: non 'cosa succede") print(f" se arriva un crash' ma 'cosa succede se ne arriva uno DUE VOLTE piu' grande'.") # ----- replica del criterio B di edge_watch (protezione per anno) print("\n Criterio B di `edge_watch` replicato qui (anni con DD buy&hold > 10%):") tp_only, _, _ = crypto(1.0) ser_bh = pd.Series(bh.values, index=IDX) ser_tp = pd.Series(tp_only, index=IDX) print(f" {'anno':>6} {'DD b&h':>9} {'DD TP01':>9} {'protezione':>12} {'DD SKH01':>10} " f"{'DD 75/25':>10}") skh_only, _, _ = crypto(0.0) ser_sk = pd.Series(skh_only, index=IDX) ser_75 = pd.Series(R75, index=IDX) n_sin = n_ok = 0 for y, g in ser_bh.groupby(ser_bh.index.year): d_bh = maxdd(g.values) if d_bh > -0.10: continue n_sin += 1 d_tp = maxdd(ser_tp.loc[g.index].values) d_sk = maxdd(ser_sk.loc[g.index].values) d_75 = maxdd(ser_75.loc[g.index].values) ok = (d_tp / d_bh) <= 0.75 n_ok += ok print(f" {y:>6} {d_bh:>9.1%} {d_tp:>9.1%} {d_bh/d_tp:>11.1f}x {d_sk:>10.1%} " f"{d_75:>10.1%} {'ok' if ok else 'FAIL'}") print(f" -> anni di sinistro {n_sin}, superati {n_ok}. **Ogni anno e' un anno di sinistro") print(f" per il buy&hold**: la 'frequenza del sinistro' non e' un evento raro, e' lo") print(f" stato normale dell'asset. Cio' che e' raro e' la TAGLIA (riga sopra).") # ----- definizione degli episodi hr("§1-bis. GLI EPISODI DI SINISTRO — regola di selezione DICHIARATA PRIMA") print(f" Regola: le {3} finestre di {EPI_LEN} giorni NON sovrapposte con il peggior rendimento") print(f" buy&hold nel 2019-2023 (greedy sul minimo), PIU' l'FTX 2022-11-05 aggiunto **per") print(f" NOME** (evento noto, non selezionato dal minimo: e' un controllo, non una scelta).") cs = np.concatenate(([0.0], np.cumsum(lbh))) win = np.expm1(cs[EPI_LEN:] - cs[:-EPI_LEN]) # finestra che INIZIA in t pre_mask = IDX[:len(win)] < pd.Timestamp(RECENT_START, tz="UTC") cand = np.where(pre_mask)[0] order = cand[np.argsort(win[cand])] picked = [] for t in order: if all(abs(int(t) - p) >= EPI_LEN for p in picked): picked.append(int(t)) if len(picked) == 3: break t_ftx = int(np.searchsorted(IDX, pd.Timestamp("2022-11-05", tz="UTC"))) epis = sorted(picked + [t_ftx]) NAMES = {} print(f"\n {'episodio':>12} {'dal':>12} {'al':>12} {'b&h 30g':>10} {'peggior 1g b&h':>16}") for t in epis: lab = ("COVID" if IDX[t].year == 2020 else "MAG-2021" if (IDX[t].year, IDX[t].month) == (2021, 5) else "LUNA" if (IDX[t].year, IDX[t].month) in ((2022, 5), (2022, 6)) else "FTX" if (IDX[t].year, IDX[t].month) == (2022, 11) else f"EPI-{IDX[t].date()}") NAMES[t] = lab seg = bh.values[t:t + EPI_LEN] print(f" {lab:>12} {str(IDX[t].date()):>12} {str(IDX[t+EPI_LEN-1].date()):>12} " f"{win[t]:>10.1%} {seg.min():>16.1%}") crash_days = np.concatenate([np.arange(t, t + EPI_LEN) for t in epis]) freq_day = len(crash_days) / n_full print(f"\n giorni di sinistro {len(crash_days)} su {n_full} = **{freq_day:.1%} del tempo**") print(f" ⚠️ AMPIEZZA EFFETTIVA DELLA CODA = **4 EPISODI**. Nessun bootstrap la aumenta.") print(f" Ogni numero di questo file eredita l'errore di un campione di quattro eventi.") tick("§1 sinistri") # ============================================================ §2. IL CONTO DELL'ASSICURAZIONE hr("§2. IL CONTO DELL'ASSICURAZIONE — premio fuori dal sinistro, indennizzo dentro") print(" Confronto DUE ripartizioni della coppia crypto, appaiato giorno per giorno:") print(" LIVE = TP01 75 / SKH01 25 (il libro che gira)") print(" OPT = TP01 38 / SKH01 62 (l'ottimo di PROP-ALLOC sul campione 2019+)") in_sin = np.zeros(n_full, bool) in_sin[crash_days] = True print(f"\n {'config':>10} {'drift fuori':>12} {'drift dentro':>13} {'Sharpe full':>12} " f"{'maxDD':>8} {'peggior 1g':>11} {'peggior min 1g':>15}") prof = {} for lab, w in (("LIVE 75/25", 0.75), ("OPT 38/62", 0.375), ("TP01 puro", 1.0), ("SKH01 puro", 0.0)): R, G, _ = crypto(w) R = R - deluck_offset(w) out_d = R[~in_sin].mean() * 365.0 in_d = R[in_sin].mean() * 365.0 vol = R.std() * np.sqrt(365.0) prof[lab] = dict(out=out_d, ind=in_d, sh=R.mean() * 365.0 / vol, dd=maxdd(R), w1=R.min(), wm=(R + G).min()) p = prof[lab] print(f" {lab:>10} {p['out']:>12.1%} {p['ind']:>13.1%} {p['sh']:>12.2f} {p['dd']:>8.1%} " f"{p['w1']:>11.2%} {p['wm']:>15.2%}") prem = prof["LIVE 75/25"]["out"] - prof["OPT 38/62"]["out"] ind = prof["LIVE 75/25"]["ind"] - prof["OPT 38/62"]["ind"] R_live = crypto(0.75)[0] - deluck_offset(0.75) R_opt = crypto(0.375)[0] - deluck_offset(0.375) cum_live = float(np.expm1(np.log1p(R_live[in_sin]).sum())) cum_opt = float(np.expm1(np.log1p(R_opt[in_sin]).sum())) print(f"\n PREMIO (LIVE - OPT) fuori dal sinistro : {prem:+.2%}/anno = " f"{prem*100_000:+,.0f} $/anno su un funded da $100k") print(f" INDENNIZZO (LIVE - OPT) sui 120 giorni di sinistro, CUMULATO (non annualizzato,") print(f" che su 120 giorni sarebbe una finta precisione): LIVE {cum_live:+.1%} contro " f"OPT {cum_opt:+.1%} = {cum_live-cum_opt:+.1%}") print(f" su un funded da $100k: {(cum_live-cum_opt)*100_000:+,.0f} $ per l'INTERO " f"monte-sinistri di {n_full/365.0:.1f} anni") print(f" frequenza: {freq_day:.1%} del tempo dentro un sinistro (4 episodi in " f"{n_full/365.0:.1f} anni = uno ogni {n_full/365.0/4:.1f} anni)") if ind < 0: print("\n 🚨 **IL SEGNO E' ROVESCIATO RISPETTO ALLA MIA IPOTESI.** Dentro il sinistro il") print(" libro con MENO TP01 fa MEGLIO, non peggio. Il meccanismo si legge in una riga:") print(" TP01 nel crash va FLAT (non perde, non guadagna: e' assicurazione), SKH01 va") print(" SHORT (guadagna). Su un conto a barriera *guadagnare batte non-perdere*.") print("\n Rendimento dei 4 episodi, per peso di TP01 (30 giorni ciascuno, chiusura e MINIMO):") print(f" {'w TP01':>8} " + " ".join(f"{NAMES[t]:>18}" for t in epis) + f" {'peggior min':>13}") for w in (1.0, 0.75, 0.5, 0.375, 0.25, 0.0): R, G, _ = crypto(w) cells = [] wm = 0.0 for t in epis: seg = R[t:t + EPI_LEN] gp = G[t:t + EPI_LEN] eq = np.cumprod(1.0 + seg) mn = float(np.minimum(eq - 1.0, np.concatenate(([1.0], eq[:-1])) * (1 + seg + gp) - 1.0).min()) wm = min(wm, mn) cells.append(f"{float(eq[-1]-1.0):+7.1%}/{mn:+7.1%}") print(f" {w:>8.3f} " + " ".join(f"{c:>18}" for c in cells) + f" {wm:>13.1%}") print(" (formato: chiusura a 30g / minimo intraday cumulato dell'episodio)") print("\n ⚠️ QUANTA POTENZA HA QUESTO SEGNO? Con 4 episodi, '4 su 4 a favore' ha un test dei") print(" segni con p = 0.125 a due code: **non e' significativo al 5%**. Il modo onesto di") print(" guadagnare potenza non e' ripetere il bootstrap (non crea eventi) ma ESTENDERE la") print(" stessa regola di selezione a piu' finestre. Le 12 peggiori finestre di 30g NON") print(" sovrapposte su TUTTO il 2019+ (stessa regola greedy, K da 3 a 12):") w12 = worst_windows(lbh, IDX, 12, EPI_LEN) print(f"\n {'finestra':>12} {'b&h 30g':>9} {'LIVE 75/25':>11} {'OPT 38/62':>11} " f"{'TP01 puro':>10} {'SKH01 puro':>11} {'OPT>LIVE':>9}") wins = 0 wins_sk = 0 lr_live, lr_opt = np.log1p(R_live), np.log1p(R_opt) R_tp = crypto(1.0)[0] - deluck_offset(1.0) R_sk = crypto(0.0)[0] - deluck_offset(0.0) for t in w12: a = float(np.expm1(lr_live[t:t + EPI_LEN].sum())) b = float(np.expm1(lr_opt[t:t + EPI_LEN].sum())) c = float(np.expm1(np.log1p(R_tp[t:t + EPI_LEN]).sum())) d = float(np.expm1(np.log1p(R_sk[t:t + EPI_LEN]).sum())) wins += b > a wins_sk += d > c print(f" {str(IDX[t].date()):>12} {win[t]:>9.1%} {a:>11.1%} {b:>11.1%} " f"{c:>10.1%} {d:>11.1%} {('si' if b > a else 'no'):>9}") pre = [t for t in w12 if IDX[t] < pd.Timestamp(RECENT_START, tz="UTC")] wins_pre = sum(1 for t in pre if float(np.expm1(lr_opt[t:t + EPI_LEN].sum())) > float(np.expm1(lr_live[t:t + EPI_LEN].sum()))) print(f"\n OPT batte LIVE in **{wins}/12** finestre -> test dei segni " f"p = {binom_two_sided(wins, 12):.4f}") print(f" SKH01 puro batte TP01 puro in **{wins_sk}/12** -> p = " f"{binom_two_sided(wins_sk, 12):.4f}") print(f" ⚠️ {12-len(pre)} di queste 12 finestre stanno nel 2024+, cioe' DENTRO la finestra") print(" su cui SKH01 e' stato tarato e su cui PROP-ALLOC ha ottimizzato: la potenza sale") print(" ma l'indipendenza scende. **Sul solo sotto-campione pre-2024**, che e' quello") print(f" indipendente: OPT batte LIVE in **{wins_pre}/{len(pre)}** -> p = " f"{binom_two_sided(wins_pre, len(pre)):.4f}. E' questo il numero da citare.") tick("§2 conto assicurazione") # ============================================================ §3. SOTTO BARRIERA hr("§3. SOTTO BARRIERA — griglia peso TP01 x leva su TRE lenti di campione") print(f" {len(W_GRID)} pesi x {len(LEVS)} leve x 3 finestre. J = P(pass) x P(vivo 12m), HYRO") print(f" (eval +10%/-6% statico/-4% giornaliero; funded -6% statico/-4% giornaliero).") ev_f = PA.boot_idx(n_full, HORIZON, N_PATHS, seed=8230) fu_f = PA.boot_idx(n_full, HORIZON, N_PATHS, seed=8231) n_rec = n_full - i_rec ev_r = PA.boot_idx(n_rec, HORIZON, N_PATHS, seed=8240) fu_r = PA.boot_idx(n_rec, HORIZON, N_PATHS, seed=8241) seg_sin = [(t, t + EPI_LEN) for t in epis] ev_s = pool_paths(seg_sin, HORIZON, N_PATHS, seed=8250) fu_s = pool_paths(seg_sin, HORIZON, N_PATHS, seed=8251) print(f"\n {'w TP01':>7} | {'2019+ (col 2022)':>26} | {'2024+ (PROP-ALLOC)':>26} | " f"{'SOLO-SINISTRO':>26}") print(f" {'':>7} | {'leva':>6} {'J':>7} {'P(viv)':>7} | {'leva':>6} {'J':>7} {'P(viv)':>7} | " f"{'leva':>6} {'J':>7} {'P(viv)':>7}") tab = {} for w in W_GRID: R, G, _ = crypto(w) off = deluck_offset(w) Rf, Gf = R - off, G Rr, Gr = Rf[i_rec:], Gf[i_rec:] lv1, o1 = best_lev(Rf, Gf, ev_f, fu_f) lv2, o2 = best_lev(Rr, Gr, ev_r, fu_r) lv3, o3 = best_lev(Rf, Gf, ev_s, fu_s) tab[w] = dict(full=(lv1, o1), rec=(lv2, o2), sin=(lv3, o3)) print(f" {w:>7.3f} | {lv1:>6.3f} {o1['J']:>7.3f} {o1['p_alive']:>7.1%} | " f"{lv2:>6.3f} {o2['J']:>7.3f} {o2['p_alive']:>7.1%} | " f"{lv3:>6.3f} {o3['J']:>7.3f} {o3['p_alive']:>7.1%}") a_full = max(tab, key=lambda w: tab[w]["full"][1]["J"]) a_rec = max(tab, key=lambda w: tab[w]["rec"][1]["J"]) a_sin = max(tab, key=lambda w: tab[w]["sin"][1]["J"]) print(f"\n argmax: 2019+ -> TP01 {a_full:.3f} | 2024+ -> TP01 {a_rec:.3f} | " f"SOLO-SINISTRO -> TP01 {a_sin:.3f}") print(f" ⚠️ RISOLUZIONE: un LIVELLO di J ha errore MC ~{np.sqrt(0.25/N_PATHS):.3f} " f"(N={N_PATHS}) -> celle entro ~0.02 NON sono distinguibili, e infatti 0.375 e 0.500") print(" si scambiano il posto fra semi diversi. La lettura e' 'la regione 0.25-0.50',") print(" non 'il punto 0.375' (stessa lezione del null dei tilt casuali di PROP-ALLOC).") print(" ⚠️ La colonna SOLO-SINISTRO e' una lente ESTREMA e va letta come tale: l'anno intero") print(" e' fatto di soli blocchi di crash (4 episodi ricampionati). Non e' uno scenario") print(" plausibile, e' il **limite superiore del danno**: se il verso della risposta e' lo") print(" stesso qui e sul 2019+, non dipende dalla frazione di sinistro assunta.") # replica di controllo del numero pubblicato da PROP-ALLOC (E-ter) print("\n REPLICA DI CONTROLLO — PROP-ALLOC E-ter pubblica, sul 2019+: argmax della coppia") print(" crypto **38/62 @0.50x con J 0.521**, e **75/25 J 0.443**. Stesse seed, stessa lente:") j38 = tab[0.375]["full"] j75 = tab[0.75]["full"] print(f" qui: 38/62 @{j38[0]:g}x J {j38[1]['J']:.3f} | 75/25 @{j75[0]:g}x " f"J {j75[1]['J']:.3f} -> " f"{'REPLICA OK' if abs(j38[1]['J']-0.521) < 0.02 and abs(j75[1]['J']-0.443) < 0.02 else 'NON REPLICA — leggere con cautela'}") tick("§3 griglia 3 lenti") # ============================================================ §4. LO STRESS ESPLICITO hr("§4. LO STRESS ESPLICITO — iniettare nel 2024+ i crash che il 2024+ non ha") print(" Costruzione DICHIARATA. Base = blocchi del 2024+ (la finestra di ottimizzazione).") print(" S1 MISTO: ogni blocco ha probabilita' p di venire dal pool dei 4 sinistri 2019-2023.") print(f" p = {freq_day:.3f} e' la frequenza STORICA; si girano anche 2x e 4x.") print(" S2 REPLAY: un episodio VERO innestato per intero, in ordine, a data casuale dentro") print(" l'anno. E' la domanda operativa: *se il sinistro arriva, chi resta in piedi?*") print(" ⚠️ Sono SCENARI, non previsioni. E poggiano su 4 episodi.") # pool = 2024+ (blocchi normali) + gli episodi in coda, su un indice unico def stress_panel(w: float) -> tuple[np.ndarray, np.ndarray, np.ndarray, list]: R, G, _ = crypto(w) off = deluck_offset(w) Rn, Gn = R[i_rec:] - off, G[i_rec:] segs = [] Rc, Gc = [], [] cur = len(Rn) for t in epis: Rc.append(R[t:t + EPI_LEN] - off) Gc.append(G[t:t + EPI_LEN]) segs.append((cur, cur + EPI_LEN)) cur += EPI_LEN Rp = np.concatenate([Rn] + Rc) Gp = np.concatenate([Gn] + Gc) return Rp, Gp, np.array([s for s, _ in segs]), segs base_ev = PA.boot_idx(n_rec, HORIZON, N_PATHS, seed=8260) base_fu = PA.boot_idx(n_rec, HORIZON, N_PATHS, seed=8261) print(f"\n S1 — bootstrap MISTO (base 2024+, blocchi di sinistro con probabilita' p)") print(f" {'w TP01':>7} | " + " | ".join(f"p={p:.3f}: {'J':>6} {'P(viv)':>7}" for p in (0.0, freq_day, 2 * freq_day, 4 * freq_day))) s1 = {} for w in W_GRID: Rp, Gp, cst, segs = stress_panel(w) starts = np.concatenate([np.arange(a, b - BLOCK + 1) for a, b in segs]) cells = [] for j, p in enumerate((0.0, freq_day, 2 * freq_day, 4 * freq_day)): ev = base_ev if p == 0 else mix_paths(base_ev, starts, p, seed=8270 + j) fu = base_fu if p == 0 else mix_paths(base_fu, starts, p, seed=8280 + j) lv, o = best_lev(Rp, Gp, ev, fu) cells.append((lv, o)) s1[w] = cells print(f" {w:>7.3f} | " + " | ".join(f" @{c[0]:.3g}x {c[1]['J']:>6.3f} " f"{c[1]['p_alive']:>7.1%}" for c in cells)) for j, p in enumerate((0.0, freq_day, 2 * freq_day, 4 * freq_day)): am = max(s1, key=lambda w: s1[w][j][1]["J"]) print(f" p={p:.3f} -> argmax TP01 {am:.3f} (J {s1[am][j][1]['J']:.3f}; " f"75/25 fa {s1[0.75][j][1]['J']:.3f})") tick("§4 S1 misto") print(f"\n S2 — REPLAY: un episodio innestato una volta nell'anno funded (365g)") print(f" Metrica: **P(vivo a 12 mesi) con il crash dentro**, alla leva scelta sul 2024+") print(f" (cioe' la leva che uno sceglierebbe NON sapendo del crash: e' il punto).") hdr = " ".join(f"{NAMES[t]:>10}" for t in epis) print(" ⚠️ DUE colonne di leva, e servono entrambe: la leva 'da 2024+' e' quella che uno") print(" sceglierebbe non sapendo del crash (realistica, ma CONFONDE peso e leva perche' non") print(" e' la stessa per tutte le righe); la leva COMUNE 0.50x isola l'effetto del peso.") print(f"\n {'w TP01':>7} {'leva (da 2024+)':>16} {hdr:>44} {'peggiore':>10} " f"{'peggiore @0.50x':>16} {'P(viv) 2024+':>13}") s2 = {} for w in W_GRID: Rp, Gp, _, segs = stress_panel(w) lv = tab[w]["rec"][0] cells, cells_c = [], [] for k, (a, b) in enumerate(segs): fu = splice_paths(base_fu, np.arange(a, b), seed=8290 + k) cells.append(J_of(Rp, Gp, base_ev, fu, lv)["p_alive"]) cells_c.append(J_of(Rp, Gp, base_ev, fu, 0.50)["p_alive"]) s2[w] = (lv, cells, min(cells_c), tab[w]["rec"][1]["p_alive"]) print(f" {w:>7.3f} {lv:>16.3f} " + " ".join(f"{c:>10.1%}" for c in cells) + f" {min(cells):>10.1%} {min(cells_c):>16.1%} " f"{tab[w]['rec'][1]['p_alive']:>13.1%}") bw_a = max(s2, key=lambda w: min(s2[w][1])) bw_c = max(s2, key=lambda w: s2[w][2]) print(f"\n argmax della colonna 'peggiore': leva-da-2024+ -> TP01 {bw_a:.3f} | " f"leva comune 0.50x -> TP01 {bw_c:.3f}") print(" ⚠️ LE DUE COLONNE DICONO COSE DIVERSE, E LA SECONDA E' A CAMPANA: a leva COMUNE la") print(f" sopravvivenza al crash SALE col peso di TP01 fino a {bw_c:.3f} (" f"{s2[0.0][2]:.1%} a TP01=0 -> {s2[bw_c][2]:.1%}) e poi ricade.") print(" **Quindi TP01 protegge davvero: a pari leva, il libro con piu' difensivo sopravvive") print(" di piu' al crash.** Il punto e' che quella protezione si compra anche abbassando la") print(" leva — ed e' esattamente cio' che misura §5. Alla leva che ciascun libro sceglierebbe") print(" (prima colonna) il vantaggio sparisce, perche' il libro senza TP01 la sceglie piu'") print(" bassa. E' la firma del **null del de-levering**, qui in veste di 'protezione dal") print(" crash' invece che di 'meno drawdown'.") tick("§4 S2 replay") # ----- replay deterministico del 2022 print("\n S2-bis — REPLAY DETERMINISTICO DEL 2022 (2022-04-01 -> 2022-12-31, 275 giorni).") print(" Non e' un modello: e' successo. Un conto funded HYRO da $100k che parte il 1° aprile") print(" 2022 con max-loss STATICO -6% e daily-loss -4%, alla leva scelta sul 2024+:") a22 = int(np.searchsorted(IDX, pd.Timestamp("2022-04-01", tz="UTC"))) b22 = int(np.searchsorted(IDX, pd.Timestamp("2023-01-01", tz="UTC"))) def replay22(w: float, lev: float) -> tuple: R, G, _ = crypto(w) off = deluck_offset(w) r = (R[a22:b22] - off) * lev g = G[a22:b22] * lev eq, mn, bust, why = 1.0, 1.0, None, "" for i in range(len(r)): low = eq * (1.0 + r[i] + g[i]) eq = eq * (1.0 + r[i]) mn = min(mn, low) if bust is None and (low < 0.94 or (r[i] + g[i]) < -0.04): bust = IDX[a22 + i].date() why = "daily-loss -4%" if (r[i] + g[i]) < -0.04 else "max-loss -6%" return eq - 1.0, mn - 1.0, bust, why, float((r + g).min()) print(f"\n {'w TP01':>7} {'leva':>6} {'rend. 9 mesi':>13} {'peggior giorno':>15} " f"{'min equity':>11} {'esito':>22}") for w in W_GRID: lv = tab[w]["rec"][0] ret, mn, bust, why, wd = replay22(w, lv) esito = "VIVO" if bust is None else f"BUSTATO {bust}" print(f" {w:>7.3f} {lv:>6.3f} {ret:>13.1%} {wd:>15.2%} {mn:>11.2%} {esito:>22}") print("\n Tutti VIVI, e non e' una buona notizia mal letta: **alle leve funded il 2022 non") print(" era una minaccia per questo libro** (min equity -1.4/-2.4% contro una barriera al") print(" -6%). Cio' che uccide un conto a barriera non e' il crash del sottostante: e' la") print(" regola a UN giorno su una giornata qualunque. Ecco fin dove si puo' spingere la leva:") print(f"\n {'w TP01':>7} {'leva max viva sul 2022':>23} {'rend. a quella leva':>20} " f"{'primo vincolo che morde':>25}") for w in W_GRID: lo, hi = 0.1, 8.0 for _ in range(18): mid = 0.5 * (lo + hi) if replay22(w, mid)[2] is None: lo = mid else: hi = mid ret, _, _, _, _ = replay22(w, lo) _, _, _, why, _ = replay22(w, hi) print(f" {w:>7.3f} {lo:>23.2f}x {ret:>20.1%} {why:>25}") print("\n 📌 La colonna 'leva max viva' CRESCE col peso di TP01 (fino a TP01=0.875; a TP01=1") print(" ricade perche' li' a mordere non e' piu' il giorno singolo ma la perdita CUMULATA).") print(" **L'assicurazione e' reale e si vede: piu' TP01 = piu' leva sopportabile nel 2022.**") print(" E accanto c'e' il prezzo: alla PROPRIA leva massima il libro senza TP01 porta a casa") print(" il +23.3% e quello a TP01 pieno il -5.9%. TP01 compra capacita' di leva, e la") print(" capacita' di leva non si converte in rendimento perche' cio' che TP01 toglie e'") print(" proprio il rendimento. E' il null del de-levering scritto in una riga sola.") print(" ⚠️ Cambia anche il VINCOLO che morde: sotto TP01 0.50 e' la regola a UN GIORNO,") print(" sopra e' il max-loss cumulato. Due libri diversi muoiono di due morti diverse, e una") print(" sola delle due lenti (l'accoppiata) vede la prima.") tick("§4 replay 2022") # ============================================================ §5. NULL DEL DE-LEVERING hr("§5. IL NULL DEL DE-LEVERING — la protezione di TP01 e' altro rispetto a meno leva?") print(" Il null classico ('esiste k<1 che da' lo stesso rischio con risultato migliore?') qui") print(" prende la forma giusta per una barriera: **frontiera ISO-SOPRAVVIVENZA**. Per ogni") print(f" peso si cerca la leva k(w) tale che P(vivo 12m | replay del sinistro PEGGIORE) = " f"{SURV_TARGET:.0%},") print(" poi si confronta il PAYOUT atteso sulla finestra normale a quella leva. Se un libro") print(" con meno TP01 e meno leva paga di piu' a pari sopravvivenza, la protezione di TP01") print(" e' de-levering; se paga di meno, TP01 compra qualcosa che la leva non compra.") _SP: dict = {} def p_alive_stress(w: float, lev: float, seg: tuple) -> float: """P(vivo 12m) col replay del sinistro dentro. Solo `funded_sim`: l'eval non entra nella definizione della frontiera, e calcolarlo raddoppierebbe il costo per niente.""" global N_EVAL N_EVAL += 1 k = (round(w, 6), seg) if k not in _SP: Rp, Gp, _, _ = stress_panel(w) _SP[k] = (Rp, Gp, splice_paths(base_fu, np.arange(seg[0], seg[1]), seed=8291)) Rp, Gp, fu = _SP[k] return PA.funded_sim(Rp, Gp, fu, lev, "HYRO", 100_000.0, "coupled")["p_alive"] print(f"\n {'w TP01':>7} {'k iso-surv':>11} {'P(viv|crash)':>13} {'E[pay/a] normale':>17} " f"{'P(pass) normale':>16} {'J normale':>11}") front = {} for w in W_GRID: Rp, Gp, _, segs = stress_panel(w) # episodio peggiore per QUESTO peso (il piu' severo, misurato) worst_seg = min(segs, key=lambda sg: np.prod(1.0 + Rp[sg[0]:sg[1]])) lo, hi = 0.05, 3.0 for _ in range(14): mid = 0.5 * (lo + hi) if p_alive_stress(w, mid, worst_seg) >= SURV_TARGET: lo = mid else: hi = mid k = lo R, G, _ = crypto(w) off = deluck_offset(w) o = J_of(R[i_rec:] - off, G[i_rec:], ev_r, fu_r, k) front[w] = (k, o) print(f" {w:>7.3f} {k:>11.3f} {p_alive_stress(w, k, worst_seg):>13.1%} " f"{o['e_payout']:>16,.0f}$ {o['p_pass']:>16.1%} {o['J']:>11.3f}") kbest = max(front, key=lambda w: front[w][1]["e_payout"]) jbest = max(front, key=lambda w: front[w][1]["J"]) print(f"\n argmax a ISO-SOPRAVVIVENZA: payout -> TP01 {kbest:.3f} | J -> TP01 {jbest:.3f}") print(" ⚠️ La leva k(w) e' tarata SU un episodio che nel campione di ottimizzazione non c'e':") print(" e' un esercizio con l'informazione del futuro. Serve a rispondere alla domanda del") print(" filone ('se sapessi che arriva, come mi allocherei?'), NON e' una politica.") tick("§5 frontiera iso-sopravvivenza") # ============================================================ §6. IL TERZO SLEEVE hr("§6. XS01 — la parte della domanda che NON e' rispondibile, e perche'") print(" L'ottimo di PROP-ALLOC mette il 50% su XS01, che **nasce nel 2024**: per lui non") print(" esiste NESSUN sinistro nel campione. Qui non lo si simula col bootstrap della sua") print(" finestra (sarebbe assumere che nel crash si comporti come in un giorno normale, che") print(" e' esattamente l'assunzione sotto esame). Si dichiarano TRE comportamenti e si") print(" guarda se la conclusione dipende da quale:") print(" A) FLAT nel crash (il gate di dispersione lo spegne) -> generosa") print(" B) la sua PEGGIORE finestra di 30g del 2024+, contigua e in ordine -> severa") print(" C) uno shock una-tantum di taglia X sul giorno peggiore del crash -> si cerca") print(" la X di ROTTURA, cioe' quanto grande deve essere per portare P(vivo) sotto 50%") xs = pc.xsec_daily_tuples() xsR = xs["R"].values xsG = np.minimum(xs["m"].values, xsR) - xsR lxs = np.log1p(xsR) cxs = np.concatenate(([0.0], np.cumsum(lxs))) wxs30 = np.expm1(cxs[EPI_LEN:] - cxs[:-EPI_LEN]) t_xw = int(np.argmin(wxs30)) print(f"\n XS01: {len(xs)} giorni {xs.index[0].date()} -> {xs.index[-1].date()}; peggior " f"giorno {xsR.min():.2%}, peggior minimo {xs['m'].min():.2%}") print(f" peggior finestra di {EPI_LEN}g di XS01: {xs.index[t_xw].date()} -> " f"{xs.index[t_xw+EPI_LEN-1].date()}, cumulato {wxs30[t_xw]:.1%} " f"(ipotesi B). Per confronto la coppia crypto nel COVID fa -5.4% a TP01 pieno.") print(" 📌 Il numero che conta e' questo: **la peggior finestra di 30g mai vista da XS01 vale") print(f" {wxs30[t_xw]:.1%}**. Se nel crash XS01 facesse cosi', al 50% di peso porterebbe") print(f" {0.5*wxs30[t_xw]:.1%} al libro. La domanda non e' se XS01 sopravvive a cio' che ha") print(" gia' visto, e' se il suo comportamento nel crash somiglia a cio' che ha gia' visto —") print(" e su questo il campione non ha NIENTE da dire.") # ⚠ le ultime tre righe hanno XS01 FISSO al 50%: senza quel controllo si confonderebbe # "piu' TP01" con "meno XS01", che e' un'altra domanda. W3 = [(0.75, 0.25, 0.00), (0.375, 0.625, 0.00), (0.50, 0.25, 0.25), (0.125, 0.375, 0.50), (0.25, 0.25, 0.50), (0.375, 0.125, 0.50)] com = xs.index.intersection(IDX[i_rec:]) xs_al = xs.reindex(com) LEV_COM = 0.625 # leva comune per il blocco a XS01=50% (la minima fra le tre righe) print(f"\n {'TP/SKH/XS':>16} {'ipotesi XS':>12} {'leva':>6} {'J normale':>10} " f"{'P(viv|replay pegg.)':>21} {'idem @0.625x':>14} {'shock di rottura':>18}") for cfg in W3: wtp, wsk, wxs = cfg base = wtp + wsk wtp_n = wtp / base if base > 0 else 0.0 Rc, Gc, Ic = crypto(wtp_n) offc = deluck_offset(wtp_n) m1 = Ic.isin(com) Rn = base * (Rc[m1] - offc) + wxs * xs_al["R"].values Gn = base * Gc[m1] + wxs * (np.minimum(xs_al["m"].values, xs_al["R"].values) - xs_al["R"].values) nn = len(Rn) evn = PA.boot_idx(nn, HORIZON, N_PATHS, seed=8300) fun = PA.boot_idx(nn, HORIZON, N_PATHS, seed=8301) lv, on = best_lev(Rn, Gn, evn, fun) bfu = PA.boot_idx(nn, HORIZON, N_PATHS, seed=8302) lab = f"{int(round(wtp*100))}/{int(round(wsk*100))}/{int(round(wxs*100))}" def build(hyp: str, shock: float = 0.0, lev_: float | None = None): segsR, segsG = [], [] for t in epis: rc = base * (crypto(wtp_n)[0][t:t + EPI_LEN] - offc) gc = base * crypto(wtp_n)[1][t:t + EPI_LEN] if wxs > 0: if hyp == "A-flat": rx = np.zeros(EPI_LEN); gx = np.zeros(EPI_LEN) elif hyp == "B-peggiore": rx = xsR[t_xw:t_xw + EPI_LEN].copy() gx = xsG[t_xw:t_xw + EPI_LEN].copy() else: rx = np.zeros(EPI_LEN); gx = np.zeros(EPI_LEN) rx[int(np.argmin(rc))] = -shock segsR.append(rc + wxs * rx); segsG.append(gc + wxs * gx) else: segsR.append(rc); segsG.append(gc) Rp = np.concatenate([Rn] + segsR) Gp = np.concatenate([Gn] + segsG) sg = [(nn + k * EPI_LEN, nn + (k + 1) * EPI_LEN) for k in range(len(epis))] worst = 1.0 for k, g2 in enumerate(sg): fu = splice_paths(bfu, np.arange(g2[0], g2[1]), seed=8320 + k) worst = min(worst, J_of(Rp, Gp, evn, fu, lv if lev_ is None else lev_)["p_alive"]) return worst if wxs == 0: print(f" {lab:>16} {'n/a (no XS)':>12} {lv:>6.3f} {on['J']:>10.3f} " f"{build('A-flat'):>21.1%} {build('A-flat', lev_=LEV_COM):>14.1%} {'-':>18}") continue for hyp in ("A-flat", "B-peggiore"): print(f" {lab:>16} {hyp:>12} {lv:>6.3f} {on['J']:>10.3f} " f"{build(hyp):>21.1%} {build(hyp, lev_=LEV_COM):>14.1%} {'-':>18}") lo, hi = 0.0, 0.40 # shock di rottura: bisezione su P(vivo) = 50% for _ in range(9): mid = 0.5 * (lo + hi) if build("C-shock", mid) >= 0.50: lo = mid else: hi = mid lo2, hi2 = 0.0, 0.40 for _ in range(9): mid = 0.5 * (lo2 + hi2) if build("C-shock", mid, lev_=LEV_COM) >= 0.50: lo2 = mid else: hi2 = mid print(f" {lab:>16} {'C-shock':>12} {lv:>6.3f} {on['J']:>10.3f} {'(50% per def.)':>21} " f"{'(50% per def.)':>14} {-lo:>17.1%} / {-lo2:.1%} @0.625x") print("\n Lettura: 'shock di rottura' = il crollo di UN giorno su XS01 che porta P(vivo) al") print(" 50% quando il crash arriva. Piu' e' piccolo, piu' l'allocazione XS01-pesante e' a un") print(" giorno di coda dalla morte. Non e' una previsione: e' la taglia della scommessa.") print(" ⚠️ Le ultime tre righe hanno XS01 FISSO al 50% e differiscono SOLO per come si") print(" divide il resto fra TP01 e SKH01: e' li' che si legge se TP01 fa da zavorra contro") print(" l'ignoto di XS01. **La colonna @0.625x e' quella da leggere**, perche' a leva libera") print(" la riga con meno TP01 sceglie una leva piu' bassa e vince per quello.") tick("§6 XS01") # ============================================================ §7. BANDA + VERDETTO hr("§7. SENSIBILITA' AL DE-LUCK — l'ORDINE fra i pesi sopravvive alla banda?") print(" La banda d'ancora non e' ri-misurabile sotto questa lente (costo, dichiarato anche da") print(" PROP-ALLOC). Si applica il fattore misurato x0.89 e si controlla l'unica cosa che") print(" conta: se l'argmax sui pesi si muove fra 1.00 (nessuna correzione) e 0.60 (la stima") print(" a occhio del 25/07, poi ri-misurata troppo severa).") print(f"\n {'fattore':>9} {'argmax 2019+':>14} {'argmax 2024+':>14} {'argmax SOLO-SIN':>16} " f"{'J(75/25) 2019+':>16} {'J(38/62) 2019+':>16}") for fct in (1.00, 0.89, 0.60): best_f = best_r = best_s = None vals = {} for w in W_GRID: R, G, _ = crypto(w) o = deluck_offset(w, fct) Rf = R - o j1 = best_lev(Rf, G, ev_f, fu_f)[1]["J"] j2 = best_lev(Rf[i_rec:], G[i_rec:], ev_r, fu_r)[1]["J"] j3 = best_lev(Rf, G, ev_s, fu_s)[1]["J"] vals[w] = (j1, j2, j3) best_f = max(vals, key=lambda w: vals[w][0]) best_r = max(vals, key=lambda w: vals[w][1]) best_s = max(vals, key=lambda w: vals[w][2]) print(f" {fct:>9.2f} {best_f:>14.3f} {best_r:>14.3f} {best_s:>16.3f} " f"{vals[0.75][0]:>16.3f} {vals[0.375][0]:>16.3f}") hr("§8. I GATE — cosa e' girato, cosa NON e' girato e perche'") print(" Questo filone non propone una strategia nuova: propone (o rifiuta) un'ALLOCAZIONE per") print(" un conto che non esiste. I gate del progetto vanno dichiarati uno per uno.") try: sys.path.insert(0, str(ROOT / "scripts" / "research" / "alt")) import altlib as A # noqa: E402 for lab, w in (("LIVE 75/25", 0.75), ("OPT 38/62", 0.375)): R = crypto(w)[0] - deluck_offset(w) d = pd.Series(R, index=IDX) n_act = int((np.abs(R) > 1e-12).sum()) n_los = int((R[np.abs(R) > 1e-12] < 0).sum()) f = A.implausible_sharpe(d, n_trades=n_act, n_losing_trades=n_los) print(f" implausible_sharpe {lab:>11}: implausible={f['implausible']} " f"(Sharpe {f['sharpe']:.2f}, maxDD {f['maxdd']:.1%}, quota barre in perdita " f"{f['loss_frac']:.1%}, Calmar {f['calmar']:.2f}) {f['reasons']}") except Exception as exc: # pragma: no cover print(f" implausible_sharpe: NON GIRATO ({exc})") print(" marginal_vs_tp01 / study_family_honest / deflated_sharpe : **NON GIRATI, e non") print(" sono applicabili** — non c'e' uno stream candidato da confrontare con TP01: gli") print(" stream sono i due sleeve gia' ammessi, e l'oggetto e' il loro PESO. Il gate dei") print(" pesi del progetto e' `weights_tilt_null`, che pero' giudica lo SHARPE del libro") print(" di RICERCA: qui l'obiettivo e' J su una barriera, e il conto e' separato.") print(" (PROP-ALLOC lo ha girato: gate_pass False. Non cambia nulla qui: nessuna") print(" proposta di questo file tocca il libro live.)") print(" causality_ok : **non girato, non applicabile** — nessun segnale nuovo; le serie") print(" sono quelle di produzione, gia' verificate leak-free a monte.") print(" anchor_luck_band : **non ri-misurata** (costo, come in PROP-ALLOC). Si e' girata") print(" la sensibilita' al FATTORE di de-luck (§7), che e' meno: dice se l'ORDINE regge,") print(" non qual e' la banda. Distorsione dichiarata: la lente e' l'ancora CANONICA e") print(" SKH01 e' lo sleeve con la fortuna d'ancora piu' grande da restituire (26/07)") print(" -> **la distorsione e' A FAVORE della conclusione di questo file**, cioe' del") print(" peso basso su TP01. E' il caveat piu' importante del filone.") print(" null del de-levering : **GIRATO** (§5, frontiera iso-sopravvivenza) — e superato") print(" NEL VERSO OPPOSTO a quello atteso: e' la protezione di TP01 a essere replicabile") print(" con meno leva, e meglio.") hr("§9. LA RISPOSTA OPERATIVA") print(f" valutazioni MC totali di questo file: **{N_EVAL}** " f"(ognuna = {N_PATHS} percorsi di eval + {N_PATHS} di funded)") print(f"\n {'lente':>26} {'argmax w TP01':>14} {'J argmax':>10} {'J a 0.75':>10} " f"{'J a 0.375':>11}") for lab, key in (("2019+ (contiene il 2022)", "full"), ("2024+ (PROP-ALLOC)", "rec"), ("SOLO-SINISTRO", "sin")): am = max(tab, key=lambda w: tab[w][key][1]["J"]) print(f" {lab:>26} {am:>14.3f} {tab[am][key][1]['J']:>10.3f} " f"{tab[0.75][key][1]['J']:>10.3f} {tab[0.375][key][1]['J']:>11.3f}") print("\n ESISTE UN PESO DI COMPROMESSO? Il criterio dichiarato: (a) dentro il 5% del") print(" massimo di J sulla finestra 2024+ (la 'regione robusta' del funded) E (b) dentro il") print(" 5% del massimo di J sulla finestra 2019+, che il sinistro ce l'ha dentro.") jr_max = max(tab[w]["rec"][1]["J"] for w in W_GRID) jf_max = max(tab[w]["full"][1]["J"] for w in W_GRID) js_max = max(tab[w]["sin"][1]["J"] for w in W_GRID) print(f"\n {'w TP01':>7} {'J 2024+':>9} {'entro 5%':>9} {'J 2019+':>9} {'entro 5%':>9} " f"{'J solo-sin':>11} {'entro 5%':>9} {'compromesso':>13}") ok_both = [] for w in W_GRID: a, b, c = (tab[w]["rec"][1]["J"], tab[w]["full"][1]["J"], tab[w]["sin"][1]["J"]) oa, ob, oc = a >= 0.95 * jr_max, b >= 0.95 * jf_max, c >= 0.95 * js_max if oa and ob: ok_both.append(w) print(f" {w:>7.3f} {a:>9.3f} {('si' if oa else 'no'):>9} {b:>9.3f} " f"{('si' if ob else 'no'):>9} {c:>11.3f} {('si' if oc else 'no'):>9} " f"{('SI' if (oa and ob) else '-'):>13}") print(f"\n -> pesi che soddisfano (a) E (b): {ok_both if ok_both else 'NESSUNO'}") print(f" il libro live 75/25 e' {'dentro' if 0.75 in ok_both else 'FUORI'} da questa regione.") print("\n LA RISPOSTA, IN QUATTRO RIGHE") print(" 1) La premessa del caveat e' sbagliata nella FORMA: il 2024+ un crash ce l'ha (b&h") print(" -60% di drawdown, -39.6% in 20 giorni). Manca la TAGLIA, ~2x sulla coda veloce.") print(" 2) Il compromesso ESISTE e non e' un compromesso: lo stesso peso (TP01 ~0.375) e'") print(" l'argmax sulla finestra con il 2022 dentro E su quella senza. Il caveat di") print(" PROP-ALLOC e' onesto come cautela e **falsificato come previsione**.") print(" 3) TP01 protegge davvero (a pari leva sopravvive di piu' al crash, e alza la leva") print(" sopportabile nel 2022 da 0.93x a 2.20x) ma la stessa protezione si compra piu' a") print(" buon mercato ABBASSANDO LA LEVA: a iso-sopravvivenza il payout e' massimo a") print(" TP01 0.125-0.250 e cala monotonamente da li'. **7ª occorrenza del null del") print(" de-levering**, in una veste nuova: 'protezione dal crash' invece di 'meno DD'.") print(" 4) Il rischio vero dell'ottimo di PROP-ALLOC NON e' TP01 sotto-pesato: e' XS01 al") print(" 50% con un comportamento nel crash che il campione non contiene. Un solo giorno") print(" a ~-10% su XS01 dimezza la sopravvivenza quando il crash arriva.") print(" ⚠️ IPOTESI MIA, NATA E REFUTATA IN SESSIONE: avevo scritto che TP01 fa da") print(" ZAVORRA contro quell'ignoto (a XS01 25% serviva uno shock -12.3% contro -8.0% a") print(" XS01 50%). Il controllo a **XS01 FISSO al 50% e leva comune** lo smonta: lo shock") print(" di rottura e' -10.1 / -10.2 / -10.2% muovendo TP01 da 12% a 38%, cioe' INVARIANTE,") print(" e la sopravvivenza sotto l'ipotesi severa CALA (74.1 -> 72.6 -> 64.7%). Cio' che") print(" protegge dall'ignoto di XS01 e' **meno XS01**, non piu' TP01: il primo numero") print(" confrontava due pesi di XS01 diversi e attribuiva a TP01 il merito dell'altro.") print("\n ⚠️ COSA MI SMENTIREBBE: un crash in cui SKH01 NON si gira short in tempo (gap") print(" notturno oltre lo stop, o venue fermo). Il campione ne ha quattro e in tutti e") print(" quattro SKH01 ha girato. Basterebbe un episodio in cui SKH01 perde quanto il") print(" sottostante per rovesciare §2 e §5 — e con n=4 quell'episodio non e' improbabile.") print("\n" + "=" * 106) print(f" fatto in {time.time()-t0:.0f}s | valutazioni: {N_EVAL}") print("=" * 106) if __name__ == "__main__": main()