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
@@ -0,0 +1,333 @@
"""Famiglia SHAPE-PIVOT: geometria a punti di svolta (PIP / pivot) -> bias futuro.
Idea (causale, no look-ahead):
- a ogni barra i comprimo la finestra di L barre terminante a close[i] nei suoi
P punti percettivamente importanti (PIP, Perceptually Important Points: i punti
di massima deviazione dalla retta congiungente — Fu et al.);
- la sequenza di P punti e' una POLILINEA = forma geometrica grezza;
- la classifico con feature interpretabili e CAUSALI:
* trend dei pivot interni: higher-highs/higher-lows (HH/HL) vs lower-* (LH/LL);
* convergenza/divergenza delle pendenze (triangoli/cunei);
* distanza % di close[i] dall'ultimo pivot alto/basso (vicino a R / a S);
* pendenza dell'ultimo segmento (slancio recente);
- per ogni CLASSE geometrica stimo l'esito medio a H barre usando SOLO occorrenze
passate il cui esito era gia' realizzato prima di i (statistica causale rolling);
- entro a close[i] nella direzione del bias di classe se l'edge passato e' netto;
exit a H barre o TP/SL in ATR.
VINCOLI (CLAUDE.md "metodologia obbligatoria" + "lezione squeeze look-ahead"):
- PIP/pivot calcolati SOLO su close[i-L+1 .. i]; nessun pivot "confermato dal futuro".
- ogni statistica per-classe usa solo campioni con esito (entry+H) <= i-1.
- ingresso eseguibile a close[i]; netto fee (0.10% RT base, sweep a 0.20%); leva 3x,
pos 0.15; validazione OOS (ultimo 30%) + robustezza griglia + >=2 asset.
- check di causalita' esplicito (perturbo il futuro: la forma a i non cambia).
Riusa l'engine netto-fee + OOS di explore_lab (simulate/evaluate/robust).
"""
from __future__ import annotations
import sys
from pathlib import Path
import numpy as np
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, _dt, OOS_FRAC,
)
# =========================================================================
# PIP — Perceptually Important Points (causale, solo su close[a..b])
# =========================================================================
def pip_indices(seg: np.ndarray, p: int) -> list[int]:
"""Estrae p indici PIP dalla serie `seg` (inclusi i 2 estremi).
Algoritmo Fu et al.: parti dai 2 estremi; aggiungi iterativamente il punto a
massima distanza VERTICALE dalla retta che unisce i due PIP adiacenti, finche'
non hai p punti. Tutto sul segmento dato -> nessun look-ahead se seg=close[..i].
"""
n = len(seg)
if p >= n:
return list(range(n))
pts = [0, n - 1]
while len(pts) < p:
best_d, best_k = -1.0, -1
for s in range(len(pts) - 1):
l, r = pts[s], pts[s + 1]
if r - l < 2:
continue
x1, y1 = l, seg[l]
x2, y2 = r, seg[r]
dx = x2 - x1
# distanza verticale dalla retta (interpolazione lineare in x)
for k in range(l + 1, r):
if dx == 0:
dist = abs(seg[k] - y1)
else:
yline = y1 + (y2 - y1) * (k - x1) / dx
dist = abs(seg[k] - yline)
if dist > best_d:
best_d, best_k = dist, k
if best_k < 0:
break
# inserisci mantenendo l'ordine
for s in range(len(pts) - 1):
if pts[s] < best_k < pts[s + 1]:
pts.insert(s + 1, best_k)
break
return pts
# =========================================================================
# Classe geometrica della polilinea PIP (feature causali interpretabili)
# =========================================================================
def shape_class(seg: np.ndarray, p: int) -> tuple | None:
"""Ritorna una tupla-classe discreta della forma PIP di `seg`, o None se degenere.
Feature (tutte da seg=close[..i], causali):
- dir_seq: per ogni pivot interno, segno della variazione vs precedente
(sequenza su/giu) -> cattura HH/HL vs LH/LL e zig-zag;
- conv: convergenza pendenze inizio vs fine (triangolo/cuneo): segno di
(|slope_last| - |slope_first|) discretizzato;
- loc: posizione di close[i] nel range della finestra (vicino a max=resistenza,
vicino a min=supporto), in 3 bucket.
La classe e' invariante a livello/scala (z-norm implicito su forma).
"""
idx = pip_indices(seg, p)
if len(idx) < 3:
return None
y = seg[idx]
rng = y.max() - y.min()
if rng <= 0:
return None
yn = (y - y.min()) / rng # forma normalizzata 0..1
# sequenza direzioni dei segmenti (su=1 / giu=0)
diffs = np.diff(yn)
dir_seq = tuple(int(x > 0) for x in diffs)
# convergenza: pendenza primo vs ultimo segmento
s_first = abs(diffs[0])
s_last = abs(diffs[-1])
if s_last > s_first * 1.3:
conv = 1 # divergente (slancio finale)
elif s_last < s_first * 0.77:
conv = -1 # convergente (compressione, triangolo/cuneo)
else:
conv = 0
# posizione di close[i] (=ultimo punto) nel range: 0..1 in 3 bucket
last = yn[-1]
loc = 0 if last < 0.33 else (2 if last > 0.67 else 1)
return (dir_seq, conv, loc)
# =========================================================================
# Strategia: bias per-classe stimato CAUSALMENTE (rolling, esito realizzato)
# =========================================================================
def pivot_entries(df, L=48, P=5, H=12, min_lib=1000, min_samples=20,
edge=0.0, tp_atr=None, sl_atr=None,
trend_max=None, ema_long=200, mode="bias") -> list[dict]:
"""Entries dalla geometria PIP con bias di classe causale.
L: lunghezza finestra-forma. P: n. punti PIP. H: orizzonte (=max_bars).
min_lib: barre minime prima di operare. min_samples: campioni minimi per fidarsi
della statistica di una classe. edge: |rendimento medio classe| minimo
(frazione, es. 0.002 = 0.2%) per entrare. mode:
- "bias": entra nel verso del rendimento medio passato della classe (momentum
della forma: la classe X storicamente -> su/giu);
- "fade": entra nel verso OPPOSTO (test mean-reversion della forma).
Statistica per-classe accumulata SOLO con esiti realizzati < i (causale stretta).
"""
close = df["close"].values
high = df["high"].values
low = df["low"].values
n = len(close)
a = atr(df, 14)
el = ema(close, ema_long) if trend_max is not None else None
# stato rolling per classe: somma rendimenti e conteggio (solo esiti < i)
cls_sum: dict[tuple, float] = {}
cls_cnt: dict[tuple, int] = {}
# coda di campioni la cui forma e' stata calcolata ma esito non ancora maturo
# pending[t] = (classe, indice_entry t) -> matura quando t+H <= i-1
pending: list[tuple] = [] # (mature_at, cls, t)
pend_ptr = 0
entries: list[dict] = []
for i in range(min_lib, n - 1):
# 1) integra nello storico tutti i campioni il cui esito e' realizzato (< i)
# un campione formato a t matura quando t+H <= i-1 => mature_at = t+H+1 <= i
while pend_ptr < len(pending) and pending[pend_ptr][0] <= i:
_, cls_p, t = pending[pend_ptr]
ret_real = (close[t + H] - close[t]) / close[t]
cls_sum[cls_p] = cls_sum.get(cls_p, 0.0) + ret_real
cls_cnt[cls_p] = cls_cnt.get(cls_p, 0) + 1
pend_ptr += 1
# 2) forma corrente (solo close fino a i)
seg = close[i - L + 1: i + 1]
cls = shape_class(seg, P)
if cls is None:
continue
# registra il campione corrente come pending (esito da realizzare in futuro)
pending.append((i + H + 1, cls, i))
# 3) decisione con statistica PASSATA della classe
cnt = cls_cnt.get(cls, 0)
if cnt < min_samples:
continue
mean_ret = cls_sum[cls] / cnt
if abs(mean_ret) < edge:
continue
d = 1 if mean_ret > 0 else -1
if mode == "fade":
d = -d
# filtro trend opzionale
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
# =========================================================================
# Filone (c): distanza da supporto/resistenza locale (ultimo pivot alto/basso)
# =========================================================================
def sr_entries(df, L=48, P=7, H=12, near=0.5, mode="fade",
tp_atr=None, sl_atr=None, trend_max=None, ema_long=200) -> list[dict]:
"""Filone (c): close[i] vicino all'ultimo pivot alto (R) o basso (S) della forma.
Usa i PIP per individuare l'ultimo massimo/minimo locale (resistenza/supporto) e
misura la distanza % di close[i]. Se close e' entro `near`*ATR da R -> bias short
(mode='fade': rimbalzo da R) o long (mode='break': rottura). Simmetrico per S.
Tutto causale: PIP su close[..i], decisione a close[i].
"""
close = df["close"].values
n = len(close)
a = atr(df, 14)
el = ema(close, ema_long) if trend_max is not None else None
entries: list[dict] = []
for i in range(L, n - 1):
seg = close[i - L + 1: i + 1]
idx = pip_indices(seg, P)
if len(idx) < 3 or a[i] <= 0:
continue
y = seg[idx]
# pivot interni (escludi i 2 estremi e l'ultimo punto = close[i])
inner = y[1:-1]
if len(inner) == 0:
continue
res = inner.max() # resistenza locale
sup = inner.min() # supporto locale
cur = close[i]
dist_r = (res - cur) / a[i]
dist_s = (cur - sup) / a[i]
d = None
if 0 <= dist_r <= near: # appena sotto R
d = -1 if mode == "fade" else 1
elif 0 <= dist_s <= near: # appena sopra S
d = 1 if mode == "fade" else -1
if d is None:
continue
if trend_max is not None and abs(cur - el[i]) / a[i] > trend_max:
continue
e = {"i": i, "d": d, "max_bars": H}
if tp_atr is not None:
e["tp"] = cur + d * tp_atr * a[i]
if sl_atr is not None:
e["sl"] = cur - d * sl_atr * a[i]
entries.append(e)
return entries
# =========================================================================
# Check causalita' esplicito
# =========================================================================
def check_no_lookahead(df, L=48, P=5) -> bool:
"""La classe-forma a i non deve cambiare se perturbo il FUTURO (>i)."""
close = df["close"].values.copy()
i = len(close) // 2
seg0 = close[i - L + 1: i + 1].copy()
c0 = shape_class(seg0, P)
close2 = close.copy()
close2[i + 1:] *= 1.7 # stravolge il futuro
seg1 = close2[i - L + 1: i + 1]
c1 = shape_class(seg1, P)
ok = (c0 == c1)
print(f" no-lookahead classe-forma a i={i}: {'OK' if ok else 'VIOLATO'} "
f"(c0={c0} c1={c1})")
# check su PIP indices
p0 = pip_indices(seg0, P)
p1 = pip_indices(seg1, P)
ok2 = (p0 == p1)
print(f" no-lookahead indici PIP: {'OK' if ok2 else 'VIOLATO'}")
return ok and ok2
# =========================================================================
# run() riproducibile
# =========================================================================
def run():
print("=" * 100)
print(" SHAPE-PIVOT RESEARCH — geometria PIP/pivot -> bias futuro | netto fee, OOS")
print("=" * 100)
df_btc = get_df("BTC", "1h")
print("\n[CAUSALITA']")
check_no_lookahead(df_btc, L=48, P=5)
assets = ["BTC", "ETH", "SOL", "ADA"]
dfs = {a: get_df(a, "1h") for a in assets}
# ---- A) bias di classe PIP (momentum della forma) ----
print("\n[A] BIAS di classe PIP (entra nel verso del rendimento medio passato della classe)")
print(" sweep L/P/H, edge=0.002, min_samples=25, time-exit a H")
A_grid = [(48, 5, 12), (48, 5, 24), (72, 6, 24), (36, 5, 12), (96, 7, 24), (48, 7, 12)]
for L, P, H in A_grid:
print(f" -- L{L} P{P} H{H} --")
for a in assets:
ents = pivot_entries(dfs[a], L=L, P=P, H=H, edge=0.002, min_samples=25, mode="bias")
evaluate(f"{a} bias L{L}P{P}H{H}", ents, dfs[a])
# ---- B) fade di classe PIP (mean-reversion della forma) ----
print("\n[B] FADE di classe PIP (entra opposto al bias storico -> test mean-reversion)")
for L, P, H in A_grid:
print(f" -- L{L} P{P} H{H} --")
for a in assets:
ents = pivot_entries(dfs[a], L=L, P=P, H=H, edge=0.002, min_samples=25, mode="fade")
evaluate(f"{a} fade L{L}P{P}H{H}", ents, dfs[a])
# ---- C) supporto/resistenza locale dai pivot ----
print("\n[C] S/R locale dai PIP — FADE (rimbalzo da R/S) vs BREAK (rottura)")
for mode in ("fade", "break"):
for near in (0.5, 1.0):
print(f" -- mode={mode} near={near} ATR, TP/SL 1.5/1.5 ATR, H=12 --")
for a in assets:
ents = sr_entries(dfs[a], L=48, P=7, H=12, near=near, mode=mode,
tp_atr=1.5, sl_atr=1.5)
evaluate(f"{a} SR-{mode} near{near}", ents, dfs[a])
# ---- D) miglior candidato con TP/SL ATR + filtro trend (se A o B mostra segnali) ----
print("\n[D] FADE di classe con TP/SL ATR (2.0/1.5) + filtro trend 3.0, L48 P5 H24")
for a in assets:
ents = pivot_entries(dfs[a], L=48, P=5, H=24, edge=0.002, min_samples=25,
mode="fade", tp_atr=2.0, sl_atr=1.5, trend_max=3.0)
res = evaluate(f"{a} fadeTPSL L48P5H24", ents, dfs[a])
if robust(res):
print(f" ^^^ {a} ROBUSTO")
print("\n" + "=" * 100)
print(" Verdetto: cerca righe con FULL>0 E OOS>0 E fee0.2% OOS>0 su >=2 asset.")
print("=" * 100)
if __name__ == "__main__":
run()