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:
@@ -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%
|
||||
Reference in New Issue
Block a user