From c6236ed5d9a194934309655d63898634e0fb84d6 Mon Sep 17 00:00:00 2001 From: AdrianoDev Date: Sat, 20 Jun 2026 11:24:40 +0200 Subject: [PATCH] feat: integra VRP01 come sleeve del portafoglio (put credit spread + gate IV-rank) src/portfolio/sleeves.py: _vrp_combo_returns + vrp_sleeve, self-contained in src/ (pricing BS + gate causali inline, DVOL da data/raw). Settimanale->giornaliero col lump sul giorno di scadenza (preserva lo Sharpe annualizzato, peso costante). Registry: TP01 0.55 / XS01 0.25 / VRP01 0.20 (TP01 resta maggioranza; VRP e' un lead modellato, non deploy pieno). TP01+VRP01 monotono: FULL 1.30->1.44, HOLD 0.31->0.40 a peso 20%. Scorrelato a TP01 (+0.01). Test tests/test_vrp_sleeve.py (5 pass). CLAUDE.md + diario aggiornati. Co-Authored-By: Claude Opus 4.8 (1M context) --- CLAUDE.md | 21 ++++- .../2026-06-20-financeold-analysis-vrp-v2.md | 21 +++++ src/portfolio/sleeves.py | 91 ++++++++++++++++++- tests/test_vrp_sleeve.py | 61 +++++++++++++ 4 files changed, 187 insertions(+), 7 deletions(-) create mode 100644 tests/test_vrp_sleeve.py diff --git a/CLAUDE.md b/CLAUDE.md index 1af6f24..9e2e5b5 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -45,10 +45,21 @@ Prima ondata di ricerca onesta su BTC/ETH certificati (5 track, harness condivis monitor forward. NB il gate concentra XS nei regimi dispersi (2025-26 = hold-out alta-dispersione). Ricerca `scripts/portfolio/{xsec_research,xsec_blend,xsec_dispgate}.py`. Diari `2026-06-19-hyperliquid-xsec` / `-xsec-blend` / `-xsec-dispgate` / `-xsec-universe-expansion` / `-trend-multiasset`. -- **PORTAFOGLIO ATTIVO = TP01 (70%) + XS01 (30%)** (`src/portfolio/sleeves.active_sleeves`): - combinato **FULL Sharpe 1.55, HOLD-OUT 1.55, DD 4.4%**, positivo quasi ogni anno, ~€1.9/g su 2k. - Report `scripts/portfolio/run_portfolio.py`. Sleeve a date d'inizio diverse → outer-join con pesi - rinormalizzati (TP01 da solo 2019-23, blend dal 2024). +- **PORTAFOGLIO ATTIVO = TP01 (55%) + XS01 (25%) + VRP01 (20%)** (`src/portfolio/sleeves.active_sleeves`): + TP01+XS01 combinato **FULL Sharpe 1.55, HOLD-OUT 1.55, DD 4.4%**. Aggiunto **VRP01** (options + short-vol, sotto): TP01+VRP01 da solo fa FULL Sh 1.30→1.44 / HOLD 0.31→0.40 a peso 20% (3-way da + validare locale con dati HL). Report `scripts/portfolio/run_portfolio.py`. Sleeve a date d'inizio + diverse → outer-join con pesi rinormalizzati (TP01 da solo 2019-20, VRP dal 2021, blend pieno dal 2024). +- **VRP01 Options Short-Vol — DIVERSIFICATORE da FinanceOld/OptionsAgent** — `src/portfolio/sleeves._vrp_combo_returns`. + Put credit spread settimanale (vendi put -0.28, compra put -0.10) gated su IV-rank. Idee portate da + `../FinanceOld/OptionsAgent` (Bear Call Spread + gate d'ingresso). Migliora il lead VRP nudo + (options_vrp_lab): **(a) defined-risk** taglia la coda (worst-week -16.6%→-7.4%, DD 33%→14%); + **(b) gate IV-rank>0.30** = vendi vol solo ricca → ribalta HOLD-OUT da -0.25 a +0.28 (l'alpha è il + filtro di regime). Standalone **FULL Sh 1.10, HOLD 0.60, DD 12%**, positivo/piatto ogni anno (2022 + crash incluso). Scorrelato a TP01 (~+0.01-0.07). **CAVEAT:** premio MODELLATO su DVOL ATM (skew non + esplicito), book a 1d, f di stress reale non catturato → LEAD robusto, non deploy pieno. Ricerca + `scripts/research/options_vrp_v2.py` (vs baseline `options_vrp_lab.py`). Test `tests/test_vrp_sleeve.py`. + Diario `2026-06-20-financeold-analysis-vrp-v2.md`. - **Universo Hyperliquid: ESPANDERLO NON aiuta XS01** (provato): 52-asset / top-liquidità dinamico / trend-multi-asset → tutti peggiori (small-cap/memecoin diluiscono il momentum relativo; il trend multi-asset è ridondante con TP01, corr 0.74). I margini su XS sono nella STRUTTURA DEL SEGNALE @@ -94,7 +105,7 @@ src/strategies/indicators.py → indicatori condivisi (ema, atr, keltner, ...) src/strategies/trend_portfolio.py → TP01: strategia DIFENSIVA robusta (PORT LF1d, >=12h), causale src/portfolio/ → PORTAFOGLIO DI STRATEGIE estensibile (Sleeve + StrategyPortfolio) portfolio.py → combina N sleeve per peso su griglia giornaliera; metriche FULL/hold-out/anno - sleeves.py → REGISTRY sleeve attivi: TP01 (70%) + XS01 (30%). Aggiungere = una riga + sleeves.py → REGISTRY sleeve attivi: TP01 (55%) + XS01 (25%) + VRP01 (20%). Aggiungere = una riga src/fractal/ → indicatori frattali (patterns.py, indicators.py, similarity.py) src/backtest/engine.py → engine di backtesting riusabile src/backtest/harness.py → harness ONESTO (load BTC/ETH, backtest_signals no-leakage, OOS) diff --git a/docs/diary/2026-06-20-financeold-analysis-vrp-v2.md b/docs/diary/2026-06-20-financeold-analysis-vrp-v2.md index fa96fff..e8911a7 100644 --- a/docs/diary/2026-06-20-financeold-analysis-vrp-v2.md +++ b/docs/diary/2026-06-20-financeold-analysis-vrp-v2.md @@ -70,3 +70,24 @@ quantificato e onesto, del miglior lead income che avevamo. Prossimo passo: riva quando cerbero-bite cattura un crash, e validare lo skew reale sul long wing (-0.10). Script: `scripts/research/options_vrp_v2.py`. Baseline: `scripts/research/options_vrp_lab.py`. + +## Integrazione come sleeve (VRP01) + +La COMBO è stata integrata nel portafoglio come **VRP01** (`src/portfolio/sleeves._vrp_combo_returns`, +`vrp_sleeve()`). Implementazione self-contained in `src/` (niente import da `scripts/`): pricing BS + +strike-from-delta + gate causali inline, DVOL da `data/raw/dvol_*.parquet`. + +**Settimanale → giornaliero (onesto):** il rendimento settimanale è piazzato sul **giorno di +scadenza**, 0.0 sugli altri giorni dello span. Questo PRESERVA lo Sharpe annualizzato (niente +smoothing che gonfierebbe il daily Sharpe) e tiene lo sleeve presente ogni giorno → peso costante +nell'outer-join del portafoglio. Verificato: lo sleeve daily replica i numeri settimanali +(FULL Sh 1.09, HOLD 0.60, DD 12%), corr daily vs TP01 = +0.01. + +**Pesi (per evidenza, engine reale):** TP01+VRP01 monotòno fino al 40% VRP (FULL 1.30→1.55, +HOLD 0.31→0.52, DD fermo 14%). Essendo VRP un lead MODELLATO (non deploy pieno), non lo sovrappeso: +registry = **TP01 0.55 / XS01 0.25 / VRP01 0.20** (TP01 resta maggioranza, l'unico deployable pieno). +La validazione 3-way completa richiede i dati Hyperliquid (XS01, gitignored, token Cerbero) → gira +locale con `scripts/portfolio/run_portfolio.py`. + +Test: `tests/test_vrp_sleeve.py` (5 pass: monotonìa BS, ordering strike, determinismo+griglia +giornaliera, gate riducono l'attività, coda tagliata <-15%). diff --git a/src/portfolio/sleeves.py b/src/portfolio/sleeves.py index fd50c65..fb4537a 100644 --- a/src/portfolio/sleeves.py +++ b/src/portfolio/sleeves.py @@ -121,11 +121,98 @@ def xsec_sleeve(weight: float = 0.3) -> Sleeve: return Sleeve("XS01_xsec_hl", weight, _xsec_returns) +# ----------------------------- VRP01: Options Short-Vol (put credit spread + gate IV-rank) ----------------------------- +# Income short-vol settimanale. Idee da FinanceOld/OptionsAgent (Bear Call Spread + gate d'ingresso) +# portate sul VRP: (a) PUT CREDIT SPREAD rischio-definito (vendi put -0.28, compra put -0.10) che +# CAPPA la coda (worst-week -16.6%->-7.4%, DD 33%->14%); (b) GATE IV-RANK>0.30 causale = vendi vol solo +# quando ricca -> ribalta l'HOLD-OUT da -0.25 a +0.28 (e' l'alpha); (c) crash-skip IV-rank>0.90. +# Premio BS su DVOL reale (data/raw/dvol_*.parquet via scripts/research/fetch_dvol.py), payoff sul path +# certificato, fee opzioni Deribit (cap 12.5% del premio). CAVEAT ONESTI: premio MODELLATO su IV-ATM +# (skew non esplicito), book a 1d, f di stress reale non catturato -> e' un LEAD robusto, non deploy +# pieno. Scorrelato a TP01 (~+0.07). Ricerca: scripts/research/options_vrp_v2.py. +# Diario 2026-06-20-financeold-analysis-vrp-v2.md. +from scipy.stats import norm as _norm + +VRP_CFG = dict(short_delta=-0.28, long_delta=-0.10, f=1.0, tenor_d=7, + gate_ivr=0.30, crash_skip=0.90, gate_vrp=True, fee_frac=0.125) + + +def _bs_put(S, K, T, sig): + if T <= 0 or sig <= 0: + return max(K - S, 0.0) + d1 = (np.log(S / K) + 0.5 * sig ** 2 * T) / (sig * np.sqrt(T)) + return K * _norm.cdf(-(d1 - sig * np.sqrt(T))) - S * _norm.cdf(-d1) # r=0 + + +def _strike_from_delta(S, T, sig, target_delta): + return S * np.exp(0.5 * sig ** 2 * T - (-_norm.ppf(-target_delta)) * sig * np.sqrt(T)) + + +def _vrp_weekly_asset(asset: str) -> pd.Series: + """Put credit spread settimanale con gate causali. Ritorna rendimenti SETTIMANALI (su collaterale + = strike corto, cash-secured) indicizzati alla data di scadenza. Causale: strike/premio/gate da + dati <= sell-date; payoff a scadenza sui prezzi certificati.""" + 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); cfg = VRP_CFG; tn = cfg["tenor_d"]; T = tn / 365.25 + rets = {} + i = 60 + while i + tn < n: + S0 = px[i]; sig = dvf[i] + skip = False + if cfg["gate_vrp"] and i >= 31: # VRP>0: DVOL > RV30 causale + rv = np.std(np.diff(np.log(px[i - 30:i + 1]))) * np.sqrt(365.25) + if (sig - rv) <= 0: + skip = True + if not skip and (cfg["gate_ivr"] > 0 or cfg["crash_skip"] < 1.0) and i >= 60: + ivr = float((dvf[:i] < dvf[i]).mean()) # IV-rank espandente causale + if cfg["gate_ivr"] > 0 and ivr < cfg["gate_ivr"]: + skip = True + if cfg["crash_skip"] < 1.0 and ivr > cfg["crash_skip"]: + skip = True + if skip: + rets[idx[i + tn]] = 0.0; i += tn; continue + Ks = _strike_from_delta(S0, T, sig, cfg["short_delta"]) + Kl = _strike_from_delta(S0, T, sig, cfg["long_delta"]) + net_prem = (_bs_put(S0, Ks, T, sig) - _bs_put(S0, Kl, T, sig)) * cfg["f"] + S1 = px[i + tn] + payoff = max(0.0, Ks - S1) - max(0.0, Kl - S1) + pnl = net_prem - payoff - cfg["fee_frac"] * abs(net_prem) + rets[idx[i + tn]] = pnl / Ks + i += tn + return pd.Series(rets) + + +def _vrp_combo_returns() -> pd.Series: + """Sleeve VRP01: book 50/50 BTC+ETH del put credit spread gated, su griglia GIORNALIERA. + Il rendimento settimanale e' piazzato sul giorno di scadenza, 0.0 sugli altri giorni (preserva + lo Sharpe annualizzato senza smoothing): cosi' lo sleeve e' presente ogni giorno (peso costante).""" + rB = _vrp_weekly_asset("BTC"); rE = _vrp_weekly_asset("ETH") + wk = pd.concat({"B": rB, "E": rE}, axis=1, join="inner").mean(axis=1).sort_index() + if wk.empty: + return wk + days = pd.date_range(wk.index.min().normalize(), wk.index.max().normalize(), freq="1D", tz="UTC") + daily = pd.Series(0.0, index=days) + daily.loc[wk.index.normalize()] = wk.values # lump settimanale sul giorno scadenza + return daily + + +def vrp_sleeve(weight: float = 0.20) -> Sleeve: + return Sleeve("VRP01_shortvol", weight, _vrp_combo_returns) + + # ----------------------------- REGISTRY ----------------------------- def active_sleeves() -> list[Sleeve]: """Sleeve ATTIVI nel portafoglio (pesi rinormalizzati; sleeve a date diverse si attivano quando parte la loro storia). Aggiungere qui SOLO strategie validate col gauntlet.""" return [ - tp01_sleeve(weight=0.70), # trend difensivo, BTC/ETH, dal 2019 - xsec_sleeve(weight=0.30), # cross-sectional momentum Hyperliquid, dal 2024 (scorrelato, stat-mode) + tp01_sleeve(weight=0.55), # trend difensivo, BTC/ETH, dal 2019 (l'unico deployable pieno) + xsec_sleeve(weight=0.25), # cross-sectional momentum Hyperliquid, dal 2024 (scorrelato, stat-mode) + vrp_sleeve(weight=0.20), # options short-vol (put credit spread + gate IV-rank), dal 2021 (lead modellato, scorrelato) ] diff --git a/tests/test_vrp_sleeve.py b/tests/test_vrp_sleeve.py new file mode 100644 index 0000000..0ca61e1 --- /dev/null +++ b/tests/test_vrp_sleeve.py @@ -0,0 +1,61 @@ +"""Test dello sleeve VRP01 (options short-vol: put credit spread + gate IV-rank).""" +import sys +from pathlib import Path + +import numpy as np +import pandas as pd +import pytest + +PROJECT_ROOT = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(PROJECT_ROOT)) + +from src.portfolio.sleeves import ( + _bs_put, _strike_from_delta, _vrp_combo_returns, vrp_sleeve, _HL_DIR) + +_HAS_DVOL = (_HL_DIR / "dvol_btc.parquet").exists() and (_HL_DIR / "dvol_eth.parquet").exists() +_skip_data = pytest.mark.skipif(not _HAS_DVOL, reason="serve data/raw/dvol_*.parquet (scripts/research/fetch_dvol.py)") + + +def test_bs_put_monotonic_in_strike(): + """Put piu' ITM (strike piu' alto) vale di piu'.""" + S, T, sig = 100.0, 7 / 365.25, 0.6 + vals = [_bs_put(S, K, T, sig) for K in (80, 90, 100, 110)] + assert all(b < a for b, a in zip(vals, vals[1:])) # crescente nello strike + + +def test_strike_from_delta_ordering(): + """La put venduta delta -0.28 ha strike piu' alto (piu' vicino) della comprata -0.10.""" + S, T, sig = 100.0, 7 / 365.25, 0.6 + Ks = _strike_from_delta(S, T, sig, -0.28) + Kl = _strike_from_delta(S, T, sig, -0.10) + assert Kl < Ks < S # entrambe OTM, long piu' lontana + + +@_skip_data +def test_sleeve_is_deterministic_and_daily(): + a = vrp_sleeve().daily() + b = _vrp_combo_returns() + assert isinstance(a.index, pd.DatetimeIndex) and a.index.tz is not None + assert (a.index.normalize() == a.index).all() # griglia giornaliera + # presente ogni giorno nel suo span (nessun buco) -> peso costante nel portafoglio + full = pd.date_range(a.index.min(), a.index.max(), freq="1D", tz="UTC") + assert len(a) == len(full) + np.testing.assert_array_equal(a.values, vrp_sleeve().daily().values) # deterministico + + +@_skip_data +def test_gates_reduce_activity(): + """I gate (IV-rank/VRP/crash-skip) devono lasciare flat parte delle settimane: i giorni con + rendimento != 0 sono molto meno del totale (lump settimanale + settimane saltate).""" + s = _vrp_combo_returns() + active = float((s != 0).mean()) + assert 0.0 < active < 0.25 # ~1/7 (lump weekly) e meno per i gate + + +@_skip_data +def test_sleeve_positive_and_capped_tail(): + """Lo sleeve e' profittevole e la coda e' tagliata dal long wing (worst-day moderato).""" + s = _vrp_combo_returns() + nz = s[s != 0] + assert s.sum() > 0 # somma rendimenti positiva + assert nz.min() > -0.15 # defined-risk: nessuna settimana < -15%