14522262e6
Reset del progetto su fondamenta verificate dopo la scoperta che l'intera libreria "validata OOS" era artefatto di feed contaminato (print fantasma del feed Cerbero TESTNET + storico Binance/USDT). - Storico ricostruito da Deribit MAINNET (ccxt pubblico, tokenless) e CERTIFICATO (certify_feed.py): BTC/ETH puliti su TUTTA la storia (mediana 2-6 bps vs Coinbase USD), integrita' OHLC + coerenza resample (maxΔ 0.00) + cross-venue OK. Alt esclusi (illiquidi/divergenti: LTC/DOGE 50-82% barre flat; XRP/BNB non certificabili). - Verdetto sul feed pulito: FADE / PAIRS / XS01 / TSM01 morti (ogni portafoglio Sharpe -2.3..-3.0, DD ~40%); solo SH01 e frammenti HONEST con segnale residuo, da ri-validare in isolamento. - Cleanup "restart pulito": strategie, stack live (src/live, src/portfolio, runner/executor, yml, docker), ~100 script ricerca/gate, waste/games/ portfolios, dati non certificati + cache e 60+ diari -> archiviati in Old/ (preservati, non cancellati). Diario consolidato in un unico documento. - Skeleton ricerca tenuto: Strategy ABC + indicatori + src/fractal + src/backtest/engine + load_data; tool dati certificati (rebuild_history, certify_feed, audit_feed, multi_source_check). - Universo dati ATTIVO: solo BTC/ETH (5m/15m/1h); guardrail fisico (load_data su alt -> FileNotFoundError). Esecuzione DISABILITATA, conto flat. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
107 lines
4.3 KiB
Python
107 lines
4.3 KiB
Python
"""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:
|
|
# vettoriale (log1p+rolling sum) invece di apply(np.prod): identico
|
|
# numericamente, ~100x piu' veloce del callback Python per-finestra
|
|
roll = np.expm1(np.log1p(r).rolling(win).sum())
|
|
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__":
|
|
# exit code 1 su warn: utilizzabile da cron/script come canale d'allarme
|
|
# (coerente con reconcile_account; prima il bool era calcolato e buttato via)
|
|
sys.exit(1 if main() else 0)
|