"""GATE DIP01 + PORT06: estendere EXIT-16 (close-confirm SL) a DIP01 (sweep punto 9). DIP01 e' l'unico sleeve BTC con esecuzione REALE round-trip, e gira ancora col branch SL intrabar wick-sensitive. EXIT-16 e' stato validato SULLE FADE: estenderlo a una strategia honest richiede la validazione sul grid proprio di DIP01, con engine GAP-AWARE (lezione exit-lab: l'engine canonico filla gli stop "al livello" anche su gap-through -> bias PRO stop intrabar stretti; il confronto onesto filla lo SL a worse(livello, open)). Protocollo: [1] parita': replay engine 'orig' (fill al livello) == equity canonica DIP01_BTC [2] grid 3x3x2 (z_in x sl_atr x max_bars) su BTC (deployato) ed ETH (robustezza): orig GAP-AWARE vs EXIT-16(buf 0.5), ret/DD/Sharpe train (pre-OOS) e OOS [3] plateau buffer {0.4, 0.5, 0.75, 1.0} sulla cella canonica [4] gate PORT06: DIP01_BTC exit16 innestato nel canonico, pesi cap -> PROMOSSO se OOS Sharpe non peggiora E FULL/DD non degradano materialmente. NB hurst_max NON valutato: il gate trendmax (2026-06-07) ha mostrato che il loss-guard Hurst e' ridondante-dannoso POST-EXIT-16 (stesso regime target). uv run python scripts/analysis/dip01_exit16_impact.py """ from __future__ import annotations import sys from pathlib import Path import numpy as np import pandas as pd PROJECT_ROOT = Path(__file__).resolve().parents[2] sys.path.insert(0, str(PROJECT_ROOT)) from src.data.downloader import load_data from scripts.analysis.strategy_research import atr from scripts.analysis.combine_portfolio import _norm, IDX, metrics, SPLIT, OOS_DATE from scripts.analysis._port06_gate_common import port_metrics from scripts.portfolios._defs import PORTFOLIOS FEE_RT, LEV, POS, INIT = 0.001, 3.0, 0.15, 1000.0 BUFFER = 0.5 GRID_Z = (2.0, 2.5, 3.0) GRID_SL = (2.0, 2.5, 3.0) GRID_MB = (24, 48) CANON = dict(n=50, z_in=2.5, sl_atr=2.5, max_bars=24) def dip_entries(df, n=50, z_in=2.5, sl_atr=2.5, max_bars=24): """Entries DIP01 == honest_improve2.dip_market_gated (market_n=0): crossing di z sotto -z_in. Ritorna [{i, tp, sl, mb}] (long-only).""" c = df["close"].values ma = pd.Series(c).rolling(n).mean().values sd = pd.Series(c).rolling(n).std().values a = atr(df, 14) z = (c - ma) / np.where(sd == 0, np.nan, sd) out = [] for i in range(n + 14, len(c)): if np.isnan(z[i]) or np.isnan(a[i]): continue if z[i] <= -z_in and z[i - 1] > -z_in: out.append({"i": i, "tp": ma[i], "sl": c[i] - sl_atr * a[i], "mb": max_bars}) return out def dip_trades(ents, df, mode, buffer=BUFFER): """Engine exit DIP01 (long-only), non-overlap come il canonico. mode="orig" : SL intrabar fill AL LIVELLO (== canonico, per la parita') mode="orig_gap" : SL intrabar fill a worse(livello, open[j]) — gap-aware mode="exit16" : SL intrabar OFF; TP intrabar al livello (priorita' nel bar); stop solo se close[j] < sl - buffer*ATR14[j], fill a close[j] """ h, l, c, o = df["high"].values, df["low"].values, df["close"].values, df["open"].values n = len(c) a = atr(df, 14) fee = FEE_RT * LEV out = [] last = -1 for e in ents: i = e["i"] if i <= last or i + 1 >= n: continue tp, sl, mb = e["tp"], e["sl"], e["mb"] exit_p = c[min(i + mb, n - 1)] j = min(i + mb, n - 1) for k in range(1, mb + 1): j = i + k if j >= n: j = n - 1 exit_p = c[j] break if mode in ("orig", "orig_gap"): if l[j] <= sl: exit_p = sl if mode == "orig" else min(sl, o[j]) break if h[j] >= tp: exit_p = tp break if k == mb: exit_p = c[j] else: # exit16 if h[j] >= tp: exit_p = tp break aj = a[j] if np.isfinite(a[j]) else 0.0 if c[j] < sl - buffer * aj: exit_p = c[j] break if k == mb: exit_p = c[j] ret = (exit_p - c[i]) / c[i] * LEV - fee out.append((i, j, ret)) last = j return out def daily_equity(df, trades): """Equity giornaliera con la convenzione CANONICA honest (_daily_equity su punti trade-exit). NB: la serie a punti-trade reindexata su IDX ancora il primo valore al PRIMO trade dentro IDX (bfill), non al capitale portato avanti da prima — convenzione discutibile ma e' quella di build_everything: per la parita' (e il confronto col PORT06 canonico) va replicata esattamente.""" from scripts.analysis.honest_improve2 import _daily_equity ts = pd.to_datetime(df["timestamp"], unit="ms", utc=True) cap = INIT eq_ts, eq_v = [], [] for i, j, ret in sorted(trades, key=lambda t: t[1]): cap = max(cap + cap * POS * ret, 10.0) eq_ts.append(ts.iloc[j]) eq_v.append(cap) return _norm(_daily_equity(eq_ts, eq_v, IDX)) def cell_metrics(eq): dr = eq.pct_change().fillna(0.0) return metrics(dr), metrics(dr, lo=SPLIT) def main(): p = PORTFOLIOS["PORT06"] print("=" * 104) print(" GATE DIP01 EXIT-16 (close-confirm 0.5 ATR) — grid gap-aware + PORT06") print(f" OOS da {OOS_DATE} | fee {FEE_RT*100:.2f}%RT x lev{LEV:.0f} | pos {POS}") print("=" * 104) print("\n[1] build_everything() canonico (cache)...") from src.portfolio.sleeves import all_sleeve_equities eq_base = dict(all_sleeve_equities()) dfs = {a: load_data(a, "1h") for a in ("BTC", "ETH")} # --- parita' --- ents = dip_entries(dfs["BTC"], **CANON) rep = daily_equity(dfs["BTC"], dip_trades(ents, dfs["BTC"], "orig")) base = eq_base["DIP01_BTC"] corr = base.pct_change().fillna(0).corr(rep.pct_change().fillna(0)) rb = (base.iloc[-1] / base.iloc[0] - 1) * 100 rr = (rep.iloc[-1] / rep.iloc[0] - 1) * 100 print(f"\n[1] PARITA' orig vs canonico: corr={corr:.5f} ret {rb:+.0f}% vs {rr:+.0f}%") if not (corr > 0.999 and abs(rr - rb) <= max(1.0, abs(rb) * 0.01)): print(" >>> PARITA' FALLITA: STOP.") return # --- [2] grid gap-aware --- for asset in ("BTC", "ETH"): df = dfs[asset] print(f"\n[2] GRID {asset} — orig GAP-AWARE vs EXIT-16 (train | OOS: ret% e Sharpe)") print(f" {'cella':<16s}{'tr retO':>9s}{'tr retE':>9s} {'oos retO':>9s}{'oos retE':>9s}" f" {'oos ShO':>8s}{'oos ShE':>8s} {'ddO':>6s}{'ddE':>6s} esito") wins_tr = wins_oos = cells = 0 for z in GRID_Z: for slm in GRID_SL: for mb in GRID_MB: ents = dip_entries(df, n=50, z_in=z, sl_atr=slm, max_bars=mb) eo = daily_equity(df, dip_trades(ents, df, "orig_gap")) ee = daily_equity(df, dip_trades(ents, df, "exit16")) fo, oo = cell_metrics(eo) fe, oe = cell_metrics(ee) tr_o = fo["ret"] - oo["ret"]; tr_e = fe["ret"] - oe["ret"] # ~train (full-oos, approssimato su ret composti: usare segni) # train ret esatto: equity al SPLIT tr_o = (eo.iloc[SPLIT] / eo.iloc[0] - 1) * 100 tr_e = (ee.iloc[SPLIT] / ee.iloc[0] - 1) * 100 cells += 1 w_tr = tr_e >= tr_o w_oos = oe["ret"] >= oo["ret"] wins_tr += w_tr wins_oos += w_oos tag = ("OK" if (w_tr and w_oos) else "tr-" if w_oos else "oos-" if w_tr else "KO") print(f" z{z} sl{slm} mb{mb:<3d}{tr_o:>9.0f}{tr_e:>9.0f} " f"{oo['ret']:>9.0f}{oe['ret']:>9.0f} {oo['sharpe']:>8.2f}{oe['sharpe']:>8.2f}" f" {fo['dd']:>6.1f}{fe['dd']:>6.1f} {tag}") print(f" -> EXIT-16 >= orig-gap: train {wins_tr}/{cells}, OOS {wins_oos}/{cells}") # --- [3] plateau buffer (BTC, cella canonica) --- print("\n[3] Plateau buffer EXIT-16 (BTC, cella canonica):") ents = dip_entries(dfs["BTC"], **CANON) for buf in (0.4, 0.5, 0.75, 1.0): ee = daily_equity(dfs["BTC"], dip_trades(ents, dfs["BTC"], "exit16", buffer=buf)) fe, oe = cell_metrics(ee) print(f" buf {buf:<5}FULL ret {fe['ret']:>+7.0f}% DD {fe['dd']:>5.1f} Sh {fe['sharpe']:>5.2f}" f" | OOS ret {oe['ret']:>+6.0f}% DD {oe['dd']:>5.1f} Sh {oe['sharpe']:>5.2f}") # --- [4] gate PORT06 --- ee = daily_equity(dfs["BTC"], dip_trades(ents, dfs["BTC"], "exit16")) members_b = dict(eq_base) members_e = dict(eq_base) members_e["DIP01_BTC"] = ee f_b, o_b = port_metrics(members_b, p) f_e, o_e = port_metrics(members_e, p) print("\n" + "=" * 104) print(f" [4] PORT06 (pesi cap {p.caps}) — DIP01_BTC orig vs EXIT-16") print("=" * 104) print(f" {'variante':<10s}{'FULL Sh':>9s}{'FULL DD%':>10s}{'CAGR':>6s} | {'OOS Sh':>7s}{'OOS DD%':>8s}{'CAGR':>6s}") for nm, (f, o) in (("BASE", (f_b, o_b)), ("EXIT-16", (f_e, o_e))): print(f" {nm:<10s}{f['sharpe']:>9.2f}{f['dd']:>10.2f}{f['cagr']:>5.0f}% | " f"{o['sharpe']:>7.2f}{o['dd']:>8.2f}{o['cagr']:>5.0f}%") oos_ok = o_e["sharpe"] >= o_b["sharpe"] - 0.02 and o_e["dd"] <= o_b["dd"] + 0.20 full_ok = f_e["sharpe"] >= f_b["sharpe"] - 0.02 and f_e["dd"] <= f_b["dd"] + 0.20 promoted = oos_ok and full_ok print(f"\n GATE: OOS {'OK' if oos_ok else 'KO'} | FULL {'OK' if full_ok else 'KO'}") print(" VERDETTO: " + (">>> PROMOSSO <<<" if promoted else ">>> BOCCIATO <<<")) if __name__ == "__main__": main()