diff --git a/CLAUDE.md b/CLAUDE.md index af8368e..80dcb39 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -222,8 +222,8 @@ i segnali dopo barre flat PEGGIORA tutti gli sleeve ETH: la candela-gap è l'ove fada). Aggiunto alert Telegram `STALE_FEED` (≥2 barre 1h flat → notifica + gap % al risveglio, solo osservabilità). Diario `docs/diary/2026-06-05-confirm-sl-forming-bar.md`. -**EXIT-16 close-confirm SL (ATTIVO LIVE, 2026-06-04).** Le fade accettano `sl_confirm_atr` (live: -0.5 in `_defs.py`): lo SL **intrabar è disattivato** e lo stop scatta solo se il **CLOSE** della barra +**EXIT-16 close-confirm SL (ATTIVO LIVE, 2026-06-04; esteso a DIP01 il 2026-06-07).** Le fade E +DIP01 accettano `sl_confirm_atr` (live: 0.5 in `_defs.py`): lo SL **intrabar è disattivato** e lo stop scatta solo se il **CLOSE** della barra sfonda `sl ∓ 0.5·ATR14`, con uscita al close (TP intrabar al livello e max_bars invariati; in modalità confirm il TP ha priorità nel bar). Scoperta della ricerca exit-lab (34 agenti, 23 famiglie esplorate + 10 verifiche avversariali + test PORT06): **gli stop intrabar da wick sono falsi negativi** — l'overshoot diff --git a/docs/diary/2026-06-07-dip01-exit16.md b/docs/diary/2026-06-07-dip01-exit16.md new file mode 100644 index 0000000..5589446 --- /dev/null +++ b/docs/diary/2026-06-07-dip01-exit16.md @@ -0,0 +1,49 @@ +# 2026-06-07 — DIP01 EXIT-16: PROMOSSO (gate gap-aware, il più netto della serie) + +Punto 9 della roadmap sweep: DIP01 era l'unico sleeve BTC con esecuzione REALE +round-trip e girava ancora sul branch SL intrabar wick-sensitive. EXIT-16 era +validato SOLO sulle fade: estensione a una strategia honest dietro doppio gate +(grid proprio di DIP01 + PORT06), con engine GAP-AWARE come da lezione exit-lab. + +## Metodo (`scripts/analysis/dip01_exit16_impact.py`) + +- Parità con la equity canonica: corr **1.00000**, ret +322% identico. +- **Engine gap-aware**: nel ramo orig lo SL filla a `worse(livello, open[j])` — + l'engine canonico filla "al livello" anche su gap-through (54% dei casi per stop + tight) e regalerebbe al ramo intrabar un vantaggio fittizio. +- Grid 3×3×2 (z_in × sl_atr × max_bars) su BTC (deployato) ed ETH (robustezza); + train (pre 2024-10-12) e OOS separati. Buffer plateau {0.4, 0.5, 0.75, 1.0}. +- NB `hurst_max` NON valutato su DIP01: il gate trendmax dello stesso giorno ha + mostrato che è ridondante-dannoso post-EXIT-16. + +### Trappola di metodo trovata: l'ancora bfill di `_daily_equity` + +La prima parità falliva (corr 0.9956, −16pp) con **engine identici** (662 trade, +stessi return, stesso capitale finale). Causa: la convenzione canonica +`_daily_equity` (serie a punti trade-exit → `reindex(IDX).ffill().bfill()`) +**ancora il primo valore della finestra al PRIMO trade dentro IDX** (es. 942.54 +dopo le perdite di gennaio 2021), non al capitale portato avanti dall'ultimo trade +del 2020 (979.23). Il mio replay hourly era più "corretto" ma per parità va +replicata la convenzione. **Finding aperto**: questo bias tocca le metriche +canoniche di TUTTI gli sleeve a punti-trade (honest/pairs/tsm) — l'ancoraggio +distorce leggermente il punto di partenza della finestra. Da valutare a parte se +correggerlo OVUNQUE in un colpo solo (cambierebbe i numeri canonici di riferimento). + +## Risultati + +- **Grid BTC: 36/36** (EXIT-16 ≥ orig gap-aware in train E OOS in tutte le 18 + celle). OOS Sharpe ~2-4x (cella canonica 1.47→3.48), DD giù in 14/18 celle. +- **Grid ETH: 35/36** (unica eccezione la cella estrema z3.0/sl3.0/mb48, marginale). +- **Plateau buffer piatto**: OOS DD 6.4% identico da 0.4 a 1.0; scelto 0.5 (== fade). +- **PORT06**: FULL Sharpe 6.43→6.61, DD 3.96→3.58 | OOS Sharpe 8.58→8.77, DD + 1.36→1.34. Migliora tutto. **PROMOSSO**. + +È la 5ª conferma indipendente del principio EXIT-16: gli stop intrabar da wick +sono falsi negativi per le strategie mean-reversion — l'overshoot che buca lo +stop è il movimento che si sta comprando. + +## Deploy + +`_defs.py`: `"sl_confirm_atr": 0.5` nei params DIP01_BTC (il worker legge già il +param — zero codice). Live path: TP intrabar invariato, SL solo su close confermato, +disaster-bracket on-book già attivo per gli outage. diff --git a/scripts/analysis/dip01_exit16_impact.py b/scripts/analysis/dip01_exit16_impact.py new file mode 100644 index 0000000..4793da9 --- /dev/null +++ b/scripts/analysis/dip01_exit16_impact.py @@ -0,0 +1,238 @@ +"""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, port_returns, metrics, SPLIT, OOS_DATE, +) +from scripts.portfolios._defs import PORTFOLIOS +from src.portfolio import weighting as W + +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 port_metrics(members, p): + ids = p.sleeve_ids + dr = pd.DataFrame({i: members[i].pct_change().fillna(0.0) for i in ids}) + w = W.weight_vector(p.weighting, ids, dr, weights=p.weights, + caps=p.caps, clusters=p.clusters, lookback=p.vol_lookback) + drp = port_returns({i: members[i] for i in ids}, w) + return metrics(drp), metrics(drp, 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() diff --git a/scripts/portfolios/_defs.py b/scripts/portfolios/_defs.py index f0f56b9..544e6f6 100644 --- a/scripts/portfolios/_defs.py +++ b/scripts/portfolios/_defs.py @@ -46,8 +46,12 @@ FADE = [SleeveSpec(kind="single", name=c, sid=f"{c}_{a}", asset=a, cluster=f"{a} for a in ("BTC", "ETH") for c in ("MR01", "MR02", "MR07")] HONEST = [ # DIP01: single-asset 1h -> StrategyWorker (Strategy DIP01_dip_buy). TR01/ROT02: multi-asset. + # EXIT-16 esteso a DIP01 (2026-06-07, sweep punto 9): gate dip01_exit16_impact.py con + # engine GAP-AWARE — grid 36/36 BTC e 35/36 ETH (train E oos), plateau buffer piatto, + # PORT06 FULL Sh 6.43->6.61 DD 3.96->3.58, OOS 8.58->8.77. Era l'unico sleeve BTC con + # esecuzione reale ancora sul branch SL intrabar wick-sensitive. SleeveSpec(kind="single", name="DIP01", sid="DIP01_BTC", asset="BTC", cluster="BTC-rev", - params={"min_tp_frac": MIN_TP_FRAC}), + params={"min_tp_frac": MIN_TP_FRAC, "sl_confirm_atr": SL_CONFIRM_ATR}), SleeveSpec(kind="basket", name="TR01", sid="TR01_basket", cluster="trend", params={"universe": ["BNB", "BTC", "DOGE", "SOL", "XRP"], "tf": "4h"}), SleeveSpec(kind="rotation", name="ROT02", sid="ROT02_rot", cluster="rotation",