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) <noreply@anthropic.com>
This commit is contained in:
2026-06-20 11:24:40 +02:00
parent 75e1aacd19
commit c6236ed5d9
4 changed files with 187 additions and 7 deletions
+61
View File
@@ -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%