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:
@@ -0,0 +1,338 @@
|
||||
"""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()
|
||||
Reference in New Issue
Block a user