chore(reset): v2.0.0 — storico certificato Deribit mainnet, ripartenza pulita

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>
This commit is contained in:
Adriano Dal Pastro
2026-06-19 15:16:03 +00:00
parent 8401a280b9
commit 14522262e6
383 changed files with 1971 additions and 779 deletions
+278
View File
@@ -0,0 +1,278 @@
"""Discovery + validazione strumenti per gli exchange implementati (via Cerbero MCP).
Per ogni exchange (Deribit, Hyperliquid — esclusi Alpaca/stocks e Bybit, il cui
feed testnet e' farlocco) enumera i perpetui, ne verifica i dati e produce un
registry di strumenti VALIDATI.
Solo gli strumenti nel registry possono essere usati per la raccolta dati
(vedi gate in src/data/downloader.py).
Controlli di validazione (uno strumento e' valido solo se TUTTI passano):
- exists : la storia daily ritorna candele
- ohlc_sane : high>=low, high>=max(o,c), low<=min(o,c), prezzi>0
- not_flat : non e' un contratto morto (quasi tutte le barre O==H==L==C)
- liquid : volume_24h>0 dal ticker
- congruent : il prezzo concorda (entro tolleranza) con la MEDIANA dello
stesso base-coin su tutti gli exchange. Scarta i feed testnet
farlocchi (es. Bybit BTC=300k) e i contratti sbagliati
(es. Deribit SOL-PERPETUAL=9.6 vs SOL reale ~82).
NB: il token Cerbero punta a TESTNET; la congruenza cross-exchange e' il filtro
che distingue i feed realistici (Deribit, Hyperliquid) da quelli farlocchi.
"""
from __future__ import annotations
import json
import statistics
from dataclasses import dataclass, field
from datetime import datetime, timezone
from pathlib import Path
import pandas as pd
from src.live.cerbero_client import CerberoClient
REGISTRY_PATH = Path(__file__).resolve().parents[2] / "data" / "instruments_registry.json"
# I nostri timestep -> codice risoluzione per ciascun exchange
TF_CODES = {
"deribit": {"1m": "1", "5m": "5", "15m": "15", "1h": "60", "1d": "1D"},
"hyperliquid": {"1m": "1m", "5m": "5m", "15m": "15m", "1h": "1h", "1d": "1d"},
}
CONGRUENCE_TOL = 0.05 # 5% di scostamento dalla mediana del base-coin
def _today() -> str:
return datetime.now(timezone.utc).strftime("%Y-%m-%d")
@dataclass
class Quote:
base: str
symbol: str
last: float | None = None
volume_24h: float | None = None
open_interest: float | None = None
# --------------------------- adapters ---------------------------
class ExchangeAdapter:
name = "base"
def __init__(self, client: CerberoClient):
self.c = client
def _post(self, tool: str, payload: dict) -> dict:
return self.c._post(f"/mcp-{self.name}/tools/{tool}", payload)
def list_symbols(self) -> list[Quote]:
"""Lista perpetui (economica). I prezzi possono essere None (vedi ticker)."""
raise NotImplementedError
def ticker(self, q: Quote) -> None:
"""Riempie last/volume/OI sul Quote (per-simbolo). No-op se gia' pieni."""
def candles(self, symbol: str, tf: str, start: str, end: str) -> pd.DataFrame:
raise NotImplementedError
class DeribitAdapter(ExchangeAdapter):
name = "deribit"
def list_symbols(self) -> list[Quote]:
perps, offset = [], 0
while True:
r = self._post("get_instruments", {"currency": "any", "kind": "future",
"offset": offset, "limit": 100})
insts = r.get("instruments", [])
perps += [i["name"] for i in insts if i.get("name", "").endswith("-PERPETUAL")]
if not r.get("has_more") or not insts:
break
offset += len(insts)
if offset > 2000:
break
out = []
for name in perps:
base = name.split("-PERPETUAL")[0].replace("_USDC", "").replace("_USD", "")
out.append(Quote(base, name))
return out
def ticker(self, q: Quote) -> None:
t = self._post("get_ticker", {"instrument": q.symbol})
q.last, q.volume_24h, q.open_interest = t.get("last_price"), t.get("volume_24h"), t.get("open_interest")
def candles(self, symbol, tf, start, end) -> pd.DataFrame:
r = self._post("get_historical", {"instrument": symbol, "start_date": start,
"end_date": end, "resolution": TF_CODES["deribit"][tf]})
return _to_df(r.get("candles", []))
class HyperliquidAdapter(ExchangeAdapter):
name = "hyperliquid"
def list_symbols(self) -> list[Quote]:
r = self._post("get_markets", {})
markets = r if isinstance(r, list) else r.get("markets", [])
return [Quote(m["asset"], m["asset"], m.get("mark_price"),
m.get("volume_24h"), m.get("open_interest")) for m in markets]
# prezzi gia' presenti da get_markets -> ticker no-op
def candles(self, symbol, tf, start, end) -> pd.DataFrame:
r = self._post("get_historical", {"asset": symbol, "start_date": start, "end_date": end,
"resolution": TF_CODES["hyperliquid"][tf], "limit": 5000})
return _to_df(r.get("candles", []))
ADAPTERS = {"deribit": DeribitAdapter, "hyperliquid": HyperliquidAdapter}
def _to_df(candles: list[dict]) -> pd.DataFrame:
if not candles:
return pd.DataFrame()
df = pd.DataFrame(candles)
df["timestamp"] = df["timestamp"].astype("int64")
return df.sort_values("timestamp").reset_index(drop=True)
# --------------------------- validazione ---------------------------
def _ohlc_sane(df: pd.DataFrame) -> bool:
if df.empty:
return False
o, h, l, c = df["open"], df["high"], df["low"], df["close"]
ok = (h >= l) & (h >= o) & (h >= c) & (l <= o) & (l <= c) & (c > 0) & (l > 0)
return bool(ok.mean() > 0.99)
def _not_flat(df: pd.DataFrame) -> bool:
if df.empty:
return False
flat = (df["open"] == df["high"]) & (df["high"] == df["low"]) & (df["low"] == df["close"])
return bool(flat.mean() < 0.90)
def build_registry(exchanges: list[str] | None = None,
tf_check: tuple[str, ...] = ("1m", "5m", "15m", "1h"),
start_scan_from: str = "2017-01-01",
save: bool = True) -> dict:
exchanges = exchanges or ["deribit", "hyperliquid"] # NO alpaca, NO bybit (testnet farlocco)
client = CerberoClient()
adapters = {ex: ADAPTERS[ex](client) for ex in exchanges}
# 1) lista economica per ogni exchange
listed: dict[str, list[Quote]] = {}
for ex, ad in adapters.items():
try:
listed[ex] = ad.list_symbols()
print(f" [{ex}] {len(listed[ex])} strumenti elencati")
except Exception as e:
print(f" [{ex}] discovery FALLITA: {type(e).__name__}: {e}")
listed[ex] = []
# 2) universo = base-coin presenti su Deribit (il nostro venue). Bybit/HL
# vengono validati solo sull'overlap (cross-check), non su 500+ simboli.
deribit_bases = {q.base for q in listed.get("deribit", [])}
selected: dict[str, list[Quote]] = {}
for ex, qs in listed.items():
selected[ex] = qs if ex == "deribit" else [q for q in qs if q.base in deribit_bases]
# 3) timeframe disponibili per exchange (testati su BTC recente)
ref = {"deribit": "BTC-PERPETUAL", "hyperliquid": "BTC"}
tf_by_ex: dict[str, list[str]] = {}
for ex, ad in adapters.items():
oks = []
for tf in tf_check:
try:
if not ad.candles(ref[ex], tf, _today(), _today()).empty:
oks.append(tf)
except Exception:
pass
tf_by_ex[ex] = oks
print(f" [{ex}] timeframe ok: {oks}")
# 4) UNA fetch daily per strumento: e' il dato che davvero raccoglieremmo.
# Da qui ricaviamo esistenza, OHLC, not-flat, start-date, prezzo-per-congruenza
# (ultima close STORICA, non il ticker) e liquidita' (volume daily recente).
scan: dict[tuple[str, str], dict] = {}
for ex, ad in adapters.items():
for q in selected[ex]:
rec = {"reasons": [], "last_close": None, "start_date": None, "vol": 0.0}
try:
d = ad.candles(q.symbol, "1d", start_scan_from, _today())
if d.empty:
rec["reasons"].append("no_history")
else:
if not _ohlc_sane(d):
rec["reasons"].append("ohlc_insane")
if not _not_flat(d):
rec["reasons"].append("flat_dead")
rec["last_close"] = float(d["close"].iloc[-1])
rec["vol"] = float(d["volume"].tail(7).mean())
rec["start_date"] = str(pd.to_datetime(d["timestamp"].iloc[0], unit="ms", utc=True).date())
except Exception as e:
rec["reasons"].append(f"history_err:{type(e).__name__}")
scan[(ex, q.symbol)] = rec
# 5) mediana per base-coin dall'ULTIMA CLOSE STORICA (riferimento congruenza)
by_base: dict[str, list[float]] = {}
for (ex, sym), rec in scan.items():
base = next(q.base for q in selected[ex] if q.symbol == sym)
if rec["last_close"] and rec["last_close"] > 0:
by_base.setdefault(base, []).append(rec["last_close"])
median_px = {b: statistics.median(v) for b, v in by_base.items()}
# 6) finalizza validazione
registry: dict = {"generated_at": datetime.now(timezone.utc).isoformat(),
"congruence_tol": CONGRUENCE_TOL, "testnet": True, "exchanges": {}}
for ex, ad in adapters.items():
registry["exchanges"][ex] = {"timeframes": tf_by_ex[ex], "instruments": {}}
for q in selected[ex]:
rec = scan[(ex, q.symbol)]
reasons = list(rec["reasons"])
px, med, n_src = rec["last_close"], median_px.get(q.base), len(by_base.get(q.base, []))
if not (rec["vol"] and rec["vol"] > 0):
reasons.append("no_volume")
if px is None or px <= 0:
if "no_history" not in reasons:
reasons.append("no_price")
elif med and n_src >= 2 and abs(px - med) / med > CONGRUENCE_TOL:
reasons.append(f"incongruent(px={px:.4g},med={med:.4g})")
valid = len(reasons) == 0
registry["exchanges"][ex]["instruments"][q.symbol] = {
"base": q.base, "valid": valid, "reasons": reasons,
"last_price": px, "start_date": rec["start_date"],
"timeframes": tf_by_ex[ex] if valid else [],
}
if save:
REGISTRY_PATH.write_text(json.dumps(registry, indent=2))
print(f" registry salvato in {REGISTRY_PATH}")
return registry
# --------------------------- gate per il downloader ---------------------------
def load_registry() -> dict:
return json.loads(REGISTRY_PATH.read_text()) if REGISTRY_PATH.exists() else {}
def is_validated(symbol: str, tf: str, exchange: str = "deribit") -> bool:
"""True solo se lo strumento e' nel registry come valido per quel timeframe."""
inst = load_registry().get("exchanges", {}).get(exchange, {}).get("instruments", {}).get(symbol)
return bool(inst and inst.get("valid") and tf in inst.get("timeframes", []))
if __name__ == "__main__":
reg = build_registry()
print("\n" + "=" * 96)
print(" REGISTRY STRUMENTI VALIDATI")
print("=" * 96)
for ex, exd in reg["exchanges"].items():
insts = exd["instruments"]
valid = {s: i for s, i in insts.items() if i["valid"]}
print(f"\n {ex.upper()} | tf={exd['timeframes']} | validi {len(valid)}/{len(insts)}")
for s, i in sorted(valid.items(), key=lambda kv: kv[1]["base"]):
print(f" {s:30s} {i['base']:10s} px={i['last_price']:<12.6g} dal {i['start_date']}")
bad = {s: i for s, i in insts.items() if not i["valid"]}
if bad:
shown = list(bad.items())[:6]
print(f" -- scartati {len(bad)} (primi {len(shown)}):")
for s, i in shown:
print(f" {s:30s} {','.join(i['reasons'])[:64]}")