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>
133 lines
5.8 KiB
Python
133 lines
5.8 KiB
Python
"""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()
|