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>
214 lines
9.4 KiB
Python
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()
|