research(xsec): sweep cross-sectional su Hyperliquid (43 script/257 config) + verifica avversariale
Nuova harness condivisa xslib.py (panel HL certificato, score per-asset causale, book long-k/short-k vol-targeted leak-free) + 43 script in runs/ su 11 famiglie (MOM/REV/VOL/ DIST/LIQ/VAL/STRUCT/UNIV). Scoring = earns_slot (full>0 AND hold-out>0 AND marginal ADDS al portafoglio live AND corr XS01<0.6, con jackknife drop-one-month). Find: 42/257 config earns_slot=True, ma TUTTE con corr TP01 -0.2..-0.4 e PnL ~solo 2025. Verify (verify_survivors.py, 3 scettici deterministici): - S1 redundancy: cluster low-vol = UNA scommessa (XV01=XU02=1.00, XV02/XV03 r 0.44-0.67); XM09/XL02/XS06b/XR02 distinti (corr media off-diag +0.20). - S2 short-beta: cluster low-vol carica 0.44-0.70 su short-market -> NON market-neutral, e' un tilt short-alt-beta di regime. XM09(0.08)/XR02(-0.21) NON short-beta. - S3 per-anno: cluster low-vol decade (XV01/XU02 2026 -0.09); XL02 morto (2025 -0.14, 2026 -0.43); XM09 (0.82/0.50/0.74) e XR02 (0.84/0.40/2.68) positivi in tutti e 3 gli anni. Esito: nessuna sleeve nuova. Cluster low-vol RIGETTATO (regime-bet), XL02 RIGETTATO (overfit). 2 LEAD genuini (XM09 trend-gated x-sec momentum, XR02 reversal vol-gated) -> forward-monitor, non deployabili (panel 2.5y regime unico + STAT-MODE esecuzione). Portafoglio live invariato. Incluso anche options_vrp_managed.py (A/B VRP01 hold-to-expiry vs gestione attiva del doc credit-spread): la gestione attiva DISTRUGGE l'edge (combo FULL managed Sh -1.29 vs HtE +0.96, il delta-exit taglia i vincenti) -> scartata, VRP01 resta hold-to-expiry. Diari: 2026-06-20-xsec-strategies-sweep.md, 2026-06-20-vrp-active-management.md. gitignore: data/paper_portfolio/ (stato runtime paper) + scripts/research/xsec/runs/out/ (output rigenerabile). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,164 @@
|
||||
"""VRP01 + GESTIONE ATTIVA (test del doc 'strategia-credit-spread-eth', 2026-06-20).
|
||||
|
||||
Innesta sul put credit spread di VRP01 le regole di gestione intra-trade del documento:
|
||||
- profit-take 50% del credito
|
||||
- stop-loss stretto 1.5x il credito (debito di chiusura)
|
||||
- VOL-STOP: chiudi se DVOL sale >=10 punti dall'apertura (regola crypto-specifica, NUOVA)
|
||||
- delta-exit: chiudi se |delta| dello short put >= 0.30 (niente rolling/difesa)
|
||||
- time-stop 7 DTE
|
||||
Confronto A/B ONESTO sugli STESSI ingressi gated (VRP>0 + IV-rank>0.30) e dati certificati:
|
||||
BASE = hold-to-expiry (come VRP01) vs MANAGED = stesso trade con la gestione attiva.
|
||||
Il MTM giornaliero dello spread usa BS sul path certificato + DVOL reale (causale: decisione al
|
||||
giorno j con dati <= j). CAVEAT invariato: premio MODELLATO su DVOL ATM (no skew), nessun fill di
|
||||
stress reale -> LEAD, non deploy. Qui misuriamo solo SE la gestione attiva taglia la coda.
|
||||
|
||||
uv run python scripts/research/options_vrp_managed.py
|
||||
"""
|
||||
from __future__ import annotations
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
from scipy.stats import norm
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
|
||||
from src.data.downloader import load_data
|
||||
from src.strategies.trend_portfolio import resample_1d
|
||||
from src.portfolio.portfolio import to_daily, metrics, HOLDOUT
|
||||
from src.portfolio.sleeves import _bs_put, _strike_from_delta, VRP_CFG, _HL_DIR
|
||||
|
||||
CFG = dict(VRP_CFG) # short_delta -0.28, long_delta -0.10, f 1.0, gate_ivr 0.30, crash_skip 0.90, fee_frac 0.125
|
||||
|
||||
|
||||
def _put_delta_mag(S, K, T, sig):
|
||||
if T <= 0 or sig <= 0:
|
||||
return 1.0 if S < K else 0.0
|
||||
d1 = (np.log(S / K) + 0.5 * sig ** 2 * T) / (sig * np.sqrt(T))
|
||||
return float(norm.cdf(-d1)) # |delta| dello short put (=N(-d1))
|
||||
|
||||
|
||||
def simulate(asset: str, tenor_d: int, mode: str = "hte"):
|
||||
"""mode: 'hte' hold-to-expiry | 'full' tutte le regole | 'volstop' solo vol-stop DVOL+10 (+PT50).
|
||||
Ritorna (serie rendimenti per-trade indicizzata alla data di uscita, dict conteggio exit)."""
|
||||
manage = mode != "hte"
|
||||
full = mode == "full"
|
||||
df = resample_1d(load_data(asset, "1h"))
|
||||
s = pd.Series(df["close"].values.astype(float), index=pd.to_datetime(df["datetime"]))
|
||||
if s.index.tz is None:
|
||||
s.index = s.index.tz_localize("UTC")
|
||||
dv = pd.read_parquet(_HL_DIR / f"dvol_{asset.lower()}.parquet")
|
||||
d = pd.Series(dv["close"].values.astype(float), index=pd.to_datetime(dv["timestamp"], unit="ms", utc=True))
|
||||
J = pd.concat({"px": s, "dvol": d}, axis=1, join="inner").sort_index().dropna()
|
||||
px = J["px"].values
|
||||
dvf = J["dvol"].values / 100.0
|
||||
idx = J.index
|
||||
n = len(px)
|
||||
tn = tenor_d
|
||||
f, fee = CFG["f"], CFG["fee_frac"]
|
||||
rets, exits = {}, {}
|
||||
i = 60
|
||||
while i + tn < n:
|
||||
S0, sig0 = px[i], dvf[i]
|
||||
# --- gates d'ingresso identici a VRP01 (causali) ---
|
||||
skip = False
|
||||
if i >= 31:
|
||||
rv = np.std(np.diff(np.log(px[i - 30:i + 1]))) * np.sqrt(365.25)
|
||||
if (sig0 - rv) <= 0: # VRP>0
|
||||
skip = True
|
||||
if not skip and i >= 60:
|
||||
ivr = float((dvf[:i] < dvf[i]).mean()) # IV-rank espandente causale
|
||||
if ivr < CFG["gate_ivr"] or ivr > CFG["crash_skip"]:
|
||||
skip = True
|
||||
if skip:
|
||||
i += tn
|
||||
continue
|
||||
T0 = tn / 365.25
|
||||
Ks = _strike_from_delta(S0, T0, sig0, CFG["short_delta"])
|
||||
Kl = _strike_from_delta(S0, T0, sig0, CFG["long_delta"])
|
||||
net_prem = (_bs_put(S0, Ks, T0, sig0) - _bs_put(S0, Kl, T0, sig0)) * f
|
||||
if net_prem <= 0:
|
||||
i += tn
|
||||
continue
|
||||
reason, pnl, exit_j = None, None, i + tn
|
||||
if manage:
|
||||
for j in range(i + 1, i + tn): # giorni STRETTAMENTE prima della scadenza
|
||||
Trem = (i + tn - j) / 365.25
|
||||
Sj, sigj = px[j], dvf[j]
|
||||
sval = _bs_put(Sj, Ks, Trem, sigj) - _bs_put(Sj, Kl, Trem, sigj) # MTM dello spread
|
||||
if sval <= 0.5 * net_prem:
|
||||
reason, pnl, exit_j = "PT50", net_prem - sval, j; break
|
||||
if (sigj - sig0) >= 0.10: # VOL-STOP (la regola crypto nuova del doc)
|
||||
reason, pnl, exit_j = "VOLSTOP", net_prem - sval, j; break
|
||||
if full and sval >= 1.5 * net_prem:
|
||||
reason, pnl, exit_j = "SL150", net_prem - sval, j; break
|
||||
if full and _put_delta_mag(Sj, Ks, Trem, sigj) >= 0.30:
|
||||
reason, pnl, exit_j = "DELTA", net_prem - sval, j; break
|
||||
if full and (i + tn - j) <= 7:
|
||||
reason, pnl, exit_j = "TIME7", net_prem - sval, j; break
|
||||
if reason is None: # scadenza
|
||||
S1 = px[i + tn]
|
||||
payoff = max(0.0, Ks - S1) - max(0.0, Kl - S1)
|
||||
pnl, reason, exit_j = net_prem - payoff, "expiry", i + tn
|
||||
pnl -= fee * abs(net_prem) # fee d'ingresso (su entrambe le gambe via net_prem)
|
||||
if reason != "expiry":
|
||||
pnl -= fee * abs(net_prem) # fee di chiusura anticipata (ricompro lo spread)
|
||||
rets[idx[exit_j]] = pnl / Ks
|
||||
exits[reason] = exits.get(reason, 0) + 1
|
||||
i += tn
|
||||
return pd.Series(rets).sort_index(), exits
|
||||
|
||||
|
||||
def daily(series):
|
||||
if series.empty:
|
||||
return series
|
||||
days = pd.date_range(series.index.min().normalize(), series.index.max().normalize(), freq="1D", tz="UTC")
|
||||
out = pd.Series(0.0, index=days)
|
||||
out.loc[series.index.normalize()] = series.values
|
||||
return out
|
||||
|
||||
|
||||
def report(label, perTrade):
|
||||
dl = to_daily(daily(perTrade))
|
||||
m = metrics(dl)
|
||||
mh = metrics(dl[dl.index >= HOLDOUT])
|
||||
wins = float((perTrade > 0).mean()) * 100
|
||||
worst = float(perTrade.min()) * 100
|
||||
print(f" {label:<22s} n={len(perTrade):>3d} win={wins:>4.0f}% ret={m['ret']*100:>+6.0f}% "
|
||||
f"Sh={m['sharpe']:>5.2f} DD={m['maxdd']*100:>4.1f}% HOLD Sh={mh['sharpe']:>+5.2f} "
|
||||
f"worst-trade={worst:>+5.1f}%")
|
||||
return dl
|
||||
|
||||
|
||||
def main():
|
||||
print("=" * 100)
|
||||
print(" VRP01 hold-to-expiry vs GESTIONE ATTIVA (vol-stop DVOL+10, SL 1.5x, PT50, delta-exit, 7DTE)")
|
||||
print(" Stessi ingressi gated (VRP>0 + IV-rank>0.30), dati certificati, premio MODELLATO su DVOL (no skew)")
|
||||
print("=" * 100)
|
||||
combos = {}
|
||||
for asset in ("ETH", "BTC"):
|
||||
print(f"\n--- {asset} ---")
|
||||
report("VRP01 live (7d HtE)", simulate(asset, 7, "hte")[0]) # riferimento live
|
||||
# confronto equo a tenor 14 (range del doc), STESSI ingressi
|
||||
b14, _ = simulate(asset, 14, "hte")
|
||||
v14, exv = simulate(asset, 14, "volstop") # SOLO vol-stop (la regola nuova)
|
||||
m14, exm = simulate(asset, 14, "full") # tutte le regole del doc
|
||||
report("14d hold-to-expiry", b14)
|
||||
report("14d +vol-stop only", v14); print(f" exit volstop: {exv}")
|
||||
report("14d FULL managed", m14); print(f" exit full: {exm}")
|
||||
combos[asset] = dict(base14=daily(b14), vol14=daily(v14), man14=daily(m14))
|
||||
|
||||
# combo 50/50 BTC+ETH (come lo sleeve VRP01) — il confronto che conta per il portafoglio
|
||||
print("\n--- COMBO 50/50 BTC+ETH (sleeve-level) ---")
|
||||
for tag, key in (("14d hold-to-expiry", "base14"), ("14d +vol-stop only", "vol14"), ("14d FULL managed", "man14")):
|
||||
J = pd.concat({"B": combos["BTC"][key], "E": combos["ETH"][key]}, axis=1, join="outer").fillna(0.0)
|
||||
combo = to_daily(0.5 * J["B"] + 0.5 * J["E"])
|
||||
m, mh = metrics(combo), metrics(combo[combo.index >= HOLDOUT])
|
||||
print(f" {tag:<22s} Sh={m['sharpe']:>5.2f} DD={m['maxdd']*100:>4.1f}% ret={m['ret']*100:>+6.0f}% "
|
||||
f"HOLD Sh={mh['sharpe']:>+5.2f}")
|
||||
print("\n Lettura: la gestione attiva VALE se taglia maxDD e worst-trade SENZA distruggere Sharpe/ritorno.")
|
||||
print(" Caveat invariato: premio modellato su DVOL ATM (no skew) + nessun fill di stress reale -> LEAD, non deploy.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user