"""AUDIT INTEGRITA' — confronta OGNI parquet storico col prezzo REALE Binance. Dopo la scoperta che fade/pairs/DIP01 erano edge FINTI (print fantasma del feed testnet Cerbero), serve CERTEZZA del dato: quanto e' contaminato OGNI file? Per ogni parquet confronta il CLOSE col Binance spot allineato (merge_asof nearest) e riporta la quota di barre >1% e >3% fuori dalla realta', per file e per anno peggiore. Riferimento: Binance 5m (BTC/ETH sub-orari) e 1h (tutti), provato ~ mainnet (disc <0.13%). NON modifica nulla: solo lettura + report. Cache ref in data/raw/_ref_*.parquet. uv run python scripts/analysis/audit_feed.py """ from __future__ import annotations import sys from pathlib import Path PROJECT_ROOT = Path(__file__).resolve().parents[2] sys.path.insert(0, str(PROJECT_ROOT)) import numpy as np, pandas as pd, ccxt RAW = PROJECT_ROOT / "data" / "raw" SYM = {"btc": "BTC/USDT", "eth": "ETH/USDT", "ada": "ADA/USDT", "bnb": "BNB/USDT", "doge": "DOGE/USDT", "ltc": "LTC/USDT", "sol": "SOL/USDT", "xrp": "XRP/USDT"} TF_MIN = {"5m": 5, "8m": 8, "13m": 13, "15m": 15, "19m": 19, "30m": 30, "1h": 60, "2h": 120, "3h": 180, "4h": 240, "5h": 300, "6h": 360, "8h": 480, "12h": 720, "13h": 780, "19h": 1140, "24h": 1440, "36h": 2160, "48h": 2880} _EX = None def ex(): global _EX if _EX is None: _EX = ccxt.binance({"enableRateLimit": True}) return _EX def get_ref(asset: str, base: str) -> pd.DataFrame: """Serie Binance di riferimento (cache). Riusa _real_/_ref_ se presenti.""" for pref in ("_real_", "_ref_"): c = RAW / f"{pref}{asset}_{base}.parquet" if c.exists(): return pd.read_parquet(c)[["timestamp", "close"]] cache = RAW / f"_ref_{asset}_{base}.parquet" start = "2018-01-01" if base == "5m" else "2017-08-01" start_ms = int(pd.Timestamp(start, tz="UTC").timestamp() * 1000) end_ms = int(pd.Timestamp("2026-05-27", tz="UTC").timestamp() * 1000) tf_ms = TF_MIN[base] * 60 * 1000 rows, since = [], start_ms while since <= end_ms: for _ in range(3): try: r = ex().fetch_ohlcv(SYM[asset], base, since=since, limit=1000); break except Exception: r = [] if not r: break rows += r nxt = int(r[-1][0]) + tf_ms if nxt <= since: break since = nxt df = pd.DataFrame(rows, columns=["timestamp", "open", "high", "low", "close", "volume"]) df = df.drop_duplicates("timestamp").sort_values("timestamp").reset_index(drop=True) df[["timestamp", "open", "high", "low", "close", "volume"]].to_parquet(cache, index=False) return df[["timestamp", "close"]] def audit(asset: str, tf: str, ref5, ref1h): f = RAW / f"{asset}_{tf}.parquet" if not f.exists(): return None df = pd.read_parquet(f)[["timestamp", "close"]].copy() df["timestamp"] = df["timestamp"].astype("int64") df = df.dropna(subset=["close"]).sort_values("timestamp").reset_index(drop=True) base = "5m" if (TF_MIN[tf] < 60 and asset in ("btc", "eth")) else "1h" ref = (ref5 if base == "5m" else ref1h).get(asset) if ref is None or len(ref) == 0: return {"file": f"{asset}_{tf}", "rows": len(df), "no_ref": True} ref = ref.rename(columns={"close": "cref"}).sort_values("timestamp") tol = TF_MIN[base] * 60 * 1000 m = pd.merge_asof(df, ref, on="timestamp", direction="nearest", tolerance=tol) m = m.dropna(subset=["cref"]) m = m[m["cref"] > 0] if len(m) == 0: return {"file": f"{asset}_{tf}", "rows": len(df), "covered": 0, "no_ref": True} disc = (m["close"] - m["cref"]).abs() / m["cref"] yr = pd.to_datetime(m["timestamp"], unit="ms", utc=True).dt.year worst_y, worst_p = 0, 0.0 for y, g in disc.groupby(yr): p = float((g > 0.01).mean() * 100) if p > worst_p: worst_p, worst_y = p, int(y) return {"file": f"{asset}_{tf}", "rows": len(df), "covered": len(m) / len(df) * 100, "p1": float((disc > 0.01).mean() * 100), "p3": float((disc > 0.03).mean() * 100), "med": float(disc.median() * 100), "worst_y": worst_y, "worst_p": worst_p} def main(): assets = list(SYM) print("Fetch riferimenti Binance (1h tutti + 5m BTC/ETH)...", flush=True) ref1h = {a: get_ref(a, "1h") for a in assets} ref5 = {a: get_ref(a, "5m") for a in ("btc", "eth")} print("\n" + "=" * 96) print(" AUDIT INTEGRITA' FEED — % barre con CLOSE >1% (e >3%) fuori da Binance spot") print("=" * 96) print(f" {'file':<10s}{'righe':>8s}{'cov%':>6s}{'med%':>7s}{'>1%':>7s}{'>3%':>7s}{'worst-anno':>13s} giudizio") print(" " + "-" * 92) rows = [] for a in assets: tfs = [tf for tf in TF_MIN if (RAW / f"{a}_{tf}.parquet").exists()] # ordina per minuti for tf in sorted(tfs, key=lambda t: TF_MIN[t]): r = audit(a, tf, ref5, ref1h) if r: rows.append(r) # ordina per contaminazione discendente clean = [r for r in rows if not r.get("no_ref")] clean.sort(key=lambda r: -r.get("p1", 0)) poisoned = sum(1 for r in clean if r["p1"] >= 1.0) for r in clean: verdict = "PULITO" if r["p1"] < 0.5 else ("sospetto" if r["p1"] < 1.0 else "CONTAMINATO") print(f" {r['file']:<10s}{r['rows']:>8d}{r['covered']:>6.0f}{r['med']:>7.2f}" f"{r['p1']:>7.1f}{r['p3']:>7.1f}{('%d:%.0f%%'%(r['worst_y'],r['worst_p'])):>13s} {verdict}") noref = [r for r in rows if r.get("no_ref")] print(" " + "-" * 92) print(f" Totale file audited: {len(clean)} | CONTAMINATI (>1% barre fuori): {poisoned} | " f"PULITI (<0.5%): {sum(1 for r in clean if r['p1']<0.5)} | senza-ref: {len(noref)}") if noref: print(" senza riferimento Binance:", ", ".join(r["file"] for r in noref)) if __name__ == "__main__": main()