"""r0701_xs — RESIDUAL (IDIOSYNCRATIC) MOMENTUM cross-sectional sui 19 major Hyperliquid. TESI (2026-07-01). STATARB-RESID (thread 4, 2026-06-29) ha mostrato che il MOMENTUM del residuo ETH−β·BTC (β OLS rolling, sgn=+1: le dislocazioni CONTINUANO a 1d) passa quasi tutti i gate su 2 gambe, fallendo SOLO il deflated-Sharpe (0.929<0.95). Angolo nuovo: lo stesso meccanismo CROSS-SECTIONAL sui 19 major di XS01 — per ogni alt, residuo vs β·BTC (β OLS rolling B giorni), momentum del residuo su lookback (blend z-score [30,90] come XS01, o singolo), rank cross-section, long top-k / short bottom-k, vol-target 20%. Ipotesi: la breadth (18 stream invece di 1) alza il DSR dove il 2-gambe falliva. DISTINZIONE da quanto gia' testato: * IREV (xsec_v2_nonmom, idio-REVERSAL, sgn=-1): FALLITO. Qui sgn=+1 (idio-MOMENTUM). * IMOM (xsec_v2_nonmom): residuo vs mercato EQUAL-WEIGHT, B=60 fisso, no blend, era solo "riferimento momentum". Qui: fattore = BTC (come STATARB-RESID), B in griglia, blend z-score [30,90] + probe con gate di dispersione (parita' strutturale con XS01), selezione IN-SAMPLE. IL BAR (fondamentale): una variante di XS01 e' utile SOLO se (a) SOSTITUISCE XS01 (meglio standalone E nel portafoglio 4-sleeve) oppure (b) AGGIUNGE come 5o sleeve (corr bassa a XS01 E TP01, uplift del PORTAFOGLIO). corr>0.6 a XS01 senza batterlo -> REDUNDANT/SCARTATO. Baseline XS01: standalone FULL Sh ~1.50 / HOLD ~1.71 / DD ~11%. GATE (CLAUDE.md, metodologia obbligatoria): 1. CAUSALE: score a close[i], peso tenuto in i+1 (engine shifta W[i-1]*dret[i]); barre vol=0 escluse; prefix-check di causalita' sulla cella scelta. 2. NETTO fee 0.10% RT per gamba per ribilancio + sweep {0.05, 0.10, 0.20, 0.30}% RT/gamba. 3. Selezione cella IN-SAMPLE-ONLY (pre-2025, anti selection-on-holdout), poi hold-out bloccato. 4. DEFLATED Sharpe su TUTTI i trial della griglia (serve >=0.95). 5. Confronto PORTAFOGLIO: sostituzione di XS01 a parita' di peso + aggiunta 5o sleeve @10/15% (riusa StrategyPortfolio/active_sleeves senza modificarli) + marginal vs il BOOK a 4 sleeve. 6. CAVEAT IMMUTABILI: storia HL ~2.5 anni; book L/S a ~2k gambe -> STAT-MODE a $600 (dichiaro comunque l'haircut small-cap $600/min$5). uv run python scripts/research/r0701_xs_residmom.py """ from __future__ import annotations import sys from pathlib import Path PROJECT_ROOT = Path(__file__).resolve().parents[2] sys.path.insert(0, str(PROJECT_ROOT / "scripts" / "research")) sys.path.insert(0, str(PROJECT_ROOT)) import numpy as np import pandas as pd import xsec_v2_nonmom as xv # harness collaudato (load_matrix, deflated_sharpe, portfolio, ...) from src.portfolio.sleeves import XS_UNIVERSE DPY, TV, FEE, HOLDOUT = xv.DPY, xv.TV, xv.FEE, xv.HOLDOUT FACTOR = "BTC" # il fattore del residuo (come STATARB-RESID), NON tradato BLEND = (30, 90) # blend z-score come XS01 # griglia (modesta, da mandato): beta-window x lookback x k x H (gate=off) BETAS = (60, 90, 120) LOOKS = ("blend", 30, 90) KS = (3, 5) HS = (5, 10, 20) # probe a parita' STRUTTURALE con XS01 (blend + gate dispersione p30, H10 k5) — contano nei trial GATED_PROBES = [dict(B=B, L="blend", k=5, H=10, gate=30) for B in BETAS] # =========================================================================== # SCORE BUILDER — residual momentum vs beta*BTC. CAUSALE (dati <= i). # Ritorna score_at(i) -> (score_blend_z[A], valid[A], disp_raw_i) + warmup. # disp_raw_i = dispersione cross-section del momentum RESIDUO grezzo (per il gate: lo z-score # blended ha std ~1 per costruzione, quindi la dispersione va misurata sul grezzo). # =========================================================================== def make_residmom(PX: pd.DataFrame, B: int, L): lookbacks = BLEND if L == "blend" else (int(L),) px = PX.values n, A = px.shape fi = list(PX.columns).index(FACTOR) DR = PX.pct_change() m = DR[FACTOR] beta, _ = xv._rolling_beta(DR, m, B) # beta_j vs BTC su finestra B (<= i) SDR = {Lk: DR.rolling(Lk, min_periods=int(0.8 * Lk)).sum().values for Lk in lookbacks} SM = {Lk: m.rolling(Lk, min_periods=int(0.8 * Lk)).sum().values for Lk in lookbacks} CNT = {Lk: DR.rolling(Lk, min_periods=1).count().values for Lk in lookbacks} def score_at(i): b = beta[i] valid = np.isfinite(px[i]) & np.isfinite(b) valid[fi] = False # BTC = fattore, fuori dal cross-section resids = [] for Lk in lookbacks: resid = SDR[Lk][i] - b * SM[Lk][i] # momentum del residuo r_j - beta_j*r_btc valid = valid & np.isfinite(resid) & (CNT[Lk][i] >= 0.8 * Lk) resids.append(resid) score = np.full(A, np.nan) disp = np.nan nv = int(valid.sum()) if nv >= 2: acc = np.zeros(nv) cnt = 0 stds = [] for resid in resids: r = resid[valid] sd = float(r.std()) stds.append(sd) if sd > 0: acc += (r - r.mean()) / sd cnt += 1 if cnt: score[valid] = acc / cnt # blend: media z-score cross-sectional disp = float(np.mean(stds)) # dispersione del momentum residuo GREZZO return score, valid, disp return score_at, max(max(lookbacks), B) + 1 # =========================================================================== # ENGINE locale (= xv.xs_engine + ritorno di W/scale + gate di dispersione opzionale). # L'uguaglianza con xv.xs_engine sulle celle non-gated e' VERIFICATA in main(). # =========================================================================== def xs_engine_w(PX, VOL, score_at, H, k, target_vol=TV, fee=FEE, min_assets=10, warmup=0, disp_pct=0, disp_minhist=20): px = PX.values vol = VOL.values n, A = px.shape dret = np.full((n, A), np.nan) dret[1:] = px[1:] / px[:-1] - 1.0 W = np.zeros((n, A)) w = np.zeros(A) disp_hist = [] for i in range(n): if i >= warmup and i % H == 0: score, valid, disp = score_at(i) valid = valid & np.isfinite(score) & (vol[i] > 0) idxv = np.where(valid)[0] if len(idxv) >= min_assets: thr = (np.percentile(disp_hist, disp_pct) if (disp_pct > 0 and len(disp_hist) >= disp_minhist) else -np.inf) if not (disp_pct > 0) or (np.isfinite(disp) and disp >= thr): kk = min(k, len(idxv) // 2) order = idxv[np.argsort(score[idxv])] lo, hi = order[:kk], order[-kk:] w = np.zeros(A) w[hi] = 0.5 / kk # long alto residual-momentum (sgn=+1) w[lo] = -0.5 / kk # short basso else: w = np.zeros(A) # regime compatto -> flat (gate) if disp_pct > 0 and np.isfinite(disp): disp_hist.append(disp) else: w = np.zeros(A) W[i] = w gross = np.zeros(n) gross[1:] = np.nansum(W[:-1] * np.nan_to_num(dret[1:]), axis=1) turn = np.zeros(n) turn[0] = np.abs(W[0]).sum() turn[1:] = np.abs(np.diff(W, axis=0)).sum(axis=1) net = gross - turn * (fee / 2.0) s = pd.Series(net, index=PX.index) rv = s.rolling(30, min_periods=15).std().shift(1) * np.sqrt(DPY) scale = np.clip(np.nan_to_num(target_vol / rv.replace(0, np.nan).values, nan=0.0), 0, 3.0) turn_py = float(turn.sum() / (n / DPY)) if n else 0.0 return pd.Series(s.values * scale, index=PX.index), turn_py, W, scale def run_cell(PX, VOL, cfg, fee=FEE): score_at, warm = make_residmom(PX, cfg["B"], cfg["L"]) daily, turn, W, scale = xs_engine_w(PX, VOL, score_at, cfg["H"], cfg["k"], fee=fee, warmup=warm, disp_pct=cfg.get("gate", 0)) return xv.to_daily(daily), turn, W, scale # =========================================================================== # CAUSALITA' (prefix-check, pattern di xv.causality_prefix_check sul nostro engine) # =========================================================================== def causality_prefix_check(PX, VOL, cfg, frac=0.85, tail=60, tol=1e-9): score_full, warm = make_residmom(PX, cfg["B"], cfg["L"]) full, *_ = xs_engine_w(PX, VOL, score_full, cfg["H"], cfg["k"], warmup=warm, disp_pct=cfg.get("gate", 0)) cut = int(len(PX) * frac) PXc, VOLc = PX.iloc[:cut], VOL.iloc[:cut] score_pre, warm2 = make_residmom(PXc, cfg["B"], cfg["L"]) pre, *_ = xs_engine_w(PXc, VOLc, score_pre, cfg["H"], cfg["k"], warmup=warm2, disp_pct=cfg.get("gate", 0)) lo = max(0, cut - tail) a, b = full.values[lo:cut], pre.values[lo:cut] worst = float(np.max(np.abs(a - b))) if len(a) else float("nan") return dict(ok=bool(worst <= tol), max_tail_diff=worst, cut=cut, tail=len(a)) # =========================================================================== # SMALL-CAP $600 (dichiarativo: il book resta STAT-MODE comunque, come XS01). # Pesi EFFETTIVI = W[i] * scale[i+1] (scale[i+1] usa net fino a i via shift(1) -> noto a close i). # Un cambio-gamba con |dw|*capital < min_order NON si esegue. Confronto realistico vs modeled # sulla STESSA simulazione a pesi (coerente internamente). # =========================================================================== def smallcap_check(PX, W, scale, fee=FEE, capital=600.0, min_order=5.0): px = PX.values n, A = px.shape dret = np.nan_to_num(np.vstack([np.zeros(A), px[1:] / px[:-1] - 1.0])) sc = np.roll(scale, -1) sc[-1] = scale[-1] T = W * sc[:, None] # target effettivo deciso a close i def sim(min_ord): held = np.zeros((n, A)) cur = np.zeros(A) n_tr = 0 for i in range(n): d = np.abs(T[i] - cur) * capital ex = d >= min_ord n_tr += int(ex.sum()) cur = np.where(ex, T[i], cur) held[i] = cur pos = np.zeros((n, A)) pos[1:] = held[:-1] turn = np.abs(np.diff(held, axis=0, prepend=np.zeros((1, A)))).sum(axis=1) net = (pos * dret).sum(axis=1) - turn * (fee / 2.0) r = net[np.isfinite(net)] sh = float(r.mean() / r.std() * np.sqrt(DPY)) if r.std() > 0 else 0.0 return sh, n_tr, float(turn.sum() / (n / DPY)) sh_real, ntr_real, turn_real = sim(min_order) sh_mod, ntr_mod, turn_mod = sim(0.0) return dict(sharpe_modeled=round(sh_mod, 3), sharpe_realistic=round(sh_real, 3), haircut=round(sh_mod - sh_real, 3), n_executed=ntr_real, n_modeled=ntr_mod, turnover_real=round(turn_real, 1), turnover_modeled=round(turn_mod, 1)) # =========================================================================== # PORTAFOGLIO — sostituzione XS01 + aggiunta 5o sleeve (pattern di xsec_v3_momstruct) # =========================================================================== _BASE = None _BASE_M = None def _base(): global _BASE, _BASE_M if _BASE is None: _BASE = xv.active_sleeves() pf = xv.StrategyPortfolio(_BASE) pf.backtest() _BASE_M = (xv.metrics(pf.combined_daily()), xv.metrics(pf.combined_daily(lo=HOLDOUT))) return _BASE, _BASE_M def add_uplift(daily, fr): base, _ = _base() wraw = fr / (1.0 - fr) cand = xv.Sleeve("R0701_cand", wraw, lambda d=daily: d) pf = xv.StrategyPortfolio(base + [cand]) return (xv.metrics(pf.combined_daily()), xv.metrics(pf.combined_daily(lo=HOLDOUT)), pf.weights().get("R0701_cand", 0.0)) def substitute_xs01(daily): base, _ = _base() sub = [xv.Sleeve("R0701_sub", s.weight, lambda d=daily: d) if s.name == "XS01_xsec_hl" else s for s in base] pf = xv.StrategyPortfolio(sub) return xv.metrics(pf.combined_daily()), xv.metrics(pf.combined_daily(lo=HOLDOUT)) def marginal_vs_book(daily): """Corr + uplift del blend 0.9*BOOK+0.1*cand vs il BOOK a 4 sleeve (full/hold + multi-cut).""" base, _ = _base() book = xv.StrategyPortfolio(base).combined_daily() J = pd.concat({"B": book, "C": daily}, axis=1, join="inner").dropna() def _sh(s): r = np.asarray(s.dropna().values, float) return float(r.mean() / r.std() * np.sqrt(DPY)) if len(r) > 2 and r.std() > 0 else 0.0 def _up(sub): return _sh(0.9 * sub["B"] + 0.1 * sub["C"]) - _sh(sub["B"]) JH = J[J.index >= HOLDOUT] cuts = {} for y in sorted(set(J.index.year))[1:]: sub = J[J.index >= pd.Timestamp(f"{y}-01-01", tz="UTC")] if len(sub) >= 120: cuts[int(y)] = round(_up(sub), 3) return dict(corr_book=round(float(J["B"].corr(J["C"])), 3), uplift_full=round(_up(J), 3), uplift_hold=round(_up(JH), 3) if len(JH) > 30 else None, multicut=cuts) # =========================================================================== def per_year(daily): return [(int(y), round(float((1 + g).prod() - 1), 3)) for y, g in daily.groupby(daily.index.year)] def tag(cfg): g = f" gate{cfg['gate']}" if cfg.get("gate") else "" return f"B{cfg['B']} L{cfg['L']} k{cfg['k']} H{cfg['H']}{g}" def main(): print("=" * 104) print(" r0701_xs — RESIDUAL MOMENTUM cross-sectional (residuo vs beta*BTC) sui 19 major HL — STAT-MODE") print("=" * 104) PX, VOL = xv.load_matrix(XS_UNIVERSE) print(f" universo 19-major: {PX.shape[1]} asset, {PX.shape[0]} giorni " f"[{PX.index[0].date()} -> {PX.index[-1].date()}] fattore={FACTOR} (escluso dal cross-section)") tp_daily = xv.tp01_sleeve().daily() xs_daily = xv.xsec_sleeve().daily() xs_f = xv.metrics(xs_daily) xs_h = xv.metrics(xs_daily[xs_daily.index >= HOLDOUT]) print(f" baseline XS01 (sleeve attivo): FULL Sh {xs_f['sharpe']:.2f} DD {xs_f['maxdd']*100:.0f}%" f" | HOLD Sh {xs_h['sharpe']:.2f}") # --- sanity: engine locale == xv.xs_engine sulle celle non-gated ----------------- chk_cfg = dict(B=90, L=30, k=5, H=10) score_at, warm = make_residmom(PX, chk_cfg["B"], chk_cfg["L"]) mine, *_ = xs_engine_w(PX, VOL, score_at, chk_cfg["H"], chk_cfg["k"], warmup=warm) ref, _ = xv.xs_engine(PX, VOL, lambda i: score_at(i)[:2], chk_cfg["H"], chk_cfg["k"], warmup=warm) dmax = float(np.nanmax(np.abs(mine.values - ref.values))) assert dmax < 1e-12, f"engine locale diverge da xv.xs_engine: {dmax}" print(f" [sanity] engine locale == xv.xs_engine (maxdiff {dmax:.1e})") # --- griglia ----------------------------------------------------------------------- grid = [dict(B=B, L=L, k=k, H=H) for B in BETAS for L in LOOKS for k in KS for H in HS] grid += GATED_PROBES rows = [] for cfg in grid: daily, turn, W, scale = run_cell(PX, VOL, cfg) if daily.std() == 0 or len(daily) < 60: continue f, h, pct = xv.evalcfg(daily) ins = daily[daily.index < HOLDOUT] is_sh = xv.metrics(ins)["sharpe"] if len(ins) > 60 else float("nan") rows.append(dict(cfg=cfg, daily=daily, W=W, scale=scale, turn=turn, full=f["sharpe"], hold=h["sharpe"], dd=f["maxdd"], ret=f["ret"], pct=pct, is_sh=is_sh, corrXS=xv._corr(daily, xs_daily), corrTP=xv._corr(daily, tp_daily))) all_sr = [r["full"] for r in rows] print(f"\n griglia: {len(rows)} celle valide su {len(grid)} " f"(trial per deflated-Sharpe = {len(all_sr)}; il conteggio VERO del programma e' >>)") hdr = f" {'cfg':<26}{'IS':>7}{'FULL':>7}{'HOLD':>7}{'DD%':>6}{'anni+':>7}{'corrXS':>8}{'corrTP':>8}{'turn/y':>8}" valid = [r for r in rows if np.isfinite(r["is_sh"])] print("\n TOP-5 per Sharpe IN-SAMPLE (pre-2025) — la selezione ONESTA:") print(hdr) for r in sorted(valid, key=lambda r: -r["is_sh"])[:5]: print(f" {tag(r['cfg']):<26}{r['is_sh']:>7.2f}{r['full']:>7.2f}{r['hold']:>7.2f}" f"{r['dd']*100:>6.0f}{r['pct']*100:>6.0f}%{r['corrXS']:>+8.2f}{r['corrTP']:>+8.2f}{r['turn']:>8.0f}") print("\n TOP-5 per HOLD (solo trasparenza — selezionare qui = selection-on-holdout):") print(hdr) for r in sorted(rows, key=lambda r: -r["hold"])[:5]: iss = f"{r['is_sh']:.2f}" if np.isfinite(r["is_sh"]) else "n/a" print(f" {tag(r['cfg']):<26}{iss:>7}{r['full']:>7.2f}{r['hold']:>7.2f}" f"{r['dd']*100:>6.0f}{r['pct']*100:>6.0f}%{r['corrXS']:>+8.2f}{r['corrTP']:>+8.2f}{r['turn']:>8.0f}") if not valid: print("\n >>> nessuna cella con in-sample valutabile. SCARTATO.") return pick = max(valid, key=lambda r: r["is_sh"]) daily = pick["daily"] print("\n" + "=" * 104) print(f" CELLA SCELTA (in-sample-only): {tag(pick['cfg'])}") print("=" * 104) print(f" IS Sh {pick['is_sh']:.2f} | FULL {pick['full']:.2f} | HOLD {pick['hold']:.2f}" f" | DD {pick['dd']*100:.0f}% | ret {pick['ret']*100:+.0f}% | anni+ {pick['pct']*100:.0f}%" f" | turnover/y {pick['turn']:.0f}") print(f" corr vs XS01 {pick['corrXS']:+.2f} | corr vs TP01 {pick['corrTP']:+.2f}" f" | per-anno {per_year(daily)}") caus = causality_prefix_check(PX, VOL, pick["cfg"]) print(f" CAUSALITA' (prefix-check): ok={caus['ok']} max_tail_diff={caus['max_tail_diff']:.2e}") dsr, sr0 = xv.deflated_sharpe(pick["full"], all_sr, daily) print(f" DEFLATED Sharpe (N={len(all_sr)} trial di QUESTA griglia): {dsr:.3f}" f" | soglia null-max annualizz. {sr0:.2f} (serve >=0.95)") print(" fee sweep (RT per gamba):", end=" ") for fee in (0.0005, 0.001, 0.002, 0.003): d_f, *_ = run_cell(PX, VOL, pick["cfg"], fee=fee) ff = xv.metrics(d_f) hh = xv.metrics(d_f[d_f.index >= HOLDOUT]) print(f"{fee*100:.2f}%: F{ff['sharpe']:+.2f}/H{hh['sharpe']:+.2f}", end=" ") print() sc = smallcap_check(PX, pick["W"], pick["scale"]) print(f" SMALL-CAP $600/min$5 (dichiarativo, resta STAT-MODE): modeled Sh {sc['sharpe_modeled']:.2f}" f" -> realistic {sc['sharpe_realistic']:.2f} (haircut {sc['haircut']:+.2f});" f" fill eseguiti {sc['n_executed']}/{sc['n_modeled']}" f" turn/y {sc['turnover_real']:.0f} vs {sc['turnover_modeled']:.0f}") # --- confronto PORTAFOGLIO ----------------------------------------------------------- print("\n PORTAFOGLIO (TP01+XS01+VRP01+SKH01, pesi canonici):") _, (bf, bh) = _base() print(f" BASE 4-sleeve FULL Sh {bf['sharpe']:.2f} DD {bf['maxdd']*100:.1f}%" f" | HOLD Sh {bh['sharpe']:.2f} DD {bh['maxdd']*100:.1f}%") sf, sh_ = substitute_xs01(daily) sub_full_d, sub_hold_d = sf["sharpe"] - bf["sharpe"], sh_["sharpe"] - bh["sharpe"] print(f" SOSTITUZIONE XS01 -> cand FULL Sh {sf['sharpe']:.2f} ({sub_full_d:+.2f}) DD {sf['maxdd']*100:.1f}%" f" | HOLD Sh {sh_['sharpe']:.2f} ({sub_hold_d:+.2f}) DD {sh_['maxdd']*100:.1f}%") up_best = (-9.0, -9.0) for fr in (0.10, 0.15): cf, ch, wgt = add_uplift(daily, fr) d_f, d_h = cf["sharpe"] - bf["sharpe"], ch["sharpe"] - bh["sharpe"] if d_h > up_best[1]: up_best = (d_f, d_h) print(f" AGGIUNTA 5o sleeve @{wgt*100:>4.1f}% FULL Sh {cf['sharpe']:.2f} ({d_f:+.2f}) DD {cf['maxdd']*100:.1f}%" f" | HOLD Sh {ch['sharpe']:.2f} ({d_h:+.2f}) DD {ch['maxdd']*100:.1f}%") mb = marginal_vs_book(daily) print(f" MARGINAL vs BOOK: corr {mb['corr_book']:+.2f} | uplift@10% full {mb['uplift_full']:+.3f}" f" hold {mb['uplift_hold']:+.3f} | multi-cut {mb['multicut']}") # --- VERDETTO ------------------------------------------------------------------------- print("\n" + "=" * 104) beats_xs_standalone = (pick["full"] > xs_f["sharpe"] and pick["hold"] > xs_h["sharpe"]) dominates = sub_full_d > 0.02 and sub_hold_d > 0.05 and beats_xs_standalone diversifies = (abs(pick["corrXS"]) < 0.6 and abs(pick["corrTP"]) < 0.5 and up_best[1] > 0.05 and mb["uplift_hold"] is not None and mb["uplift_hold"] > 0 and all(u > 0 for u in mb["multicut"].values())) dsr_ok = np.isfinite(dsr) and dsr >= 0.95 if not caus["ok"]: verdict, why = "SCARTATO", "prefix-check di causalita' fallito" elif pick["full"] <= 0.3 or pick["hold"] <= 0: verdict, why = "SCARTATO", f"standalone debole (FULL {pick['full']:+.2f}, HOLD {pick['hold']:+.2f})" elif abs(pick["corrXS"]) > 0.6 and not dominates: verdict, why = "REDUNDANT", f"corrXS {pick['corrXS']:+.2f}>0.6 e non batte XS01 (sub HOLD {sub_hold_d:+.2f})" elif dominates and dsr_ok: verdict, why = "CANDIDATO-SLEEVE (sostituto)", "batte XS01 standalone E nel book, DSR>=0.95" elif diversifies and dsr_ok: verdict, why = "CANDIDATO-SLEEVE (5o)", "scorrelato, uplift book persistente, DSR>=0.95" elif dominates or diversifies: verdict, why = "LEAD-forward", f"profilo utile ma DSR {dsr:.2f}<0.95 (storia ~2.5a, multiple-testing)" else: verdict, why = "SCARTATO", (f"ne' sostituto (sub HOLD {sub_hold_d:+.2f}) ne' additivo" f" (uplift HOLD {up_best[1]:+.2f}, corrXS {pick['corrXS']:+.2f})") print(f" VERDETTO: {verdict} — {why}") print(" CAVEAT immutabili: storia HL ~2.5 anni; in-sample = solo 2024 (selezione su finestra corta);") print(" book L/S multi-gamba -> STAT-MODE a $600 (come XS01), mai deploy a questo capitale.") print("=" * 104) if __name__ == "__main__": main()