diff --git a/CLAUDE.md b/CLAUDE.md index 8e7e14f..77ab89d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -180,7 +180,20 @@ MR07 46%→21%), edge OOS confermato (vedi `scripts/analysis/risk_management.py` Unica eccezione: MR03 BTC, dove il filtro peggiora entrambe → lasciato disattivo. Leva non robusta scartate: vol-target sizing e skip-alta-volatilità (peggiorano). -**Loss-guard Hurst (ATTIVO LIVE, 2026-06-02).** Le fade accettano `hurst_max`: saltano i segnali in +**SWAP filtri fade: hurst→trend (2026-06-07).** Il gate PORT06 sul path live +(`scripts/analysis/trendmax_port06_impact.py`, parità 1.00000 col canonico) ha mostrato che +**post-EXIT-16 il loss-guard Hurst è ridondante-dannoso**: EXIT-16 ha eliminato i wick-stop che +hurst evitava → gli ingressi saltati (66% delle barre!) sono in maggioranza tornati vincenti. +Su PORT06: LIVE hurst-only FULL Sh 7.23 / OOS 9.35-DD 1.68 vs **TREND-ONLY 7.89 / 9.91-DD 1.20** +(domina su tutte le metriche; hurst+trend insieme over-filtra: 7.11, metà dei trade; plateau +trend_max 2.5/3.0/3.5 robusto). TREND-ONLY è la config che la ricerca EXIT-16 aveva davvero +promosso (entries trend-filtrate, no hurst) e che il live non aveva mai eseguito. **Live ora: +`trend_max=3.0, ema_long=200` nelle 6 fade di `_defs.py`, `hurst_max` rimosso** (la maschera resta +in `fade_base`). Monitor: `hourly_report` traccia lo stop-rate per epoca PRE→HURST→TREND. +Lezione: ri-gateare ogni filtro quando cambia l'exit engine. Diario +`docs/diary/2026-06-07-trendmax-gate.md`. Il paragrafo sotto resta come record storico: + +**Loss-guard Hurst (storico: live dal 2026-06-02 al 2026-06-07, poi sostituito dal filtro trend).** Le fade accettano `hurst_max`: saltano i segnali in regime PERSISTENTE/trending (rolling-Hurst ≥ soglia), dove si concentrano stop-loss e perdite (diagnosi: stop-rate 43% per hurst>0.55 vs 21% anti-persistente; i peggiori 1% trade hanno hurst medio 0.61). Helper `src.strategies.fade_base.hurst_skip_mask` (rolling-Hurst causale **dalle sole diff --git a/docs/diary/2026-06-07-trendmax-gate.md b/docs/diary/2026-06-07-trendmax-gate.md new file mode 100644 index 0000000..65d3768 --- /dev/null +++ b/docs/diary/2026-06-07-trendmax-gate.md @@ -0,0 +1,64 @@ +# 2026-06-07 — Gate trend_max sulle fade live: BOCCIATA l'aggiunta, PROMOSSO lo swap hurst→trend + +Punto 7 della roadmap improvement-sweep: ripristinare `trend_max=3.0`/`ema_long=200` +sulle 6 fade live (ON nel backtest canonico, OFF in produzione). Il piano imponeva il +gate PORT06 perché hurst e trend si sovrappongono. Esito sorprendente ma coerente. + +## Metodo + +`scripts/analysis/trendmax_port06_impact.py` — engine riusato da `exit16_port06_impact` +(replay esatto del path canonico, parità verificata: corr 1.00000 e diff 0.00% su tutte +e 6 le fade) + maschera Hurst IDENTICA al live (`fade_base.hurst_skip_mask`, close-only, +w=100 step=6 — non la cache regime_lab). Tutte le varianti girano sul **path live** +(EXIT-16 close-confirm 0.5 attivo); PORT06 con pesi cap canonici (PAIRS 0.33, +SHAPE 0.0588), OOS da 2024-10-12. + +Nota di metodo: il test PORT06 del loss-guard Hurst (`fade_lossguard_port_test`, +2026-06-02) era su entries GIÀ trend-filtrate E sull'engine PRE-EXIT-16 (SL intrabar). +La config live reale (hurst senza trend, con EXIT-16) non era MAI stata gateata. + +## Risultati (PORT06, fade in path live EXIT-16) + +| Variante | FULL Sh | FULL DD | FULL CAGR | OOS Sh | OOS DD | OOS CAGR | +|---|---|---|---|---|---|---| +| LIVE (hurst, no trend) | 7.23 | 2.68% | 68% | 9.35 | 1.68% | 92% | +| CANDIDATO (hurst+trend) | 7.11 | 2.06% | 59% | 9.36 | 1.62% | 83% | +| **TREND-ONLY 3.0** | 7.89 | 2.46% | 82% | **9.91** | **1.20%** | 103% | +| NESSUNO (solo EXIT-16) | **8.07** | 3.35% | **105%** | 9.72 | 1.38% | 119% | +| TREND-2.5 | 7.75 | 2.46% | 76% | 9.76 | 1.20% | 96% | +| TREND-3.5 | 7.87 | 2.46% | 84% | 9.64 | 1.20% | 103% | + +## Letture + +1. **CANDIDATO BOCCIATO** (gate formale): il DD scende (2.68→2.06) ma FULL Sharpe + −0.12 e CAGR −9pp — over-filtering, i due filtri tagliano lo stesso regime + (n trade −50%, es. MR02_ETH 911→436). +2. **Il loss-guard Hurst è ridondante-dannoso post-EXIT-16**: NESSUNO batte LIVE su + Sharpe e ritorno ovunque. Spiegazione meccanica: hurst evitava i regimi stop-heavy + saltando gli ingressi; EXIT-16 ha eliminato i wick-stop alla radice → gli ingressi + che hurst salta sono in maggioranza tornati vincenti. Il 66% delle barre è oltre + soglia hurst → il filtro toglieva metà dell'esposizione per un beneficio che ormai + non c'è. +3. **TREND-ONLY domina LIVE su tutte e 4 le metriche** ed è ESATTAMENTE la config che + la ricerca EXIT-16 del 2026-06-04 aveva promosso (entries trend-filtrate, no hurst: + FULL 7.84/2.60, OOS 10.06/1.15 — i 7.89/2.46 e 9.91/1.20 di oggi combaciano al + netto di refresh dati e cap SHAPE). Il live non aveva mai eseguito quella config. +4. **Plateau robusto**: trend_max 2.5/3.0/3.5 quasi indistinguibili (OOS DD 1.20 + identico su tutte) → non è una soglia tunata. +5. Il trend filter vs NESSUNO è un trade-off deliberato: −23pp CAGR FULL per + FULL DD 3.35→2.46 e OOS Sharpe 9.72→9.91. Coerente con la filosofia del progetto + (DD control > ritorno marginale). + +## Decisione (utente, 2026-06-07) + +**SWAP hurst→trend nelle 6 fade live** (`_defs.py`): `trend_max=3.0, ema_long=200`, +`hurst_max` rimosso. `hurst_skip_mask` resta in `fade_base` (param opzionale). +Monitor live aggiornato: `hourly_report` ora traccia lo stop-rate per epoca +PRE (nessun filtro) → HURST → TREND, verdetto a n≥30 nell'epoca TREND. + +## Lezione + +Ogni meccanismo anti-perdite va RI-gateato quando cambia l'exit engine: hurst era +genuinamente il migliore sull'engine a SL intrabar (unico su ~10 candidati), ma +EXIT-16 ne ha assorbito il lavoro. I filtri si valutano sul path live corrente, +non sui numeri storici di promozione. diff --git a/scripts/analysis/trendmax_port06_impact.py b/scripts/analysis/trendmax_port06_impact.py new file mode 100644 index 0000000..a73ec1b --- /dev/null +++ b/scripts/analysis/trendmax_port06_impact.py @@ -0,0 +1,259 @@ +"""GATE PORT06: trend_max=3.0/ema_long=200 sulle 6 fade LIVE (improvement-sweep punto 7). + +Contesto. Il backtest canonico (build_everything) applica il filtro trend alle fade, +ma in produzione le SleeveSpec di _defs.py NON lo passano -> le fade live fadano anche +i trend/crolli estesi (es. MR01/MR02_ETH long ripetuti nel crash ETH del 2026-06-05). +Anche il test PORT06 del loss-guard Hurst (fade_lossguard_port_test) fu misurato su +entries GIA' trend-filtrate -> la config live attuale (hurst SENZA trend) non e' mai +stata gateata. hurst e trend si sovrappongono (entrambi tagliano il regime trending): +il delta marginale va misurato sul path live, non assunto dai numeri canonici. + +Confronto, a livello PORT06 (stessa matematica pesi cap di Portfolio.backtest): + LIVE = fade con hurst_max=0.55 + EXIT-16 (sl_confirm 0.5 ATR), trend OFF [prod oggi] + CANDIDATO = LIVE + trend_max=3.0 / ema_long=200 [proposta] + TREND-ONLY= EXIT-16 + trend, senza hurst [diagnostica overlap] + +Parita' preliminare: il replay (mode=orig, trend ON, no hurst, no EXIT-16) deve +riprodurre le equity fade canoniche (come exit16_port06_impact). + +La maschera Hurst e' quella ESATTA del live: fade_base.hurst_skip_mask (close-only, +window=100, step=6) — non la cache di regime_lab. + +GATE (sweep): PROMOSSO se OOS Sharpe non peggiora (>= base-0.02) E il DD scende, +e in FULL non degrada. + + uv run python scripts/analysis/trendmax_port06_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 src.strategies.fade_base import hurst_skip_mask +from scripts.analysis.strategy_research import atr +from scripts.analysis.risk_management import strats_for, FEE_RT, LEV, POS, INIT +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 + +BUFFER = 0.5 # EXIT-16 close-confirm (come in produzione) +HURST_MAX = 0.55 # loss-guard live +TREND_MAX = 3.0 +EMA_LONG = 200 + + +def build_trades_variant(ents, df, mode, trend_max, hurst_mask=None, + buffer=BUFFER, lev=LEV, fee_rt=FEE_RT, ema_long=EMA_LONG): + """Engine di exit16_port06_impact.build_trades_variant + skip Hurst opzionale. + + mode="orig" : SL intrabar al livello (SL prima del TP) == canonico. + mode="exit16" : SL intrabar OFF; TP intrabar al livello (priorita' nel bar); + SL solo se il CLOSE sfonda sl0 -/+ buffer*ATR14[j], fill a close[j]. + trend_max : None = filtro OFF (live attuale); 3.0 = candidato. + hurst_mask : bool[i]=True -> salta l'ingresso (loss-guard live). + """ + h, l, c = df["high"].values, df["low"].values, df["close"].values + n = len(c) + a = atr(df, 14) + el = pd.Series(c).ewm(span=ema_long, adjust=False).mean().values + fee = fee_rt * lev + out = [] + last = -1 + for e in ents: + i, d = e["i"], e["d"] + if i <= last or i + 1 >= n: + continue + if hurst_mask is not None and hurst_mask[i]: + continue + if trend_max is not None and a[i] and abs(c[i] - el[i]) / a[i] > trend_max: + continue + entry = c[i] + tp, sl0, mb = e["tp"], e["sl"], e["max_bars"] + 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: + exit_p = c[n - 1] + break + if mode == "orig": + hs = (d == 1 and l[j] <= sl0) or (d == -1 and h[j] >= sl0) + ht = (d == 1 and h[j] >= tp) or (d == -1 and l[j] <= tp) + if hs: + exit_p = sl0 + break + if ht: + exit_p = tp + break + if k == mb: + exit_p = c[j] + else: # exit16 + ht = (d == 1 and h[j] >= tp) or (d == -1 and l[j] <= tp) + if ht: + exit_p = tp + break + aj = a[j] if np.isfinite(a[j]) else 0.0 + confirm = (d == 1 and c[j] < sl0 - buffer * aj) or \ + (d == -1 and c[j] > sl0 + buffer * aj) + if confirm: + exit_p = c[j] + break + if k == mb: + exit_p = c[j] + ret = (exit_p - entry) / entry * d * lev - fee + out.append((i, j, ret)) + last = j + return out + + +def equity_from_trades(df, trades): + ts = pd.to_datetime(df["timestamp"], unit="ms", utc=True) + n = len(df) + eq = np.full(n, INIT, dtype=float) + cap = INIT + for i, j, ret in sorted(trades, key=lambda t: t[1]): + cap = max(cap + cap * POS * ret, 10.0) + eq[j:] = cap + s = pd.Series(eq, index=ts).resample("1D").last().reindex(IDX).ffill().bfill() + return _norm(s) + + +def port_metrics(members: dict[str, pd.Series], 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 _dd(s): + pk = s.cummax() + return float(((pk - s) / pk).max() * 100) + + +def main(): + p = PORTFOLIOS["PORT06"] + fade_ids = [s.sid for s in p.sleeves if s.sid.startswith("MR")] + print("=" * 100) + print(" GATE PORT06 — trend_max=3.0/ema200 sulle fade LIVE (hurst 0.55 + EXIT-16 gia' attivi)") + print(f" fade sleeve: {fade_ids} | caps={p.caps}") + print("=" * 100) + + print("\n[1] build_everything() canonico (cache)...") + from src.portfolio.sleeves import all_sleeve_equities + eq_base = dict(all_sleeve_equities()) + print(f" sleeve totali: {len(eq_base)}") + + # dati + entries + maschera hurst (identica al live: close-only, w=100, step=6) + dfs, masks, entries = {}, {}, {} + for asset in ("BTC", "ETH"): + dfs[asset] = load_data(asset, "1h") + masks[asset] = hurst_skip_mask(dfs[asset], HURST_MAX) + print(f" {asset}: {len(dfs[asset])} barre, hurst-skip {masks[asset].mean()*100:.1f}% delle barre") + for nm, (fn, params) in strats_for(asset).items(): + sid = f"{nm}_{asset}" + if sid in fade_ids: + entries[sid] = (asset, fn(dfs[asset], **params)) + + # --- [2] PARITA': mode=orig, trend ON, no hurst, no exit16 == canonico --- + print("\n[2] PARITA' replay (orig, trend ON, no hurst) vs canonico:") + print(f" {'sleeve':<10s}{'corr':>10s}{'ret_canon%':>14s}{'ret_replay%':>14s}{'diff%':>9s}") + parity_ok = True + for sid in fade_ids: + asset, ents = entries[sid] + rep = equity_from_trades(dfs[asset], build_trades_variant( + ents, dfs[asset], mode="orig", trend_max=TREND_MAX)) + base = eq_base[sid] + 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 + flag = "" if (corr > 0.999 and abs(rr - rb) <= max(1.0, abs(rb) * 0.01)) else " <-- MISMATCH" + if flag: + parity_ok = False + print(f" {sid:<10s}{corr:>10.5f}{rb:>14.1f}{rr:>14.1f}{rr-rb:>+9.2f}{flag}") + print(f"\n PARITA' {'OK' if parity_ok else 'FALLITA'}.") + if not parity_ok: + print(" >>> STOP: non procedo col gate su un engine non in parita'.") + return + + # --- [3] varianti live-path delle 6 fade --- + VARIANTS = { + "LIVE": dict(trend_max=None, hurst=True), # produzione oggi + "CANDIDATO": dict(trend_max=TREND_MAX, hurst=True), # + trend filter + "TREND-ONLY": dict(trend_max=TREND_MAX, hurst=False), # swap hurst->trend + "NESSUNO": dict(trend_max=None, hurst=False), # solo EXIT-16 (baseline filtri) + "TREND-2.5": dict(trend_max=2.5, hurst=False), # sensibilita' soglia + "TREND-3.5": dict(trend_max=3.5, hurst=False), + } + eqs = {v: {} for v in VARIANTS} + ntr = {v: {} for v in VARIANTS} + for sid in fade_ids: + asset, ents = entries[sid] + for v, cfg in VARIANTS.items(): + tr = build_trades_variant(ents, dfs[asset], mode="exit16", + trend_max=cfg["trend_max"], + hurst_mask=masks[asset] if cfg["hurst"] else None) + eqs[v][sid] = equity_from_trades(dfs[asset], tr) + ntr[v][sid] = len(tr) + + # --- [4] PORT06 per variante --- + print("\n" + "=" * 100) + print(f" [4] PORT06 (pesi cap, OOS da {OOS_DATE}) — fade in path LIVE (exit16+hurst)") + print("=" * 100) + print(f" {'variante':<12s}{'FULL Sh':>9s}{'FULL DD%':>10s}{'FULL CAGR':>11s}" + f" | {'OOS Sh':>8s}{'OOS DD%':>9s}{'OOS CAGR':>10s}") + print(" " + "-" * 94) + res = {} + for v in VARIANTS: + members = dict(eq_base) + for sid in fade_ids: + members[sid] = eqs[v][sid] + f, o = port_metrics(members, p) + res[v] = (f, o) + print(f" {v:<12s}{f['sharpe']:>9.2f}{f['dd']:>10.2f}{f['cagr']:>10.0f}%" + f" | {o['sharpe']:>8.2f}{o['dd']:>9.2f}{o['cagr']:>9.0f}%") + f_l, o_l = res["LIVE"] + f_c, o_c = res["CANDIDATO"] + print(" " + "-" * 94) + print(f" {'DELTA C-L':<12s}{f_c['sharpe']-f_l['sharpe']:>+9.2f}{f_c['dd']-f_l['dd']:>+10.2f}" + f"{f_c['cagr']-f_l['cagr']:>+10.0f}% | {o_c['sharpe']-o_l['sharpe']:>+8.2f}" + f"{o_c['dd']-o_l['dd']:>+9.2f}{o_c['cagr']-o_l['cagr']:>+9.0f}%") + + # --- [5] per-sleeve --- + print("\n Per-sleeve (FULL): ret% | DD% | n trade [LIVE -> CANDIDATO]") + print(f" {'sleeve':<10s}{'ret L%':>10s}{'ret C%':>10s}{'DD L%':>8s}{'DD C%':>8s}" + f"{'ntr L':>7s}{'ntr C':>7s}") + for sid in fade_ids: + el_, ec = eqs["LIVE"][sid], eqs["CANDIDATO"][sid] + rl = (el_.iloc[-1] / el_.iloc[0] - 1) * 100 + rc = (ec.iloc[-1] / ec.iloc[0] - 1) * 100 + print(f" {sid:<10s}{rl:>10.1f}{rc:>10.1f}{_dd(el_):>8.1f}{_dd(ec):>8.1f}" + f"{ntr['LIVE'][sid]:>7d}{ntr['CANDIDATO'][sid]:>7d}") + + # --- GATE --- + print("\n" + "=" * 100) + print(" GATE (sweep): PROMOSSO se OOS Sharpe >= LIVE-0.02 E OOS DD scende, e FULL non degrada") + print("=" * 100) + oos_sh_ok = o_c["sharpe"] >= o_l["sharpe"] - 0.02 + oos_dd_ok = o_c["dd"] < o_l["dd"] + full_ok = f_c["sharpe"] >= f_l["sharpe"] - 0.02 and f_c["dd"] <= f_l["dd"] + 0.20 + promoted = oos_sh_ok and oos_dd_ok and full_ok + print(f" OOS Sharpe {o_l['sharpe']:.2f} -> {o_c['sharpe']:.2f} ({'OK' if oos_sh_ok else 'KO'})") + print(f" OOS DD% {o_l['dd']:.2f} -> {o_c['dd']:.2f} ({'OK' if oos_dd_ok else 'KO'})") + print(f" FULL Sharpe {f_l['sharpe']:.2f} -> {f_c['sharpe']:.2f} | " + f"FULL DD {f_l['dd']:.2f} -> {f_c['dd']:.2f} ({'OK' if full_ok else 'KO'})") + print("\n VERDETTO: " + (">>> PROMOSSO <<<" if promoted else ">>> BOCCIATO <<<")) + + +if __name__ == "__main__": + main() diff --git a/scripts/portfolios/_defs.py b/scripts/portfolios/_defs.py index 30c0308..f0f56b9 100644 --- a/scripts/portfolios/_defs.py +++ b/scripts/portfolios/_defs.py @@ -18,11 +18,19 @@ UNIVERSE8 = ["ADA", "BNB", "BTC", "DOGE", "ETH", "LTC", "SOL", "XRP"] # MR02/MR07 lo ignorano (**params). Vedi docs/diary/2026-06-01-tp-min-edge.md. MIN_TP_FRAC = 0.0015 -# Loss-guard Hurst (live): salta le fade in regime PERSISTENTE/trending (rolling-Hurst >= 0.55), -# dove si concentrano stop-loss e perdite (stop-rate 43% vs 21% anti-persistente). DIMEZZA il DD -# del PORT06 (FULL 4.10%->2.39%) alzando lo Sharpe. Calcolato dalle SOLE close (no feed esterno). -# Validato 2026-06-02, vedi docs/diary/2026-06-02-fade-lossguard.md. -HURST_MAX = 0.55 +# Filtro TREND (live, swap dal loss-guard Hurst il 2026-06-07): salta le fade quando il prezzo +# e' esteso oltre trend_max*ATR(14) dalla EMA(ema_long) — non si fada un trend/crollo esteso. +# E' la config che la ricerca EXIT-16 (2026-06-04) ha effettivamente promosso (entries +# trend-filtrate, niente hurst). Gate PORT06 sul path live (trendmax_port06_impact, 2026-06-07): +# TREND-ONLY domina la config hurst-only su TUTTE le metriche (FULL Sharpe 7.23->7.89, +# OOS Sharpe 9.35->9.91, OOS DD 1.68->1.20), plateau robusto su trend_max 2.5/3.0/3.5. +# Il loss-guard Hurst (validato 2026-06-02 sull'engine PRE-EXIT-16 con SL intrabar) e' +# RIDONDANTE-DANNOSO dopo EXIT-16: attacca lo stesso regime (trending) saltando ingressi che +# con EXIT-16 sono tornati vincenti; la combinazione hurst+trend over-filtra (meta' dei trade, +# FULL Sharpe 7.11). hurst_skip_mask resta disponibile in fade_base (param hurst_max). +# Vedi docs/diary/2026-06-07-trendmax-gate.md. +TREND_MAX = 3.0 +EMA_LONG = 200 # EXIT-16 close-confirm SL (live, 2026-06-04): lo SL intrabar fisso veniva triggerato dai # wick transitori (falsi negativi: l'overshoot che buca lo stop rientra — e' il movimento @@ -33,8 +41,8 @@ HURST_MAX = 0.55 SL_CONFIRM_ATR = 0.5 FADE = [SleeveSpec(kind="single", name=c, sid=f"{c}_{a}", asset=a, cluster=f"{a}-rev", - params={"min_tp_frac": MIN_TP_FRAC, "hurst_max": HURST_MAX, - "sl_confirm_atr": SL_CONFIRM_ATR}) + params={"min_tp_frac": MIN_TP_FRAC, "trend_max": TREND_MAX, + "ema_long": EMA_LONG, "sl_confirm_atr": SL_CONFIRM_ATR}) 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. diff --git a/scripts/portfolios/hourly_report.py b/scripts/portfolios/hourly_report.py index 1a8b54d..eb31ebf 100644 --- a/scripts/portfolios/hourly_report.py +++ b/scripts/portfolios/hourly_report.py @@ -47,15 +47,20 @@ def _short(wid: str) -> str: return f"{code}/{tag}" if tag else code -# --- monitor loss-guard Hurst: stop-rate fade PRIMA/DOPO l'attivazione (hurst_max=0.55, v1.0.0) --- +# --- monitor filtri fade: stop-rate per epoca di config --- +# epoche: PRE (nessun filtro) -> HURST (loss-guard 0.55, v1.0.0) -> TREND (swap +# hurst->trend_max=3.0, 2026-06-07: gate trendmax_port06_impact, hurst ridondante +# post-EXIT-16). Confronta lo stop-rate live di ogni epoca col backtest. LOSSGUARD_SINCE = "2026-06-02T14:34:30" -FADE_PREFIXES = ("MR01", "MR02", "MR07") # le 3 fade con hurst_max attivo +TRENDSWAP_SINCE = "2026-06-07T10:10:00" +FADE_PREFIXES = ("MR01", "MR02", "MR07") LOSSGUARD_MIN_SAMPLE = 30 def lossguard_section() -> str: - before = [0, 0] # [closes, stops] prima dell'attivazione - after = [0, 0] # dopo + pre = [0, 0] # [closes, stops] nessun filtro + hurst = [0, 0] # epoca loss-guard Hurst + trend = [0, 0] # epoca filtro trend (config attuale) for sp in glob.glob(str(PAPER / "*" / "status.json")): wid = Path(sp).parent.name if not wid.startswith(FADE_PREFIXES): @@ -69,7 +74,8 @@ def lossguard_section() -> str: ev = json.loads(line) if ev.get("event") != "CLOSE": continue - b = after if ev.get("ts", "") >= LOSSGUARD_SINCE else before + ts = ev.get("ts", "") + b = trend if ts >= TRENDSWAP_SINCE else hurst if ts >= LOSSGUARD_SINCE else pre b[0] += 1 if ev.get("reason") == "stop_loss": b[1] += 1 @@ -77,14 +83,15 @@ def lossguard_section() -> str: def rate(b): return b[1] / b[0] * 100 if b[0] else 0.0 - L = [f"🛡️ Loss-guard Hurst (fade, dal {LOSSGUARD_SINCE[:16].replace('T', ' ')} UTC)"] - L.append(f" stop-rate PRIMA {rate(before):.0f}% (n={before[0]}) → DOPO {rate(after):.0f}% (n={after[0]})") - if after[0] >= LOSSGUARD_MIN_SAMPLE: - delta = rate(before) - rate(after) - L.append(f" VERDETTO (n≥{LOSSGUARD_MIN_SAMPLE}): {delta:+.0f}pp → " + L = ["🛡️ Filtri fade — stop-rate per epoca"] + L.append(f" PRE {rate(pre):.0f}% (n={pre[0]}) → HURST {rate(hurst):.0f}% (n={hurst[0]})" + f" → TREND {rate(trend):.0f}% (n={trend[0]})") + if trend[0] >= LOSSGUARD_MIN_SAMPLE: + delta = rate(pre) - rate(trend) + L.append(f" VERDETTO epoca TREND (n≥{LOSSGUARD_MIN_SAMPLE}): {delta:+.0f}pp vs PRE → " f"{'✅ riduce gli stop' if delta > 0 else '⚠️ nessuna riduzione'}") else: - L.append(f" campione DOPO {after[0]}/{LOSSGUARD_MIN_SAMPLE} → verdetto rimandato") + L.append(f" campione TREND {trend[0]}/{LOSSGUARD_MIN_SAMPLE} → verdetto rimandato") return "\n".join(L)