"""macro_regime_gate.py — Filone D: MACRO REGIME-GATE sul book crypto (eseguibile). TESI ---- Usare segnali macro/cross-market — equity (SPY/QQQ/IWM), credito (HYG/LQD), oro (GLD/SLV), tassi (TLT/IEF) — come GATE risk-on/risk-off applicato al book crypto (BTC/ETH) per migliorare il TIMING del drawdown di TP01. Quando il regime macro e' risk-off (credito che cede, equity sotto trend, fuga sui bond) -> riduci/azzera l'esposizione crypto; risk-on -> lascia agire TP01. E' ESEGUIBILE perche' GATA solo BTC/ETH perp (non aggiunge gambe). NON e' un lead-lag direzionale (gia' esplorato e morto: vedi 2026-06-22/-23 leadlag diaries). L'angolo nuovo = un OVERLAY binario/continuo di DE-RISK sul book. IL RISCHIO (da CLAUDE.md): il gate di de-risk rischia di essere RIDONDANTE col trend — TP01 e' gia' long-flat e va a 0 nei crash (lezione DVOL-spike "ridondante col trend, Delta 0.00"). Questo script DEVE dimostrare che il gate aggiunge OLTRE quel che TP01 fa da solo, altrimenti SCARTATO. CAUSALITA' (fusi orari, regola di prim'ordine) ---------------------------------------------- - Barre equity: open-labeled a 00:00 del giorno di trading; il CLOSE e' ~20:00-21:00 UTC dello STESSO giorno (NYSE 16:00 ET). - Barre crypto 1d: open-labeled a 00:00; il CLOSE e' a 00:00 UTC del giorno DOPO. TP01 decide la posizione a close[i] e la TIENE durante la barra i+1 (eval_weights shift-a per te). - Quindi: gate[i] allineato (merge_asof backward, equity-label <= crypto-label day i) usa il close equity del giorno i (noto ~20:00 day i) per la posizione tenuta durante la barra i+1 (giorno i+1). Margine causale >= 4h. Leak-free. Variante STRICT (equity-label < crypto-label) come margine extra. Verifica con al.causality_ok + day_boundary_robust. USO: uv run python scripts/research/macro_regime_gate.py """ from __future__ import annotations import sys 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" / "alt")) import altlib as al # noqa: E402 from src.strategies.trend_portfolio import CANONICAL, TrendPortfolio # noqa: E402 DATA = _ROOT / "data" / "raw" HOLDOUT = al.HOLDOUT ASSETS = ("BTC", "ETH") # =========================================================================== # MACRO FRAME — ETF daily, allineati causalmente sul calendario SPY (master). # =========================================================================== def _load_eq(sym: str) -> pd.DataFrame: p = DATA / f"eq_{sym}_1d.parquet" df = pd.read_parquet(p).sort_values("timestamp").reset_index(drop=True) df["dt"] = pd.to_datetime(df["timestamp"], unit="ms", utc=True) return df[["timestamp", "dt", "close"]].rename(columns={"close": sym}) def macro_frame(syms=("spy", "qqq", "iwm", "hyg", "lqd", "gld", "slv", "tlt", "ief")) -> pd.DataFrame: """Frame macro sul calendario NYSE (master = SPY). Ogni colonna e' il close equity, allineato causale (merge_asof backward) -> nessun valore futuro per riga.""" base = _load_eq("spy")[["timestamp", "dt"]].copy() out = base for s in syms: e = _load_eq(s)[["timestamp", s]] out = pd.merge_asof(out, e, on="timestamp", direction="backward") return out # =========================================================================== # GATE BUILDERS — ognuno ritorna (timestamp_ms, gate in [0,1]) sul calendario equity. # Tutti CAUSALI: la riga i usa solo close <= riga i (rolling/SMA, niente expanding-future). # gate=1 => risk-on (TP01 pieno); gate=g_off (0 o 0.5) => risk-off (de-risk). # =========================================================================== def _sma(x: np.ndarray, n: int) -> np.ndarray: return pd.Series(x).rolling(n, min_periods=n).mean().values def _ret(x: np.ndarray, n: int) -> np.ndarray: r = np.full(len(x), np.nan) r[n:] = x[n:] / x[:-n] - 1.0 return r def gate_trend(mf: pd.DataFrame, col: str, n: int, g_off: float) -> pd.DataFrame: """Risk-on se col_close > SMA(col, n). Filtro di trend classico (SPY200, HYG, ...).""" c = mf[col].values.astype(float) on = c > _sma(c, n) g = np.where(on, 1.0, g_off) g[~np.isfinite(c)] = np.nan g[np.isnan(_sma(c, n))] = np.nan return pd.DataFrame({"timestamp": mf["timestamp"].values, "gate": g}) def gate_ratio(mf: pd.DataFrame, num: str, den: str, n: int, g_off: float) -> pd.DataFrame: """Risk-on se ratio num/den (proxy spread di credito) > la sua SMA(n). HYG/LQD o HYG/IEF in calo = spread che si allarga = risk-off.""" ratio = (mf[num].values.astype(float) / mf[den].values.astype(float)) on = ratio > _sma(ratio, n) g = np.where(on, 1.0, g_off) g[~np.isfinite(ratio)] = np.nan g[np.isnan(_sma(ratio, n))] = np.nan return pd.DataFrame({"timestamp": mf["timestamp"].values, "gate": g}) def gate_combo(mf: pd.DataFrame, n: int, g_off: float, thr: float = 0.5, continuous: bool = False) -> pd.DataFrame: """Score di regime = media di 3 voti risk-on: SPY>SMA, HYG>SMA, HYG/LQD>SMA. continuous=True -> gate = g_off + (1-g_off)*score (de-risk graduale). continuous=False -> gate = 1 se score>=thr else g_off (binario su maggioranza).""" spy = mf["spy"].values.astype(float) hyg = mf["hyg"].values.astype(float) ratio = hyg / mf["lqd"].values.astype(float) votes = np.vstack([spy > _sma(spy, n), hyg > _sma(hyg, n), ratio > _sma(ratio, n)]).astype(float) warm = np.isnan(_sma(spy, n)) | np.isnan(_sma(hyg, n)) | np.isnan(_sma(ratio, n)) score = votes.mean(axis=0) if continuous: g = g_off + (1.0 - g_off) * score else: g = np.where(score >= thr, 1.0, g_off) g[warm] = np.nan return pd.DataFrame({"timestamp": mf["timestamp"].values, "gate": g}) def gate_flight(mf: pd.DataFrame, safe: str, risk: str, n: int, g_off: float) -> pd.DataFrame: """Fuga verso la sicurezza: risk-off quando il safe-asset (TLT/GLD) sale MENTRE il risk (SPY) scende sull'orizzonte n. Divergenza risk-off classica (flight-to-quality).""" s = mf[safe].values.astype(float) rk = mf[risk].values.astype(float) off = (_ret(s, n) > 0) & (_ret(rk, n) < 0) g = np.where(off, g_off, 1.0) warm = np.isnan(_ret(s, n)) | np.isnan(_ret(rk, n)) g[warm] = np.nan return pd.DataFrame({"timestamp": mf["timestamp"].values, "gate": g}) def gate_eqvol(mf: pd.DataFrame, n: int, win: int, z: float, g_off: float) -> pd.DataFrame: """Regime di vol equity: de-risk quando la vol realizzata SPY (win g) e' alta vs la sua storia espandente-causale (z-score > z). Proxy 'VIX spike' senza VIX.""" spy = mf["spy"].values.astype(float) r = np.zeros(len(spy)); r[1:] = spy[1:] / spy[:-1] - 1.0 rv = pd.Series(r).rolling(win, min_periods=win).std().values zsc = al.zscore(rv, n) off = zsc > z g = np.where(off, g_off, 1.0) g[~np.isfinite(zsc)] = np.nan return pd.DataFrame({"timestamp": mf["timestamp"].values, "gate": g}) # =========================================================================== # ALIGN GATE -> CRYPTO (causale) + GATED TARGET # =========================================================================== def align_gate(gate_df: pd.DataFrame, crypto_df: pd.DataFrame, strict: bool = False) -> np.ndarray: """Allinea il gate (calendario equity) alle barre crypto. merge_asof backward: crypto-label day i -> ultimo gate equity con label <= day i (strict: < day i). NaN pre-storia -> gate=1 (nessun de-risk quando non c'e' info).""" left = pd.DataFrame({"timestamp": crypto_df["timestamp"].astype("int64").values}) g = gate_df.dropna(subset=["gate"]).sort_values("timestamp") m = pd.merge_asof(left, g, on="timestamp", direction="backward", allow_exact_matches=not strict) return pd.Series(m["gate"].values).ffill().fillna(1.0).values def tp01_pos(df: pd.DataFrame) -> np.ndarray: return TrendPortfolio(**CANONICAL).target_series(df) def make_target_fn(gate_builder, strict: bool = False): """Ritorna target_fn(df, asset) = posizione TP01 * gate macro allineato (causale). gate_builder() costruisce il gate sul calendario equity una volta (cache esterna).""" _MF = macro_frame() gate_df = gate_builder(_MF) def target_fn(df: pd.DataFrame, asset: str = "") -> np.ndarray: pos = tp01_pos(df) g = align_gate(gate_df, df, strict=strict) return pos * g return target_fn, gate_df # =========================================================================== # EVALUATION — solo vs gated, combo 50/50 + per-asset, FULL/HOLD/DD/CAGR. # =========================================================================== def _combo_daily(target_fn) -> pd.Series: series = {} for a in ASSETS: df = al.get(a, "1d") ev = al.eval_weights(df, target_fn(df, a)) series[a] = pd.Series(ev["net"], index=ev["idx"]) J = pd.concat(series, axis=1, join="inner").fillna(0.0) return al._to_daily(0.5 * J[ASSETS[0]] + 0.5 * J[ASSETS[1]]) def _sh(s): return al._sh(s) def _metrics(s: pd.Series) -> dict: sh = _sh(s) eq = np.cumprod(1.0 + s.values) pk = np.maximum.accumulate(eq) dd = float(np.max((pk - eq) / pk)) if len(eq) else 0.0 yrs = max((s.index[-1] - s.index[0]).days / 365.25, 1e-6) cagr = eq[-1] ** (1 / yrs) - 1 if len(eq) and eq[-1] > 0 else -1.0 return dict(sharpe=round(sh, 3), dd=round(dd, 4), cagr=round(cagr, 4)) def eval_solo_vs_gated(target_fn): """Combo 50/50: solo (gate=1) vs gated. Ritorna dict con FULL e HOLD per entrambi.""" solo_fn = lambda df, a="": tp01_pos(df) solo = _combo_daily(solo_fn) gated = _combo_daily(target_fn) J = pd.concat({"solo": solo, "gated": gated}, axis=1, join="inner").dropna() JH = J[J.index >= HOLDOUT] return dict( full_solo=_metrics(J["solo"]), full_gated=_metrics(J["gated"]), hold_solo=_metrics(JH["solo"]), hold_gated=_metrics(JH["gated"]), n=len(J), nh=len(JH), solo_series=J["solo"], gated_series=J["gated"]) def per_asset_table(target_fn) -> dict: out = {} for a in ASSETS: df = al.get(a, "1d") solo = al.eval_weights(df, tp01_pos(df)) gated = al.eval_weights(df, target_fn(df, a)) out[a] = dict( full_solo=dict(sharpe=solo["full"]["sharpe"], dd=solo["full"]["maxdd"], cagr=solo["full"]["cagr"]), full_gated=dict(sharpe=gated["full"]["sharpe"], dd=gated["full"]["maxdd"], cagr=gated["full"]["cagr"]), hold_solo=dict(sharpe=solo["holdout"].get("sharpe", 0.0)), hold_gated=dict(sharpe=gated["holdout"].get("sharpe", 0.0))) return out # =========================================================================== # REDUNDANCY DIAGNOSTIC — il controllo DECISIVO contro il trend. # =========================================================================== def redundancy_diag(gate_df: pd.DataFrame) -> dict: """Quanto FA davvero il gate, dato che TP01 e' gia' flat nei crash? - exposure TP01 nei giorni risk-off (gate<1) vs risk-on: se gia' ~0 -> ridondante. - quota di giorni in cui il gate riduce una posizione NON gia' flat (lavoro effettivo). - corr fra (1-gate) e (1-exposure_norm).""" lev = CANONICAL["leverage"] rows = [] for a in ASSETS: df = al.get(a, "1d") pos = tp01_pos(df) expo = np.clip(np.abs(pos) / lev, 0, 1) # esposizione normalizzata 0..1 g = align_gate(gate_df, df) dt = pd.to_datetime(df["datetime"], utc=True) m = pd.DataFrame({"dt": dt, "expo": expo, "g": g}) m = m[m["dt"] >= dt.iloc[0]] # tutto roff = m["g"] < 0.999 ron = ~roff flat_thr = 0.05 # lavoro effettivo: gate<1 E TP01 non gia' flat work = roff & (m["expo"] > flat_thr) rows.append(dict( asset=a, expo_riskoff=round(float(m.loc[roff, "expo"].mean()) if roff.any() else 0.0, 3), expo_riskon=round(float(m.loc[ron, "expo"].mean()) if ron.any() else 0.0, 3), pct_days_riskoff=round(float(roff.mean()), 3), pct_days_gate_works=round(float(work.mean()), 3), corr_1mg_1mexpo=round(float(np.corrcoef(1 - m["g"], 1 - m["expo"])[0, 1]) if m["g"].std() > 0 else float("nan"), 3), )) return {r["asset"]: r for r in rows} # =========================================================================== # RUNNER # =========================================================================== GATES = { # binari classici "SPY>MA200": lambda mf: gate_trend(mf, "spy", 200, 0.0), "SPY>MA100": lambda mf: gate_trend(mf, "spy", 100, 0.0), "SPY>MA50": lambda mf: gate_trend(mf, "spy", 50, 0.0), "SPY>MA200_half": lambda mf: gate_trend(mf, "spy", 200, 0.5), "HYG>MA100": lambda mf: gate_trend(mf, "hyg", 100, 0.0), "HYG/LQD>MA50": lambda mf: gate_ratio(mf, "hyg", "lqd", 50, 0.0), "HYG/LQD>MA100": lambda mf: gate_ratio(mf, "hyg", "lqd", 100, 0.0), # combinati / score "COMBO_maj100": lambda mf: gate_combo(mf, 100, 0.0, thr=0.5), "COMBO_all100": lambda mf: gate_combo(mf, 100, 0.0, thr=0.99), "COMBO_cont100": lambda mf: gate_combo(mf, 100, 0.0, continuous=True), "COMBO_cont100h": lambda mf: gate_combo(mf, 100, 0.5, continuous=True), # flight-to-quality / vol "TLTup&SPYdn20": lambda mf: gate_flight(mf, "tlt", "spy", 20, 0.0), "GLDup&SPYdn20": lambda mf: gate_flight(mf, "gld", "spy", 20, 0.0), "SPYvol_z1": lambda mf: gate_eqvol(mf, 250, 20, 1.0, 0.0), } def delever_frontier(target_vols=(0.10, 0.12, 0.14, 0.16, 0.18, 0.20)) -> dict: """CONTROLLO DECISIVO (lezione DVOL-overlay): per meno DD la leva e' target_vol, non un overlay. Frontiera DD/Sharpe di TP01 puro a target_vol decrescente. Se il miglior gate sta SOPRA (DD piu' alto a parita' di Sharpe, o Sharpe piu' basso a parita' di DD) di questa frontiera, il suo taglio di DD e' solo de-levering replicabile meglio senza macro.""" out = {} for tv in target_vols: cfg = {**CANONICAL, "target_vol": tv} fn = lambda df, a="", cfg=cfg: TrendPortfolio(**cfg).target_series(df) c = _combo_daily(fn) out[tv] = _metrics(c) return out def fmt_cmp(label, m_solo, m_gated) -> str: ds = m_gated["sharpe"] - m_solo["sharpe"] dd = m_gated["dd"] - m_solo["dd"] dc = m_gated.get("cagr", 0) - m_solo.get("cagr", 0) return (f" {label:5s} Sh {m_solo['sharpe']:+.2f}->{m_gated['sharpe']:+.2f} (d{ds:+.2f}) " f"DD {m_solo['dd']*100:4.1f}%->{m_gated['dd']*100:4.1f}% (d{dd*100:+.1f}pp) " f"CAGR {m_solo.get('cagr',0)*100:+5.1f}%->{m_gated.get('cagr',0)*100:+5.1f}% (d{dc*100:+.1f}pp)") def main(): pd.set_option("display.width", 160) print("=" * 92) print("MACRO REGIME-GATE sul book crypto (TP01 BTC/ETH) — Filone D") print(f" TP01 CANONICAL = {CANONICAL}") print(f" HOLD-OUT >= {HOLDOUT.date()} fee {al.FEE_SIDE*2*100:.2f}%RT") mf = macro_frame() print(f" Macro frame: {len(mf)} barre {mf['dt'].iloc[0].date()} -> {mf['dt'].iloc[-1].date()} " f"cols={[c for c in mf.columns if c not in ('timestamp','dt')]}") print("=" * 92) # ---- 1) SWEEP DI TUTTI I GATE (combo 50/50) ------------------------------------- print("\n[1] SWEEP GATE — combo 50/50 BTC+ETH, FULL & HOLD-OUT, vs TP01-solo\n") results = {} for name, builder in GATES.items(): tf, gate_df = make_target_fn(builder) cmp = eval_solo_vs_gated(tf) results[name] = (cmp, gate_df, tf) print(f"GATE {name}") print(fmt_cmp("FULL", cmp["full_solo"], cmp["full_gated"])) print(fmt_cmp("HOLD", cmp["hold_solo"], cmp["hold_gated"])) # baseline (solo) numbers come from any cmp any_cmp = next(iter(results.values()))[0] print(f"\n [baseline TP01-solo] FULL Sh {any_cmp['full_solo']['sharpe']} DD {any_cmp['full_solo']['dd']*100:.1f}% " f"CAGR {any_cmp['full_solo']['cagr']*100:.1f}% | HOLD Sh {any_cmp['hold_solo']['sharpe']} " f"DD {any_cmp['hold_solo']['dd']*100:.1f}% CAGR {any_cmp['hold_solo']['cagr']*100:.1f}%") # ---- 2) SELEZIONE: miglior gate per HOLD-OUT Sharpe, poi per riduzione DD -------- def score(name): c = results[name][0] return (c["hold_gated"]["sharpe"], -c["full_gated"]["dd"]) best = max(results, key=score) # anche il "miglior DD-cutter" che non peggiora troppo lo Sharpe FULL dd_best = min(results, key=lambda n: results[n][0]["full_gated"]["dd"]) print(f"\n[2] Miglior gate per HOLD-OUT Sharpe: {best}") print(f" Miglior gate per DD FULL ridotto : {dd_best}") # ---- CONTROLLO DECISIVO: de-lever frontier (target_vol) ------------------------- print("\n[2b] CONTROLLO DECISIVO — TP01 puro a target_vol piu' basso (de-lever) vs gate:") fr = delever_frontier() for tv, m in fr.items(): print(f" target_vol {tv:.2f}: FULL Sh {m['sharpe']:+.2f} DD {m['dd']*100:4.1f}% CAGR {m['cagr']*100:+5.1f}%") print(" -> i gate de-leveranti (COMBO_cont, SPYvol) vanno confrontati con QUESTA frontiera:") for n in ("COMBO_cont100", "COMBO_cont100h", "SPYvol_z1", "SPY>MA200_half"): if n in results: g = results[n][0]["full_gated"] print(f" {n:16s}: FULL Sh {g['sharpe']:+.2f} DD {g['dd']*100:4.1f}% CAGR {g['cagr']*100:+5.1f}%") for tag, name in [("BEST-HOLD", best), ("BEST-DD", dd_best)]: if tag == "BEST-DD" and dd_best == best: continue deep_dive(tag, name, results) def deep_dive(tag, name, results): cmp, gate_df, tf = results[name] print("\n" + "=" * 92) print(f"[DEEP DIVE {tag}] GATE = {name}") print("=" * 92) # per-asset print("\n Per-asset (TP01-solo -> TP01+gate):") pa = per_asset_table(tf) for a in ASSETS: d = pa[a] print(f" {a}: FULL Sh {d['full_solo']['sharpe']:+.2f}->{d['full_gated']['sharpe']:+.2f} " f"DD {d['full_solo']['dd']*100:.0f}%->{d['full_gated']['dd']*100:.0f}% " f"CAGR {d['full_solo']['cagr']*100:+.0f}%->{d['full_gated']['cagr']*100:+.0f}% | " f"HOLD Sh {d['hold_solo']['sharpe']:+.2f}->{d['hold_gated']['sharpe']:+.2f}") # ---- 3) CONTROLLO RIDONDANZA COL TREND ------------------------------------------ print("\n [3] CONTROLLO RIDONDANZA COL TREND (il test decisivo):") rd = redundancy_diag(gate_df) for a in ASSETS: r = rd[a] print(f" {a}: exposure TP01 nei giorni risk-off={r['expo_riskoff']} vs risk-on={r['expo_riskon']} " f"| giorni risk-off {r['pct_days_riskoff']*100:.0f}% " f"giorni in cui il gate LAVORA (riduce pos non-flat) {r['pct_days_gate_works']*100:.0f}% " f"| corr(1-gate, 1-expo)={r['corr_1mg_1mexpo']}") print(" -> se exposure-risk-off ~ exposure-risk-on e 'gate-lavora' e' basso => RIDONDANTE col trend.") # ---- 4) MARGINAL SCORER ----------------------------------------------------------- print("\n [4] MARGINAL SCORER vs TP01 (gate come candidato-sleeve):") rep = al.study_marginal(f"GATE[{name}]", tf, tf="1d") print(al.fmt_marginal(rep)) # overlay-delta: lo STREAM incrementale del gate = gated - solo (e' alpha o hedge?) delta = (cmp["gated_series"] - cmp["solo_series"]).dropna() print("\n overlay-delta (gated - solo) come stream a se':") md = al.marginal_vs_tp01(delta) print(f" verdict={md.get('marginal_verdict')} corr->TP01 {md.get('corr_full')} " f"is_hedge={md.get('is_hedge')} uplift TP01-up {md.get('uplift_tp01_up')} / " f"TP01-down {md.get('uplift_tp01_down')} cand-Sh full {md.get('cand_full_sharpe')}") # ---- 5) FEE SWEEP ----------------------------------------------------------------- print("\n [5] FEE SWEEP (combo 50/50 gated, FULL Sharpe):") for f in al.FEE_SWEEP: series = {} for a in ASSETS: df = al.get(a, "1d") ev = al.eval_weights(df, tf(df, a), fee_side=f) series[a] = pd.Series(ev["net"], index=ev["idx"]) J = pd.concat(series, axis=1, join="inner").fillna(0.0) d = al._to_daily(0.5 * J[ASSETS[0]] + 0.5 * J[ASSETS[1]]) print(f" {2*f*100:.2f}%RT -> Sh {_sh(d):+.2f}") # ---- 6) ESEGUIBILITA' a $600 ------------------------------------------------------ print("\n [6] ESEGUIBILITA' a $600 (eval_weights_smallcap, haircut reale):") for a in ASSETS: df = al.get(a, "1d") sc = al.eval_weights_smallcap(df, tf(df, a), capital=600, min_order=5) print(f" {a}: modeled Sh {sc['modeled']['sharpe']:+.2f} -> real Sh {sc['realistic']['sharpe']:+.2f} " f"haircut {sc['sharpe_haircut']:+.2f} trade eseguiti {sc['n_executed_trades']}") # ---- 7) LEAK / BOUNDARY ----------------------------------------------------------- print("\n [7] LEAK-FREE & BOUNDARY:") cz = al.causality_ok(tf, tf="1d") print(f" causality_ok={cz['ok']} (max_tail_diff {cz['max_tail_diff']}, checked {cz['checked']})") # variante STRICT (equity-label < crypto-label): margine causale extra tf_strict, _ = make_target_fn(GATES[name], strict=True) cmp_s = eval_solo_vs_gated(tf_strict) print(f" STRICT align (1 barra equity extra di lag): FULL Sh {cmp_s['full_gated']['sharpe']:+.2f} " f"(vs {cmp['full_gated']['sharpe']:+.2f}) HOLD Sh {cmp_s['hold_gated']['sharpe']:+.2f} " f"(vs {cmp['hold_gated']['sharpe']:+.2f}) -> robusto se ~uguale") db = al.day_boundary_robust(tf, tf="1d") print(f" day_boundary_robust={db['verdict']} (spread {db.get('spread')}, per-offset {db.get('per_offset')})") # ---- 8) PLATEAU (solo per i trend MA) --------------------------------------------- if name.startswith("SPY>MA") or name.startswith("HYG"): print("\n [8] PLATEAU su finestra MA (SPY trend), g_off=0:") for n in (50, 100, 150, 200, 250): tfn, _ = make_target_fn(lambda mf, n=n: gate_trend(mf, "spy", n, 0.0)) cc = eval_solo_vs_gated(tfn) print(f" SPY>MA{n:3d}: FULL Sh {cc['full_gated']['sharpe']:+.2f} DD {cc['full_gated']['dd']*100:.1f}% " f"HOLD Sh {cc['hold_gated']['sharpe']:+.2f} DD {cc['hold_gated']['dd']*100:.1f}%") if __name__ == "__main__": main()