Files
Adriano Dal Pastro 14522262e6 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>
2026-06-19 15:20:59 +00:00

339 lines
15 KiB
Python

"""GATE PORT06: i top candidati sostituiscono MR02/ETH. Misura FULL+OOS Sharpe/DD.
Ogni candidato genera i trade ETH 1h con l'ENGINE INTRABAR identico al sleeve canonico
(explore_lab.simulate: SL/TP intrabar al livello, fee 0.10% RT, lev 3x), equity giornaliera
normalizzata su IDX (2021-01-01 -> 2026-05-26), swap su all_sleeve_equities()['MR02_ETH'],
e ri-misura PORT06 (cap weighting PAIRS 0.33 / SHAPE 0.0588).
uv run python scripts/analysis/mr02eth_port06_gate.py
"""
from __future__ import annotations
import sys
from pathlib import Path
import numpy as np
import pandas as pd
PROJECT_ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(PROJECT_ROOT))
from scripts.analysis.explore_lab import get_df, atr, ema
from scripts.analysis.combine_portfolio import IDX, SPLIT, OOS_DATE, metrics, port_returns, _norm
from src.portfolio.sleeves import all_sleeve_equities
from src.portfolio import weighting as W
FEE_RT, LEV, POS = 0.001, 3.0, 0.15
CAPS = {"PAIRS": 0.33, "SHAPE": 0.0588}
# ----------------------- engine intrabar (== explore_lab.simulate) -----------------------
def build_trades(entries, df, fee_rt=FEE_RT, lev=LEV):
h, l, c = df["high"].values, df["low"].values, df["close"].values
n = len(c); out = []; last = -1
for e in entries:
i, d = e["i"], e["d"]
if i <= last or i + 1 >= n:
continue
entry = c[i]; tp, sl, mb = e.get("tp"), e.get("sl"), e["max_bars"]
exit_p = c[min(i + mb, n - 1)]; j = min(i + mb, n - 1)
for k in range(1, mb + 1):
j = i + k
if j >= n:
exit_p = c[n - 1]; break
hit_sl = sl is not None and ((d == 1 and l[j] <= sl) or (d == -1 and h[j] >= sl))
hit_tp = tp is not None and ((d == 1 and h[j] >= tp) or (d == -1 and l[j] <= tp))
if hit_sl:
exit_p = sl; break
if hit_tp:
exit_p = tp; break
if k == mb:
exit_p = c[j]
out.append((i, j, (exit_p - entry) / entry * d * lev - fee_rt * lev)); last = j
return out
def build_trades_exit16(entries, df, sl_confirm=0.5, fee_rt=FEE_RT, lev=LEV,
dvol=None, otm=None, skew=1.10, tenor_mult=1.0):
"""Engine EXIT-16 close-confirm (== config LIVE): SL intrabar OFF, lo stop scatta solo se il
CLOSE sfonda sl ∓ sl_confirm*ATR14; TP intrabar ha priorita'.
Se dvol+otm sono dati, AGGIUNGE un overlay opzione (put se long / call se short) a otm OTM."""
from scripts.analysis.option_overlay_lab import bs_put, bs_call
h, l, c = df["high"].values, df["low"].values, df["close"].values
a = atr(df, 14); n = len(c); out = []; last = -1
for e in entries:
i, d = e["i"], e["d"]
if i <= last or i + 1 >= n:
continue
entry = c[i]; tp, sl, mb = e.get("tp"), e.get("sl"), e["max_bars"]
exit_p = c[min(i + mb, n - 1)]; j = min(i + mb, n - 1)
for k in range(1, mb + 1):
j = i + k
if j >= n:
exit_p = c[n - 1]; break
hit_tp = tp is not None and ((d == 1 and h[j] >= tp) or (d == -1 and l[j] <= tp))
if hit_tp:
exit_p = tp; break
if sl is not None and not np.isnan(a[j]):
buf = sl_confirm * a[j]
if (d == 1 and c[j] < sl - buf) or (d == -1 and c[j] > sl + buf):
exit_p = c[j]; break
if k == mb:
exit_p = c[j]
ret = (exit_p - entry) / entry * d * lev - fee_rt * lev
if dvol is not None and otm is not None:
T = max(mb * tenor_mult, 1.0) / _HOURS_YEAR; sig = dvol[i] * skew
if d == 1:
K = entry * (1 - otm); prem = bs_put(entry, K, T, sig) / entry; pay = max(K - exit_p, 0.0) / entry
else:
K = entry * (1 + otm); prem = bs_call(entry, K, T, sig) / entry; pay = max(exit_p - K, 0.0) / entry
ret += lev * (pay - prem)
out.append((i, j, ret)); last = j
return out
def blend_equity(eqs, weights=None) -> pd.Series:
"""Combina N equity giornaliere mediando i rendimenti giornalieri (ribilancio daily)."""
drs = [e.pct_change().fillna(0.0) for e in eqs]
w = weights or [1.0 / len(drs)] * len(drs)
dr = sum(wi * di for wi, di in zip(w, drs))
return _norm((1 + dr).cumprod())
def daily_equity(trades, df) -> pd.Series:
ts = pd.to_datetime(df["timestamp"], unit="ms", utc=True)
n = len(df); eq = np.full(n, 1000.0); cap = 1000.0
for i, j, ret in sorted(trades, key=lambda t: t[1]):
cap = max(cap + cap * POS * ret, 10.0); eq[j:] = cap
s = pd.Series(eq, index=ts).resample("1D").last().reindex(IDX).ffill().bfill()
return _norm(s)
# ----------------------- indicatori -----------------------
def choppiness(df, n=14):
h, l, c = df["high"].values, df["low"].values, df["close"].values
pc = np.roll(c, 1); pc[0] = c[0]
tr = np.maximum(h - l, np.maximum(np.abs(h - pc), np.abs(l - pc)))
str_ = pd.Series(tr).rolling(n).sum().values
hh = pd.Series(h).rolling(n).max().values
ll = pd.Series(l).rolling(n).min().values
rng = hh - ll
with np.errstate(divide="ignore", invalid="ignore"):
ci = 100.0 * np.log10(str_ / rng) / np.log10(n)
return ci
def var_ratio(close, k=30, win=100):
r1 = pd.Series(close).pct_change()
rk = pd.Series(close).pct_change(k)
v1 = r1.rolling(win).var().values
vk = rk.rolling(win).var().values
with np.errstate(divide="ignore", invalid="ignore"):
vr = vk / (k * v1)
return vr
def donchian_levels(df, n):
hh = pd.Series(df["high"].values).rolling(n).max().shift(1).values
ll = pd.Series(df["low"].values).rolling(n).min().shift(1).values
return hh, ll
# ----------------------- generatori di segnale (candidati) -----------------------
def gen_donchian_base(df, n=20, sl_atr=2.0, max_bars=24, trend_max=None, ema_long=200, gate=None, use_sl=True):
"""gate(i)->bool: True = consenti il segnale alla barra i. None = sempre. use_sl=False -> sl=None (no-SL)."""
h, l, c = df["high"].values, df["low"].values, df["close"].values
a = atr(df, 14); hh, ll = donchian_levels(df, n); em = ema(c, ema_long)
ents = []
for i in range(max(n, ema_long, 14) + 1, len(c)):
if np.isnan(hh[i]) or np.isnan(ll[i]) or np.isnan(a[i]) or a[i] <= 0:
continue
if trend_max is not None and not np.isnan(em[i]) and abs(c[i] - em[i]) / a[i] > trend_max:
continue
if gate is not None and not gate(i):
continue
tp = (hh[i] + ll[i]) / 2.0
if c[i] < ll[i] and c[i - 1] >= ll[i - 1]:
ents.append({"i": i, "d": 1, "tp": tp, "sl": (c[i] - sl_atr * a[i]) if use_sl else None, "max_bars": max_bars})
elif c[i] > hh[i] and c[i - 1] <= hh[i - 1]:
ents.append({"i": i, "d": -1, "tp": tp, "sl": (c[i] + sl_atr * a[i]) if use_sl else None, "max_bars": max_bars})
return ents
# ----------------------- engine intrabar + overlay opzione (per i candidati no-SL) -----------------------
_HOURS_YEAR = 24 * 365.0
def build_trades_hedged(entries, df, dvol, otm=0.10, skew=1.10, tenor_mult=1.0, fee_rt=FEE_RT, lev=LEV):
from scripts.analysis.option_overlay_lab import bs_put, bs_call
h, l, c = df["high"].values, df["low"].values, df["close"].values
n = len(c); out = []; last = -1
for e in entries:
i, d = e["i"], e["d"]
if i <= last or i + 1 >= n:
continue
entry = c[i]; tp, sl, mb = e.get("tp"), e.get("sl"), e["max_bars"]
exit_p = c[min(i + mb, n - 1)]; j = min(i + mb, n - 1)
for k in range(1, mb + 1):
j = i + k
if j >= n:
exit_p = c[n - 1]; break
hit_sl = sl is not None and ((d == 1 and l[j] <= sl) or (d == -1 and h[j] >= sl))
hit_tp = tp is not None and ((d == 1 and h[j] >= tp) or (d == -1 and l[j] <= tp))
if hit_sl:
exit_p = sl; break
if hit_tp:
exit_p = tp; break
if k == mb:
exit_p = c[j]
base = (exit_p - entry) / entry * d * lev - fee_rt * lev
T = max(mb * tenor_mult, 1.0) / _HOURS_YEAR; sig = dvol[i]; sigb = sig * skew
if d == 1:
K = entry * (1.0 - otm); prem = bs_put(entry, K, T, sigb) / entry; pay = max(K - exit_p, 0.0) / entry
else:
K = entry * (1.0 + otm); prem = bs_call(entry, K, T, sigb) / entry; pay = max(exit_p - K, 0.0) / entry
out.append((i, j, base + lev * (pay - prem))); last = j
return out
def cand_choppiness_gate_fade(df):
ci = choppiness(df, 14)
return gen_donchian_base(df, n=20, sl_atr=2.0, trend_max=None,
gate=lambda i: not np.isnan(ci[i]) and ci[i] >= 50.0)
def cand_choppiness_donchian(df):
ci = choppiness(df, 14)
return gen_donchian_base(df, n=14, sl_atr=2.0, trend_max=3.0,
gate=lambda i: not np.isnan(ci[i]) and ci[i] > 61.8)
def cand_varratio_gate_fade(df):
vr = var_ratio(df["close"].values, k=30, win=100)
return gen_donchian_base(df, n=20, sl_atr=2.0, trend_max=3.0,
gate=lambda i: not np.isnan(vr[i]) and vr[i] < 0.95)
def cand_baseline_recon(df):
"""MR02/ETH canonico ricostruito col MIO engine (sanity check vs all_sleeve_equities)."""
return gen_donchian_base(df, n=20, sl_atr=2.0, trend_max=3.0)
def cand_vrp_neg_dvol_low(df):
from scripts.analysis.regime_lab import load, regime_features
rdf = load("ETH", "1h")
R = regime_features(rdf)
# allinea per indice posizionale (regime_lab.load parte da get_df, stesso ordinamento)
vrp = R["vrp"]; dvp = R["dvol_pct"]
m = min(len(df), len(vrp))
def gate(i):
if i >= m:
return False
return (not np.isnan(vrp[i]) and vrp[i] < 0) and (not np.isnan(dvp[i]) and dvp[i] < 0.60)
return gen_donchian_base(df, n=20, sl_atr=2.0, trend_max=None, gate=gate)
# ----------------------- gate PORT06 -----------------------
def port_metrics(members, ids):
w = W.weight_vector("cap", ids, None, caps=CAPS)
drp = port_returns({i: members[i] for i in ids}, w)
return metrics(drp), metrics(drp, lo=SPLIT)
def main():
print("=" * 104)
print(f" GATE PORT06 — sostituire MR02/ETH | finestra {IDX[0].date()}..{IDX[-1].date()} | OOS da {OOS_DATE}")
print("=" * 104)
eq = dict(all_sleeve_equities())
ids = [k for k in eq if k in {
"MR01_BTC","MR02_BTC","MR07_BTC","MR01_ETH","MR02_ETH","MR07_ETH",
"DIP01_BTC","TR01_basket","ROT02_rot",
"PR_ETHBTC","PR_LTCETH","PR_ADAETH","PR_BTCLTC","PR_ETHSOL","TSM01","SH_BTC","SH_ETH"}]
print(f" sleeve PORT06: {len(ids)} | MR02_ETH presente: {'MR02_ETH' in ids}")
f_b, o_b = port_metrics(eq, ids)
print(f"\n {'variante':<22s}{'FULL Sh':>8s}{'FULL DD':>8s}{'FULL CAGR':>10s} |{'OOS Sh':>8s}{'OOS DD':>8s}{'OOS CAGR':>9s}")
print(" " + "-" * 100)
print(f" {'BASELINE (canonico)':<22s}{f_b['sharpe']:>8.2f}{f_b['dd']:>8.2f}{f_b['cagr']:>9.0f}% |{o_b['sharpe']:>8.2f}{o_b['dd']:>8.2f}{o_b['cagr']:>8.0f}%")
df = get_df("ETH", "1h")
from scripts.analysis.option_overlay_lab import dvol_for
dvol = dvol_for(df, "ETH")
# candidati: (nome, builder) dove builder(df)->trades
def b_signal(fn):
return lambda df: build_trades(fn(df), df)
base_ents = gen_donchian_base(df, n=20, sl_atr=2.0, trend_max=3.0)
def b_exit16(df):
return build_trades_exit16(base_ents, df, sl_confirm=0.5)
def b_exit16_put10(df):
return build_trades_exit16(base_ents, df, sl_confirm=0.5, dvol=dvol, otm=0.10)
def b_noSL(df):
return build_trades(gen_donchian_base(df, n=20, sl_atr=2.0, trend_max=3.0, use_sl=False), df)
def b_noSL_put10(df):
ents = gen_donchian_base(df, n=20, sl_atr=2.0, trend_max=3.0, use_sl=False)
return build_trades_hedged(ents, df, dvol, otm=0.10)
cands = {
"MR02recon(sanity)": b_signal(cand_baseline_recon),
"varratio_gate": b_signal(cand_varratio_gate_fade),
"choppiness_donch": b_signal(cand_choppiness_donchian),
"vrp_neg_dvol_low": b_signal(cand_vrp_neg_dvol_low),
"EXIT16(live)": b_exit16,
"EXIT16+put10%OTM": b_exit16_put10,
"noSL_raw": b_noSL,
"noSL_put10%OTM": b_noSL_put10,
}
rows = []
equ = {} # salva le equity per i blend
for name, fn in cands.items():
try:
tr = fn(df)
ce = daily_equity(tr, df)
equ[name] = ce
m2 = dict(eq); m2["MR02_ETH"] = ce
f_c, o_c = port_metrics(m2, ids)
rows.append((name, len(tr), f_c, o_c))
print(f" {name:<22s}{f_c['sharpe']:>8.2f}{f_c['dd']:>8.2f}{f_c['cagr']:>9.0f}% |{o_c['sharpe']:>8.2f}{o_c['dd']:>8.2f}{o_c['cagr']:>8.0f}%"
f" ({len(tr)} trade)")
except Exception as ex:
print(f" {name:<22s} ERRORE: {ex}")
# ---- BLEND within-sleeve: riempi lo sleeve ETH con EXIT-16 + un candidato decorrelato ----
print(" " + "-" * 100 + "\n BLEND within-sleeve (lo sleeve ETH = mix di 2 strategie, peso PORT06 invariato):")
blends = {
"50/50 EXIT16+varratio": (["EXIT16(live)", "varratio_gate"], [0.5, 0.5]),
"50/50 EXIT16+chopDonch": (["EXIT16(live)", "choppiness_donch"], [0.5, 0.5]),
"50/50 EXIT16+vrp": (["EXIT16(live)", "vrp_neg_dvol_low"], [0.5, 0.5]),
"70/30 EXIT16+vrp": (["EXIT16(live)", "vrp_neg_dvol_low"], [0.7, 0.3]),
"50/50 EXIT16put+vrp": (["EXIT16+put10%OTM", "vrp_neg_dvol_low"], [0.5, 0.5]),
"tri EXIT16put+vrp+chop": (["EXIT16+put10%OTM", "vrp_neg_dvol_low", "choppiness_donch"], [0.5, 0.25, 0.25]),
}
for name, (keys, wts) in blends.items():
try:
be = blend_equity([equ[k] for k in keys], wts)
m2 = dict(eq); m2["MR02_ETH"] = be
f_c, o_c = port_metrics(m2, ids)
rows.append((name, -1, f_c, o_c))
print(f" {name:<22s}{f_c['sharpe']:>8.2f}{f_c['dd']:>8.2f}{f_c['cagr']:>9.0f}% |{o_c['sharpe']:>8.2f}{o_c['dd']:>8.2f}{o_c['cagr']:>8.0f}%")
except Exception as ex:
print(f" {name:<22s} ERRORE: {ex}")
# riferimento ONESTO = EXIT-16 (config LIVE), non il canonico intrabar-SL
ex = next((r for r in rows if r[0] == "EXIT16(live)"), None)
f_l, o_l = (ex[2], ex[3]) if ex else (f_b, o_b)
print("\n " + "=" * 100)
print(f" GATE vs LIVE EXIT-16 (FULL {f_l['sharpe']:.2f}/{f_l['dd']:.2f} OOS {o_l['sharpe']:.2f}/{o_l['dd']:.2f}):")
print(" MIGLIORIA = nessuna metrica peggiora oltre il rumore E almeno una migliora (Sharpe +>=0.03 o DD -)")
print(" " + "-" * 100)
for name, ntr, f_c, o_c in rows:
if name.startswith("MR02recon") or name == "EXIT16(live)":
continue
dfs, dfd = f_c['sharpe'] - f_l['sharpe'], f_c['dd'] - f_l['dd']
dos, dod = o_c['sharpe'] - o_l['sharpe'], o_c['dd'] - o_l['dd']
no_worse = dfs >= -0.03 and dos >= -0.03 and dfd <= 0.05 and dod <= 0.03
better = dfs >= 0.03 or dos >= 0.03 or dfd <= -0.03 or dod <= -0.03
ok = no_worse and better
print(f" {name:<22s} ΔFULL Sh {dfs:+.2f} DD {dfd:+.2f} | ΔOOS Sh {dos:+.2f} DD {dod:+.2f} -> "
f"{'>>> MIGLIORIA' if ok else ('= pari' if no_worse else 'peggiora')}")
if __name__ == "__main__":
main()