From 6605008f49358a3a8e6e904d8edd2ff38192fa5b Mon Sep 17 00:00:00 2001 From: Adriano Dal Pastro Date: Tue, 2 Jun 2026 14:54:37 +0000 Subject: [PATCH] feat(report): monitor loss-guard Hurst nel report orario Telegram Traccia lo stop-rate delle fade (MR01/MR02/MR07) PRIMA/DOPO l'attivazione del loss-guard (2026-06-02 14:34 UTC). Verdetto automatico quando il campione DOPO >= 30 trade. Conferma gia' visibile: stop-rate live PRIMA 42% (n=36) == backtest 42.1%. Gira host-side (cron), no rebuild. Co-Authored-By: Claude Opus 4.8 (1M context) --- scripts/portfolios/hourly_report.py | 44 +++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/scripts/portfolios/hourly_report.py b/scripts/portfolios/hourly_report.py index 20b14b0..235e669 100644 --- a/scripts/portfolios/hourly_report.py +++ b/scripts/portfolios/hourly_report.py @@ -47,6 +47,47 @@ 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) --- +LOSSGUARD_SINCE = "2026-06-02T14:34:30" +FADE_PREFIXES = ("MR01", "MR02", "MR07") # le 3 fade con hurst_max attivo +LOSSGUARD_MIN_SAMPLE = 30 + + +def lossguard_section() -> str: + before = [0, 0] # [closes, stops] prima dell'attivazione + after = [0, 0] # dopo + for sp in glob.glob(str(PAPER / "*" / "status.json")): + wid = Path(sp).parent.name + if not wid.startswith(FADE_PREFIXES): + continue + tp = Path(sp).parent / "trades.jsonl" + if not tp.exists(): + continue + for line in tp.read_text().splitlines(): + if not line.strip(): + continue + ev = json.loads(line) + if ev.get("event") != "CLOSE": + continue + b = after if ev.get("ts", "") >= LOSSGUARD_SINCE else before + b[0] += 1 + if ev.get("reason") == "stop_loss": + b[1] += 1 + + 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 → " + f"{'āœ… riduce gli stop' if delta > 0 else 'āš ļø nessuna riduzione'}") + else: + L.append(f" campione DOPO {after[0]}/{LOSSGUARD_MIN_SAMPLE} → verdetto rimandato") + return "\n".join(L) + + def collect(): closed = [] # (sleeve, reason, net_return, pnl, win) open_pos = [] # dict per posizione aperta @@ -125,6 +166,9 @@ def build_report() -> str: rows.append(f"{p['sleeve']:<14}{d:<2}{p['bars']:>6} {es}") L.append("
" + "\n".join(rows) + "
") + # 2b) monitor loss-guard + L.append(lossguard_section()) + # 3) TOTALE L.append(f"šŸ’° PnL realizzato totale: €{realized:+.2f}") if eq is not None: