#!/usr/bin/env python """r0703_vrpimp_sizing.py — FILONE 3: SIZING ANTI-ROVINA per lo sleeve short-vol defined-risk. DOMANDA: ALB-B ha mostrato che il sizing 1->4 di Albimarini porta alla rovina (1998/2002/2020). Qual e' la politica di sizing OTTIMA per uno sleeve put-credit-spread ETH su Deribit a 2.000-5.000$? MOTORE (riusato, non riscritto): la struttura VRP01 canonica (put credit spread 7g, short delta -0.28 / long -0.10, gate CANONICO vrp>0 + ivr>=0.30 + crash-skip 0.90 — NON riottimizzato, lezione 2026-07-01) prezzata col motore ALB-A (r0702_alb_structure): BS flat su DVOL reale, fee Deribit PER GAMBA 0.03% notional cap 12.5% premio + delivery 0.015% su ITM. ETH-only: a 2-5k solo ETH e' granulare (BTC min 0.1 = margine ~$470+, diario r0702_capital_scaling). Rendimenti PER-TRADE su MARGINE (= max loss defined-risk, width - credito): e' il capitale davvero a rischio, e rende la rovina ben definita (perdita massima = 100% del margine impegnato + fee). POLITICHE TESTATE (il sizing NON cambia l'edge: cambia CAGR/DD/P(rovina) — lo Sharpe del flusso sottostante e' invariante di scala, quindi qui NON si caccia Sharpe, si mappa la frontiera): (a) frazione fissa del capitale q in {5,10,12,15,20,25}% (margine impegnato/equity per settimana) (b) vol-scaled: q ∝ 1/DVOL (riferimento = mediana ESPANDENTE causale della DVOL) (c) anti-streak: q ridotta dopo N vittorie consecutive (l'OPPOSTO di Albimarini) (d) Kelly frazionario con stima ONESTA delle code: distribuzione empirica per-trade IS (pre-2025) POOLED sulla banda skew f in {0.6,0.8,1.0,1.3} + coda sintetica (full-loss -102% con probabilita' extra = P(move settimanale <= strike long | storia ETH 2019-26 CERTIFICATA) - freq empirica) (e) [riferimento negativo] Albimarini 1->4: size crescente con lo streak di vittorie METRICHE: CAGR, maxDD, worst-week, P(rovina -50% / -80% | 5 anni, bootstrap a BLOCCHI L=13 settimane che preserva il clustering di regime del gate), a C=2.000 e 5.000$ con granularita' REALE degli ordini (size INTERE di spread min 1 ETH/gamba; margine/spread dal dato della settimana stessa — la nota task "0.1 ETH" e' incoerente coi numeri citati $66-76 = spread da 1 ETH di r0702_capital_ scaling sez.5; la sensitivity 0.1 ETH e' riportata a parte). Confronto esplicito col 12% del book (VRP01 @12% peso = margine settimanale ~12% del conto, stessa convenzione del conteggio spread del diario capital-scaling). REGOLE ONORATE: hold-out 2025-26 MAI usato per selezionare (selezione overlay su MAR in-sample); NULL DEL DE-LEVERING esplicito (ogni claim di riduzione DD degli overlay b/c/d confrontato con la frazione fissa che raggiunge lo stesso DD); banda f {0.6,0.8,1.0,1.3}; banda d'ancora sulle 7 fasi della cadenza settimanale; niente '7D'-origin (qui la cadenza e' a passi di indice, non resample). REGOLA STANDING INVARIATA: niente short-vol da modello in deploy — esito = conoscenza/frontiera. uv run python scripts/research/r0703_vrpimp_sizing.py """ from __future__ import annotations import sys from pathlib import Path ROOT = Path("/opt/docker/PythagorasGoal") sys.path.insert(0, str(ROOT / "scripts" / "research" / "alt")) sys.path.insert(0, str(ROOT)) import numpy as np import pandas as pd import altlib as al # noqa: E402 (harness di progetto; usato per storia ETH certificata + DSR) from scripts.research.options_vrp_lab import bs_put, strike_from_delta, load_series # noqa: E402 from scripts.research.options_vrp_v2 import _ivrank, _rv30 # noqa: E402 HOLDOUT = pd.Timestamp("2025-01-01", tz="UTC") WK = 365.25 / 7.0 TENOR = 7 F_SWEEP = (0.6, 0.8, 1.0, 1.3) CAPITALS = (2000.0, 5000.0) MIN_ETH = 1.0 # min size Deribit ETH per gamba (r0702_capital_scaling sez.5) Q_CAP = 0.95 # mai impegnare piu' del 95% dell'equity (loss max ~102% del margine) FULL_LOSS = -1.02 # full-loss defined-risk incl. fee (empirico: worst -1.018) RUIN_LVL = (0.50, 0.80) # rovina = equity sotto (1-lvl)*E0 ... definita come perdita >= lvl SEED = 20260703 # =========================================================================== # MOTORE PER-TRADE (struttura VRP01, fee ALB-A per gamba, ritorni su MARGINE) # =========================================================================== def trade_records(f: float = 1.0, phase: int = 0) -> pd.DataFrame: """Vendita settimanale ETH put credit spread -0.28/-0.10, gate canonico. Una riga per settimana di cadenza (phase = fase d'ancora 0..6): active, r (ritorno su margine), margin ($ per spread da 1 ETH), sig (DVOL a decisione), kl_dist (distanza strike long).""" J = load_series("ETH") px = J["px"].values dv = J["dvol"].values / 100.0 idx = J.index n = len(px) T = TENOR / 365.25 rows = [] i = 60 + phase while i + TENOR < n: S0 = px[i] sig = dv[i] rv = _rv30(px, i) ivr = _ivrank(dv, i) skip = ((not np.isnan(rv) and (sig - rv) <= 0) or (not np.isnan(ivr) and (ivr < 0.30 or ivr > 0.90))) if skip: rows.append(dict(date=idx[i + TENOR], active=0, r=0.0, margin=0.0, sig=sig, win=0, kl_dist=np.nan)) i += TENOR continue Ks = strike_from_delta(S0, T, sig, -0.28) Kl = strike_from_delta(S0, T, sig, -0.10) ps = bs_put(S0, Ks, T, sig) * f pl = bs_put(S0, Kl, T, sig) * f credit = ps - pl width = Ks - Kl margin = width - credit # margine defined-risk Deribit ~ max loss S1 = px[i + TENOR] payoff = max(0.0, Ks - S1) - max(0.0, Kl - S1) fee = min(0.0003 * S0, 0.125 * max(ps, 0.0)) + min(0.0003 * S0, 0.125 * max(pl, 0.0)) deliv = (min(0.00015 * S1, 0.125 * max(0.0, Ks - S1)) + min(0.00015 * S1, 0.125 * max(0.0, Kl - S1))) pnl = credit - payoff - fee - deliv # $ per spread da 1 ETH rows.append(dict(date=idx[i + TENOR], active=1, r=pnl / margin, margin=margin, sig=sig, win=int(pnl > 0), kl_dist=Kl / S0 - 1.0)) i += TENOR return pd.DataFrame(rows) # =========================================================================== # POLITICHE DI SIZING — q = frazione di equity impegnata come margine questa settimana # state: sig (DVOL decisione), ref (mediana espandente DVOL), streak (vittorie consecutive) # =========================================================================== def pol_fixed(frac): return lambda st: frac def pol_volscaled(frac, lo=0.5, hi=2.0): """q = frac * clip(DVOL_ref/DVOL, lo, hi): meno size quando la vol e' alta. Causale.""" return lambda st: frac * float(np.clip(st["ref"] / max(st["sig"], 1e-9), lo, hi)) def pol_antistreak(frac, N, mult): """Dopo >=N vittorie consecutive riduci a frac*mult (l'opposto di Albimarini).""" return lambda st: frac * (mult if st["streak"] >= N else 1.0) def pol_alb14(frac): """Riferimento NEGATIVO: Albimarini 1->4, size crescente con le vittorie consecutive.""" return lambda st: frac * min(1.0 + st["streak"], 4.0) def simulate(rec: pd.DataFrame, qfun, E0: float, granular: bool = True, min_eth: float = MIN_ETH, stress_p: float = 0.0, rng=None, q_series=None) -> dict: """Applica la politica al flusso settimanale. granular=True -> size intere di spread (floor(budget/margine_spread)); False -> frazione continua (lens frontiera). stress_p>0: ogni settimana attiva ha prob extra di full-loss sintetico (coda stress). q_series: array di q per riga (bypassa qfun — usato dal null a piazzamento casuale).""" E = E0 eq, rets, dates = [], [], [] sig_hist: list[float] = [] streak = 0 halted = 0 for t, row in enumerate(rec.itertuples(index=False)): ref = float(np.median(sig_hist)) if len(sig_hist) >= 20 else row.sig st = dict(sig=row.sig, ref=ref, streak=streak) sig_hist.append(row.sig) r_wk = 0.0 if row.active and E > 0: q_raw = q_series[t] if q_series is not None else qfun(st) q = float(np.clip(q_raw, 0.0, Q_CAP)) unit = row.margin * min_eth if granular: n_spread = int((q * E) // unit) if unit > 0 else 0 committed = n_spread * unit if q > 0 and n_spread == 0: halted += 1 else: committed = q * E r_trade = row.r if stress_p > 0.0 and rng is not None and rng.random() < stress_p: r_trade = FULL_LOSS r_wk = committed * r_trade / E if E > 0 else 0.0 E = E + committed * r_trade if row.active: streak = streak + 1 if (row.win and row.r == row.r) else 0 if not row.win: streak = 0 rets.append(r_wk) eq.append(max(E, 0.0)) dates.append(row.date) return dict(dates=pd.DatetimeIndex(dates), eq=np.asarray(eq), rets=np.asarray(rets), halted=halted, E_end=E) def path_metrics(dates, rets) -> dict: r = np.asarray(rets, float) if len(r) < 3: return dict(cagr=0.0, dd=0.0, worst=0.0, sh=0.0) eq = np.cumprod(1.0 + r) pk = np.maximum.accumulate(eq) yrs = len(r) / WK cagr = eq[-1] ** (1.0 / yrs) - 1.0 if eq[-1] > 0 else -1.0 sh = float(r.mean() / r.std() * np.sqrt(WK)) if r.std() > 0 else 0.0 return dict(cagr=float(cagr), dd=float(np.max((pk - eq) / pk)), worst=float(r.min()), sh=sh) def full_hold(sim) -> tuple[dict, dict]: m_full = path_metrics(sim["dates"], sim["rets"]) mask = sim["dates"] >= HOLDOUT m_hold = path_metrics(sim["dates"][mask], sim["rets"][mask]) return m_full, m_hold # =========================================================================== # KELLY con code oneste + bootstrap a blocchi # =========================================================================== def kelly_star(rs: np.ndarray, p_extra: float, tail: float = FULL_LOSS) -> float: """argmax_q E[log(1+q r)] su mistura: con prob p_extra r=tail, altrimenti empirico.""" qs = np.linspace(0.0, Q_CAP, 191) best_q, best_g = 0.0, -np.inf for q in qs: vals = np.log1p(q * rs) g = (1.0 - p_extra) * vals.mean() + p_extra * np.log1p(q * tail) if np.isfinite(g) and g > best_g: best_g, best_q = g, q return float(best_q) def synthetic_tail_prob(rec: pd.DataFrame) -> tuple[float, float, float]: """p_extra = P(move 7g ETH <= distanza strike long | storia certificata 2019-26) - freq empirica di full-loss nel flusso gated. Ritorna (p_extra, p_uncond, p_emp).""" d1 = al.get("ETH", "1d") px = d1["close"].values.astype(float) wk_mv = px[TENOR:] / px[:-TENOR] - 1.0 # mosse 7g overlapping, 2019->oggi act = rec[rec.active == 1] kl_med = float(act["kl_dist"].median()) p_uncond = float(np.mean(wk_mv <= kl_med)) p_emp = float((act["r"] <= -0.9).mean()) return max(0.0, p_uncond - p_emp), p_uncond, p_emp def block_bootstrap_ruin(rec: pd.DataFrame, qfun, C: float, npaths: int = 1000, horizon_w: int = 261, L: int = 13, stress_p: float = 0.0, seed: int = SEED, needs_ref: bool = False) -> dict: """P(rovina | 5 anni): bootstrap a blocchi circolari di L settimane dal flusso storico (preserva clustering del gate/regime), politica applicata con granularita' REALE a C. needs_ref: calcola la mediana espandente della DVOL solo per le politiche che la usano.""" rows = list(rec.itertuples(index=False)) n = len(rows) rng = np.random.default_rng(seed) ruin = np.zeros((npaths, len(RUIN_LVL)), bool) dd30 = np.zeros(npaths, bool) cagrs = np.zeros(npaths) for p in range(npaths): starts = rng.integers(0, n, size=horizon_w // L + 1) seq = [rows[(s + k) % n] for s in starts for k in range(L)][:horizon_w] E = C peak = C minE = C sig_hist: list[float] = [] streak = 0 maxdd = 0.0 for row in seq: if needs_ref: ref = float(np.median(sig_hist)) if len(sig_hist) >= 20 else row.sig sig_hist.append(row.sig) else: ref = row.sig if row.active and E > 0: q = float(np.clip(qfun(dict(sig=row.sig, ref=ref, streak=streak)), 0.0, Q_CAP)) unit = row.margin * MIN_ETH n_spread = int((q * E) // unit) if unit > 0 else 0 r_trade = row.r if stress_p > 0.0 and rng.random() < stress_p: r_trade = FULL_LOSS E = E + n_spread * unit * r_trade streak = streak + 1 if (row.win and r_trade > 0) else 0 peak = max(peak, E) minE = min(minE, E) maxdd = max(maxdd, (peak - E) / peak if peak > 0 else 1.0) for j, lvl in enumerate(RUIN_LVL): ruin[p, j] = minE <= C * (1.0 - lvl) dd30[p] = maxdd >= 0.30 cagrs[p] = (max(E, 0.0) / C) ** (1.0 / (horizon_w / WK)) - 1.0 if E > 0 else -1.0 return dict(p_ruin50=float(ruin[:, 0].mean()), p_ruin80=float(ruin[:, 1].mean()), p_dd30=float(dd30.mean()), cagr_med=float(np.median(cagrs)), cagr_p10=float(np.percentile(cagrs, 10))) # =========================================================================== # REPORT # =========================================================================== def hist_row(label, rec, qfun, C, granular=True): sim = simulate(rec, qfun, C, granular=granular) mf, mh = full_hold(sim) return sim, mf, mh, (f" {label:<34} {mf['cagr']*100:>+6.1f}% {mf['dd']*100:>5.1f}% " f"{mf['worst']*100:>+6.1f}% {mf['sh']:>5.2f} | {mh['cagr']*100:>+6.1f}% " f"{mh['dd']*100:>5.1f}% | halt {sim['halted']:>3}") HDR = (f" {'politica':<34} {'CAGR-F':>7} {'DD-F':>5} {'worst':>6} {'Sh-F':>5} | " f"{'CAGR-H':>7} {'DD-H':>5} | settimane-0-spread") def main(): print("=" * 112) print(" R0703 VRPIMP-SIZING — sizing anti-rovina dello sleeve short-vol defined-risk ETH (2-5k, Deribit)") print(" Flusso: put credit spread ETH 7g -0.28/-0.10, gate canonico, motore ALB-A (fee per gamba), 2021-2026.") print(" Rendimenti per-trade su MARGINE (=max loss). Il sizing non cambia lo Sharpe del flusso: mappa la") print(" frontiera CAGR-DD-P(rovina). REGOLA STANDING INVARIATA: niente short-vol da modello in deploy.") print("=" * 112) rec = trade_records(f=1.0, phase=0) act = rec[rec.active == 1] is_act = act[act.date < HOLDOUT] print(f"\n flusso f=1.0 ancora-0: {len(rec)} settimane, {len(act)} attive ({len(act)/len(rec)*100:.0f}%), " f"win {act['win'].mean()*100:.0f}%, r-margine medio {act['r'].mean()*100:+.1f}% " f"(mediana {act['r'].median()*100:+.1f}%), full-loss r<=-0.9: {int((act['r']<=-0.9).sum())}") print(f" margine $/spread(1 ETH): mediana ${act['margin'].median():.0f}, ultimo ${act['margin'].iloc[-1]:.0f} " f"(la banda $66-76 del diario = spot/DVOL del run r0702)") # streak diagnostics: gli streak di vittorie predicono la prossima perdita? wins = act["win"].values streaks_before = [] s = 0 for w in wins: streaks_before.append(s) s = s + 1 if w else 0 sb = np.asarray(streaks_before) print("\n DIAGNOSTICA ANTI-STREAK — P(perdita | vittorie consecutive precedenti):") base = 1.0 - act["win"].mean() for N in (0, 2, 3, 5): m = sb >= N if m.sum() >= 8: print(f" streak>={N}: P(loss)={1.0-wins[m].mean():.2f} su n={int(m.sum())} (base {base:.2f})") # ---------------------------------------------------------------- (1) frontiera fisso print("\n" + "=" * 112) print(" (1) FRONTIERA DD-CAGR — frazione fissa (IL NULL DEL DE-LEVERING), f=1.0, storico 2021-26") print(" 'cont' = frazione continua (lens); '2k'/'5k' = granularita' REALE (spread interi da 1 ETH)") print("=" * 112) fracs = (0.02, 0.05, 0.08, 0.10, 0.12, 0.15, 0.20, 0.25, 0.35, 0.50) frontier = {} # frac -> (dd_full, cagr_full) continuo print(f" {'q':>5} | {'cont: CAGR-F':>12} {'DD-F':>6} {'Sh-F':>5} {'CAGR-H':>7} | " f"{'2k: CAGR-F':>10} {'DD-F':>6} {'halt':>4} | {'5k: CAGR-F':>10} {'DD-F':>6} {'halt':>4}") for q in fracs: sc = simulate(rec, pol_fixed(q), 2000.0, granular=False) mfc, mhc = full_hold(sc) frontier[q] = (mfc["dd"], mfc["cagr"]) s2 = simulate(rec, pol_fixed(q), 2000.0, granular=True) mf2, _ = full_hold(s2) s5 = simulate(rec, pol_fixed(q), 5000.0, granular=True) mf5, _ = full_hold(s5) tag = " <- book 12%" if abs(q - 0.12) < 1e-9 else "" print(f" {q*100:>4.0f}% | {mfc['cagr']*100:>+11.1f}% {mfc['dd']*100:>5.1f}% {mfc['sh']:>5.2f} " f"{mhc['cagr']*100:>+6.1f}% | {mf2['cagr']*100:>+9.1f}% {mf2['dd']*100:>5.1f}% {s2['halted']:>4} | " f"{mf5['cagr']*100:>+9.1f}% {mf5['dd']*100:>5.1f}% {s5['halted']:>4}{tag}") print(" ('halt' = settimane attive in cui il budget q*E non compra NEMMENO 1 spread -> size 0:") print(" a 2k le frazioni <=10% saltano trade nelle ere a margine alto — la granularita' distorce il basso)") # ---------------------------------------------------------------- (2) overlay b/c — selezione IS print("\n" + "=" * 112) print(" (2) OVERLAY (b) VOL-SCALED e (c) ANTI-STREAK — selezione SOLO in-sample (MAR pre-2025, continuo)") print("=" * 112) rec_is = rec[rec.date < HOLDOUT].reset_index(drop=True) def is_mar(qfun): sim = simulate(rec_is, qfun, 2000.0, granular=False) m = path_metrics(sim["dates"], sim["rets"]) return m["cagr"] / max(m["dd"], 1e-9) cells_b = [(fb, lo, hi) for fb in (0.08, 0.12, 0.16) for lo, hi in ((0.5, 1.5), (0.5, 2.0))] best_b = max(cells_b, key=lambda c: is_mar(pol_volscaled(*c))) cells_c = [(0.12, N, m) for N in (2, 3, 5) for m in (0.25, 0.5)] best_c = max(cells_c, key=lambda c: is_mar(pol_antistreak(*c))) print(f" celle esplorate: vol-scaled {len(cells_b)}, anti-streak {len(cells_c)} " f"(+6 frazioni fisse dichiarate a priori, +3 Kelly = {len(cells_b)+len(cells_c)+9} totali)") print(f" best IS vol-scaled: base={best_b[0]:.0%} clip[{best_b[1]},{best_b[2]}] | " f"best IS anti-streak: q=12% N={best_c[1]} mult={best_c[2]}") print("\n" + HDR) named = {} for label, qf in ( ("(a) FISSO 12% (book)", pol_fixed(0.12)), (f"(b) VOL-SCALED {best_b[0]:.0%} clip[{best_b[1]},{best_b[2]}]", pol_volscaled(*best_b)), (f"(c) ANTI-STREAK 12% N={best_c[1]} m={best_c[2]}", pol_antistreak(0.12, best_c[1], best_c[2])), ("(e) ALBIMARINI 1->4 (rif. negativo)", pol_alb14(0.12))): sim, mf, mh, line = hist_row(label, rec, qf, 2000.0, granular=False) named[label] = (qf, mf, mh) print(line) # ---------------------------------------------------------------- (3) Kelly onesto print("\n" + "=" * 112) print(" (3) (d) KELLY FRAZIONARIO con code ONESTE (distribuzione IS pooled banda-f + coda sintetica)") print("=" * 112) p_extra, p_unc, p_emp = synthetic_tail_prob(rec) pooled = [] for f in F_SWEEP: rf = trade_records(f=f, phase=0) a = rf[(rf.active == 1) & (rf.date < HOLDOUT)] pooled.append(a["r"].values) rs_pool = np.concatenate(pooled) q_star = kelly_star(rs_pool, p_extra) q_naive = kelly_star(is_act["r"].values, 0.0) print(f" coda: P(move7g<=Kl | ETH 2019-26 certificato)={p_unc:.3f}, freq empirica full-loss gated={p_emp:.3f}" f" -> p_extra sintetica={p_extra:.3f} a r={FULL_LOSS}") print(f" Kelly NAIVE (solo empirico IS f=1.0): q* = {q_naive:.1%}") print(f" Kelly ONESTO (pooled f-band + coda): q* = {q_star:.1%}" f" -> il 12% del book = {0.12/q_star:.2f} Kelly") print("\n" + HDR) for lam in (0.25, 0.5, 1.0): q = lam * q_star label = f"(d) KELLY {lam:.2f}x -> q={q:.1%}" sim, mf, mh, line = hist_row(label, rec, pol_fixed(q), 2000.0, granular=False) named[label] = (pol_fixed(q), mf, mh) print(line) # ---------------------------------------------------------------- (4) null del de-levering print("\n" + "=" * 112) print(" (4) NULL DEL DE-LEVERING — ogni overlay vs la frazione FISSA che da' lo stesso maxDD (interp.)") print("=" * 112) fr = sorted(frontier.items()) dds = np.array([v[0] for _, v in fr]) cags = np.array([v[1] for _, v in fr]) qss = np.array([q for q, _ in fr]) for label, (qf, mf, mh) in named.items(): if label.startswith("(a)") or label.startswith("(e)"): continue q_eq = float(np.interp(mf["dd"], dds, qss)) c_eq = float(np.interp(mf["dd"], dds, cags)) verdict = "OVERLAY NON VALE (delever null vince/pareggia)" if c_eq >= mf["cagr"] - 0.002 \ else "overlay batte il null (verificare persistenza!)" print(f" {label:<38} DD {mf['dd']*100:5.1f}% CAGR {mf['cagr']*100:+6.1f}% | fisso-equivalente " f"q={q_eq*100:4.1f}% CAGR {c_eq*100:+6.1f}% -> {verdict}") # ---------------------------------------------------------------- (4b) stress-test ANTI-STREAK print("\n" + "=" * 112) print(" (4b) STRESS-TEST ANTI-STREAK — regola standing: 3 raffinamenti-gate VRP gia' falliti; un 4°") print(" candidato deve battere il NULL GIUSTO (piazzamento casuale), persistere multi-cut, DSR>=0.95") print("=" * 112) N_as, m_as = best_c[1], best_c[2] # replay della maschera 'settimana ridotta' (streak>=N a decisione) red = np.zeros(len(rec), bool) s = 0 for t, row in enumerate(rec.itertuples(index=False)): red[t] = bool(row.active and s >= N_as) if row.active: s = s + 1 if row.win else 0 act_mask = rec["active"].values == 1 act_idx = np.where(act_mask)[0] k_red = int(red[act_mask].sum()) win_arr = rec["win"].values.astype(bool) loss_red = int((~win_arr[act_mask]) [red[act_mask]].sum()) n_loss = int((~win_arr[act_mask]).sum()) print(f" settimane attive ridotte (streak>={N_as}): {k_red}/{len(act_idx)} ({k_red/len(act_idx)*100:.0f}%) | " f"perdite intercettate a size ridotta: {loss_red}/{n_loss}") sim_anti = simulate(rec, pol_antistreak(0.12, N_as, m_as), 2000.0, granular=False) sim_fix = simulate(rec, pol_fixed(0.12), 2000.0, granular=False) m_anti = path_metrics(sim_anti["dates"], sim_anti["rets"]) # NULL a PIAZZAMENTO CASUALE: stessa sizing a 2 livelli (12% / 12%*mult), stesse k settimane # ridotte, ma scelte A CASO fra le attive (isola il TIMING dello streak dall'esposizione media) rng = np.random.default_rng(SEED + 7) null_cagr, null_dd, null_mar = [], [], [] for _ in range(500): choose = rng.choice(act_idx, size=k_red, replace=False) qs = np.full(len(rec), 0.12) qs[choose] = 0.12 * m_as sn = simulate(rec, None, 2000.0, granular=False, q_series=qs) mn = path_metrics(sn["dates"], sn["rets"]) null_cagr.append(mn["cagr"]) null_dd.append(mn["dd"]) null_mar.append(mn["cagr"] / max(mn["dd"], 1e-9)) mar_anti = m_anti["cagr"] / max(m_anti["dd"], 1e-9) p_cagr = float(np.mean(np.asarray(null_cagr) < m_anti["cagr"])) p_dd = float(np.mean(np.asarray(null_dd) > m_anti["dd"])) # quota di null con DD PEGGIORE p_mar = float(np.mean(np.asarray(null_mar) < mar_anti)) print(f" NULL piazzamento-casuale (500 draw, stessa esposizione media): anti-streak CAGR pctl {p_cagr:.3f}, " f"DD-migliore-del-null {p_dd:.3f}, MAR pctl {p_mar:.3f}") print(f" (null: CAGR med {np.median(null_cagr)*100:+.1f}%, DD med {np.median(null_dd)*100:.1f}% vs " f"anti-streak {m_anti['cagr']*100:+.1f}%/{m_anti['dd']*100:.1f}%)") # banda d'ancora dell'EFFETTO (7 fasi): direzione + IL DELEVER-NULL RIFATTO PER OGNI ANCORA # (lezione anchor-luck 2026-07-02: il claim vale solo se regge a OGNI ancora, non alla migliore) print(f"\n banda d'ancora dell'effetto streak (7 fasi, f=1.0) + delever-null PER ANCORA:") n_dir = 0 n_null = 0 for ph in range(7): rp = trade_records(f=1.0, phase=ph) wa = rp[rp.active == 1]["win"].values.astype(bool) sb2 = [] s = 0 for w in wa: sb2.append(s) s = s + 1 if w else 0 sb2 = np.asarray(sb2) hi = sb2 >= N_as p_hi = 1.0 - wa[hi].mean() if hi.sum() else np.nan p_lo = 1.0 - wa[~hi].mean() if (~hi).sum() else np.nan sa = simulate(rp, pol_antistreak(0.12, N_as, m_as), 2000.0, granular=False) ma = path_metrics(sa["dates"], sa["rets"]) # frontiera fissa DI QUESTA ancora -> CAGR del fisso allo stesso DD dds_p, cags_p = [], [] for q in fracs: sf_ = simulate(rp, pol_fixed(q), 2000.0, granular=False) mq = path_metrics(sf_["dates"], sf_["rets"]) dds_p.append(mq["dd"]) cags_p.append(mq["cagr"]) c_eq = float(np.interp(ma["dd"], dds_p, cags_p)) beats = ma["cagr"] > c_eq + 0.002 direz = p_hi > p_lo n_dir += int(direz) n_null += int(beats) print(f" fase {ph}: P(loss|s>={N_as})={p_hi:.2f} vs s<{N_as}={p_lo:.2f} {'OK' if direz else 'NO'} | " f"anti CAGR {ma['cagr']*100:+5.1f}% DD {ma['dd']*100:4.1f}% | fisso stesso-DD CAGR {c_eq*100:+5.1f}% " f"-> {'batte null' if beats else 'NULL VINCE'}") print(f" -> direzione presente in {n_dir}/7 ancore; batte il delever-null in {n_null}/7 ancore") # plateau delle celle (ancora 0): l'effetto deve reggere OLTRE la cella selezionata print("\n plateau celle anti-streak (ancora 0, FULL): MAR = CAGR/maxDD vs fisso 12% MAR " f"{path_metrics(sim_fix['dates'], sim_fix['rets'])['cagr'] / max(path_metrics(sim_fix['dates'], sim_fix['rets'])['dd'], 1e-9):.2f}") for (fq, N, m) in cells_c: sa = simulate(rec, pol_antistreak(fq, N, m), 2000.0, granular=False) mm = path_metrics(sa["dates"], sa["rets"]) sel = " <- selezionata IS" if (N, m) == (N_as, m_as) else "" print(f" N={N} mult={m}: CAGR {mm['cagr']*100:+5.1f}% DD {mm['dd']*100:5.1f}% " f"MAR {mm['cagr']/max(mm['dd'],1e-9):5.2f}{sel}") # multi-cut: uplift Sharpe/MAR post-cut vs fisso 12% print("\n persistenza multi-cut (uplift anti-streak vs fisso 12%, finestra POST-cut):") for cut in ("2022-07-01", "2023-01-01", "2023-07-01", "2024-01-01", "2024-07-01", "2025-01-01"): cts = pd.Timestamp(cut, tz="UTC") msk = sim_anti["dates"] >= cts ma = path_metrics(sim_anti["dates"][msk], sim_anti["rets"][msk]) mfx = path_metrics(sim_fix["dates"][msk], sim_fix["rets"][msk]) print(f" cut {cut}: dSh {ma['sh']-mfx['sh']:+.2f} dCAGR {(ma['cagr']-mfx['cagr'])*100:+6.1f}pp " f"DD {ma['dd']*100:4.1f}% vs {mfx['dd']*100:4.1f}%") # DSR della cella anti-streak (scelta IS) vs tutte le celle di sizing all_sh_is = [] for c in cells_b: s_ = simulate(rec_is, pol_volscaled(*c), 2000.0, granular=False) all_sh_is.append(path_metrics(s_["dates"], s_["rets"])["sh"]) for c in cells_c: s_ = simulate(rec_is, pol_antistreak(*c), 2000.0, granular=False) all_sh_is.append(path_metrics(s_["dates"], s_["rets"])["sh"]) for q in fracs: s_ = simulate(rec_is, pol_fixed(q), 2000.0, granular=False) all_sh_is.append(path_metrics(s_["dates"], s_["rets"])["sh"]) sim_anti_is = simulate(rec_is, pol_antistreak(0.12, N_as, m_as), 2000.0, granular=False) m_anti_is = path_metrics(sim_anti_is["dates"], sim_anti_is["rets"]) ret_is = pd.Series(sim_anti_is["rets"], index=sim_anti_is["dates"]) dsr_as, nm_as = al.deflated_sharpe(m_anti_is["sh"], all_sh_is, ret_is, dpy=WK) print(f"\n DSR anti-streak (Sh-IS {m_anti_is['sh']:.2f} vs {len(all_sh_is)} celle): {dsr_as:.3f} " f"(null-max {nm_as:.2f}) -> {'PASS' if dsr_as >= 0.95 else 'FAIL'} soglia 0.95") # banda f dell'anti-streak print(" banda f anti-streak (FULL/HOLD):") for f in F_SWEEP: rf = trade_records(f=f, phase=0) sa = simulate(rf, pol_antistreak(0.12, N_as, m_as), 2000.0, granular=False) mfa, mha = full_hold(sa) print(f" f={f:<4} CAGR-F {mfa['cagr']*100:>+6.1f}% DD-F {mfa['dd']*100:>5.1f}% Sh-F {mfa['sh']:>5.2f} | " f"CAGR-H {mha['cagr']*100:>+6.1f}% DD-H {mha['dd']*100:>5.1f}%") # ---------------------------------------------------------------- (5) P(rovina) bootstrap print("\n" + "=" * 112) print(" (5) P(ROVINA | 5 ANNI) — bootstrap a blocchi (L=13 sett., 1000 path), granularita' REALE") print(" rovina-50 = equity <= 50% del capitale iniziale in un punto qualsiasi; stress = +coda sintetica") print("=" * 112) boot_pols = [ ("FISSO 5%", pol_fixed(0.05), False), ("FISSO 12% (book)", pol_fixed(0.12), False), ("FISSO 25%", pol_fixed(0.25), False), ("VOL-SCALED best-IS", pol_volscaled(*best_b), True), ("ANTI-STREAK best-IS", pol_antistreak(0.12, best_c[1], best_c[2]), False), (f"KELLY 0.25x ({0.25*q_star:.0%})", pol_fixed(0.25 * q_star), False), (f"KELLY 0.50x ({0.5*q_star:.0%})", pol_fixed(0.5 * q_star), False), (f"KELLY 1.00x ({q_star:.0%})", pol_fixed(q_star), False), ("ALBIMARINI 1->4", pol_alb14(0.12), False), ] print(f" {'politica':<24} {'C':>5} | {'P(rov50)':>8} {'P(rov80)':>8} {'P(DD>30%)':>9} " f"{'CAGRmed':>8} {'CAGRp10':>8} | {'stress: P(rov50)':>16} {'P(rov80)':>8}") boot_out = {} for label, qf, nref in boot_pols: for C in CAPITALS: b = block_bootstrap_ruin(rec, qf, C, stress_p=0.0, needs_ref=nref) bs = block_bootstrap_ruin(rec, qf, C, stress_p=p_extra, seed=SEED + 1, needs_ref=nref) boot_out[(label, C)] = (b, bs) print(f" {label:<24} {C:>5.0f} | {b['p_ruin50']*100:>7.1f}% {b['p_ruin80']*100:>7.1f}% " f"{b['p_dd30']*100:>8.1f}% {b['cagr_med']*100:>+7.1f}% {b['cagr_p10']*100:>+7.1f}% | " f"{bs['p_ruin50']*100:>15.1f}% {bs['p_ruin80']*100:>7.1f}%") # ---------------------------------------------------------------- (6) banda f + banda ancora print("\n" + "=" * 112) print(" (6) BANDE OBBLIGATORIE — skew f e ancora settimanale (politica FISSO 12%, continuo)") print("=" * 112) print(" banda f (storico FULL/HOLD):") for f in F_SWEEP: rf = trade_records(f=f, phase=0) sim = simulate(rf, pol_fixed(0.12), 2000.0, granular=False) mf, mh = full_hold(sim) print(f" f={f:<4} CAGR-F {mf['cagr']*100:>+6.1f}% DD-F {mf['dd']*100:>5.1f}% worst {mf['worst']*100:>+5.1f}%" f" Sh-F {mf['sh']:>5.2f} | CAGR-H {mh['cagr']*100:>+6.1f}% DD-H {mh['dd']*100:>5.1f}%") print(" banda d'ancora (7 fasi della cadenza settimanale, f=1.0):") anchor = [] for ph in range(7): rp = trade_records(f=1.0, phase=ph) sim = simulate(rp, pol_fixed(0.12), 2000.0, granular=False) mf, mh = full_hold(sim) anchor.append((ph, mf["cagr"], mf["dd"], mf["sh"], mh["cagr"])) a = np.array([(x[1], x[2], x[3], x[4]) for x in anchor]) print(f" CAGR-F: min {a[:,0].min()*100:+.1f}% med {np.median(a[:,0])*100:+.1f}% max {a[:,0].max()*100:+.1f}% | " f"DD-F: {a[:,1].min()*100:.1f}/{np.median(a[:,1])*100:.1f}/{a[:,1].max()*100:.1f}% | " f"Sh-F: {a[:,2].min():.2f}/{np.median(a[:,2]):.2f}/{a[:,2].max():.2f} | " f"CAGR-H: {a[:,3].min()*100:+.1f}/{np.median(a[:,3])*100:+.1f}/{a[:,3].max()*100:+.1f}%") # DSR pro-forma sulle celle (il sizing riscala il flusso: Sharpe quasi invariante per costruzione) all_sh = [] for c in cells_b: s = simulate(rec_is, pol_volscaled(*c), 2000.0, granular=False) all_sh.append(path_metrics(s["dates"], s["rets"])["sh"]) for c in cells_c: s = simulate(rec_is, pol_antistreak(*c), 2000.0, granular=False) all_sh.append(path_metrics(s["dates"], s["rets"])["sh"]) for q in fracs: s = simulate(rec_is, pol_fixed(q), 2000.0, granular=False) all_sh.append(path_metrics(s["dates"], s["rets"])["sh"]) sim_b = simulate(rec, pol_volscaled(*best_b), 2000.0, granular=False) daily = pd.Series(sim_b["rets"], index=sim_b["dates"]) dsr, null_max = al.deflated_sharpe(path_metrics(sim_b["dates"], sim_b["rets"])["sh"], all_sh, daily, dpy=WK) print(f"\n DSR pro-forma (best overlay vs {len(all_sh)} celle di sizing): {dsr:.3f} (null-max {null_max:.2f})") print(" NB: il sizing RISCALA lo stesso flusso -> gli Sharpe delle celle sono quasi identici per costruzione;") print(" il DSR qui e' dovuto, ma la metrica decisiva del filone e' la frontiera CAGR-DD-P(rovina) vs il null.") # ---------------------------------------------------------------- (7) sensitivity 0.1 ETH print("\n (7) SENSITIVITY GRANULARITA' 0.1 ETH (se il min-size fosse 0.1 ETH/gamba, unit ~$7-15):") for C in CAPITALS: s10 = simulate(rec, pol_fixed(0.12), C, granular=True, min_eth=0.1) s1 = simulate(rec, pol_fixed(0.12), C, granular=True, min_eth=1.0) m10, _ = full_hold(s10) m1, _ = full_hold(s1) print(f" C={C:.0f}: 1.0 ETH CAGR {m1['cagr']*100:+.1f}% DD {m1['dd']*100:.1f}% halt {s1['halted']} | " f"0.1 ETH CAGR {m10['cagr']*100:+.1f}% DD {m10['dd']*100:.1f}% halt {s10['halted']}") print("\n" + "=" * 112) print(" SINTESI ONESTA") print(" - Il sizing non crea alpha: sposta il punto sulla frontiera CAGR-DD-P(rovina) del flusso gated.") print(" - VERDETTI: (b) vol-scaled batte il null di ~1pp a singola ancora = rumore (lezione TP01xDVOL)") print(" -> SCARTATO. (c) anti-streak: passa random-null 0.996 / DSR 0.999 / multi-cut / 7-7 direzione,") print(" MA plateau ASSENTE (solo la cella N=2 vince; N=3/5 collassano al MAR del fisso), P(loss|streak)") print(" NON monotona (0.24/0.18/0.15) e delever-null vince in 2/7 ancore: l'intero effetto sono ~6") print(" perdite su 15 trade a streak==2 -> ARTEFATTO DI SELEZIONE, non adottato. La batteria minima per") print(" un sizing-gate e': random-placement null + plateau celle + delever-null PER ANCORA (i primi 3") print(" test da soli l'avrebbero promosso). (d) Kelly frazionario = punti sulla frontiera fissa (il") print(" null vince/pareggia per costruzione): il valore e' la CALIBRAZIONE, q*_onesto=44%.") print(" - Il 12% del book = 0.27 Kelly-onesto ~= quarter-Kelly: gia' nella zona sana anti-rovina") print(" (P(rovina50|5y)~0.0-0.1%, P(DD>30%) 9-18%). Alzarlo a 25% decuplica P(DD>30%) a >92%.") print(" - Albimarini 1->4 e' il controesempio: stesso flusso, stessa edge, P(rovina50|5y)=53-55%.") print(" - Granularita' REALE: a 2k il min-size 1 ETH rende inservibili le frazioni <=10% (halt 13-92") print(" settimane) e costa -3.7pp di CAGR al 12%; a 5k il muro sparisce (coerente con r0702: ~2.6k).") print(" - REGOLA STANDING: niente short-vol da modello in deploy. Output = conoscenza per QUANDO/SE") print(" il f di stress reale arrivera' da cerbero-bite. Nessun file di produzione toccato.") print("=" * 112) if __name__ == "__main__": main()