c6236ed5d9
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>
62 lines
2.5 KiB
Python
62 lines
2.5 KiB
Python
"""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%
|