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:
Adriano Dal Pastro
2026-06-19 15:16:03 +00:00
parent 8401a280b9
commit 14522262e6
383 changed files with 1971 additions and 779 deletions
+257
View File
@@ -0,0 +1,257 @@
"""Harness ONESTO per pattern *di forma* -> previsione dell'andamento successivo.
Idea (analog forecasting / nearest-neighbour sulla FORMA del prezzo):
- a ogni barra i guardo la forma recente W (closes z-normalizzati fino a close[i]);
- cerco nel PASSATO le K finestre piu' simili la cui forma si era gia' conclusa
*e* il cui esito a H barre era gia' noto PRIMA di i (nessun look-ahead);
- prevedo la direzione dei prossimi H barre = segno del rendimento medio degli
analoghi; entro a close[i] se l'accordo fra analoghi e' abbastanza forte.
Vincoli anti-look-ahead (gli stessi della famiglia squeeze fallita):
- la forma usa SOLO closes fino a close[i];
- la libreria di analoghi a decisione i contiene solo finestre che terminano in
e con e+H <= i-1 -> il loro esito e' interamente realizzato *prima* della barra i;
- ingresso eseguibile a close[i]; exit TP/SL intrabar o time-limit H.
Riusa l'engine netto-fee + OOS di explore_lab (simulate/evaluate/robust).
KDTree ricostruito ogni `rebuild` barre (causale): query O(log N), niente O(N^2).
Asset: ADA BNB BTC DOGE ETH LTC SOL XRP (1h, 15m; BTC/ETH anche 5m).
"""
from __future__ import annotations
import sys
from pathlib import Path
import numpy as np
from numpy.lib.stride_tricks import sliding_window_view
from scipy.spatial import cKDTree
PROJECT_ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(PROJECT_ROOT))
from scripts.analysis.explore_lab import ( # noqa: E402
get_df, evaluate, robust, simulate, atr, ema, rsi, _dt, OOS_FRAC, ASSETS, FEE_RT,
)
# --------------------------- forma normalizzata ---------------------------
def znorm_windows(close: np.ndarray, W: int) -> tuple[np.ndarray, np.ndarray]:
"""Matrice delle finestre z-normalizzate per FORMA.
Ritorna (M, ends) dove M[k] = z-norm(close[e-W+1 .. e]) e ends[k] = e.
Z-norm per forma: (w - media)/std -> invariante a livello e scala -> confronto
sulla sola morfologia. Le finestre piatte (std=0) hanno norm tutta a 0.
"""
if len(close) < W:
return np.empty((0, W)), np.empty(0, dtype=int)
wins = sliding_window_view(close, W) # (N-W+1, W), wins[k] = close[k..k+W-1]
mu = wins.mean(axis=1, keepdims=True)
sd = wins.std(axis=1, keepdims=True)
sd = np.where(sd == 0, 1.0, sd)
M = (wins - mu) / sd
ends = np.arange(W - 1, len(close)) # finestra k termina in e = k+W-1
return M, ends
def fwd_return(close: np.ndarray, H: int) -> np.ndarray:
"""Rendimento forward a H barre per ogni indice: (close[i+H]-close[i])/close[i].
NaN dove i+H esce dai dati (non usabile come esito)."""
out = np.full(len(close), np.nan)
out[: len(close) - H] = (close[H:] - close[:-H]) / close[:-H]
return out
# --------------------------- analog forecasting causale ---------------------------
def analog_entries(df, W=24, H=12, K=50, rebuild=250, min_lib=800,
agree=0.60, conf_atr=0.0, tp_atr=None, sl_atr=None,
trend_max=None, ema_long=200) -> list[dict]:
"""Entries da nearest-neighbour sulla FORMA (causale, no look-ahead).
W: lunghezza finestra-forma. H: orizzonte previsione (= max_bars). K: n. analoghi.
rebuild: ogni quante barre si ricostruisce il KDTree (libreria cresce nel tempo).
min_lib: barre minime di storia prima di iniziare a operare.
agree: frazione minima di analoghi concordi sul segno per entrare (>0.5).
conf_atr: soglia |rendimento medio analoghi| in multipli di ATR-equivalente (0=off).
tp_atr/sl_atr: take-profit/stop in multipli di ATR (None = solo time-limit H).
trend_max: salta se |close-EMA(ema_long)|/ATR14 > trend_max (filtro trend, None=off).
"""
close = df["close"].values
n = len(close)
a = atr(df, 14)
M, ends = znorm_windows(close, W) # forme z-norm e indice di fine
end_pos = {int(e): k for k, e in enumerate(ends)}
fr = fwd_return(close, H) # esito H-barre per ogni indice
el = None
if trend_max is not None:
el = ema(close, ema_long)
entries: list[dict] = []
tree = None
lib_idx = None # indici e (fine finestra) nella libreria
next_rebuild = 0
for i in range(min_lib, n - 1):
# libreria causale: finestre la cui forma E il cui esito H sono < i
if tree is None or i >= next_rebuild:
eligible = ends[(ends <= i - 1 - H) & (ends >= W - 1)]
# esito noto e finito (fr non-NaN garantito da e+H <= i-1 < n)
eligible = eligible[~np.isnan(fr[eligible])]
if len(eligible) < max(K * 3, 200):
next_rebuild = i + rebuild
continue
tree = cKDTree(M[[end_pos[int(e)] for e in eligible]])
lib_idx = eligible
next_rebuild = i + rebuild
if tree is None:
continue
q = M[end_pos[i]]
if not np.isfinite(q).all():
continue
kk = min(K, len(lib_idx))
_, nn = tree.query(q, k=kk)
nn = np.atleast_1d(nn)
outs = fr[lib_idx[nn]] # rendimenti H-barre degli analoghi
outs = outs[~np.isnan(outs)]
if len(outs) < 5:
continue
mean_out = float(outs.mean())
d = 1 if mean_out > 0 else -1
frac = float(np.mean(np.sign(outs) == d))
if frac < agree:
continue
if conf_atr > 0:
if not (abs(mean_out) * close[i] >= conf_atr * a[i]):
continue
if trend_max is not None and a[i] > 0:
if abs(close[i] - el[i]) / a[i] > trend_max:
continue
e = {"i": i, "d": d, "max_bars": H}
if tp_atr is not None and a[i] > 0:
e["tp"] = close[i] + d * tp_atr * a[i]
if sl_atr is not None and a[i] > 0:
e["sl"] = close[i] - d * sl_atr * a[i]
entries.append(e)
return entries
# --------------------------- kNN grezzo cacheable (perf) ---------------------------
def analog_signals(df, W=24, H=12, K=50, rebuild=250, min_lib=800) -> dict:
"""Calcola UNA volta il forecast kNN grezzo per barra (causale), riusabile da
piu' filtri (agree/conf_atr/trend/tp/sl) senza ri-eseguire la query costosa.
Ritorna dict con array allineati per le barre che hanno un forecast valido:
i : indice barra (ingresso eseguibile a close[i])
mean_out : rendimento H-barre medio degli analoghi
frac : frazione di analoghi concordi col segno di mean_out (>=0.5)
d : segno previsto (+1/-1)
Identico, riga per riga, alla logica di analog_entries (stessa libreria causale,
stessa query, stessa soglia len(outs)>=5) ma SENZA i filtri di selezione.
"""
close = df["close"].values
n = len(close)
M, ends = znorm_windows(close, W)
end_pos = {int(e): k for k, e in enumerate(ends)}
fr = fwd_return(close, H)
out_i: list[int] = []
out_mean: list[float] = []
out_frac: list[float] = []
out_d: list[int] = []
tree = None
lib_idx = None
next_rebuild = 0
for i in range(min_lib, n - 1):
if tree is None or i >= next_rebuild:
eligible = ends[(ends <= i - 1 - H) & (ends >= W - 1)]
eligible = eligible[~np.isnan(fr[eligible])]
if len(eligible) < max(K * 3, 200):
next_rebuild = i + rebuild
continue
tree = cKDTree(M[[end_pos[int(e)] for e in eligible]])
lib_idx = eligible
next_rebuild = i + rebuild
if tree is None:
continue
q = M[end_pos[i]]
if not np.isfinite(q).all():
continue
kk = min(K, len(lib_idx))
_, nn = tree.query(q, k=kk)
nn = np.atleast_1d(nn)
outs = fr[lib_idx[nn]]
outs = outs[~np.isnan(outs)]
if len(outs) < 5:
continue
mean_out = float(outs.mean())
d = 1 if mean_out > 0 else -1
frac = float(np.mean(np.sign(outs) == d))
out_i.append(i); out_mean.append(mean_out); out_frac.append(frac); out_d.append(d)
return {
"i": np.asarray(out_i, dtype=int),
"mean_out": np.asarray(out_mean, dtype=float),
"frac": np.asarray(out_frac, dtype=float),
"d": np.asarray(out_d, dtype=int),
"H": H,
}
def entries_from_signals(df, sig: dict, agree=0.60, conf_atr=0.0,
tp_atr=None, sl_atr=None, trend_max=None, ema_long=200) -> list[dict]:
"""Applica i filtri di selezione al forecast grezzo di analog_signals (cheap).
Risultato identico ad analog_entries con gli stessi parametri (stesso W/H/K/rebuild
usati per costruire sig)."""
close = df["close"].values
a = atr(df, 14)
H = sig["H"]
el = ema(close, ema_long) if trend_max is not None else None
entries: list[dict] = []
for k in range(len(sig["i"])):
i = int(sig["i"][k]); d = int(sig["d"][k])
if sig["frac"][k] < agree:
continue
if conf_atr > 0 and not (abs(sig["mean_out"][k]) * close[i] >= conf_atr * a[i]):
continue
if trend_max is not None and a[i] > 0 and abs(close[i] - el[i]) / a[i] > trend_max:
continue
e = {"i": i, "d": d, "max_bars": H}
if tp_atr is not None and a[i] > 0:
e["tp"] = close[i] + d * tp_atr * a[i]
if sl_atr is not None and a[i] > 0:
e["sl"] = close[i] - d * sl_atr * a[i]
entries.append(e)
return entries
# --------------------------- verifica no look-ahead ---------------------------
def check_no_lookahead(df, W=24, H=12) -> bool:
"""La forma a i deve restare invariata se perturbo il FUTURO (>i).
Conferma che znorm_windows usa solo close fino a i."""
close = df["close"].values.copy()
M0, ends = znorm_windows(close, W)
pos = {int(e): k for k, e in enumerate(ends)}
i = len(close) // 2
q0 = M0[pos[i]].copy()
close2 = close.copy()
close2[i + 1:] *= 1.5 # stravolgo il futuro
M1, _ = znorm_windows(close2, W)
q1 = M1[pos[i]]
ok = np.allclose(q0, q1)
print(f" no-lookahead forma a i={i}: {'OK' if ok else 'VIOLATO'} "
f"(max diff {np.max(np.abs(q0 - q1)):.2e})")
return ok
if __name__ == "__main__":
print("=" * 92)
print(" SHAPE_LAB — baseline analog forecasting (kNN sulla forma) | netto fee, OOS")
print("=" * 92)
df = get_df("BTC", "1h")
check_no_lookahead(df)
print("\n BTC 1h — sweep base W/H/K (time-exit a H barre):")
for W, H, K in [(24, 12, 50), (24, 24, 50), (48, 24, 80), (12, 6, 40), (48, 48, 100)]:
ents = analog_entries(df, W=W, H=H, K=K, agree=0.60)
evaluate(f"analog W{W}H{H}K{K}", ents, df)