Files
PythagorasGoal/scripts/analysis/rebuild_history.py
Adriano Dal Pastro 14522262e6 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>
2026-06-19 15:20:59 +00:00

214 lines
9.4 KiB
Python

"""REBUILD STORICO — fonte di verita' = DERIBIT MAINNET (il venue dove ESEGUIAMO).
Decisione (analisi 2026-06-19, vedi docs/diary/2026-06-19-deribit-history.md):
la contaminazione di data/raw nasce dal downloader che pesca la fase "Deribit" da
Cerbero col token TESTNET (feed farlocco) + la fase storica da Binance/USDT (wedge
USDT ~10bps, fino a 3% sotto depeg). Il cross-check multi-venue ha mostrato che il
venue PIU' vicino al consenso e' Deribit mainnet (0-1 bps), che e' anche dove si
esegue. Quindi: ricostruire lo storico da Deribit MAINNET (ccxt pubblico, NO token).
ARCHITETTURA (la "via migliore"):
- SORGENTE: ccxt.deribit pubblico = api.deribit.com MAINNET (reale, tokenless;
il token serve solo per trading/account, NON per gli OHLCV).
- STRUMENTO per il PREZZO:
BTC/ETH -> INVERSE perp (BTC/USD:BTC, ETH/USD:ETH): storia lunga (2018-08/2019-03)
e ~3 bps dal lineare USDC che eseguiamo (misurato) -> proxy fedele.
alt -> LINEARE USDC (X/USDC:USDC): unica opzione su Deribit, dal 2022.
- BASE UNICA + RESAMPLE: si scarica UN solo timeframe base (default 5m) e si
derivano tutti gli altri per aggregazione -> coerenza interna GARANTITA
(15m == agg dei 5m per costruzione) e si valida UNA serie, non 20.
- COPERTURA ONESTA a due livelli: major 2018/2019->oggi; alt SOLO dal 2022
(prima non erano tradabili su Deribit -> backtestarli su Binance pre-2022 =
validare un edge su un mercato che non potevamo eseguire).
NB: le daily NATIVE Deribit chiudono alle 08:00 UTC (settlement); qui il 1d e' DERIVATO
dal base per resample -> confini 00:00 UTC standard e coerenti con gli altri TF.
uv run python scripts/analysis/rebuild_history.py --smoke # prova su slice, NON tocca data/raw
uv run python scripts/analysis/rebuild_history.py --smoke --asset ETH
uv run python scripts/analysis/rebuild_history.py --asset BTC ETH # FULL rebuild (scrive data/raw, con backup)
uv run python scripts/analysis/rebuild_history.py # FULL tutti gli asset
"""
from __future__ import annotations
import sys
import time
import shutil
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
from src.data.downloader import _parquet_path, DATA_DIR
BACKUP = PROJECT_ROOT / "data" / "_feed_backup"
# asset -> (simbolo ccxt Deribit, start storico reale sul venue)
DERIBIT_INSTR = {
"BTC": ("BTC/USD:BTC", "2018-08-14"), # inverse, storia lunga ~ lineare entro 3 bps
"ETH": ("ETH/USD:ETH", "2019-03-14"),
"SOL": ("SOL/USDC:USDC", "2022-03-15"), # alt: solo lineare USDC, solo dal 2022
"ADA": ("ADA/USDC:USDC", "2022-03-21"),
"XRP": ("XRP/USDC:USDC", "2022-03-16"),
"LTC": ("LTC/USDC:USDC", "2022-04-28"),
"DOGE": ("DOGE/USDC:USDC","2022-04-28"),
"BNB": ("BNB/USDC:USDC", "2024-10-01"), # ATTENZIONE: storia cortissima (~1.7y)
}
BASE_TF = "5m" # base unica (5m: supporta tutti i TF standard)
TARGET_TFS = ["5m", "15m", "1h"] # = set del download_all; 4h/1d il runner li resampla dal 1h
SCHEMA = ["timestamp", "open", "high", "low", "close", "volume"]
COINBASE = ccxt.coinbase({"enableRateLimit": True})
def _deribit():
return ccxt.deribit({"enableRateLimit": True})
def fetch_base(symbol: str, tf: str, start: str, end: str | None = None) -> pd.DataFrame:
"""OHLCV Deribit mainnet paginato in AVANTI (since esplicito che avanza — evita
l'ancoraggio-a-now di ccxt deribit con since storico). Ritorna schema canonico."""
ex = _deribit()
tf_ms = ex.parse_timeframe(tf) * 1000
start_ms = int(pd.Timestamp(start, tz="UTC").timestamp() * 1000)
end_ms = int(pd.Timestamp(end, tz="UTC").timestamp() * 1000) if end else int(time.time() * 1000)
rows: dict[int, list] = {}
since, guard = start_ms, 0
while since <= end_ms and guard < 5000:
guard += 1
for attempt in range(3):
try:
r = ex.fetch_ohlcv(symbol, tf, since=since, limit=1000)
break
except Exception as e:
if attempt == 2:
print(f" ! {type(e).__name__}: {str(e)[:60]}")
r = []
time.sleep(2 ** attempt)
r = [x for x in r if int(x[0]) >= since]
if not r:
break
for x in r:
t = int(x[0])
if start_ms <= t <= end_ms:
rows[t] = [t, float(x[1]), float(x[2]), float(x[3]), float(x[4]), float(x[5] or 0)]
nxt = int(r[-1][0]) + tf_ms
if nxt <= since:
break
since = nxt
if not rows:
return pd.DataFrame(columns=SCHEMA)
df = pd.DataFrame(rows.values(), columns=SCHEMA)
return df.drop_duplicates("timestamp").sort_values("timestamp").reset_index(drop=True)
def resample(base: pd.DataFrame, tf: str) -> pd.DataFrame:
"""Aggrega il base al timeframe tf (confini 00:00 UTC). Coerente per costruzione."""
if tf == BASE_TF:
return base.copy()
g = base.copy()
g.index = pd.to_datetime(g["timestamp"], unit="ms", utc=True)
rule = {"5m": "5min", "15m": "15min", "30m": "30min", "1h": "1h", "2h": "2h",
"3h": "3h", "4h": "4h", "6h": "6h", "12h": "12h", "1d": "1D"}[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"])
# ms epoch resolution-independent (pandas 2.x indicizza datetime64[ms]: view/asi8
# cambiano risoluzione e darebbero ts corrotti -> uso una differenza di Timedelta)
epoch = pd.Timestamp("1970-01-01", tz="UTC")
ts_ms = ((out.index - epoch) // pd.Timedelta(milliseconds=1)).astype("int64")
out.insert(0, "timestamp", ts_ms)
return out.reset_index(drop=True)[SCHEMA]
def cross_audit(df1h: pd.DataFrame, asset: str, start: str, end: str) -> str:
"""Certifica il 1h ricostruito contro Coinbase USD (venue indipendente, NON USDT)."""
sym = {"BTC": "BTC/USD", "ETH": "ETH/USD", "SOL": "SOL/USD", "ADA": "ADA/USD",
"XRP": "XRP/USD", "LTC": "LTC/USD", "DOGE": "DOGE/USD", "BNB": None}.get(asset)
if sym is None:
return "no-coinbase-ref"
try:
if not COINBASE.markets:
COINBASE.load_markets()
if sym not in COINBASE.markets:
return f"coinbase: simbolo {sym} assente"
s_ms = int(pd.Timestamp(start, tz="UTC").timestamp() * 1000)
e_ms = int(pd.Timestamp(end, tz="UTC").timestamp() * 1000)
out, since = {}, s_ms
while since <= e_ms:
r = COINBASE.fetch_ohlcv(sym, "1h", since=since, limit=300)
r = [x for x in r if int(x[0]) >= since]
if not r:
break
for x in r:
if s_ms <= int(x[0]) <= e_ms and x[4]:
out[int(x[0])] = float(x[4])
nxt = int(r[-1][0]) + 3600_000
if nxt <= since:
break
since = nxt
ref = pd.Series(out)
except Exception as e:
return f"coinbase-err {type(e).__name__}"
a = df1h.set_index("timestamp")["close"]
m = pd.concat([a.rename("a"), ref.rename("b")], axis=1, join="inner")
if len(m) == 0:
return "0 overlap"
d = (m["a"] - m["b"]).abs() / m["b"] * 1e4
return f"vs Coinbase USD: barre={len(m)} med {d.median():.1f} bps p95 {d.quantile(.95):.1f} max {d.max():.1f}"
def build(asset: str, write: bool, smoke: bool) -> None:
symbol, full_start = DERIBIT_INSTR[asset]
if smoke:
start, end = "2026-04-15", "2026-05-27"
else:
start, end = full_start, None
print(f"\n [{asset}] {symbol} base {BASE_TF} da {start}{' -> '+end if end else ' -> oggi'}")
base = fetch_base(symbol, BASE_TF, start, end)
if base.empty:
print(" VUOTO — skip"); return
f0 = pd.to_datetime(base['timestamp'].iloc[0], unit='ms', utc=True).date()
f1 = pd.to_datetime(base['timestamp'].iloc[-1], unit='ms', utc=True).date()
print(f" base: {len(base)} barre {BASE_TF} [{f0} -> {f1}]")
built = {tf: resample(base, tf) for tf in TARGET_TFS}
for tf, d in built.items():
print(f" {tf:<4} -> {len(d):>7} barre")
# certifica il 1h
aud_start = "2026-04-15" if smoke else "2026-03-01"
aud_end = "2026-05-27" if smoke else f1.isoformat()
print(f" AUDIT 1h {cross_audit(built['1h'], asset, aud_start, aud_end)}")
if not write:
print(" (smoke: NON scrivo data/raw)")
return
BACKUP.mkdir(parents=True, exist_ok=True)
for tf, d in built.items():
path = _parquet_path(asset, tf)
if path.exists():
shutil.copy2(path, BACKUP / f"{asset.lower()}_{tf}.parquet.prebuild.bak")
tmp = path.with_suffix(".parquet.tmp")
d.to_parquet(tmp, index=False)
tmp.replace(path)
print(f" scritto {path.name}")
def main():
argv = sys.argv[1:]
smoke = "--smoke" in argv
assets = []
if "--asset" in argv:
i = argv.index("--asset")
assets = [a.upper() for a in argv[i + 1:] if not a.startswith("--")]
assets = assets or list(DERIBIT_INSTR)
print("=" * 78)
print(f" REBUILD STORICO da DERIBIT MAINNET — {'SMOKE (no write)' if smoke else 'FULL (scrive data/raw, backup)'}")
print(f" asset: {assets} base={BASE_TF} target={TARGET_TFS}")
print("=" * 78)
for a in assets:
build(a, write=not smoke, smoke=smoke)
if __name__ == "__main__":
main()