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:
@@ -0,0 +1,259 @@
|
||||
"""CERTIFICAZIONE STORICO — dimostrare che data/raw (rebuild Deribit mainnet) e' PULITO.
|
||||
|
||||
Dopo che il feed contaminato (print fantasma testnet) ha invalidato l'intera libreria,
|
||||
NESSUNA nuova ricerca vale finche' lo storico non e' CERTIFICATO. Ricostruire non basta:
|
||||
va dimostrato pulito, su TUTTA la storia (non solo il recente) e su tutte le dimensioni.
|
||||
|
||||
Quattro blocchi, read-only (nessuna scrittura):
|
||||
(1) INTEGRITA' STRUTTURALE (locale, tutti asset/TF): invarianti OHLC (high>=low,
|
||||
high>=max(o,c), low<=min(o,c)), prezzi>0, volume>=0, NaN, ts monotono/dup, GAP
|
||||
(barre mancanti vs griglia attesa), run di barre FLAT (O=H=L=C).
|
||||
(2) COERENZA RESAMPLE (locale): ricalcolo 15m/1h dal 5m e confronto col salvato ->
|
||||
la "base unica + resample" e' davvero internamente coerente (errore ~0).
|
||||
(3) SCAN SPIKE / FAKE-WICK (locale): firma della contaminazione (high/low che sfora
|
||||
>THR il cluster di close vicino). Sul feed pulito deve essere ~0.
|
||||
(4) ACCORDO CROSS-VENUE (rete): close 1h vs COINBASE USD (venue indipendente, USD non
|
||||
USDT), STORIA INTERA per-anno: bps med/p95/max + %barre >50bps/>1%/>3%. E' il test
|
||||
di VERITA' del prezzo, specie sui vecchi anni dove viveva la contaminazione.
|
||||
|
||||
uv run python scripts/analysis/certify_feed.py # tutto (locale + rete)
|
||||
uv run python scripts/analysis/certify_feed.py --local # solo blocchi 1-3 (veloce)
|
||||
uv run python scripts/analysis/certify_feed.py --asset BTC ETH
|
||||
"""
|
||||
from __future__ import annotations
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
PROJECT_ROOT = Path(__file__).resolve().parents[2]
|
||||
sys.path.insert(0, str(PROJECT_ROOT))
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
import ccxt
|
||||
|
||||
RAW = PROJECT_ROOT / "data" / "raw"
|
||||
ASSETS = ["BTC", "ETH", "SOL", "ADA", "XRP", "LTC", "DOGE", "BNB"]
|
||||
TFS = ["5m", "15m", "1h"]
|
||||
TF_MIN = {"5m": 5, "15m": 15, "1h": 60}
|
||||
SPIKE_THR = 0.10 # high/low oltre +-10% dal cluster di close vicino = sospetto
|
||||
# Coinbase USD: venue indipendente da Deribit, denominato USD (non USDT). BNB non listato.
|
||||
COINBASE_SYM = {"BTC": "BTC/USD", "ETH": "ETH/USD", "SOL": "SOL/USD", "ADA": "ADA/USD",
|
||||
"XRP": "XRP/USD", "LTC": "LTC/USD", "DOGE": "DOGE/USD", "BNB": None}
|
||||
|
||||
|
||||
def load(asset, tf):
|
||||
p = RAW / f"{asset.lower()}_{tf}.parquet"
|
||||
if not p.exists():
|
||||
return None
|
||||
return pd.read_parquet(p)
|
||||
|
||||
|
||||
# ----------------------------- (1) INTEGRITA' -----------------------------
|
||||
def integrity(asset, tf, df):
|
||||
o, h, l, c = (df[x].values for x in ("open", "high", "low", "close"))
|
||||
v = df["volume"].values
|
||||
ts = df["timestamp"].values
|
||||
issues = []
|
||||
nan = int(df[["open", "high", "low", "close", "volume"]].isna().sum().sum())
|
||||
if nan: issues.append(f"NaN={nan}")
|
||||
nonpos = int((np.minimum.reduce([o, h, l, c]) <= 0).sum())
|
||||
if nonpos: issues.append(f"prezzo<=0={nonpos}")
|
||||
if (v < 0).sum(): issues.append(f"vol<0={int((v<0).sum())}")
|
||||
hl = int((h < l).sum())
|
||||
if hl: issues.append(f"high<low={hl}")
|
||||
ho = int((h < np.maximum(o, c)).sum())
|
||||
if ho: issues.append(f"high<max(o,c)={ho}")
|
||||
lo = int((l > np.minimum(o, c)).sum())
|
||||
if lo: issues.append(f"low>min(o,c)={lo}")
|
||||
dts = np.diff(ts)
|
||||
dup = int((dts == 0).sum())
|
||||
if dup: issues.append(f"ts-dup={dup}")
|
||||
if (dts < 0).sum(): issues.append(f"ts-non-monotono={int((dts<0).sum())}")
|
||||
step = TF_MIN[tf] * 60_000
|
||||
gaps = dts[dts > step]
|
||||
miss = int((gaps // step - 1).sum()) if len(gaps) else 0
|
||||
flat = int(((o == h) & (h == l) & (l == c)).sum())
|
||||
# run flat piu' lunga
|
||||
fmask = (o == h) & (h == l) & (l == c)
|
||||
longest = cur = 0
|
||||
for f in fmask:
|
||||
cur = cur + 1 if f else 0
|
||||
longest = max(longest, cur)
|
||||
status = "OK" if not issues else "**" + " ".join(issues)
|
||||
return dict(n=len(df), gaps=len(gaps), miss=miss, flat=flat, flatpct=flat/len(df)*100,
|
||||
flatrun=longest, status=status)
|
||||
|
||||
|
||||
# ----------------------------- (2) RESAMPLE -----------------------------
|
||||
def resample(base, tf):
|
||||
g = base.copy()
|
||||
g.index = pd.to_datetime(g["timestamp"], unit="ms", utc=True)
|
||||
rule = {"15m": "15min", "1h": "1h"}[tf]
|
||||
out = g.resample(rule, label="left", closed="left").agg(
|
||||
{"open": "first", "high": "max", "low": "min", "close": "last", "volume": "sum"})
|
||||
out = out.dropna(subset=["open"])
|
||||
epoch = pd.Timestamp("1970-01-01", tz="UTC")
|
||||
out.insert(0, "timestamp", ((out.index - epoch) // pd.Timedelta(milliseconds=1)).astype("int64"))
|
||||
return out.reset_index(drop=True)
|
||||
|
||||
|
||||
def resample_coherence(asset):
|
||||
base = load(asset, "5m")
|
||||
if base is None:
|
||||
return "no 5m"
|
||||
res = []
|
||||
for tf in ("15m", "1h"):
|
||||
stored = load(asset, tf)
|
||||
if stored is None:
|
||||
res.append(f"{tf}:no-file"); continue
|
||||
rb = resample(base, tf)
|
||||
m = stored.merge(rb, on="timestamp", suffixes=("_s", "_r"), how="inner")
|
||||
if len(m) == 0:
|
||||
res.append(f"{tf}:0-overlap"); continue
|
||||
maxerr = 0.0
|
||||
for col in ("open", "high", "low", "close"):
|
||||
d = (m[f"{col}_s"] - m[f"{col}_r"]).abs() / m[f"{col}_r"].replace(0, np.nan)
|
||||
maxerr = max(maxerr, float(d.max(skipna=True)) if len(d) else 0.0)
|
||||
nmiss = abs(len(stored) - len(rb))
|
||||
res.append(f"{tf}: maxΔ {maxerr*1e4:.2f}bps Δrows {nmiss}")
|
||||
return " | ".join(res)
|
||||
|
||||
|
||||
# ----------------------------- (3) SPIKE -----------------------------
|
||||
def spike_scan(df, thr=SPIKE_THR):
|
||||
c = df["close"].values.astype(float)
|
||||
h = df["high"].values.astype(float)
|
||||
l = df["low"].values.astype(float)
|
||||
n = len(c)
|
||||
s = pd.Series(c)
|
||||
# cluster di close vicino (mediana finestra 7 centrata, causale-agnostica = audit)
|
||||
cluster = s.rolling(7, center=True, min_periods=3).median().values
|
||||
hi = h > cluster * (1 + thr)
|
||||
lo = l < cluster * (1 - thr)
|
||||
susp = hi | lo
|
||||
idx = np.where(susp)[0]
|
||||
worst = []
|
||||
if len(idx):
|
||||
dev = np.where(hi, h / cluster - 1, np.where(lo, 1 - l / cluster, 0.0))
|
||||
order = idx[np.argsort(-np.abs(dev[idx]))][:3]
|
||||
ts = pd.to_datetime(df["timestamp"].values, unit="ms", utc=True)
|
||||
for i in order:
|
||||
worst.append(f"{ts[i].date()} {dev[i]*100:+.0f}%")
|
||||
return int(susp.sum()), worst
|
||||
|
||||
|
||||
# ----------------------------- (4) CROSS-VENUE -----------------------------
|
||||
def fetch_coinbase_1h(sym, start_ms, end_ms):
|
||||
ex = ccxt.coinbase({"enableRateLimit": True})
|
||||
if not ex.markets:
|
||||
ex.load_markets()
|
||||
if sym not in ex.markets:
|
||||
return None, f"simbolo {sym} assente su Coinbase"
|
||||
out, since, guard = {}, start_ms, 0
|
||||
while since <= end_ms and guard < 4000:
|
||||
guard += 1
|
||||
for attempt in range(3):
|
||||
try:
|
||||
r = ex.fetch_ohlcv(sym, "1h", since=since, limit=300)
|
||||
break
|
||||
except Exception as e:
|
||||
if attempt == 2:
|
||||
return (pd.Series(out) if out else None), f"err {type(e).__name__}"
|
||||
time.sleep(2 ** attempt)
|
||||
r = [x for x in r if int(x[0]) >= since]
|
||||
if not r:
|
||||
break
|
||||
for x in r:
|
||||
if start_ms <= int(x[0]) <= end_ms and x[4]:
|
||||
out[int(x[0])] = float(x[4])
|
||||
nxt = int(r[-1][0]) + 3600_000
|
||||
if nxt <= since:
|
||||
break
|
||||
since = nxt
|
||||
return (pd.Series(out) if out else None), f"{len(out)} barre"
|
||||
|
||||
|
||||
def cross_venue(asset):
|
||||
sym = COINBASE_SYM[asset]
|
||||
if sym is None:
|
||||
return None, "no-coinbase-ref (BNB non listato)"
|
||||
df1h = load(asset, "1h")
|
||||
if df1h is None:
|
||||
return None, "no 1h locale"
|
||||
ts = pd.to_datetime(df1h["timestamp"], unit="ms", utc=True)
|
||||
s_ms = int(df1h["timestamp"].iloc[0])
|
||||
e_ms = int(df1h["timestamp"].iloc[-1])
|
||||
ref, note = fetch_coinbase_1h(sym, s_ms, e_ms)
|
||||
if ref is None:
|
||||
return None, note
|
||||
a = df1h.set_index("timestamp")["close"]
|
||||
m = pd.concat([a.rename("a"), ref.rename("b")], axis=1, join="inner").dropna()
|
||||
if len(m) == 0:
|
||||
return None, "0 overlap"
|
||||
m["bps"] = (m["a"] - m["b"]).abs() / m["b"] * 1e4
|
||||
m["year"] = pd.to_datetime(m.index, unit="ms", utc=True).year
|
||||
rows = []
|
||||
for y, g in m.groupby("year"):
|
||||
rows.append((y, len(g), g["bps"].median(), g["bps"].quantile(.95), g["bps"].max(),
|
||||
(g["bps"] > 50).mean()*100, (g["bps"] > 100).mean()*100, (g["bps"] > 300).mean()*100))
|
||||
return rows, f"overlap {len(m)} barre vs Coinbase {sym}"
|
||||
|
||||
|
||||
def main():
|
||||
argv = sys.argv[1:]
|
||||
local_only = "--local" in argv
|
||||
assets = ASSETS
|
||||
if "--asset" in argv:
|
||||
i = argv.index("--asset")
|
||||
assets = [a.upper() for a in argv[i+1:] if not a.startswith("--")]
|
||||
|
||||
print("=" * 100)
|
||||
print(" (1) INTEGRITA' STRUTTURALE — per asset/TF")
|
||||
print("=" * 100)
|
||||
print(f" {'file':<11s}{'barre':>9s}{'gap':>6s}{'mancanti':>9s}{'flat':>8s}{'flat%':>7s}{'flatRun':>8s} status")
|
||||
print(" " + "-" * 96)
|
||||
for a in assets:
|
||||
for tf in TFS:
|
||||
df = load(a, tf)
|
||||
if df is None:
|
||||
print(f" {a.lower()+'_'+tf:<11s} (assente)"); continue
|
||||
r = integrity(a, tf, df)
|
||||
print(f" {a.lower()+'_'+tf:<11s}{r['n']:>9d}{r['gaps']:>6d}{r['miss']:>9d}{r['flat']:>8d}"
|
||||
f"{r['flatpct']:>6.1f}%{r['flatrun']:>8d} {r['status']}")
|
||||
|
||||
print("\n" + "=" * 100)
|
||||
print(" (2) COERENZA RESAMPLE (5m -> 15m/1h ricalcolato == salvato)")
|
||||
print("=" * 100)
|
||||
for a in assets:
|
||||
print(f" {a:<5s} {resample_coherence(a)}")
|
||||
|
||||
print("\n" + "=" * 100)
|
||||
print(f" (3) SCAN SPIKE / FAKE-WICK (high/low oltre +-{SPIKE_THR*100:.0f}% dal cluster close vicino)")
|
||||
print("=" * 100)
|
||||
for a in assets:
|
||||
for tf in TFS:
|
||||
df = load(a, tf)
|
||||
if df is None:
|
||||
continue
|
||||
n, worst = spike_scan(df)
|
||||
tag = "OK" if n == 0 else f"** {n} sospetti — {', '.join(worst)}"
|
||||
print(f" {a.lower()+'_'+tf:<11s} {tag}")
|
||||
|
||||
if local_only:
|
||||
print("\n (--local: salto il blocco rete cross-venue)")
|
||||
return
|
||||
|
||||
print("\n" + "=" * 100)
|
||||
print(" (4) ACCORDO CROSS-VENUE — close 1h vs COINBASE USD, per anno (bps; 1bps=0.01%)")
|
||||
print("=" * 100)
|
||||
for a in assets:
|
||||
rows, note = cross_venue(a)
|
||||
print(f"\n {a} ({note})")
|
||||
if rows is None:
|
||||
continue
|
||||
print(f" {'anno':>5s}{'barre':>7s}{'med':>7s}{'p95':>7s}{'max':>8s}{'>50bp%':>8s}{'>1%':>7s}{'>3%':>7s}")
|
||||
for y, n, med, p95, mx, g50, g100, g300 in rows:
|
||||
print(f" {y:>5d}{n:>7d}{med:>7.1f}{p95:>7.1f}{mx:>8.1f}{g50:>8.1f}{g100:>7.1f}{g300:>7.1f}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user