Files
PythagorasGoal/scripts/analysis/mr02eth_port06_gate.py
T
Adriano Dal Pastro df8bfbceaa research(mr02eth): ricerca sostituto + integrazione dati opzioni cerbero-bite
Ricerca sostituto/miglioria a MR02/ETH (127 strategie + 18 overlay opzioni,
verifica avversariale, gate PORT06). Esito: miglioria = no-SL (gia' ~catturata
da EXIT-16 live); tail-hedge opzioni NO-GO empirico su prezzi reali.

Infrastruttura opzioni REALE (muro ARGO/GEX caduto):
- options_fetcher.py: importa lo storico per-strike + market_snapshots da
  cerbero-bite -> data/options/ (chain bid/ask/IV/greche/OI + pannello regime
  con net-GEX dealer/gamma-flip/VRP/funding/liquidation).
- options_chain.py: loader + skew_curve/premium_levels (aggregati reali) +
  quote() causale + load_market() (pannello regime).
- option_overlay_lab.py: overlay opzioni BS su DVOL (pricing sintetico).
- mr02eth_port06_gate.py / eth_collar_gate.py: gate swap-sleeve e collar.
- mr02eth_search/options.workflow.js: i 2 workflow.

Numeri reali: skew put 10%OTM ~1.1 (liquido), premio ~1.0%/mese; niente strike
10%OTM a 24h (overlay per-trade infattibile); collar standing paga nei crash ma
net-negativo a PORT06 (alza OOS DD). Diario + CLAUDE.md aggiornati.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 07:05:00 +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()