2 Commits

Author SHA1 Message Date
Adriano Dal Pastro 003875f2c3 docs(report): strategie_attive.html rigenerato su dati freschi (tutti gli 8 asset al 2026-06-12)
- parquet rinfrescati post-fix downloader: BTC+ETH 1h/15m/5m completi via v2,
  alt 1h incrementali (erano fermi al 28-29/05, panel multi-asset incoerenti)
- make_strategy_doc.yearly_stats: clamp di j (l'engine live-path lascia j>=n
  per il trade aperto al bordo serie -> IndexError coi dati freschi)
- header v1.1.26, card XS01 con phase-tranching, metodologia con verita'
  d'esecuzione/netting. Backtest canonico STABILE su +2 settimane di dati:
  FULL Sharpe 7.34 / DD 3.46 — OOS 10.07 / 1.48 (identico al 2026-06-11)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 06:17:39 +00:00
Adriano Dal Pastro e18adba4a6 fix(data): downloader su endpoint v2 + guard anti-troncamento (post-mortem refresh notturno)
Il refresh notturno e' rimasto 10h appeso e ha TRONCATO i parquet BTC al 2018:
cerbero-mcp ha rimosso l'endpoint legacy /mcp-deribit/tools/get_historical (404),
il downloader skippava in silenzio OGNI chunk (3 retry + sleep esponenziale x
migliaia di chunk) e a fine giro scriveva comunque il file con la sola Fase 1
storica. Aggravante: TIMEFRAMES includeva '1m' (1 giorno/richiesta = ~3000
richieste/asset). ETH salvato in tempo (kill prima della sovrascrittura);
BTC ripristinato via v2 (1h/15m/5m completi 2018->oggi in ~30 min).

- _fetch_deribit -> endpoint v2 /mcp/tools/get_historical (lo stesso del runner)
- guard chunk: >50% skippati = endpoint rotto -> RuntimeError, niente parziali
- guard anti-regressione in download_asset: mai sovrascrivere un parquet con
  dati che finiscono PRIMA dell'esistente
- '1m' fuori da TIMEFRAMES (refresh torna 5m/15m/1h; il 1m ad-hoc se serve)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 06:08:42 +00:00
3 changed files with 60 additions and 17 deletions
File diff suppressed because one or more lines are too long
+4 -1
View File
@@ -440,7 +440,10 @@ def yearly_stats(trades, ts):
cap, peak, tot_peak, tot_dd = 1000.0, 1000.0, 1000.0, 0.0 cap, peak, tot_peak, tot_dd = 1000.0, 1000.0, 1000.0, 0.0
cur = None cur = None
for i, j, ret in sorted(trades, key=lambda t: t[1]): for i, j, ret in sorted(trades, key=lambda t: t[1]):
ey, xy = ts.iloc[i].year, ts.iloc[j].year # clamp: l'engine live-path lascia j >= n per il trade aperto al bordo
# della serie (con dati freschi a fine-serie c'e' quasi sempre)
ey = ts.iloc[i].year
xy = ts.iloc[min(j, len(ts) - 1)].year
d = years.setdefault(ey, {"n": 0, "pnl": 0.0, "dd": 0.0}) d = years.setdefault(ey, {"n": 0, "pnl": 0.0, "dd": 0.0})
d["n"] += 1 d["n"] += 1
d["pnl"] += ret * 100 d["pnl"] += ret * 100
+38 -5
View File
@@ -32,7 +32,10 @@ ASSETS = {
}, },
} }
TIMEFRAMES = ["1m", "5m", "15m", "1h"] # NB: "1m" RIMOSSO da download_all (2026-06-12): con MAX_DAYS_PER_REQUEST=1 sono
# ~3000 richieste/asset — il refresh notturno e' rimasto 10h su BTC. Il 1m si
# scarica ad-hoc con download_asset("BTC", "1m") quando serve davvero.
TIMEFRAMES = ["5m", "15m", "1h"]
DERIBIT_RESOLUTION = {"1m": "1", "5m": "5", "15m": "15", "1h": "60"} DERIBIT_RESOLUTION = {"1m": "1", "5m": "5", "15m": "15", "1h": "60"}
@@ -45,15 +48,20 @@ def _parquet_path(asset: str, tf: str) -> Path:
return DATA_DIR / f"{asset.lower()}_{tf}.parquet" return DATA_DIR / f"{asset.lower()}_{tf}.parquet"
def _fetch_deribit(instrument: str, resolution: str, start: str, end: str) -> list[dict]: def _fetch_deribit(instrument: str, tf: str, start: str, end: str) -> list[dict]:
"""Endpoint v2 unificato (2026-06-12): il legacy /mcp-deribit/tools/get_historical
e' stato RIMOSSO da cerbero-mcp (404) — il refresh notturno ha skippato in
silenzio ogni chunk e scritto parquet troncati al 2018. Il v2 e' lo stesso
endpoint usato dal runner live (interval '1h'/'15m'/...)."""
resp = requests.post( resp = requests.post(
f"{CERBERO_URL}/mcp-deribit/tools/get_historical", f"{CERBERO_URL}/mcp/tools/get_historical",
headers=CERBERO_HEADERS, headers=CERBERO_HEADERS,
json={ json={
"exchange": "deribit",
"instrument": instrument, "instrument": instrument,
"interval": tf,
"start_date": start, "start_date": start,
"end_date": end, "end_date": end,
"resolution": resolution,
}, },
timeout=30, timeout=30,
) )
@@ -84,6 +92,7 @@ def _download_cerbero_range(
) )
all_candles: list[dict] = [] all_candles: list[dict] = []
max_days = MAX_DAYS_PER_REQUEST[tf] max_days = MAX_DAYS_PER_REQUEST[tf]
chunks = skipped = 0
current = datetime.fromisoformat(start_date) current = datetime.fromisoformat(start_date)
end = datetime.fromisoformat(end_date) end = datetime.fromisoformat(end_date)
@@ -100,19 +109,29 @@ def _download_cerbero_range(
for attempt in range(3): for attempt in range(3):
try: try:
candles = _fetch_deribit(instrument, resolution, start_str, end_str) candles = _fetch_deribit(instrument, tf, start_str, end_str)
all_candles.extend(candles) all_candles.extend(candles)
break break
except Exception as e: except Exception as e:
if attempt == 2: if attempt == 2:
print(f" SKIP {start_str}{end_str}: {e}") print(f" SKIP {start_str}{end_str}: {e}")
skipped += 1
time.sleep(2 ** attempt) time.sleep(2 ** attempt)
chunks += 1
pbar.update(max_days) pbar.update(max_days)
current = chunk_end current = chunk_end
pbar.close() pbar.close()
# GUARD (2026-06-12): se la maggioranza dei chunk e' stata skippata l'endpoint
# e' rotto, non il singolo range — ritornare il parziale farebbe scrivere un
# parquet troncato in silenzio (successo: 10h di SKIP e file fermi al 2018)
if chunks and skipped > chunks // 2:
raise RuntimeError(
f"{instrument} {tf}: {skipped}/{chunks} chunk falliti — endpoint rotto? "
"NON scrivo dati parziali.")
if not all_candles: if not all_candles:
return pd.DataFrame() return pd.DataFrame()
@@ -210,6 +229,20 @@ def download_asset(asset: str, tf: str) -> pd.DataFrame:
df = pd.concat(parts).drop_duplicates(subset="timestamp").sort_values("timestamp").reset_index(drop=True) df = pd.concat(parts).drop_duplicates(subset="timestamp").sort_values("timestamp").reset_index(drop=True)
# GUARD anti-regressione (2026-06-12): MAI sovrascrivere un parquet buono con
# dati che finiscono PRIMA dell'esistente (fase Cerbero vuota per endpoint
# rotto = file troncato al 2018 scritto in silenzio; il bootstrap SH01 e i
# backtest leggono questi file)
if path.exists():
try:
old_last = int(pd.read_parquet(path, columns=["timestamp"])["timestamp"].max())
except Exception:
old_last = 0
if int(df["timestamp"].max()) < old_last:
raise RuntimeError(
f"{path.name}: i nuovi dati finiscono PRIMA dell'esistente — "
"NON sovrascrivo (endpoint rotto?)")
path.parent.mkdir(parents=True, exist_ok=True) path.parent.mkdir(parents=True, exist_ok=True)
df.to_parquet(path, index=False) df.to_parquet(path, index=False)