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,213 @@
|
||||
"""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()
|
||||
Reference in New Issue
Block a user