chore(analysis): dedup engine gate PORT06 + drift monitor giornaliero + impact bfill

- _port06_gate_common.py: build_trades_variant/equity_from_trades/port_metrics/dd
  fattorizzati dai 3 gate exit16/trendmax/dip01 (-214 righe duplicate). Nessun
  copy-drift trovato; versione promossa = trendmax (superset con hurst_mask).
  Output dei 3 gate verificato BYTE-IDENTICO prima/dopo. dip_trades resta nel suo
  script (sibling deliberato long-only/orig_gap, non una copia).
- drift_monitor.py: rolling-return per famiglia vs distribuzione storica propria
  (warn sotto p5; oggi: FADE 120g al p2). In crontab host giornaliero 07:15 UTC
  con report Telegram. Osservabilita', non filtro di trading.
- daily_equity_bfill_impact.py: bug bfill _daily_equity QUANTIFICATO -> non
  materiale (OOS invariato per costruzione, FULL DD 3.46->3.67 col fix, nessun
  verdetto gate a rischio). Lasciato documentato in TODO, niente fix.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Adriano Dal Pastro
2026-06-11 20:04:23 +00:00
parent 51baed22e4
commit 8a2b065dd7
7 changed files with 483 additions and 222 deletions
+102
View File
@@ -0,0 +1,102 @@
"""Drift monitor per-famiglia — il rolling-return corrente di ogni famiglia vs la
DISTRIBUZIONE STORICA dei propri rolling-return (stessa finestra, storia 2021+).
Non è un filtro di trading: è OSSERVABILITÀ (la protezione giusta contro il drift è
accorgersene presto, non ritoccare i parametri — lezione 2026-06-11: le FADE al 2°
percentile sul 120g sono state trovate a mano; questo script lo rende ripetibile).
Percentile basso = la famiglia sta attraversando uno dei suoi tratti peggiori:
- sotto P_WARN (5%): segnalato — coerente con la coda storica, OSSERVARE;
- il PORT06 complessivo sotto P_WARN è più serio (la diversificazione non copre).
Equity dal builder canonico (all_sleeve_equities → parità coi gate).
uv run python scripts/analysis/drift_monitor.py # stampa
uv run python scripts/analysis/drift_monitor.py --telegram # + invio Telegram
"""
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 scripts.analysis.combine_portfolio import port_returns
from scripts.portfolios._defs import PORTFOLIOS
from src.portfolio.sleeves import all_sleeve_equities
from src.portfolio import weighting as W
WINDOWS = (60, 120) # giorni
P_WARN = 5.0 # percentile sotto cui segnalare
def family_returns():
"""Rendimenti daily per famiglia (equal-weight intra-famiglia) + PORT06 (pesi cap)."""
p = PORTFOLIOS["PORT06"]
eq = dict(all_sleeve_equities())
ids = list(p.sleeve_ids)
fams: dict[str, list] = {}
for i in ids:
fams.setdefault(W.family_of(i), []).append(i)
out = {}
for f, members in sorted(fams.items()):
out[f] = port_returns({i: eq[i] for i in members},
{i: 1 / len(members) for i in members})
dr = pd.DataFrame({i: eq[i].pct_change().fillna(0.0) for i in ids})
w = W.weight_vector("cap", ids, dr, caps=p.caps, clusters=p.clusters)
out["PORT06"] = port_returns({i: eq[i] for i in ids}, w)
return out
def drift_rows():
rows = []
for name, r in family_returns().items():
for win in WINDOWS:
roll = (1 + r).rolling(win).apply(np.prod, raw=True) - 1
roll = roll.dropna()
if len(roll) < 100:
continue
cur = float(roll.iloc[-1])
pct = float((roll < cur).mean() * 100)
rows.append(dict(name=name, win=win, cur=cur * 100, pct=pct,
p5=float(roll.quantile(0.05) * 100),
med=float(roll.median() * 100)))
return rows
def build_report(rows) -> tuple[str, bool]:
warn = [r for r in rows if r["pct"] < P_WARN]
L = ["📉 <b>Drift monitor</b> — rolling-return vs storia propria (2021+)"]
L.append("<pre>" + f"{'famiglia':<9}{'win':>5}{'corr%':>8}{'pct':>6}{'p5%':>8}{'med%':>7}")
for r in rows:
flag = " ⚠️" if r["pct"] < P_WARN else ""
L.append(f"{r['name']:<9}{r['win']:>4}g{r['cur']:>+8.1f}{r['pct']:>5.0f}%"
f"{r['p5']:>+8.1f}{r['med']:>+7.1f}{flag}")
L.append("</pre>")
if warn:
names = ", ".join(f"{r['name']} {r['win']}g (p{r['pct']:.0f})" for r in warn)
L.append(f"⚠️ sotto il p{P_WARN:.0f} storico: {names} — coda storica della famiglia: "
"OSSERVARE, non ritoccare i parametri (drift ≠ rottura; "
"vedi docs/diary/2026-06-11-stability-sweep.md)")
else:
L.append(f"✅ tutte le famiglie sopra il p{P_WARN:.0f} storico")
return "\n".join(L), bool(warn)
def main():
rows = drift_rows()
report, warned = build_report(rows)
import re
print(re.sub(r"</?(b|pre)>", "", report))
if "--telegram" in sys.argv:
from src.live.telegram_notifier import send_telegram
ok = send_telegram(report)
print(f"[telegram] inviato: {ok}")
return warned
if __name__ == "__main__":
main()