"""r0822_prop_alloc — PROGETTARE l'allocazione per la BARRIERA, non per lo Sharpe (2026-08-22). IL BUCO CHE CHIUDE ------------------ Il canale prop/funded e' l'unico misurato che moltiplica il nozionale senza possedere capitale. Il progetto lo ha gia' VALUTATO (24-25/07: `r0725_prop_ladder.py`, `r0725_prop_coupled.py`, `r0725_hyro.py`) ma non lo ha mai **PROGETTATO**: ha sempre preso il libro ottimizzato per lo Sharpe (config A = live 75/25, config B = 55/20/25 scelta a mano) e poi MISURATO P(pass) del risultato. Due punti nello spazio dei pesi, tre leve, nessuna ottimizzazione, nessun null. Qui la funzione obiettivo cambia: sotto una **barriera di maxDD STATICA per-conto** conta la coda sinistra e la DURATA, non il rapporto rendimento/vol. Si ottimizza J(w, k) = P(passare l'eval) x P(essere vivo dopo 12 mesi da funded) su pesi w e leva k, con la lente WICK ACCOPPIATA (la `close-only` e' CIECA: 0% di breach da daily-loss su ogni configurazione, la regola non scatta mai sulle chiusure). LA PREVISIONE, REGISTRATA PRIMA DI MISURARE ------------------------------------------- Nel limite gaussiano con leva LIBERA l'obiettivo si riduce allo Sharpe, e si vede in due righe. Per un moto browniano con drift mu e vol sigma a leva k, la probabilita' di toccare +T prima di -D vale P = (1-e^{-theta*D})/(1-e^{-theta*(T+D)}) con **theta = 2*mu/(k*sigma^2)**: la leva entra SOLO dividendo theta. Alla leva minima che porta a +T dentro l'orizzonte (k ~ T/mu) si ottiene theta ~ 2*mu^2/(T*sigma^2) = 2*S^2/T, con S = Sharpe: **la vol sparisce e resta lo Sharpe al quadrato**. Quindi mi aspetto PRIMA di misurare: (a) l'argmax di J vicino all'argmax di Sharpe -> la premessa "obiettivo diverso" e' in gran parte FALSA sui PESI; (b) il contenuto vero nella **LEVA**, che lo Sharpe non determina affatto, e nella deviazione dal gaussiano (skew, wick, daily-loss: una soglia ASSOLUTA non e' scale-invariante); (c) leva ottima per J **piu' bassa** di quella ottima per il payout (0.75x, 25/07): J e' una probabilita' di sopravvivenza, il payout e' un flusso. Se (a) risulta falsa, e' un risultato; se risulta vera, il filone "riallocare per la barriera" si chiude e si smette di pagarlo. CONVENZIONI DICHIARATE ---------------------- * Macchineria del wick ACCOPPIATO **riusata** da `r0725_prop_coupled.py` (import, non riscritta): `_hourly_legs` (recon MTM orario TP01/SKH01), `crypto_daily_tuples` (minimo ESATTO sul path orario condiviso), `xsec_daily_tuples` (XS01 dagli OHLC 1d, ordine condiviso avverso). * VRP01 e GTAA01 sono AGGIUNTI qui (non c'erano): GTAA01 col wick dagli OHLC dei 6 ETF (long-only co-moventi -> ogni gamba al proprio minimo = convenzione severa dichiarata); VRP01 con **gap = 0**, cioe' CIECO, perche' il suo mark infra-settimana non e' nel dataset dello sleeve (il rendimento settimanale e' lumpato sul giorno di scadenza). E' una lente GENEROSA per VRP01: la misura del 30/07 sulle quote reali dice mark peggiore infra-settimana mediano -6.9% e minimo **-81.7%** del capitale su un trade finito in UTILE. Se VRP01 perde sotto una lente generosa, perde. * de-luck sul DRIFT **x0.89** (fattore MISURATO il 26/07 sul libro live, `r0726_deluck_factor.py`), non x0.6 come nel 25/07 (li' era stimato a occhio e ri-misurato troppo severo del 31-34%). Sensibilita' a 1.00 e 0.60 girata: cio' che conta e' se l'ORDINE fra le configurazioni tiene. * bootstrap a blocchi (20g) con **indici CONDIVISI fra tutte le configurazioni** -> le differenze fra configurazioni sono APPAIATE e il loro errore MC e' molto minore di quello dei livelli. * finestra comune **2024+** (XS01 nasce li'): ~2.6 anni, ed e' la finestra in cui XS01 e' stato scoperto -> le configurazioni che lo contengono sono FAVORITE per costruzione. Dichiarato. * fisco 33% sui payout, split 80%, regole vere delle due firm (max-loss STATICO dal saldo iniziale, base ripristinata dopo il prelievo — bug del 25/07 gia' corretto a monte). Uso: `nice -n 19 timeout 900 uv run python scripts/research/r0822_prop_alloc.py` """ from __future__ import annotations import sys import time from functools import lru_cache 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.eq_splits import repair_splits # noqa: E402 from src.portfolio.gtaa import ( # noqa: E402 ANN, EQ_UNIVERSE, GTAA_DEFAULT_CAPITAL, HORIZONS, IB_MIN_TRADE_USD, REBAL_BAND_USD, REBAL_EVERY, TARGET_VOL, ib_commission, ) from src.portfolio.portfolio import metrics, weights_tilt_null # noqa: E402 from src.portfolio.sleeves import ( # noqa: E402 GTAA_BOOK_ACTIVATION, _gtaa_daily_returns, _skyhook_returns, _tp01_returns, _vrp_combo_returns, _xsec_returns, ) import r0725_prop_coupled as pc # noqa: E402 import r0725_prop_ladder as pl # noqa: E402 from r0725_capcurve import EURUSD, TAX_RATE # noqa: E402 RAW = ROOT / "data" / "raw" # ------------------------------------------------------------------ costanti dichiarate DELUCK = 0.89 # fattore d'ancora MISURATO sul drift (26/07), non stimato a occhio START_WIN = "2024-01-01" # finestra comune ai 5 sleeve (XS01 nasce qui) BLOCK = 20 # blocco del bootstrap, in giorni N_PATHS = 2500 SPLIT = pl.SPLIT # 80% al trader TARGET_EUR_DAY = 50.0 FIRMS = {f["name"]: f for f in pl.FIRMS} SLEEVES = ("TP01", "SKH01", "XS01", "VRP01", "GTAA01") CRYPTO_PROP = ("TP01", "SKH01", "XS01") # cio' che una firm CRYPTO puo' davvero far eseguire # ================================================================== §0. PANNELLO (R, wick) @lru_cache(maxsize=8) def _eq_ohlc(sym: str) -> pd.DataFrame: p = RAW / f"eq_{sym.lower()}_1d.parquet" d = pd.read_parquet(p) d.index = pd.to_datetime(d["timestamp"], unit="ms", utc=True) d = d.sort_index() d, _ = repair_splits(d) # split IWM/EFA non aggiustati (25/07) return d[["open", "high", "low", "close"]].astype(float) def _gtaa_exposure(close: pd.Series) -> np.ndarray: """Copia FEDELE di gtaa._exposure (che prende una Series di soli close).""" px = close.values n = len(px) tgt = np.zeros(n) mh = max(HORIZONS) for i in range(mh, n): tgt[i] = np.mean([1.0 if px[i] > px[i - H] else 0.0 for H in HORIZONS]) s = pd.Series(tgt, index=close.index) rv = close.pct_change().rolling(63, min_periods=20).std().shift(1) * ANN scale = np.clip(np.nan_to_num(TARGET_VOL / rv.replace(0, np.nan).values, nan=0.0), 0, 10.0) return (s * scale).clip(0, 1.0).values @lru_cache(maxsize=2) def gtaa_daily_tuples() -> pd.DataFrame: """(R, m) giornalieri di GTAA01: chiusura replicata da gtaa.gtaa_returns e minimo intraday dagli OHLC delle 6 gambe. Long-only e co-moventi -> convenzione **ogni gamba al proprio minimo insieme** (severa, la stessa famiglia del bound 'perleg' di XS01). Weekend/festivi: R = 0 e m = 0 (capitale IB fermo — convenzione del book).""" cap_leg = GTAA_DEFAULT_CAPITAL / len(EQ_UNIVERSE) legs_r, legs_m = {}, {} for sym in EQ_UNIVERSE: d = _eq_ohlc(sym) close = d["close"] ex = np.nan_to_num(_gtaa_exposure(close)) px = close.values prev = np.concatenate(([np.nan], px[:-1])) ret = np.zeros(len(px)) ret[1:] = px[1:] / px[:-1] - 1.0 ret_low = d["low"].values / prev - 1.0 # long-only: il peggio e' il minimo held = np.empty(len(ex)) comm = np.zeros(len(ex)) cur = 0.0 for i in range(len(ex)): # identico a gtaa._gated_returns if i % REBAL_EVERY == 0: notional = abs(ex[i] - cur) * cap_leg if notional >= max(REBAL_BAND_USD, IB_MIN_TRADE_USD): comm[i] = ib_commission(notional, px[i]) / cap_leg cur = ex[i] held[i] = cur pos = np.zeros(len(held)) pos[1:] = held[:-1] legs_r[sym] = pd.Series(pos * ret - comm, index=close.index) legs_m[sym] = pd.Series(pos * np.nan_to_num(ret_low) - comm, index=close.index) R = pd.concat(legs_r, axis=1, sort=True).sort_index().mean(axis=1, skipna=True).dropna() M = pd.concat(legs_m, axis=1, sort=True).sort_index().mean(axis=1, skipna=True).dropna() days = pd.date_range(R.index.min().normalize(), R.index.max().normalize(), freq="1D", tz="UTC") R = R.reindex(days).fillna(0.0) M = M.reindex(days).fillna(0.0) out = pd.DataFrame({"R": R, "m": np.minimum(M, R)}) return out[out.index >= GTAA_BOOK_ACTIVATION] @lru_cache(maxsize=2) def vrp_daily_tuples() -> pd.DataFrame: """(R, m) di VRP01 con **m = R**: il mark infra-settimana NON e' nel dataset dello sleeve (rendimento settimanale lumpato sul giorno di scadenza). Lente CIECA e GENEROSA — dichiarata.""" r = _vrp_combo_returns().dropna().sort_index() if r.index.tz is None: r.index = r.index.tz_localize("UTC") return pd.DataFrame({"R": r.values, "m": r.values}, index=r.index) def _d(s: pd.Series) -> pd.Series: s = s.dropna().sort_index() if s.index.tz is None: s.index = s.index.tz_localize("UTC") return s _CRY_CACHE: dict = {} def _crypto_stressed(w_tp: float, w_skh: float, m_tp: float, m_skh: float) -> pd.DataFrame: """Come `pc.crypto_daily_tuples` (minimo ESATTO sul path orario condiviso) ma con un taglio del DRIFT applicato alla singola gamba PRIMA di comporre il path. Serve perche' il minimo giornaliero non e' separabile per gamba: non si puo' tagliare il drift a valle.""" (J,) = pc._hourly_legs() legs = {} for nm, (rc, wc, mult) in {"tp": ("tp_r", "tp_w", m_tp), "sk": ("skh_r", "skh_w", m_skh)}.items(): r = J[rc].values.copy() w = J[wc].values.copy() if mult != 1.0: cut = (1.0 - mult) * r.mean() r = r - cut w = w - cut legs[nm] = (r, w) r_h = w_tp * legs["tp"][0] + w_skh * legs["sk"][0] w_h = w_tp * legs["tp"][1] + w_skh * legs["sk"][1] idx = J.index days = idx.floor("D") rows = [] for day, pos in pd.Series(np.arange(len(idx)), index=days).groupby(level=0): sl = pos.values r = r_h[sl] wk = w_h[sl] cum = np.cumprod(1.0 + r) cum_prev = np.concatenate(([1.0], cum[:-1])) m = min(cum.min(), (cum_prev * (1.0 + wk)).min()) - 1.0 rows.append((day, cum[-1] - 1.0, min(m, cum[-1] - 1.0))) return pd.DataFrame(rows, columns=["day", "R", "m"]).set_index("day") def _crypto(w_tp: float, w_skh: float, m_tp: float = 1.0, m_skh: float = 1.0) -> pd.DataFrame: key = (round(w_tp, 6), round(w_skh, 6), round(m_tp, 4), round(m_skh, 4)) if key not in _CRY_CACHE: _CRY_CACHE[key] = (pc.crypto_daily_tuples(w_tp, w_skh) if (m_tp == 1.0 and m_skh == 1.0) else _crypto_stressed(w_tp, w_skh, m_tp, m_skh)) return _CRY_CACHE[key] @lru_cache(maxsize=4) def _panel_index(start: str) -> pd.DatetimeIndex: """Griglia comune ai 5 sleeve (intersezione), dal `start` in poi.""" idx = _crypto(1.0, 0.0).index for f in (pc.xsec_daily_tuples(), vrp_daily_tuples(), gtaa_daily_tuples()): idx = idx.intersection(f.index) return idx[idx >= pd.Timestamp(start, tz="UTC")] _RG_CACHE: dict = {} def book_RG(w: dict, start: str = START_WIN, drift_mult: tuple = ()) -> tuple[np.ndarray, np.ndarray]: """(R, gap) giornalieri di un libro a pesi arbitrari sui 5 sleeve. Crypto TP01+SKH01: minimo ESATTO sul path orario condiviso (r0725_prop_coupled). XS01/VRP01/GTAA01: il loro minimo si SOMMA (worst simultaneo — convenzione del 25/07). `drift_mult` = (("XS01", 0.5), ...): taglia il DRIFT di quello sleeve (non la vol) — serve per lo stress giudiziale su uno sleeve scoperto DENTRO la finestra di misura.""" dm = dict(drift_mult) key = (start, drift_mult) + tuple(round(float(w.get(nm, 0.0)), 6) for nm in SLEEVES) if key in _RG_CACHE: return _RG_CACHE[key] idx = _panel_index(start) w_tp, w_skh = w.get("TP01", 0.0), w.get("SKH01", 0.0) R = np.zeros(len(idx)) M = np.zeros(len(idx)) if w_tp > 0 or w_skh > 0: c = _crypto(w_tp, w_skh, dm.get("TP01", 1.0), dm.get("SKH01", 1.0)).reindex(idx) R += c["R"].values M += c["m"].values for nm, fr in (("XS01", pc.xsec_daily_tuples()), ("VRP01", vrp_daily_tuples()), ("GTAA01", gtaa_daily_tuples())): wt = w.get(nm, 0.0) if wt > 0: f = fr.reindex(idx) rv = np.nan_to_num(f["R"].values) mv = np.nan_to_num(f["m"].values) mult = dm.get(nm, 1.0) if mult != 1.0: # taglia il drift, lascia la forma della coda cut = (1.0 - mult) * rv.mean() # NB: media PRIMA del taglio rv = rv - cut mv = mv - cut R += wt * rv M += wt * mv R = np.nan_to_num(R) M = np.nan_to_num(M) _RG_CACHE[key] = (R, np.minimum(M, R) - R) return _RG_CACHE[key] def deluck(R: np.ndarray, factor: float = DELUCK) -> np.ndarray: return R - (1.0 - factor) * R.mean() # ================================================================== §1. SIMULATORE def boot_idx(n_hist: int, n_days: int, n_paths: int, seed: int, block: int = BLOCK) -> np.ndarray: rng = np.random.default_rng(seed) n_blocks = int(np.ceil(n_days / block)) st = rng.integers(0, n_hist - block, size=(n_paths, n_blocks)) return (st[:, :, None] + np.arange(block)[None, None, :]).reshape(n_paths, -1)[:, :n_days] def eval_sim(R: np.ndarray, G: np.ndarray, idx: np.ndarray, lev: float, firm: str, lens: str = "coupled") -> dict: """P(passare l'eval) entro l'orizzonte del bootstrap + giorni al pass.""" tgt, dd, dl = FIRMS[firm]["ev"] r = R[idx] * lev g = (G[idx] * lev) if lens == "coupled" else np.zeros_like(r) P, D = r.shape eq = np.ones(P) alive = np.ones(P, bool) passed = np.zeros(P, bool) tpass = np.full(P, D, int) for t in range(D): act = alive & ~passed if not act.any(): break rt, gt = r[:, t], g[:, t] eqp = eq eq = np.where(act, eq * (1.0 + rt), eq) eq_low = np.where(act, eqp * (1.0 + rt + gt), eq) bust = act & ((eq_low < 1.0 - dd) | (rt + gt < -dl)) alive &= ~bust ok = act & ~bust & (eq >= 1.0 + tgt) tpass[ok] = t passed |= ok return dict(p_pass=float(passed.mean()), passed=passed, t_pass=float(np.median(tpass[passed])) if passed.any() else np.nan) def funded_sim(R: np.ndarray, G: np.ndarray, idx: np.ndarray, lev: float, firm: str, notional: float, lens: str = "coupled") -> dict: """P(vivo a fine orizzonte) + payout netto cumulato (prelievo mensile, base ripristinata).""" ml, dl = FIRMS[firm]["fu"] r = R[idx] * lev g = (G[idx] * lev) if lens == "coupled" else np.zeros_like(r) P, D = r.shape eq = np.ones(P) alive = np.ones(P, bool) payout = np.zeros(P) alive_at = {} for t in range(D): rt, gt = r[:, t], g[:, t] eqp = eq eq = np.where(alive, eq * (1.0 + rt), eq) eq_low = np.where(alive, eqp * (1.0 + rt + gt), eq) bust = alive & ((eq_low < 1.0 - ml) | (rt + gt < -dl)) alive &= ~bust if (t + 1) % 30 == 0: take = alive & (eq > 1.0) payout += np.where(take, (eq - 1.0) * notional, 0.0) * SPLIT * (1.0 - TAX_RATE) eq = np.where(take, 1.0, eq) if (t + 1) % 365 == 0: alive_at[(t + 1) // 365] = float(alive.mean()) return dict(p_alive=float(alive.mean()), alive_at=alive_at, e_payout=float(payout.mean()), payout=payout, alive=alive) def objective(w: dict, lev: float, firm: str, ev_idx: np.ndarray, fu_idx: np.ndarray, lens: str = "coupled", factor: float = DELUCK, notional: float = 100_000.0, drift_mult: tuple = ()) -> dict: R, G = book_RG(w, drift_mult=drift_mult) R = deluck(R, factor) e = eval_sim(R, G, ev_idx, lev, firm, lens) f = funded_sim(R, G, fu_idx, lev, firm, notional, lens) ann = float(R.mean() * 365.0) vol = float(R.std() * np.sqrt(365.0)) # stimatore per-percorso di J: eval e funded girano su indici di bootstrap INDIPENDENTI, # quindi mean(passed_i * alive_i) stima p_pass*p_alive ed e' APPAIABILE fra configurazioni. jvec = (e["passed"] & f["alive"]).astype(float) return dict(J=e["p_pass"] * f["p_alive"], p_pass=e["p_pass"], p_alive=f["p_alive"], e_payout=f["e_payout"], sharpe=(ann / vol if vol > 0 else 0.0), vol=vol, drift=ann, t_pass=e["t_pass"], jvec=jvec) def paired_delta(wA: dict, levA: float, wB: dict, levB: float, firm: str, ev_idx: np.ndarray, fu_idx: np.ndarray) -> tuple[float, float]: """Differenza APPAIATA di J fra due configurazioni (stessi indici di bootstrap) + errore standard della differenza. Il livello di J ha SE ~0.01; la DIFFERENZA molto meno.""" a = objective(wA, levA, firm, ev_idx, fu_idx)["jvec"] b = objective(wB, levB, firm, ev_idx, fu_idx)["jvec"] d = a - b return float(d.mean()), float(d.std(ddof=1) / np.sqrt(len(d))) # ================================================================== §2. IL CONTO ANALITICO def p_hit_target_first(mu: float, sig: float, lev: float, T: float, D: float) -> float: """Gambler's ruin per moto browniano con drift: P(toccare +T prima di -D), SENZA limite di tempo. theta = 2*mu/(k*sigma^2): la leva entra SOLO dividendo theta -> con leva -> inf la probabilita' tende a D/(T+D) (37.5% con 10%/6%), qualunque sia lo Sharpe.""" if sig <= 0: return float(mu > 0) theta = 2.0 * mu / (lev * sig * sig) if abs(theta) < 1e-12: return D / (T + D) return float((1.0 - np.exp(-theta * D)) / (1.0 - np.exp(-theta * (T + D)))) # ================================================================== §3. GRIGLIA / NULL def simplex_grid(names: tuple, step_den: int) -> list[dict]: """Tutti i vettori di peso a passo 1/step_den sul simplesso di `names` (somma 1).""" out = [] n = len(names) def rec(prefix, rem, k): if k == n - 1: out.append(dict(zip(names, [p / step_den for p in prefix] + [rem / step_den]))) return for v in range(rem + 1): rec(prefix + [v], rem - v, k + 1) rec([], step_den, 0) return out def wkey(w: dict) -> str: return "/".join(f"{int(round(100 * w.get(nm, 0.0))):d}" for nm in CRYPTO_PROP) def grid_scan(grid: list[dict], levs: tuple, firm: str, ev_idx: np.ndarray, fu_idx: np.ndarray, lens: str = "coupled", factor: float = DELUCK) -> pd.DataFrame: rows = [] for w in grid: for lev in levs: o = objective(w, lev, firm, ev_idx, fu_idx, lens, factor) rows.append(dict(w=wkey(w), lev=lev, **{k: v for k, v in o.items() if k != "jvec"})) return pd.DataFrame(rows) def random_weights(n: int, names: tuple, seed: int, floor: float = 0.0, caps: dict | None = None) -> list[dict]: """Stesso campionatore di `portfolio.weights_tilt_null` (dirichlet uniforme sul simplesso + rejection su floor/caps), ma qui serve a costruire il NULL della funzione obiettivo J.""" rng = np.random.default_rng(seed) caps_v = np.array([(caps or {}).get(nm, 1.0) for nm in names], float) out = [] while len(out) < n: b = rng.dirichlet(np.ones(len(names)), size=4 * n + 64) ok = (b >= floor).all(axis=1) & (b <= caps_v).all(axis=1) out.extend(b[ok]) return [dict(zip(names, v)) for v in out[:n]] # ================================================================== main def hr(t: str = "") -> None: print("\n" + "-" * 104) if t: print(f" {t}") print("-" * 104) def main() -> None: 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}") print("=" * 104) print(" PROP-ALLOC — allocare per la BARRIERA (P(pass) x P(vivo 12m)) invece che per lo Sharpe") print("=" * 104) # ---------------------------------------------------------------- A. pannello + sanity idx = _panel_index(START_WIN) print(f"\n finestra comune ai 5 sleeve: {len(idx)} giorni {idx[0].date()} -> {idx[-1].date()}" f" (XS01 nasce nel 2024: e' anche la sua finestra di SCOPERTA -> lo favorisce)") print(f" de-luck sul drift x{DELUCK} (misurato 26/07) | bootstrap a blocchi di {BLOCK}g, " f"indici CONDIVISI fra configurazioni | {N_PATHS} percorsi") tick("pannello costruito") off = {"TP01": _d(_tp01_returns()), "SKH01": _d(_skyhook_returns()), "XS01": _d(_xsec_returns()), "VRP01": _d(_vrp_combo_returns()), "GTAA01": _d(_gtaa_daily_returns())} rec = {"TP01": _crypto(1.0, 0.0)["R"], "SKH01": _crypto(0.0, 1.0)["R"], "XS01": pc.xsec_daily_tuples()["R"], "VRP01": vrp_daily_tuples()["R"], "GTAA01": gtaa_daily_tuples()["R"]} print("\n sanity — ricostruzione vs sleeve ufficiale. XS01/VRP01/GTAA01 devono coincidere") print(" BARRA PER BARRA; TP01/SKH01 no per costruzione (il recon e' un mark-to-market ORARIO,") print(" lo sleeve marca a fine-trade) -> per loro il controllo giusto e' il log-totale.") print(f" {'sleeve':>8} {'max|delta| barra':>18} {'log-tot recon':>15} " f"{'log-tot sleeve':>16} {'scarto':>9}") for nm in SLEEVES: com = off[nm].index.intersection(rec[nm].index) a = off[nm].reindex(com).values b = rec[nm].reindex(com).values dmax = float(np.abs(a - b).max()) la, lb = float(np.log1p(a).sum()), float(np.log1p(b).sum()) flag = "OK" if dmax < 1e-9 else ("lente MTM" if nm in ("TP01", "SKH01") else "DIVERGE") print(f" {nm:>8} {dmax:>18.2e} {lb:>15.4f} {la:>16.4f} {lb-la:>+9.4f} {flag}") hr("A. PROFILO DEGLI SLEEVE SOTTO BARRIERA — cio' che conta non e' lo Sharpe, e si vede qui") print(f" {'sleeve':>8} {'Sharpe':>7} {'vol a':>7} {'drift a':>8} {'skew':>7} " f"{'giorno peggiore':>16} {'gap p90':>9} {'gap peggiore':>13} {'g/vol':>7}") prof = {} for nm in SLEEVES: R, G = book_RG({nm: 1.0}) Rd = deluck(R) vol = Rd.std() * np.sqrt(365.0) dr = Rd.mean() * 365.0 sk = float(pd.Series(Rd).skew()) prof[nm] = dict(sharpe=dr / vol if vol > 0 else 0.0, vol=vol, drift=dr, skew=sk, worst=Rd.min(), gap90=np.percentile(-G, 90), gapw=G.min()) print(f" {nm:>8} {prof[nm]['sharpe']:>7.2f} {vol:>6.1%} {dr:>8.1%} {sk:>7.2f} " f"{Rd.min():>15.2%} {-np.percentile(-G, 90)*100:>8.2f}pp {G.min()*100:>12.2f}pp " f"{(-G.min()/vol) if vol > 0 else 0:>7.2f}") print("\n lettura: `gap peggiore` e' l'escursione intraday che la lente close-only NON vede;") print(" `g/vol` la misura in unita' di vol annua. VRP01 ha gap 0 **per cecita' del dato**,") print(" non per assenza di rischio (mark infra-settimana misurato il 30/07: mediana -6.9%,") print(" minimo -81.7% del capitale su un trade finito in UTILE).") tick("A. profilo sleeve") hr("B. IL CONTO ANALITICO — perche' con leva LIBERA la barriera premia lo SHARPE, non la vol") tgt, dd, _dl = FIRMS["HYRO"]["ev"] print(f" P(toccare +{tgt:.0%} prima di -{dd:.0%}), gambler's ruin senza limite di tempo, " f"theta = 2*mu/(k*sigma^2):") print(f" {'sleeve':>8} {'Sharpe':>7} | " + " ".join(f"{f'k={k:g}':>8}" for k in (0.25, 0.5, 1.0, 2.0, 4.0))) for nm in SLEEVES: p = prof[nm] cells = " ".join(f"{p_hit_target_first(p['drift'], p['vol'], k, tgt, dd):>8.1%}" for k in (0.25, 0.5, 1.0, 2.0, 4.0)) print(f" {nm:>8} {p['sharpe']:>7.2f} | {cells}") print(f" {'limite k->inf':>16} | " + " ".join([f"{dd/(tgt+dd):>8.1%}"] * 5) + " <- D/(T+D): la leva alta CANCELLA l'edge") print("\n Alla leva minima che porta a +T dentro l'orizzonte (k ~ T/mu) resta theta ~ 2*S^2/T:") print(" la VOL sparisce e conta lo Sharpe AL QUADRATO. Percio' la previsione registrata prima") print(" di misurare e': l'argmax di J sui PESI ~ l'argmax di Sharpe, e il contenuto vero e'") print(" nella LEVA (che lo Sharpe non determina) e nella deviazione dal gaussiano.") # ---------------------------------------------------------------- C. griglia n_hist = len(idx) EV_H, FU_H = 365, 365 ev_idx = boot_idx(n_hist, EV_H, N_PATHS, seed=8220) fu_idx = boot_idx(n_hist, FU_H, N_PATHS, seed=8221) LEVS = (0.25, 0.375, 0.50, 0.625, 0.75, 1.00, 1.25, 1.50) STEP = 8 grid = simplex_grid(CRYPTO_PROP, STEP) hr(f"C. GRIGLIA — {len(grid)} vettori di peso (passo 1/{STEP}) x {len(LEVS)} leve " f"= {len(grid)*len(LEVS)} celle, firm HYRO (eval +10%/-6%/-4%, funded -6%/-4%)") print(f" orizzonte eval {EV_H}g, funded {FU_H}g. Lente ACCOPPIATA. " f"J = P(pass) x P(vivo 12m).") tick("B. conto analitico") t = time.time() G3 = grid_scan(grid, LEVS, "HYRO", ev_idx, fu_idx) print(f" ({time.time()-t:.0f}s)") best = G3.loc[G3["J"].idxmax()] # argmax di SHARPE: lo Sharpe non dipende dalla leva -> si sceglie il peso, poi la leva # migliore PER QUEL PESO secondo J (altrimenti non e' un confronto onesto). w_sh = G3.loc[G3["sharpe"].idxmax(), "w"] sub_sh = G3[G3["w"] == w_sh] best_sh = sub_sh.loc[sub_sh["J"].idxmax()] live = G3[(G3["w"] == "75/25/0")] best_live = live.loc[live["J"].idxmax()] print(f"\n {'':>22} {'pesi TP/SKH/XS':>16} {'leva':>6} {'J':>7} {'P(pass)':>8} " f"{'P(vivo12m)':>11} {'Sharpe':>7} {'vol':>6} {'E[pay/a]':>10} {'g.al pass':>10}") for lab, row in (("argmax J", best), ("argmax Sharpe", best_sh), ("libro LIVE 75/25", best_live)): print(f" {lab:>22} {row['w']:>16} {row['lev']:>6.3f} {row['J']:>7.3f} " f"{row['p_pass']:>8.1%} {row['p_alive']:>11.1%} {row['sharpe']:>7.2f} " f"{row['vol']:>6.1%} {row['e_payout']:>9,.0f}$ {row['t_pass']:>10.0f}") dA, seA = paired_delta(w_argmax_dict := dict(zip(CRYPTO_PROP, [int(x)/100 for x in best["w"].split("/")])), float(best["lev"]), dict(zip(CRYPTO_PROP, [int(x)/100 for x in best_sh["w"].split("/")])), float(best_sh["lev"]), "HYRO", ev_idx, fu_idx) dB, seB = paired_delta(w_argmax_dict, float(best["lev"]), {"TP01": 0.75, "SKH01": 0.25}, float(best_live["lev"]), "HYRO", ev_idx, fu_idx) print(f"\n DIFFERENZE APPAIATE (stessi indici di bootstrap; un livello di J ha SE ~0.01, " f"una differenza molto meno):") print(f" argmax J - argmax SHARPE : {dA:+.4f} +- {seA:.4f} " f"-> {'INDISTINGUIBILI' if abs(dA) < 2*seA else 'differenza reale'}") print(f" argmax J - libro LIVE : {dB:+.4f} +- {seB:.4f} " f"-> {'INDISTINGUIBILI' if abs(dB) < 2*seB else 'differenza reale'}") quota = abs(dA) / abs(dB) if dB != 0 else float("nan") print(f" La prima riga E' il risultato del filone: riallocare per la BARRIERA invece che") print(f" per lo SHARPE vale {dA:+.3f} di J, cioe' il {quota:.0%} del {dB:+.3f} che vale") print(f" passare dal libro LIVE a un libro diversificato QUALSIASI. Il grosso del guadagno") print(f" NON e' nella funzione obiettivo: e' nell'aggiungere lo sleeve che a $600 non entra.") print("\n Le prime 8 celle per J:") print(f" {'#':>3} {'pesi':>16} {'leva':>6} {'J':>7} {'P(pass)':>8} {'P(vivo)':>8} " f"{'Sharpe':>7} {'E[pay/a]':>10}") for i, (_, r) in enumerate(G3.sort_values("J", ascending=False).head(8).iterrows(), 1): print(f" {i:>3} {r['w']:>16} {r['lev']:>6.3f} {r['J']:>7.3f} {r['p_pass']:>8.1%} " f"{r['p_alive']:>8.1%} {r['sharpe']:>7.2f} {r['e_payout']:>9,.0f}$") print("\n E per E[payout] (obiettivo DIVERSO: un flusso, non una sopravvivenza):") print(f" {'#':>3} {'pesi':>16} {'leva':>6} {'E[pay/a]':>10} {'J':>7} {'P(pass)':>8} {'P(vivo)':>8}") for i, (_, r) in enumerate(G3.sort_values("e_payout", ascending=False).head(4).iterrows(), 1): print(f" {i:>3} {r['w']:>16} {r['lev']:>6.3f} {r['e_payout']:>9,.0f}$ {r['J']:>7.3f} " f"{r['p_pass']:>8.1%} {r['p_alive']:>8.1%}") hr("C-bis. LA LEVA — la variabile che lo Sharpe non determina e che decide tutto") print(f" pesi = argmax J ({best['w']}):") print(f" {'leva':>7} {'J':>7} {'P(pass) 365g':>13} {'P(pass) 1095g':>14} {'P(vivo 12m)':>12} " f"{'E[pay/a]':>10}") ev_long = boot_idx(n_hist, 1095, N_PATHS, seed=8222) w_best = dict(zip(CRYPTO_PROP, [int(x) / 100 for x in best["w"].split("/")])) Rb, Gb = book_RG(w_best) Rb = deluck(Rb) for lev in LEVS: o = objective(w_best, lev, "HYRO", ev_idx, fu_idx) pl_long = eval_sim(Rb, Gb, ev_long, lev, "HYRO")["p_pass"] print(f" {lev:>7.3f} {o['J']:>7.3f} {o['p_pass']:>12.1%} {pl_long:>13.1%} " f"{o['p_alive']:>11.1%} {o['e_payout']:>9,.0f}$") print("\n A 365 giorni P(pass) e' a CAMPANA nella leva (a leva bassa il drift non arriva a +10%") print(" in tempo); senza limite di tempo — che e' la regola vera di HyroTrader — e' MONOTONA") print(" DECRESCENTE, come dice il conto analitico. Le due colonne dicono cose diverse e vanno") print(" lette insieme: la leva alta non aumenta la probabilita' di farcela, aumenta la FRETTA.") # ---------------------------------------------------------------- D. i null tick("C. griglia + leva") hr("D. NULL 1 — l'argmax e' speciale, o e' solo il MASSIMO DI 45 ESTRAZIONI?") rw = random_weights(200, CRYPTO_PROP, seed=8223) lev_star = float(best["lev"]) Jr = np.array([objective(w, lev_star, "HYRO", ev_idx, fu_idx)["J"] for w in rw]) pctl = float((Jr < best["J"]).mean() * 100.0) bestofk = 100.0 * len(grid) / (len(grid) + 1) print(f" J dell'argmax = {best['J']:.3f} alla leva {lev_star:g}") print(f" null: {len(rw)} vettori di peso casuali uniformi sul simplesso (stesso campionatore di") print(f" `portfolio.weights_tilt_null`), valutati alla STESSA leva") print(f" mediana {np.median(Jr):.3f} | p90 {np.percentile(Jr, 90):.3f} | " f"max {Jr.max():.3f} | frazione sopra il LIVE 75/25 = {(Jr > best_live['J']).mean():.0%}") print(f" percentile dell'argmax fra i casuali = {pctl:.1f}° contro la firma best-of-" f"{len(grid)} = {bestofk:.1f}°") print(f" ⚠ RISOLUZIONE: con {len(rw)} estrazioni un percentile vale a passi di " f"{100/len(rw):.1f} punti -> {pctl:.1f}° e {bestofk:.1f}° NON sono distinguibili.") print(f" E il fatto che decide: il MASSIMO dei {len(rw)} pesi casuali e' {Jr.max():.3f} contro" f" {best['J']:.3f} dell'argmax") print(f" della griglia -> un vettore estratto a caso lo {'EGUAGLIA' if Jr.max() >= best['J'] - 1e-9 else 'non lo raggiunge'}." f" NESSUNA evidenza che quel vettore di pesi sia speciale:") print(" cio' che conta non e' QUALE punto del simplesso, ma STARE nella regione diversificata.") print(f" (la meta' bassa del null e' pero' informativa: il LIVE 75/25 e' battuto dal " f"{(Jr > best_live['J']).mean():.0%} dei pesi CASUALI)") tick("D. null casuale") hr("D-bis. NULL 2 — selezione IN-SAMPLE-ONLY (la scelta guardando J e' selezione come ogni altra)") half = n_hist // 2 R_all = {} for w in grid: R_, G_ = book_RG(w) R_all[wkey(w)] = (deluck(R_), G_) ev_h = boot_idx(half, EV_H, N_PATHS, seed=8224) fu_h = boot_idx(half, FU_H, N_PATHS, seed=8225) def _J(key, lev, sl): R_, G_ = R_all[key] e = eval_sim(R_[sl], G_[sl], ev_h, lev, "HYRO")["p_pass"] f = funded_sim(R_[sl], G_[sl], fu_h, lev, "HYRO", 100_000.0)["p_alive"] return e * f IS, OOS = slice(0, half), slice(half, n_hist) rows = [] for w in grid: k = wkey(w) for lev in LEVS: rows.append((k, lev, _J(k, lev, IS), _J(k, lev, OOS))) S = pd.DataFrame(rows, columns=["w", "lev", "J_is", "J_oos"]) pick = S.loc[S["J_is"].idxmax()] oracle = S.loc[S["J_oos"].idxmax()] live_oos = S[(S["w"] == "75/25/0")].sort_values("J_oos", ascending=False).iloc[0] print(f" prima meta' {idx[0].date()}->{idx[half-1].date()} | seconda {idx[half].date()}->{idx[-1].date()}") print(f" {'':>28} {'pesi':>16} {'leva':>6} {'J in-sample':>12} {'J out-of-sample':>16}") print(f" {'scelto SUL SOLO in-sample':>28} {pick['w']:>16} {pick['lev']:>6.3f} " f"{pick['J_is']:>12.3f} {S[(S['w']==pick['w'])&(S['lev']==pick['lev'])]['J_oos'].iloc[0]:>16.3f}") print(f" {'oracolo (max sull OOS)':>28} {oracle['w']:>16} {oracle['lev']:>6.3f} " f"{oracle['J_is']:>12.3f} {oracle['J_oos']:>16.3f}") print(f" {'libro LIVE 75/25 (baseline)':>28} {live_oos['w']:>16} {live_oos['lev']:>6.3f} " f"{live_oos['J_is']:>12.3f} {live_oos['J_oos']:>16.3f}") rho = float(S["J_is"].corr(S["J_oos"], method="spearman")) print(f" Spearman(J in-sample, J out-of-sample) su {len(S)} celle = {rho:+.2f}") print(" -> se rho e' alto la mappa e' stabile e la scelta si trasferisce; se e' ~0 l'argmax") print(" e' rumore e la decisione va presa sul MECCANISMO (la leva), non sulla cella.") tick("D-bis. selezione in-sample") hr("D-ter. NULL DEL DE-LEVERING — la leva e' GIA' dentro l'ottimizzazione, ma va detto") print(" Il null classico ('esiste k<1 che da' lo stesso DD con Sharpe migliore?') qui e'") print(" strutturale: ogni configurazione e' valutata a OTTO leve, quindi nessuna puo' vincere") print(" solo perche' e' meno esposta. La domanda che resta e': la MIGLIOR leva del libro LIVE") print(" raggiunge il J dell'argmax?") print(f" argmax J : {best['w']:>10} @ {best['lev']:g}x -> J = {best['J']:.3f}") print(f" LIVE 75/25 al suo : {best_live['w']:>10} @ {best_live['lev']:g}x -> J = {best_live['J']:.3f}") dJ = best["J"] - best_live["J"] se = np.sqrt(0.25 / N_PATHS) * 2 print(f" delta = {dJ:+.3f} (errore MC su livelli ~+-{se:.3f}; le due celle condividono") print(f" gli indici di bootstrap -> il delta e' APPAIATO e il suo errore e' piu' piccolo)") # ---------------------------------------------------------------- E. banda d'ancora hr("E. BANDA D'ANCORA — non ri-misurata sotto questa lente (costo); si usa il FATTORE misurato") print(" Ri-fare i 24x23x10 offset col recon MTM orario e' fuori budget. Si applica il fattore") print(" di drift MISURATO il 26/07 (x0.89 sul libro live) e si controlla cio' che conta: se") print(" l'ORDINE fra le configurazioni sopravvive alla banda 0.60 (stima 25/07, ri-misurata") print(" troppo severa) - 1.00 (nessuna correzione). L'argmax e' ri-cercato sulle 12 celle-peso") print(" migliori (non su tutte le 45: costo), il che e' conservativo per la STABILITA'.") cands = [best["w"], best_sh["w"], best_live["w"]] top_keys = list(G3.sort_values("J", ascending=False).drop_duplicates("w")["w"].head(12)) top_w = [dict(zip(CRYPTO_PROP, [int(x) / 100 for x in k.split("/")])) for k in top_keys] print(f"\n {'fattore':>9} " + " ".join(f"{('J ' + c):>14}" for c in dict.fromkeys(cands)) + f" {'argmax pesi':>14} {'argmax leva':>11}") for fct in (1.00, 0.89, 0.60): cells = [] for c in dict.fromkeys(cands): wc = dict(zip(CRYPTO_PROP, [int(x) / 100 for x in c.split("/")])) sub = [objective(wc, lv, "HYRO", ev_idx, fu_idx, factor=fct)["J"] for lv in LEVS] cells.append(max(sub)) g = grid_scan(top_w, LEVS, "HYRO", ev_idx, fu_idx, factor=fct) b = g.loc[g["J"].idxmax()] print(f" {fct:>9.2f} " + " ".join(f"{v:>14.3f}" for v in cells) + f" {b['w']:>14} {b['lev']:>11.3f}") hr("E-bis. LO STRESS CHE DECIDE — XS01 e' stato SCOPERTO in questa finestra") print(" L'argmax mette il 50% su XS01, il cui unico campione (2024+) e' anche quello su cui e'") print(" stato scoperto e affinato (blend di lookback + gate di dispersione). CLAUDE.md lo") print(" dichiara: 'la TAGLIA del guadagno e' ottimista'. Qui si taglia il DRIFT di XS01 (non la") print(" vol, non la coda) e si guarda se la conclusione operativa sopravvive.") print(f"\n {'drift XS01':>12} {'config':>16} {'leva':>6} {'J':>7} {'P(pass)':>8} " f"{'P(vivo)':>8} {'E[pay/a]':>10}") for mult in (1.00, 0.75, 0.50, 0.25, 0.00): dmt = (("XS01", mult),) rows = [] for w in top_w: for lv in LEVS: rows.append((w, lv, objective(w, lv, "HYRO", ev_idx, fu_idx, drift_mult=dmt))) w_, lv_, o_ = max(rows, key=lambda x: x[2]["J"]) lvL, oL = max(((lv, objective({"TP01": 0.75, "SKH01": 0.25}, lv, "HYRO", ev_idx, fu_idx, drift_mult=dmt)) for lv in LEVS), key=lambda x: x[1]["J"]) dd_, sd_ = paired_delta(w_, lv_, {"TP01": 0.75, "SKH01": 0.25}, lvL, "HYRO", ev_idx, fu_idx) print(f" {mult:>12.2f} {wkey(w_):>16} {lv_:>6.3f} {o_['J']:>7.3f} {o_['p_pass']:>8.1%} " f"{o_['p_alive']:>8.1%} {o_['e_payout']:>9,.0f}$ LIVE {oL['J']:.3f} " f"delta {dd_:+.3f} +-{sd_:.3f}") print("\n La colonna che decide e' l'ULTIMA. Se il vantaggio svanisce col drift di XS01, il") print(" risultato e' un'ipotesi sul RENDIMENTO ATTESO di XS01 misurato sulla sua finestra di") print(" scoperta; se resta a drift 0, il vantaggio e' pura DECORRELAZIONE e non dipende da") print(" quell'ipotesi. Il numero, non la narrazione, dice quale delle due.") hr("E-quater. LA DISTORSIONE CHE MI RIGUARDA — l'ottimo si appoggia a SKH01, e SKH01 e' lo") print(" sleeve con la FORTUNA D'ANCORA piu' grande da restituire. Il LOO del 26/07: l'ancora") print(" canonica gli regala 2/3 del contributo FULL (+0.43 -> +0.14) e il 70% dell'hold-out;") print(" l'audit 02/07 lo trova al 93-98° pctl di 23 offset, 'spike, non plateau'. Questa lente") print(" gira sull'ancora CANONICA e la banda non e' ri-misurabile qui -> il mio ottimo e'") print(" distorto A FAVORE di SKH01 e va detto PRIMA di leggerlo. Taglio del drift di SKH01:") print(f"\n {'drift SKH01':>13} {'config':>16} {'leva':>6} {'J':>7} {'J del LIVE':>11} " f"{'delta appaiato':>16}") for mult in (1.00, 0.66, 0.50, 0.33): dmt = (("SKH01", mult),) rows = [(w, lv, objective(w, lv, "HYRO", ev_idx, fu_idx, drift_mult=dmt)) for w in top_w for lv in LEVS] w_, lv_, o_ = max(rows, key=lambda x: x[2]["J"]) lvL, oL = max(((lv, objective({"TP01": 0.75, "SKH01": 0.25}, lv, "HYRO", ev_idx, fu_idx, drift_mult=dmt)) for lv in LEVS), key=lambda x: x[1]["J"]) dd_, sd_ = paired_delta(w_, lv_, {"TP01": 0.75, "SKH01": 0.25}, lvL, "HYRO", ev_idx, fu_idx) print(f" {mult:>13.2f} {wkey(w_):>16} {lv_:>6.3f} {o_['J']:>7.3f} {oL['J']:>11.3f} " f"{dd_:>+11.3f} +-{sd_:.3f}") print(" NB: il taglio si applica anche al LIVE 75/25 (che ha SKH01 al 25%), quindi il delta e'") print(" un confronto onesto: entrambe le colonne perdono, la domanda e' quale perde di piu'.") hr("E-ter. IL BUCO CHE LA FINESTRA NON PUO' CHIUDERE — 2024-2026 non contiene un CRASH") print(" L'ottimizzazione toglie peso a TP01 (Sharpe 0.79 su questa finestra). Ma TP01 e' lo") print(" sleeve DIFENSIVO: il LOO del 26/07 misura il suo contributo hold-out NEGATIVO nel 99.1%") print(" delle ancore e conclude 'uno sleeve difensivo si giudica sul SINISTRO, non sul premio'.") print(" Su un conto funded la barriera morde PROPRIO nel sinistro. Qui la coppia crypto (che ha") print(" storia dal 2019, quindi il 2022 dentro) e' ri-ottimizzata sulle due finestre:") cry_full = pc.crypto_daily_tuples(1.0, 0.0).index W2 = [{"TP01": i / 8, "SKH01": 1 - i / 8} for i in range(9)] print(f"\n {'finestra':>26} {'giorni':>7} {'argmax TP/SKH':>15} {'leva':>6} {'J':>7} " f"{'J del 75/25':>12} {'Sharpe argmax':>14}") for st, lab in (("2019-03-14", "FULL 2019+ (col 2022)"), (START_WIN, "2024+ (senza crash)")): pairs = {} for w in W2: c = pc.crypto_daily_tuples(w["TP01"], w["SKH01"]) c = c[c.index >= pd.Timestamp(st, tz="UTC")] pairs[wkey(w)] = (deluck(c["R"].values), np.minimum(c["m"].values, c["R"].values) - c["R"].values) nh = len(next(iter(pairs.values()))[0]) ev2 = boot_idx(nh, EV_H, N_PATHS, seed=8230) fu2 = boot_idx(nh, FU_H, N_PATHS, seed=8231) res = [] for k, (R_, G_) in pairs.items(): for lv in LEVS: jj = (eval_sim(R_, G_, ev2, lv, "HYRO")["p_pass"] * funded_sim(R_, G_, fu2, lv, "HYRO", 100_000.0)["p_alive"]) res.append((k, lv, jj, R_.mean() * 365 / (R_.std() * np.sqrt(365)))) bk, blv, bj, bsh = max(res, key=lambda x: x[2]) j75 = max(x[2] for x in res if x[0] == "75/25/0") print(f" {lab:>26} {nh:>7d} {bk.rsplit('/', 1)[0]:>15} {blv:>6.3f} {bj:>7.3f} " f"{j75:>12.3f} {bsh:>14.2f}") print("\n Se anche sulla finestra che CONTIENE il 2022 l'ottimo scarta TP01, la sua sotto-") print(" pesatura non e' un artefatto di finestra. Se invece li' TP01 pesa, allora la") print(" raccomandazione del 2024+ e' una scommessa sull'assenza di crash — e su una barriera") print(" di rovina quella scommessa la paghi una volta sola. **Questa finestra NON puo' dirlo") print(" per XS01**, che nasce nel 2024: il buco resta aperto per lui, per costruzione.") # ---------------------------------------------------------------- F. VRP01 / GTAA01 tick("E. banda ancora") hr("F. E GLI SLEEVE 'INUTILI A $600'? — l'asimmetria del 25/07 §4 vale per XS01, non per tutti") print(" Su un conto funded da $100k gli sleeve fuori portata a $600 diventano ESEGUIBILI per") print(" TAGLIA. Ma la taglia non e' l'unico vincolo: una firm CRYPTO non lista opzioni Deribit") print(" (VRP01) ne' ETF azionari USA (GTAA01, che al retail UE e' pure bloccato dal PRIIPs).") print(" Qui si misura comunque il CONTROFATTUALE: se fossero disponibili, aiuterebbero?") print(f"\n {'aggiunta al ' + best['w']:>34} {'leva':>6} {'J':>7} {'P(pass)':>8} " f"{'P(vivo)':>8} {'Sharpe':>7} {'E[pay/a]':>10}") for extra, wx in [(None, 0.0), ("VRP01", 0.10), ("VRP01", 0.20), ("GTAA01", 0.10), ("GTAA01", 0.20)]: w2 = {k: v * (1.0 - wx) for k, v in w_best.items()} if extra: w2[extra] = wx rows = [(lv, objective(w2, lv, "HYRO", ev_idx, fu_idx)) for lv in LEVS] lv, o = max(rows, key=lambda x: x[1]["J"]) lab = "nessuna (base)" if extra is None else f"{extra} @ {wx:.0%}" print(f" {lab:>34} {lv:>6.3f} {o['J']:>7.3f} {o['p_pass']:>8.1%} {o['p_alive']:>8.1%} " f"{o['sharpe']:>7.2f} {o['e_payout']:>9,.0f}$") print("\n Le righe VRP01 e GTAA01 escono quasi IDENTICHE: non e' un errore, e' che sotto questa") print(" funzione obiettivo i due sleeve sono lo stesso oggetto (vol ~6.2%, drift ~7.7%, corr ~0") print(" col crypto). La barriera vede vol, drift e correlazione; la differenza fra i due — skew") print(" -4.6 contro -0.4 — a peso 10-20% e' diluita sotto la soglia di daily-loss.") print("\n ATTENZIONE: la riga VRP01 e' calcolata con **gap = 0** (il mark infra-settimana non") print(" esiste nel dataset dello sleeve): e' un TETTO, non una stima. Con il mark misurato il") print(" 30/07 (peggiore infra-settimana mediano -6.9%, minimo -81.7%) una barriera STATICA al") print(" 6% verrebbe sfondata dal mark, non dal risultato.") # ---------------------------------------------------------------- G. FTMO tick("F. VRP/GTAA") hr("G. LA BARRIERA CAMBIA LA RISPOSTA? — HYRO (-6% statico) vs FTMO (-10% statico)") print(f" {'firm':>6} {'argmax pesi':>16} {'leva':>6} {'J':>7} {'P(pass)':>8} {'P(vivo12m)':>11} " f"{'E[pay/a]':>10} | {'LIVE 75/25: leva':>17} {'J':>7}") for fm in ("HYRO", "FTMO"): g = G3 if fm == "HYRO" else grid_scan(grid, LEVS, fm, ev_idx, fu_idx) b = g.loc[g["J"].idxmax()] lv2 = g[g["w"] == "75/25/0"] b2 = lv2.loc[lv2["J"].idxmax()] print(f" {fm:>6} {b['w']:>16} {b['lev']:>6.3f} {b['J']:>7.3f} {b['p_pass']:>8.1%} " f"{b['p_alive']:>11.1%} {b['e_payout']:>9,.0f}$ | {b2['lev']:>17.3f} {b2['J']:>7.3f}") print("\n La barriera piu' larga di FTMO sposta l'ottimo di leva verso l'ALTO: il vincolo e' la") print(" barriera, non il libro. E' la stessa lettura del 25/07 §4 ('sul canale funded il") print(" vincolo binding e' la regola di DD, non il capitale'), qui misurata sull'ottimo.") # ---------------------------------------------------------------- H. gate ufficiale sui pesi tick("G. FTMO") hr("H. GATE UFFICIALE SUI PESI (`portfolio.weights_tilt_null`) — sullo SHARPE, come previsto") cols = {nm: off[nm] for nm in CRYPTO_PROP} try: res = weights_tilt_null(cols, {"TP01": 0.75, "SKH01": 0.25, "XS01": 0.0001}, {nm: max(w_best.get(nm, 0.0), 1e-4) for nm in CRYPTO_PROP}, floor=0.0, n=200, k_seen=len(grid) * len(LEVS)) print(f" proposta {best['w']} vs LIVE 75/25 : delta_insample {res['delta_insample']:+.4f} | " f"delta_hold {res['delta_hold']:+.4f} | pctl_hold {res['pctl_hold']:.1f}° " f"(best-of-k {res['bestofk_pctl']:.1f}°) | gate_pass = {res['gate_pass']}") except Exception as exc: # pragma: no cover print(f" non girato: {exc}") print(" NB: questo gate giudica lo SHARPE del book di ricerca, non J. Un cambio dei pesi del") print(" LIBRO LIVE resta soggetto a lui; qui si progetta un conto SEPARATO (funded), che non") print(" e' il libro live e non passa da questo gate.") # ---------------------------------------------------------------- I. tavola operativa tick("H. gate pesi") hr("I. TAVOLA OPERATIVA — le 3 configurazioni migliori, e quanti conti servono per 50 EUR/g") top = G3.sort_values("J", ascending=False).drop_duplicates("w").head(3) rows_ops = [] fu3 = boot_idx(n_hist, 1095, N_PATHS, seed=8226) print(f" {'pesi TP/SKH/XS':>16} {'leva':>6} {'P(pass)':>8} {'P(vivo12m)':>11} " f"{'E[payout 3a]':>13} {'EUR/g per conto':>16} {'conti per 50/g':>15}") for _, r in top.iterrows(): w2 = dict(zip(CRYPTO_PROP, [int(x) / 100 for x in r["w"].split("/")])) R_, G_ = book_RG(w2) R_ = deluck(R_) f3 = funded_sim(R_, G_, fu3, float(r["lev"]), "HYRO", 100_000.0) eur_day = f3["e_payout"] / EURUSD / 3.0 / 365.0 n_acct = (TARGET_EUR_DAY / eur_day) if eur_day > 0 else np.inf rows_ops.append((r["w"], float(r["lev"]), f3["e_payout"], eur_day, n_acct)) print(f" {r['w']:>16} {r['lev']:>6.3f} {r['p_pass']:>8.1%} {r['p_alive']:>11.1%} " f"{f3['e_payout']:>12,.0f}$ {eur_day:>15.2f} {n_acct:>15.1f}") print("\n E[payout 3a] e' su UN conto funded da $100k, gia' netto split 80% e fisco 33%, e") print(" MEDIA su tutti i percorsi (i morti contano zero). 'conti per 50/g' e' quindi un numero") print(" ATTESO, non una garanzia: la distribuzione e' bimodale (vivo / bustato).") cap_tot = sum(f["cap"] for f in pl.FIRMS) print(f" ⚠ TETTO STRUTTURALE: cap per trader ${FIRMS['HYRO']['cap']:,.0f} (HYRO) + " f"${FIRMS['FTMO']['cap']:,.0f} (FTMO) = ${cap_tot:,.0f} di nozionale su due firm") print(f" -> al massimo {cap_tot/100_000:.0f} conti da $100k. Se ne servono di piu', servono " f"ALTRE FIRM (e ognuna aggiunge rischio di controparte).") tick("I. tavola operativa") hr("I-bis. LA SCALA DI CONTI — macchineria del 25/07 (`r0725_prop_coupled.simulate`), " "de-luck portato a x0.89") print(f" 36 mesi, EUR {pl.START_EUR:.0f} di cassa iniziale, max {pl.MAX_CONCURRENT} conti, " f"morte-firm {pl.FIRM_DEATH_PER_YEAR:.0%}/anno, lente ACCOPPIATA") pc.DELUCK = DELUCK # il 25/07 girava a 0.6 (poi ri-misurato troppo severo) cfg_best = tuple(w_best.get(nm, 0.0) for nm in CRYPTO_PROP) cfg_live = (0.75, 0.25, 0.0) cfg_B = (0.55, 0.20, 0.25) pols = { f"argmax-J {best['w']} su tutti i conti": ([cfg_best], [0] * pl.MAX_CONCURRENT), "LIVE 75/25 su tutti i conti (baseline)": ([cfg_live], [0] * pl.MAX_CONCURRENT), "config B 55/20/25 (scelta a mano 25/07)": ([cfg_B], [0] * pl.MAX_CONCURRENT), } print(f"\n {'politica':>44} {'leva':>6} {'EUR/g med':>10} {'p90':>8} {'P(>=10/g)':>10} " f"{'P(>=50/g)':>10} {'P(zero)':>8}") for pname, (cfgs, slots) in pols.items(): for lev in sorted({round(float(best["lev"]), 3), 0.75}): r = pc.simulate(cfgs, slots, n_paths=1500, lev=lev, seed=20260822, lens="coupled", start=START_WIN) print(f" {pname:>44} {lev:>6.3f} {r['med']:>10.2f} {r['p90']:>8.2f} " f"{r['p_ge10']:>10.1%} {r['p_target']:>10.1%} {r['p_zero']:>8.1%}") print("\n ⚠ QUESTO NUMERO REVISIONA UN HEADLINE DI CLAUDE.md ('EUR600 -> 50 EUR/g ~ 6% in 3") print(" anni, P(perdere i 600) 52-65%', 25/07). Due cose sono cambiate insieme: il fattore di") print(" de-luck (0.60 -> 0.89, ri-misurato il 26/07) e l'ALLOCAZIONE. Vanno separate, o il") print(" merito finisce sulla leva sbagliata. Decomposizione 2x2 (stessa macchina, stesso seme):") print(f"\n {'de-luck':>9} {'allocazione':>26} {'EUR/g med':>10} {'P(>=50/g)':>10} {'P(zero)':>8}") for fct in (0.60, 0.89): pc.DELUCK = fct for lab, cfg in (("LIVE 75/25 (25/07)", cfg_live), (f"argmax-J {best['w']}", cfg_best)): r = pc.simulate([cfg], [0] * pl.MAX_CONCURRENT, n_paths=1500, lev=0.75, seed=20260822, lens="coupled", start=START_WIN) print(f" {fct:>9.2f} {lab:>26} {r['med']:>10.2f} {r['p_target']:>10.1%} " f"{r['p_zero']:>8.1%}") print("\n ⚠ Il headline ~6% del 25/07 NON e' la riga 'LIVE su tutti i conti' (quella li' era") print(" CONC-2SL, dichiarata 'la PEGGIORE'): era la politica MISTA, sleeve diversi su conti") print(" diversi. Replica ESATTA di quella politica, stessa macchina, de-luck 0.60, leva 0.75:") pc.DELUCK = 0.60 cfgs25 = [(0.75, 0.25, 0.0), (0.55, 0.20, 0.25), (1.0, 0.0, 0.0), (0.0, 1.0, 0.0), (0.0, 0.0, 1.0)] r25 = pc.simulate(cfgs25, [1, 4, 1, 2, 1, 4], n_paths=1500, lev=0.75, seed=20260822, lens="coupled", start=START_WIN) print(f" MISTO (25/07) @0.75x, de-luck 0.60 : EUR/g med {r25['med']:.2f} | " f"P(>=50/g) {r25['p_target']:.1%} | P(zero) {r25['p_zero']:.1%}") print(f" -> il 25/07 pubblico' P(>=50/g) 5.6-6.5% e P(zero) 52% a 0.75x. " f"{'REPLICA OK' if 0.03 <= r25['p_target'] <= 0.10 else 'NON REPLICA — leggere con cautela'}") pc.DELUCK = DELUCK print("\n Lettura della 2x2: il de-luck e l'allocazione contribuiscono ENTRAMBI, e vanno citati") print(" separati. Il fattore 0.60 era una stima a occhio poi RI-MISURATA troppo severa (26/07):") print(" usarlo ancora sarebbe sbagliato, ma il salto da 1.7% a 41.7% e' il prodotto di due") print(" correzioni, non di una scoperta.") print("\n" + "=" * 104) print(f" fatto in {time.time()-t0:.0f}s") print("=" * 104) if __name__ == "__main__": main()