2 Commits

Author SHA1 Message Date
Adriano c6236ed5d9 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>
2026-06-20 11:24:40 +02:00
Adriano 75e1aacd19 research: analisi strategie FinanceOld + VRP v2 (defined-risk spread + gate IV-rank)
Analisi 4 progetti FinanceOld. Solo il filone opzioni-VRP backtestabile sui dati
certificati (funding-arb senza dati storici; Polybot ticks corrotti/3gg/edge=latenza).

VRP v2 porta 3 idee di OptionsAgent nel framework, causale + fee-aware:
- put credit spread (rischio definito): worst-week -16.6%->-7.4%, DD 33%->21%
- gate IV-rank>0.30: ribalta HOLD-OUT da -0.25 a +0.28 (alpha = filtro regime)
- COMBO f=1.0: FULL Sh 1.10, HOLD 0.60, DD 12%, positiva/piatta ogni anno
- blend TP01 70/30 -> Sh 1.00, DD 7% (corr +0.07)

Lead quantificato, non deploy (premio modellato ATM, serve f di stress reale).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 11:16:59 +02:00
5 changed files with 439 additions and 7 deletions
+16 -5
View File
@@ -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)
@@ -0,0 +1,93 @@
# 2026-06-20 — Analisi strategie FinanceOld + VRP v2 (defined-risk + gate IV-rank)
## Contesto
Richiesta: analizzare le strategie in `../FinanceOld`, provare a migliorarle, testarle su dati storici.
Quattro progetti esaminati. Verdetto di **backtestabilità onesta** sui dati certificati (BTC/ETH
Deribit mainnet + DVOL):
| Progetto | Strategia | Backtestabile sui dati certi? |
|---|---|---|
| **FundingRateArbitrage** | Spread funding cross-exchange (perp-perp, spot-hedge) | ❌ Nessun dato funding storico nel repo (solo `exchange_settings.json`). Edge = differenza cross-venue, non ricostruibile. |
| **Polybot** | Latency-arb Polymarket (BS digital-option) + sure-bet delta-neutral | ❌ `dataVPS/collector.db` (645MB) ha solo **~3 giorni** di `poly_books`+`funding`, e la tabella `ticks` (prezzi perp = cuore dell'edge) è **corrotta** ("database disk image is malformed"). L'edge è la latenza: non riproducibile su barre OHLC comunque. |
| **OptionSpalping** (→Cerbero) | LLM autonomo su opzioni Deribit + perp Hyperliquid | ⚠️ È un agente LLM, non una regola meccanica. Il *concetto* (income short-vol su Deribit) è testabile. |
| **OptionsAgent** | **Bear Call Spread + Long VIX hedge** su IWM, con 5 gate d'ingresso | ✅ Il *concetto* (vendi premio rischio-definito, incassa VRP, gate su IV-rank/regime) mappa direttamente sul nostro `options_vrp_lab.py`. |
→ Scelta operatore: **focus VRP opzioni**. L'unico filone con dati veri + metodologia onesta.
## Baseline (options_vrp_lab.py, ora con fee)
Vendita put NUDA settimanale delta -0.28, premio BS su DVOL reale. f = premio_reale/modellato.
- `f=1.0` (conservativo): **FULL Sh 0.78, DD 33%, worst-week -16.6%, HOLD-OUT Sh -0.25** → muore OOS.
- Il rischio è la **CODA**: worst-week su LUNA (2022-06), crash 2021-05. Anno 2022 = -9%.
## VRP v2 — 3 idee di OptionsAgent portate nel framework
Nuovo script `scripts/research/options_vrp_v2.py`. Tutto **causale** (strike/premio/gate da dati
≤ sell-date; payoff a scadenza sui prezzi certificati). Fee opzioni Deribit modellate (12.5% del
premio netto per round-trip = cap del fee reale). Capitale = strike corto (cash-secured) per
entrambe le strutture → DD/worst comparabili.
1. **Rischio definito (PUT CREDIT SPREAD)** — vendi put -0.28, COMPRI put -0.10. Il long wing
**cappa la coda per costruzione**: worst-week -16.6% → **-7.4%**, DD 33% → 21%, Sh 0.78 → 0.99.
2. **Gate IV-RANK > 0.30** (cond. d'ingresso di OptionsAgent) — vendi vol solo quando ricca
(percentile espandente causale di DVOL). Trada il **58%** delle settimane → **Sh 1.35** e
ribalta **HOLD-OUT da -0.25 a +0.28**. È l'alpha vero: il filtro di regime, non la struttura.
3. **Crash-skip IV-rank > 0.90** (NO-GO, come "VIX>35" di OptionsAgent) — marginale da solo.
4. **Gate VRP>0** (DVOL>RV30 causale) — marginale (il VRP è >0 il 78% del tempo, poco selettivo).
### Risultati chiave (book 50/50 BTC+ETH, f=1.0 conservativo)
| Config | FULL Sh | DD | worst-wk | HOLD-OUT Sh | attivo |
|---|---|---|---|---|---|
| naked (baseline) | 0.78 | 33% | -16.6% | **-0.25** | 100% |
| spread | 0.99 | 21% | -7.4% | -0.26 | 100% |
| spread + ivr30 | **1.35** | 14% | -7.4% | **+0.28** | 58% |
| **COMBO** (spread+vrp+ivr30+crashskip) | 1.10 | 12% | -7.4% | **+0.60** | 41% |
COMBO f=1.0 per-anno: 2021 +26%, 2022 **-6%**, 2023 +2%, 2024 +18%, 2025 -0%, 2026 +5%
(il 2022, anno-crash che dimezzava il nudo, è quasi piatto: la coda è tagliata).
A `f=1.29` (skew reale misurato in regime calmo) la COMBO fa FULL Sh 1.87 / HOLD 1.45 / DD 9%.
### Contributo al portafoglio (COMBO f=1.0 vs TP01)
- Corr settimanale **+0.07** (scorrelato, come il VRP nudo).
- TP01 70% + OPT 30% → Sh **1.00** (TP01 solo 0.73), DD **7%**.
- TP01 50% + OPT 50% → Sh **1.19**, DD 7%.
## Conclusione onesta
Le idee di OptionsAgent **migliorano davvero** lo sleeve VRP, in modo OOS-robusto:
- la **struttura defined-risk** taglia la coda (worst -16.6%→-7.4%, DD -19pt) → meno dipendenza dal
f di stress, che era il rischio non catturato del lead nudo;
- il **gate IV-rank** è l'alpha: ribalta l'HOLD-OUT da negativo a positivo vendendo solo vol ricca.
Resta un **lead, non un deploy**: premio MODELLATO su DVOL ATM (skew non esplicito), book a 1d, e
serve la catena reale (cerbero-bite) per il f di stress in un crash. Ma è un miglioramento netto,
quantificato e onesto, del miglior lead income che avevamo. Prossimo passo: rivalutare il f di stress
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%).
+180
View File
@@ -0,0 +1,180 @@
"""OPTIONS VRP v2 — migliora lo sleeve short-vol con le idee di FinanceOld/OptionsAgent.
Baseline (options_vrp_lab): vendita put NUDA settimanale delta -0.28, premio BS su DVOL reale.
f=1.0 -> Sh 0.71, DD 33%, worst-week -26%, HOLD-OUT Sh 0.04 (muore OOS). Il rischio e' la CODA.
OptionsAgent (Bear Call Spread + VIX hedge su IWM) porta 3 idee testabili qui:
(A) RISCHIO DEFINITO: invece della put nuda, PUT CREDIT SPREAD (vendi put delta -0.28, COMPRI put
piu' OTM delta -0.10). Cap la coda: max perdita = width - premio netto. Capitale = width (margine
reale di un defined-risk). Lo Sharpe e' scale-free; DD/worst-week sul width (capitale vero a rischio).
(B) GATE VRP/IV-RANK: vendi vol SOLO quando e' ricca. Gate causale su:
- vrp: DVOL[i] - RV30(causale) > 0 (premio > vol realizzata recente)
- ivr: IV-rank = percentile espandente di DVOL[i] in DVOL[:i] > soglia
"Solo se IV Rank > 30%" e' una delle 5 condizioni d'ingresso di OptionsAgent.
(C) CRASH-SKIP: vai flat se DVOL gia' esploso sopra un percentile alto (vol-spike = NO-GO, come
"VIX>35 -> NO-GO" di OptionsAgent). Evita di vendere nel pieno del crash.
Tutto CAUSALE: strike/premio/gate usano solo dati <= sell-date; payoff realizza a scadenza sui prezzi
certificati. Fee Deribit opzioni: 0.03% del NOTIONAL per gamba (cap 12.5% del premio) -> qui modellate
come costo per-trade sul premio. NON deploy: lead quantificato e onesto.
uv run python scripts/research/options_vrp_v2.py
"""
from __future__ import annotations
import sys
from pathlib import Path
PROJECT_ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(PROJECT_ROOT))
import numpy as np, pandas as pd
from scripts.research.options_vrp_lab import bs_put, strike_from_delta, load_series, m_weekly, per_year
HOLDOUT = pd.Timestamp("2025-01-01", tz="UTC")
WK_PER_YEAR = 365.25 / 7.0
# fee Deribit opzioni: 0.0003 * spot per contratto, cap 12.5% del premio. Per uno spread sono 2 gambe.
# Modellata come frazione del premio netto incassato (conservativa: usa il cap come ordine di grandezza).
FEE_FRAC_OF_PREMIUM = 0.125 # 12.5% del premio netto, per ROUND-TRIP delle gambe (worst-case del cap)
def _rv30(px: np.ndarray, i: int) -> float:
"""RV annualizzata causale dagli ultimi 30 rendimenti giornalieri (fino a i incluso)."""
if i < 31:
return np.nan
r = np.diff(np.log(px[i - 30:i + 1]))
return float(np.std(r) * np.sqrt(365.25))
def _ivrank(dv: np.ndarray, i: int) -> float:
"""IV-rank causale: percentile di dv[i] nella storia espandente dv[:i]."""
if i < 60:
return np.nan
hist = dv[:i]
return float((hist < dv[i]).mean())
def vrp_spread_weekly(asset, short_delta=-0.28, long_delta=-0.10, f=1.0, tenor_d=7,
defined_risk=True, gate_vrp=False, gate_ivr=0.0, crash_skip=1.01,
with_fee=True):
"""Vendita settimanale di put credit spread (o nuda se defined_risk=False), con gate causali.
Ritorna serie di rendimenti settimanali su CAPITALE A RISCHIO (width per lo spread, K per la nuda)."""
J = load_series(asset)
px = J["px"].values; dv_pct = J["dvol"].values; dv = dv_pct / 100.0; idx = J.index
n = len(px); T = tenor_d / 365.25
rets = {}
i = 60 # serve storia per RV/IV-rank
while i + tenor_d < n:
S0 = px[i]; sig = dv[i]
# --- GATE causali (decisi a sell-date) ---
skip = False
if gate_vrp:
rv = _rv30(px, i)
if not np.isnan(rv) and (sig - rv) <= 0: # VRP non positivo -> non vendere
skip = True
if gate_ivr > 0:
ivr = _ivrank(dv, i)
if not np.isnan(ivr) and ivr < gate_ivr: # IV troppo bassa -> non vendere
skip = True
if crash_skip < 1.0:
ivr = _ivrank(dv, i)
if not np.isnan(ivr) and ivr > crash_skip: # vol gia' esplosa -> NO-GO
skip = True
if skip:
rets[idx[i + tenor_d]] = 0.0 # flat: nessun rischio quella settimana
i += tenor_d
continue
# --- struttura ---
Ks = strike_from_delta(S0, T, sig, short_delta) # put venduta
prem_s = bs_put(S0, Ks, T, sig) * f
S1 = px[i + tenor_d]
if defined_risk:
Kl = strike_from_delta(S0, T, sig, long_delta) # put comprata (piu' OTM, strike piu' basso)
prem_l = bs_put(S0, Kl, T, sig) * f
net_prem = prem_s - prem_l
width = Ks - Kl
payoff = max(0.0, Ks - S1) - max(0.0, Kl - S1) # quanto pago netto a scadenza
pnl = net_prem - payoff
cap = Ks # cash-secured: stesso capitale del baseline nudo -> DD/worst comparabili,
# il long wing CAPPA la coda (la differenza dal nudo e' solo la coda tagliata)
else:
pnl = prem_s - max(0.0, Ks - S1)
cap = Ks
net_prem = prem_s
if with_fee:
pnl -= FEE_FRAC_OF_PREMIUM * abs(net_prem)
rets[idx[i + tenor_d]] = pnl / cap
i += tenor_d
return pd.Series(rets)
def book(fn, **kw):
rB = fn("BTC", **kw); rE = fn("ETH", **kw)
return pd.concat({"B": rB, "E": rE}, axis=1, join="inner").mean(axis=1)
def report(name, b):
full = m_weekly(b); ho = m_weekly(b[b.index >= HOLDOUT])
worst = b.min(); active = float((b != 0).mean())
py = per_year(b)
print(f" {name:<34} FULL Sh {full['sh']:>5.2f} CAGR {full['cagr']*100:>+4.0f}% DD {full['dd']*100:>3.0f}% "
f"worst {worst*100:>+5.1f}% | HOLD Sh {ho['sh']:>5.2f} | attivo {active*100:>3.0f}%")
return full, ho, py
def main():
print("=" * 104)
print(" OPTIONS VRP v2 — defined-risk spread + gate VRP/IV-rank + crash-skip (idee OptionsAgent)")
print("=" * 104)
print(" Fee opzioni Deribit modellate: 12.5%% del premio netto per round-trip (cap del fee reale).\n")
print(" (0) BASELINE — put NUDA delta -0.28 (riproduce options_vrp_lab, ora CON fee)")
report("naked f=1.0 (no gate)", book(vrp_spread_weekly, defined_risk=False, f=1.0))
report("naked f=1.29 (reale-calm)", book(vrp_spread_weekly, defined_risk=False, f=1.29))
print("\n (1) RISCHIO DEFINITO — put credit spread -0.28/-0.10 (cap coda), capitale=width")
for f in (1.0, 1.29):
report(f"spread f={f}", book(vrp_spread_weekly, defined_risk=True, f=f))
print("\n (2) + GATE VRP>0 (vendi solo se DVOL>RV30 causale)")
for f in (1.0, 1.29):
report(f"spread+vrp f={f}", book(vrp_spread_weekly, defined_risk=True, f=f, gate_vrp=True))
print("\n (3) + GATE IV-RANK > 0.30 (vendi solo vol ricca; cond. d'ingresso OptionsAgent)")
for f in (1.0, 1.29):
report(f"spread+ivr30 f={f}", book(vrp_spread_weekly, defined_risk=True, f=f, gate_ivr=0.30))
print("\n (4) + CRASH-SKIP IV-rank>0.90 (NO-GO se vol gia' esplosa)")
for f in (1.0, 1.29):
report(f"spread+crashskip f={f}", book(vrp_spread_weekly, defined_risk=True, f=f, crash_skip=0.90))
print("\n (5) COMBO — spread + vrp + ivr30 + crash-skip (tutti i filtri, f=1.0 conservativo)")
full, ho, py = report("COMBO f=1.0", book(vrp_spread_weekly, defined_risk=True, f=1.0,
gate_vrp=True, gate_ivr=0.30, crash_skip=0.90))
print(" per-anno: " + " ".join(f"{y}:{v*100:+.0f}%" for y, v in py.items()))
full, ho, py = report("COMBO f=1.29", book(vrp_spread_weekly, defined_risk=True, f=1.29,
gate_vrp=True, gate_ivr=0.30, crash_skip=0.90))
print(" per-anno: " + " ".join(f"{y}:{v*100:+.0f}%" for y, v in py.items()))
# contributo al portafoglio TP01
print("\n (6) CORRELAZIONE + CONTRIBUTO vs TP01 (COMBO f=1.0)")
from src.portfolio.sleeves import tp01_sleeve
tp = tp01_sleeve().daily()
tp_wk = (1 + tp).resample("7D").prod() - 1
opt = book(vrp_spread_weekly, defined_risk=True, f=1.0, gate_vrp=True, gate_ivr=0.30, crash_skip=0.90)
opt_wk = opt.copy(); opt_wk.index = opt_wk.index.to_period("W").to_timestamp()
tp_wk2 = tp_wk.copy(); tp_wk2.index = tp_wk2.index.to_period("W").to_timestamp()
Jc = pd.concat({"tp": tp_wk2, "opt": opt_wk}, axis=1, join="inner").dropna()
corr = float(Jc["tp"].corr(Jc["opt"])) if len(Jc) > 5 else float("nan")
print(f" corr settimanale opt vs TP01 = {corr:+.2f}")
for w in (0.3, 0.5):
comb = (1 - w) * Jc["tp"] + w * Jc["opt"]
mt = m_weekly(Jc["tp"]); mc = m_weekly(comb)
print(f" TP01 {1-w:.0%} + OPT {w:.0%}: Sh {mc['sh']:.2f} (TP01-solo {mt['sh']:.2f}) DD {mc['dd']*100:.0f}%")
print("\n NB onesto: capitale=strike corto (cash-secured) per entrambe -> DD/worst comparabili al nudo.")
print(" Il defined-risk CAPPA la coda (-16.6%->-7.4% worst, DD 33%->14-21%) RIDUCENDO la dipendenza")
print(" dal f di stress (la coda e' tagliata per costruzione). Il gate IV-rank e' l'alpha: vendere")
print(" solo vol ricca (58%% delle settimane) ribalta l'HOLD-OUT da -0.25 a +0.28 (f=1.0). Premio")
print(" MODELLATO su DVOL ATM (no skew). Lead quantificato, non deploy (serve catena reale + f di stress).")
if __name__ == "__main__":
main()
+89 -2
View File
@@ -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)
]
+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%