research(vrp): profit-take al 50% REFUTED, e una correzione a un numero di stamattina

Book, pesi, cron, config INVARIATI.

VRP01 tiene fino a scadenza (S1 = px[i+tn], nessuna gestione infra-settimana)
e il profit-take era l'unico grado di liberta' non misurato: i 4 overlay del
03/07 erano tutti sul lato del RISCHIO, questo e' sul lato del PROFITTO.
Replica del sleeve bit-exact (max|diff| = 0.0) prima di ogni delta.

VERDETTO con fee reali per gamba: canonico ShFULL 1.32 -> PT25 0.22 /
PT50 -0.18 / PT75 -0.45. Null del de-levering REFUTED 6/6: a PT25 basta
k=0.818 per lo stesso DD con Sharpe 1.32 invece di 0.22.

MECCANISMO (confronto appaiato per ingresso): scatta sull'86-91% dei VINCENTI
e sul 19-25% dei perdenti -> tronca i vincenti del 27% e salva un perdente su
cinque. La peggior settimana e' IDENTICA (-7.27%) in ogni variante: nelle
settimane brutte lo spread non tocca mai +50%, quindi e' pura troncatura
dell'upside.

IPOTESI MIA REFUTATA IN SESSIONE: 'su 7g chi tocca +50% e' chi sarebbe scaduto
senza valore, quindi a scadenze lunghe paga'. Testata su 7/10/14/18/21/28g:
delta Sharpe negativo a tutti e sei. A 18g (il tenore di cerbero-bite) il DD
migliora 7.1->3.8% ma lo Sharpe crolla = firma del de-levering.

CORREZIONE a un numero pubblicato oggi: il sleeve modella le fee come 12.5%
del credito netto, il listino vero e' 0.03% del sottostante per gamba (cap
12.5% del premio della singola opzione, che quasi mai morde) -> sovrastima di
~2x. Il numero onesto di VRP01 con f=0.73 e' ShFULL 0.47, non 0.31.
fee_frac NON cambiato: il forfait e' conservativo e i numeri di ammissione di
questo progetto si tengono conservativi.

A $3.000 la domanda non e' il rendimento: BTC min 0.1 = $6.210 di collaterale
per lotto -> FUORI; ETH min 1 contratto = $1.832 -> 1 lotto. Il sleeve 50/50
diventa ETH-only e al peso di book (12% = $360) sono 0 lotti.

REGOLE: (a) un'uscita si giudica appaiata per INGRESSO, mai allineando le serie
sulla data di USCITA — e' quella che la variante cambia, e l'inner-join tiene
solo i casi in cui non e' successo niente (errore commesso e corretto in
sessione, congelato in un test); (b) una regola d'uscita che scatta piu' spesso
sui vincenti che sui perdenti non e' protezione, e' troncatura; (c) prima del
rendimento a un capitale dato, misurare il lotto minimo del venue.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Adriano Dal Pastro
2026-07-30 21:57:15 +00:00
parent a2f28153d8
commit 04cb572535
4 changed files with 815 additions and 0 deletions
+35
View File
@@ -169,6 +169,41 @@ Prima ondata di ricerca onesta su BTC/ETH certificati (5 track, harness condivis
(qui 1.02, implementazione indipendente). **REGOLA: il f di una struttura multi-gamba non è il f (qui 1.02, implementazione indipendente). **REGOLA: il f di una struttura multi-gamba non è il f
di una sua gamba — misurare la sola gamba venduta dà la risposta sbagliata con segno di una sua gamba — misurare la sola gamba venduta dà la risposta sbagliata con segno
rassicurante.** Congelata in `tests/test_cb_chain_vrp.py`. rassicurante.** Congelata in `tests/test_cb_chain_vrp.py`.
**PROFIT-TAKE al 50% del credito — REFUTED (2026-07-30, 3° filone).**
`scripts/research/r0730_vrp_profit_take.py`, test `tests/test_vrp_profit_take.py` (16), diario
`2026-07-30-vrp-profit-take.md`. **Book/pesi/cron/config INVARIATI.** VRP01 tiene FINO A
SCADENZA (`S1 = px[i+tn]`, nessuna gestione infra-settimana) e il profit-take era l'unico grado
di liberta' non misurato: gli overlay del 03/07 erano tutti sul lato del RISCHIO. Replica del
sleeve **bit-exact** (max|diff| = 0.0) prima di ogni delta. Esito con **fee reali per gamba**:
canonico ShFULL **1.32** → PT25 0.22 / **PT50 0.18** / PT75 0.45; **null del de-levering
REFUTED 6/6** (a PT25 basta k=0.818 per lo stesso DD con Sharpe 1.32 invece di 0.22).
**Meccanismo (confronto appaiato per ingresso):** scatta sull'**86-91% dei VINCENTI** e sul
**19-25% dei perdenti** → tronca i vincenti del **27%** e salva un perdente su cinque; la
**peggior settimana e' IDENTICA** (7.27%) in ogni variante = pura troncatura dell'upside.
**Ipotesi mia refutata in sessione:** "su 7g chi tocca +50% e' chi sarebbe scaduto senza
valore, quindi a scadenze lunghe paga" — testata su 7/10/14/18/21/28g, **ΔSh negativo a tutti e
sei** (a 18g, il tenore di cerbero-bite, il DD migliora 7.1→3.8% ma lo Sharpe crolla = firma del
de-levering). ⚠️ La colonna `Sh hold` di quello sweep **NON e' un risultato** (6 celle sul
campione pieno); il tenore >10g resta un buco reale della griglia 03/07, da chiudere con
`study_family_honest`, non con uno sweep. **Con questo gli overlay refutati su VRP01 sono 5/5.**
⚠️ **CORREZIONE a un numero pubblicato lo stesso giorno:** il sleeve modella le fee come 12.5%
del credito NETTO, il listino vero e' **0.03% del sottostante per gamba** (cap 12.5% del premio
della singola opzione, che quasi mai morde) → il modello **sovrastima le fee di ~2x**. Sleeve
ufficiale 1.08/0.58/DD 11.8% vs fee reali **1.32/0.82/DD 10.5%**. Quindi il numero onesto di
VRP01 con **f=0.73** e' **ShFULL 0.47 / DD 14.5%, non 0.31** (che era calcolato col forfait).
`fee_frac` **NON cambiato**: il forfait e' conservativo e questo progetto tiene i numeri di
ammissione conservativi. **Rientro immediato** dopo l'uscita: Sh 1.19 < 1.32 canonico; il suo
ShHOLD 2.42 non e' selezionabile in-sample → **non e' un lead**.
💰 **A $3.000 la domanda non e' il rendimento** (`min_trade_amount` letti dal venue il 30/07):
**BTC min 0.1 = $6.210 di collaterale/lotto → FUORI**; ETH min 1 contratto = $1.832 → **1 lotto**.
Il sleeve 50/50 **diventa ETH-only**, e **al peso di book (12% = $360) sono 0 lotti** — servirebbe
il **61% del conto** su un solo sleeve. **REGOLE:** (a) un'uscita si giudica su un confronto
**appaiato per INGRESSO**, mai allineando le serie sulla data di USCITA — e' proprio quella che
la variante cambia, e l'inner-join tiene solo i casi in cui non e' successo niente (errore
commesso e corretto in sessione, 3ª occorrenza della trappola dopo venue-watch e GTAA; congelato
in un test); (b) **una regola d'uscita che scatta piu' spesso sui vincenti che sui perdenti non
e' protezione, e' troncatura** — si vede in una riga, la peggior settimana non cambia; (c) prima
di misurare il rendimento a un capitale dato, misurare il **lotto minimo del venue**.
**`VRP_CFG["f"]` NON cambiato** (15 osservazioni, 7 settimane, e per giunta **0/8 settimane **`VRP_CFG["f"]` NON cambiato** (15 osservazioni, 7 settimane, e per giunta **0/8 settimane
passano il gate IV-rank>0.30**: il f è misurato nel regime in cui il sleeve sta FLAT, DVOL max passano il gate IV-rank>0.30**: il f è misurato nel regime in cui il sleeve sta FLAT, DVOL max
raccolto al 24°/28° pctl della storia) → **caveat quantificato, non nuovo parametro**; il criterio raccolto al 24°/28° pctl della storia) → **caveat quantificato, non nuovo parametro**; il criterio
+157
View File
@@ -0,0 +1,157 @@
# 2026-07-30 (3º filone) — profit-take al 50% su VRP01: REFUTED, e una correzione a un numero di stamattina
**Domanda dell'operatore:** *«fai la misura del profit-take al 50%, simula con 3K».*
**Esito: book, pesi, cron, config INVARIATI.** Il profit-take **non entra**. Ma il filone ha
prodotto una correzione favorevole a un numero pubblicato **oggi stesso**, e una mia ipotesi
refutata nella stessa sessione.
Script `scripts/research/r0730_vrp_profit_take.py`, test `tests/test_vrp_profit_take.py` (16 casi).
## Perché questa misura e non un'altra
VRP01 come codificato **tiene fino a scadenza**: in `_vrp_weekly_asset` il payoff è su
`S1 = px[i + tn]`, non esiste gestione infra-settimana. L'onda 03/07 aveva testato 4 overlay di
protezione DD (exit-spike, **stop-loss MTM 1.5/2/3×**, ala di coda, cooldown) e li aveva refutati
4/4 col null del de-levering — ma il profit-take **non era fra quelli**: è un'uscita sul lato del
*profitto*, non del rischio. Era l'ultimo grado di libertà non misurato su questo sleeve, e la
misura del 30/07 sulle quote reali diceva che sarebbe scattato in **13 settimane su 15**.
L'idea è la regola 1 del motore di cerbero-bite (`debit <= 50% del credito`), progetto dismesso lo
stesso giorno. **Non se ne eredita il giudizio: bite non ne ha uno** (59 decisioni, tutte
`no_entry`, 0 posizioni, in testnet). Si misura.
## Fondazione: replica bit-exact
Prima di ogni delta, la re-implementazione riproduce `_vrp_weekly_asset` a **max|diff| = 0.00e+00
su entrambi gli asset, zero date non condivise**. Senza questa, ogni numero sotto sarebbe rumore di
implementazione. Congelata in un test: se qualcuno tocca il sleeve, si rompe.
## Il verdetto
Fee **reali per gamba** (`min(0.03% del sottostante, 12.5% del premio)` = listino Deribit letto da
`public/get_instruments`), identiche a canonico e variante.
| variante | ShFULL | ShHOLD | maxDD | CAGR | peggior sett. | usciti prima | giorni |
|---|---|---|---|---|---|---|---|
| **canonico (a scadenza)** | **1.32** | **0.82** | 10.5% | 10.5% | 7.27% | — | 7.0 |
| PT 25% | 0.22 | 0.44 | 8.6% | 0.9% | 7.27% | 87% | 2.7 |
| **PT 50%** | **0.18** | **0.65** | 17.3% | 1.3% | 7.27% | 79% | 3.6 |
| PT 75% | 0.45 | 1.24 | 19.5% | 3.0% | 7.27% | 71% | 4.6 |
**Null del de-levering: REFUTED 6/6** (3 livelli × vol-matched e DD-matched). ⚠️ Nota onesta: sotto
scalatura moltiplicativa lo Sharpe è **invariante**, quindi il null si riduce a «la variante ha
Sharpe più alto?». Il `k` dice solo *quanto* de-levering ottiene lo stesso DD: per PT25 basta
**k=0.818** per avere lo stesso 8.6% di DD **con Sharpe 1.32 invece di 0.22**.
## Il meccanismo — e perché serviva il confronto appaiato
⚠️ **Errore di metodo commesso e corretto in sessione.** Le due varianti sono indicizzate sulla
**data di uscita**, che il profit-take cambia: un `join="inner"` fra le serie tiene solo le
settimane *senza* uscita anticipata, dove i valori coincidono per costruzione. La prima
diagnostica stampava «troncamento 0%, variazione +0%» su tutto — potenza zero, e una conclusione
esattamente opposta al vero. È la trappola dell'inner-join già codificata due volte nel progetto
(venue watch, GTAA), qui in veste nuova. Congelata in `test_confrontare_per_data_di_uscita_da_una_risposta_falsa`.
Rifatto **appaiato per ingresso** (stesso trade, due esiti):
| | BTC (71 trade) | ETH (92 trade) |
|---|---|---|
| scatta sui **vincenti** | **86%** | **91%** |
| scatta sui **perdenti** | 25% | 19% |
| vincenti | +1.419% → +1.023% (**28%**) | +1.638% → +1.218% (**26%**) |
| perdenti | 3.649% → 2.941% (19%) | 4.248% → 3.505% (17%) |
| somma PnL | +60.2% → **+40.9%** | +56.6% → **+36.5%** |
**Il profit-take tronca del 27% i trade che avrebbero vinto comunque e salva un perdente su
cinque.** La peggior settimana è **identica** (7.27%) in tutte le varianti: nelle settimane brutte
lo spread non raggiunge mai +50%, quindi si arriva a scadenza e si prende la perdita piena. È pura
troncatura dell'upside.
## ❌ Ipotesi mia, nata e refutata nella stessa sessione
Il meccanismo suggeriva una spiegazione: *su 7 giorni chi tocca +50% è proprio chi sarebbe scaduto
senza valore; a scadenze lunghe — 18g, quella di bite, e i 30-45 DTE dello standard di mestiere —
la regola dovrebbe pagare.* Testata su 6 tenori:
| tenore | Sh hold | Sh PT50 | ΔSh |
|---|---|---|---|
| 7g | 1.32 | 0.18 | 1.50 |
| 10g | 1.51 | 0.12 | 1.40 |
| 14g | 1.14 | 0.56 | 1.71 |
| **18g** (bite) | **1.51** | **0.37** | **1.14** |
| 21g | 1.09 | 0.27 | 1.36 |
| 28g | 1.31 | 0.06 | 1.25 |
**Negativo a tutti e sei.** A 18g è il meno peggio e il DD migliora davvero (7.1% → 3.8%), ma lo
Sharpe crolla: **firma del de-levering**, non di una protezione. Ipotesi refutata.
⚠️ **La colonna `Sh hold` NON è un risultato.** Sei celle scelte sul campione pieno: il tenore 18
sembra migliore del 7 (1.51 vs 1.32) ma è esattamente la selezione che `study_family_honest` +
deflated-Sharpe esistono per uccidere. La griglia 03/07 si fermava a 10 giorni, quindi il buco è
reale — ma va chiuso con lo strumento giusto, non con questo sweep.
## ✅ Correzione a un numero pubblicato stamattina
Il sleeve modella le fee come **12.5% del credito netto** round-trip. Il listino vero è **0.03% del
sottostante per gamba** (cap 12.5% *del premio della singola opzione*, che quasi mai morde): su ETH
sono $1.14 d'ingresso contro i $2.26 del forfait, cioè il modello **sovrastima le fee di ~2×**.
| lente | ShFULL | ShHOLD | maxDD | CAGR |
|---|---|---|---|---|
| sleeve ufficiale (forfait) | 1.08 | 0.58 | 11.8% | 8.2% |
| **fee reali per gamba** | **1.32** | **0.82** | 10.5% | 10.5% |
Combinato col **f = 0.73** misurato stamattina sulle quote reali, il numero onesto di VRP01 è
**ShFULL 0.47 / DD 14.5%**, non lo **0.31** che ho scritto oggi in CLAUDE.md — che era calcolato
con le fee forfettarie. Il f resta il difetto dominante; la fee lo attenua di ~+0.16.
**Nessuna azione:** `fee_frac` **non è stato cambiato**. Il forfait è conservativo, e questo
progetto preferisce i numeri di ammissione conservativi (stessa logica per cui l'audit d'ancora
del 03/07 su VRP01 è stato accolto come «numeri conservativi, non gonfiati»). Cambiarlo alzerebbe
i numeri di uno sleeve senza aggiungere edge.
## Rientro immediato dopo l'uscita
Testato come strategia a sé (non è un overlay: cambia la cadenza). PT50 + rientro fa **Sh 1.19**
contro 1.32 del canonico, CAGR 4.9% contro 10.5%, 200 trade contro 163. Il suo **ShHOLD 2.42** è
vistoso — ed è precisamente la firma che questo progetto ha imparato a non credere: non è
selezionabile in-sample (Sharpe FULL più basso), quindi crederci sarebbe selezione sull'hold-out.
**Non è un lead.**
## $3.000 — la domanda che viene prima
Parametri veri da `public/get_instruments` (30/07): **ETH `min_trade_amount` = 1 contratto = 1 ETH;
BTC = 0.1 BTC**. Collaterale = strike corto (convenzione cash-secured del sleeve).
| | spot | lotto min | collaterale/lotto | a $3.000 | fee round-trip |
|---|---|---|---|---|---|
| BTC | $63.917 | 0.1 | **$6.210** | **0 lotti — FUORI** | $7.67 = 17.8% del credito |
| ETH | $1.906 | 1.0 | $1.832 | 1 lotto | $2.29 = 12.6% del credito |
Tre conseguenze:
1. **A $3.000 il sleeve 50/50 diventa ETH-only** — cioè non è più il sleeve misurato.
2. **Al peso di book (12%) l'allocazione è $360 = 0 lotti.** Per un solo lotto ETH servirebbe il
**61% del conto** su un singolo sleeve.
3. Quindi a $3.000 la domanda «il profit-take migliora VRP01?» **non è la domanda che decide**:
VRP01 non è collocabile a quel capitale in nessuna forma sensata.
## Cosa resta
- **Profit-take: REFUTED**, a ogni livello, a ogni f, a ogni tenore, contro il null del de-levering.
Con questo il conto degli overlay refutati su VRP01 sale a **5 su 5**, e il filone «gestire VRP01
dentro il modello» è chiuso su tutti i lati misurabili.
- **Buco reale rimasto:** il tenore oltre i 10 giorni non è mai passato per `study_family_honest`.
Non è urgente (le opzioni non sono eseguibili prima di ~$2.6k, e a $3k solo ETH e a peso assurdo).
- **Il vincolo binding non è la ricerca.** Vale la nota del 26/07: qui si è misurato un ΔSharpe che
non cambia niente, mentre le leve che decidono restano il capitale che entra e il conto che non
sparisce.
## Regole
- **Un'uscita si giudica su un confronto APPAIATO PER INGRESSO**, mai allineando due serie sulla
data di uscita: la variante che stai testando è esattamente quella che cambia quella data, e il
join tiene solo i casi in cui non è successo niente.
- **Una regola d'uscita che scatta più spesso sui vincenti che sui perdenti non è protezione, è
troncatura** — e si vede in una riga: la peggior settimana non cambia.
- **Prima di misurare il rendimento a un capitale dato, misurare il lotto minimo del venue.** Qui
la risposta operativa («non è collocabile») non dipendeva da nessuno Sharpe.
+445
View File
@@ -0,0 +1,445 @@
"""R0730 VRP-PT — profit-take al 50% del credito su VRP01, e cosa ne resta a $3.000.
DOMANDA. VRP01 come codificato tiene il credit spread FINO A SCADENZA: in
`sleeves._vrp_weekly_asset` il payoff e' calcolato su `S1 = px[i + tn]` e non esiste
nessuna gestione infra-settimana. La misura del 30/07 sulle quote REALI ha trovato che
un profit-take al 50% del credito sarebbe scattato in **13 settimane su 15**, e che il
peggior mark infra-settimana e' mediana -6.9% / minimo -81.7% su trade finiti in utile.
Questo script chiede se quella gestione MIGLIORA lo sleeve, e cosa sopravvive a $3.000.
L'idea non e' nuova ed e' la regola 1 del motore di cerbero-bite (`debit <= 50% credito`),
progetto dismesso il 30/07. Qui non si eredita il suo giudizio non ne ha uno: 59
decisioni, tutte no_entry, 0 posizioni. Si misura.
COSA E' GIA' STATO RIFIUTATO, e non si rifa'. L'onda 03/07 (`r0703_vrpimp_ddoverlay`)
ha testato 4 overlay di protezione DD exit su spike DVOL, **stop-loss MTM a 1.5/2/3x
il credito**, ala di coda, cooldown e li ha REFUTATI 4/4 col null del de-levering.
Il profit-take NON era fra quelli: e' un'uscita sul lato del PROFITTO, non del rischio.
E' l'unico grado di liberta' rimasto non misurato su questo sleeve.
METODO.
(1) Replica bit-exact di `_vrp_weekly_asset` (assert max|diff| == 0). Senza questa,
ogni delta misurato dopo e' rumore di implementazione.
(2) Marks giornalieri: dentro la settimana lo spread e' riprezzato con lo STESSO
pricer dell'ingresso (BS su DVOL corrente, T residuo). Chiudo quando
`debit <= (1 - pt) * credito`.
(3) NULL DEL DE-LEVERING, obbligatorio: un claim "meno drawdown" parte REFUTED
finche' non batte il canonico semplicemente ridotto di size. Confronto a
vol-matched E a DD-matched.
(4) Fee REALI per gamba invece della convenzione forfettaria dello sleeve:
`min(0.03% del sottostante, 12.5% del premio dell'opzione)`, cioe' il listino
Deribit vero. Applicata IDENTICA a canonico e variante: un'uscita anticipata
paga 2 gambe in piu', ed e' il costo principale di questa idea.
(5) $3.000: contratti INTERI (ETH min_trade_amount=1 -> 1 ETH), BTC fuori
(min 0.1 BTC = ~$6.500 di nozionale per gamba), collaterale = strike corto
(convenzione cash-secured dello sleeve).
f. Il fattore di prezzo misurato il 30/07 (f=0.73 sulle quote reali) e' applicato
UNIFORMEMENTE al valore dello spread, all'ingresso e all'uscita: se il modello sbaglia
il prezzo di quella struttura, lo sbaglia anche quando la si ricompra. Conseguenza utile:
il TRIGGER del profit-take e' invariante in f (`f*debit <= 0.5*f*credito`), quindi f
scala il PnL ma non cambia QUANDO si esce.
uv run python scripts/research/r0730_vrp_profit_take.py
uv run python scripts/research/r0730_vrp_profit_take.py --capital 3000
"""
from __future__ import annotations
import argparse
import sys
from pathlib import Path
import numpy as np
import pandas as pd
PROJECT_ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(PROJECT_ROOT))
from src.data.downloader import load_data # noqa: E402
from src.portfolio.portfolio import metrics # noqa: E402
from src.portfolio.sleeves import ( # noqa: E402
VRP_CFG,
_bs_put,
_strike_from_delta,
_vrp_weekly_asset,
_HL_DIR,
)
from src.strategies.trend_portfolio import resample_1d # noqa: E402
HOLDOUT = pd.Timestamp("2025-01-01", tz="UTC")
ASSETS = ("BTC", "ETH")
PT_GRID = (0.25, 0.50, 0.75)
F_GRID = (1.00, 0.80, 0.73)
TENOR_GRID = (7, 10, 14, 18, 21, 28) # 18 = la scadenza di cerbero-bite
# listino Deribit opzioni, letto da public/get_instruments il 2026-07-30
FEE_UNDERLYING = 0.0003 # 0.03% del sottostante per contratto, per gamba
FEE_PREMIUM_CAP = 0.125 # ...ma mai piu' del 12.5% del premio dell'opzione
MIN_TRADE = {"BTC": 0.1, "ETH": 1.0}
# ---------------------------------------------------------------------------
# dati: identici a quelli del sleeve
# ---------------------------------------------------------------------------
def series(asset: str):
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()
return J["px"].values, J["dvol"].values / 100.0, J.index
def leg_fee(spot: float, premium: float) -> float:
"""Fee Deribit di UNA gamba: 0.03% del sottostante, cap al 12.5% del premio."""
return min(FEE_UNDERLYING * spot, FEE_PREMIUM_CAP * max(premium, 0.0))
# ---------------------------------------------------------------------------
# il motore: un solo grado di liberta' per volta
# ---------------------------------------------------------------------------
def run_asset(asset: str, pt: float | None = None, f: float = 1.0,
real_fee: bool = True, redeploy: bool = False, tenor: int | None = None):
"""Ritorna (serie settimanale, diagnostica). `pt=None` = canonico (tieni a scadenza).
`real_fee=False` riproduce la convenzione forfettaria dello sleeve (per la replica
bit-exact). `redeploy=True` rientra subito dopo un'uscita anticipata invece di
aspettare la cadenza settimanale (strategia DIVERSA, non un overlay).
"""
px, dvf, idx = series(asset)
n = len(px)
cfg = VRP_CFG
tn = int(tenor if tenor is not None else cfg["tenor_d"])
T0 = tn / 365.25
rets: dict[pd.Timestamp, float] = {}
n_trades = n_early = 0
days_held: list[int] = []
worst_mark: list[float] = []
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, T0, sig, cfg["short_delta"])
Kl = _strike_from_delta(S0, T0, sig, cfg["long_delta"])
ps0, pl0 = _bs_put(S0, Ks, T0, sig), _bs_put(S0, Kl, T0, sig)
credit = (ps0 - pl0) * f
# --- fee d'ingresso ---
if real_fee:
fee = leg_fee(S0, ps0 * f) + leg_fee(S0, pl0 * f)
else:
fee = 0.0 # forfait applicato a valle
# --- cammino infra-settimana ---
exit_j = i + tn # default: scadenza
early = False
mk_worst = 0.0
if pt is not None:
for j in range(i + 1, i + tn):
Trem = (i + tn - j) / 365.25
Sj, sj = px[j], dvf[j]
debit = (_bs_put(Sj, Ks, Trem, sj) - _bs_put(Sj, Kl, Trem, sj)) * f
mk_worst = min(mk_worst, (credit - debit) / Ks)
if debit <= (1.0 - pt) * credit:
exit_j = j
early = True
break
if early:
Trem = (i + tn - exit_j) / 365.25
Sj, sj = px[exit_j], dvf[exit_j]
psx, plx = _bs_put(Sj, Ks, Trem, sj), _bs_put(Sj, Kl, Trem, sj)
cost = (psx - plx) * f
if real_fee:
fee += leg_fee(Sj, psx * f) + leg_fee(Sj, plx * f)
pnl = credit - cost - fee
else:
S1 = px[i + tn]
payoff = max(0.0, Ks - S1) - max(0.0, Kl - S1)
if real_fee:
# a scadenza: fee di regolamento solo sulle gambe ITM (0.015% del sottostante)
if Ks > S1:
fee += 0.5 * FEE_UNDERLYING * S1
if Kl > S1:
fee += 0.5 * FEE_UNDERLYING * S1
pnl = credit - payoff - fee
if not real_fee:
pnl -= cfg["fee_frac"] * abs(credit) # convenzione dello sleeve
rets[idx[exit_j]] = rets.get(idx[exit_j], 0.0) + pnl / Ks
n_trades += 1
n_early += int(early)
days_held.append(exit_j - i)
worst_mark.append(mk_worst)
i = (exit_j + 1) if (redeploy and early) else (i + tn)
diag = dict(trades=n_trades, early=n_early,
early_pct=100.0 * n_early / max(n_trades, 1),
days_held=float(np.mean(days_held)) if days_held else float("nan"),
worst_mark=float(np.median(worst_mark)) if worst_mark else float("nan"))
return pd.Series(rets).sort_index(), diag
def paired_trades(asset: str, pt: float, f: float = 1.0) -> pd.DataFrame:
"""Per OGNI ingresso, l'esito hold-to-expiry E quello col profit-take: unico confronto
valido, perche' le due varianti escono in date diverse."""
px, dvf, idx = series(asset)
n = len(px)
cfg = VRP_CFG
tn = cfg["tenor_d"]
T0 = tn / 365.25
recs = []
i = 60
while i + tn < n:
S0, sig = px[i], dvf[i]
skip = False
if cfg["gate_vrp"] and i >= 31:
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 i >= 60:
ivr = float((dvf[:i] < dvf[i]).mean())
if ivr < cfg["gate_ivr"] or ivr > cfg["crash_skip"]:
skip = True
if skip:
i += tn
continue
Ks = _strike_from_delta(S0, T0, sig, cfg["short_delta"])
Kl = _strike_from_delta(S0, T0, sig, cfg["long_delta"])
credit = (_bs_put(S0, Ks, T0, sig) - _bs_put(S0, Kl, T0, sig)) * f
ej = None
for j in range(i + 1, i + tn):
Trem = (i + tn - j) / 365.25
debit = (_bs_put(px[j], Ks, Trem, dvf[j]) - _bs_put(px[j], Kl, Trem, dvf[j])) * f
if debit <= (1.0 - pt) * credit:
ej = j
break
S1 = px[i + tn]
hold = (credit - (max(0.0, Ks - S1) - max(0.0, Kl - S1))) / Ks
if ej is None:
ptv, early = hold, False
else:
Trem = (i + tn - ej) / 365.25
cost = (_bs_put(px[ej], Ks, Trem, dvf[ej]) - _bs_put(px[ej], Kl, Trem, dvf[ej])) * f
ptv, early = (credit - cost) / Ks, True
recs.append((hold, ptv, early))
i += tn
return pd.DataFrame(recs, columns=["hold", "pt", "early"])
def combo(pt=None, f=1.0, real_fee=True, redeploy=False, tenor=None):
"""Book 50/50 BTC+ETH su griglia giornaliera, come `_vrp_combo_returns`."""
out, diags = {}, {}
for a in ASSETS:
out[a], diags[a] = run_asset(a, pt=pt, f=f, real_fee=real_fee,
redeploy=redeploy, tenor=tenor)
wk = pd.concat({"B": out["BTC"], "E": out["ETH"]}, axis=1, join="inner").mean(axis=1).sort_index()
if wk.empty:
return wk, diags
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
return daily, diags
# ---------------------------------------------------------------------------
# metriche e null
# ---------------------------------------------------------------------------
def stats(daily: pd.Series) -> dict:
m = metrics(daily)
h = metrics(daily[daily.index >= HOLDOUT])
return dict(sh=m["sharpe"], dd=m["maxdd"], cagr=m["cagr"], sh_h=h["sharpe"],
worst=float(daily.min()))
def delever_null(canon: pd.Series, variant: pd.Series, match: str) -> dict:
"""Il canonico ridotto di size fa lo stesso lavoro? `match` in {'vol','dd'}."""
sv, sc = stats(variant), stats(canon)
if match == "vol":
k = variant.std() / canon.std() if canon.std() > 0 else 1.0
else: # DD-matched, per bisezione
lo, hi = 0.01, 3.0
for _ in range(60):
k = 0.5 * (lo + hi)
if stats(canon * k)["dd"] < sv["dd"]:
lo = k
else:
hi = k
k = 0.5 * (lo + hi)
sk = stats(canon * k)
return dict(k=k, sh_null=sk["sh"], dd_null=sk["dd"], sh_var=sv["sh"], dd_var=sv["dd"],
d_sh=sv["sh"] - sk["sh"], batte=(sv["sh"] > sk["sh"]))
# ---------------------------------------------------------------------------
# eseguibilita' a capitale dato
# ---------------------------------------------------------------------------
def executability(capital: float) -> dict:
"""Quanti contratti INTERI entrano, per asset, con collaterale = strike corto."""
rows = {}
for a in ASSETS:
px, dvf, idx = series(a)
S0, sig = px[-1], dvf[-1]
T0 = VRP_CFG["tenor_d"] / 365.25
Ks = _strike_from_delta(S0, T0, sig, VRP_CFG["short_delta"])
Kl = _strike_from_delta(S0, T0, sig, VRP_CFG["long_delta"])
unit = MIN_TRADE[a]
collat = Ks * unit # cash-secured, per lotto minimo
ps, pl = _bs_put(S0, Ks, T0, sig), _bs_put(S0, Kl, T0, sig)
credit = (ps - pl) * unit
fee_rt = 2 * (leg_fee(S0, ps) + leg_fee(S0, pl)) * unit # ingresso + uscita anticipata
rows[a] = dict(spot=S0, Ks=Ks, Kl=Kl, unit=unit, collat=collat,
lots=int(capital // collat), credit=credit, fee_rt=fee_rt,
fee_pct_credit=100.0 * fee_rt / credit if credit > 0 else float("nan"),
width=(Ks - Kl) * unit)
return rows
# ---------------------------------------------------------------------------
def main() -> int:
ap = argparse.ArgumentParser()
ap.add_argument("--capital", type=float, default=3000.0)
args = ap.parse_args()
print("=" * 92)
print(" R0730 VRP-PT — profit-take al 50% del credito su VRP01")
print("=" * 92)
# ---- (0) replica bit-exact ------------------------------------------------
print("\n (0) REPLICA del sleeve (senza questa, ogni delta dopo e' rumore)")
for a in ASSETS:
mine, _ = run_asset(a, pt=None, f=VRP_CFG["f"], real_fee=False)
ref = _vrp_weekly_asset(a)
j = pd.concat({"m": mine, "r": ref}, axis=1, join="inner")
d = float((j["m"] - j["r"]).abs().max())
n_only = len(mine) + len(ref) - 2 * len(j)
flag = "OK" if (d == 0.0 and n_only == 0) else "⚠ DIVERGE"
print(f" {a}: max|diff| = {d:.2e} date non condivise = {n_only} {flag}")
if d != 0.0 or n_only != 0:
print(" REPLICA FALLITA — mi fermo, il confronto non sarebbe valido.")
return 1
# ---- (1) griglia profit-take ----------------------------------------------
print(f"\n (1) PROFIT-TAKE — fee REALI Deribit per gamba, f={F_GRID[0]:.2f}"
f" (trigger invariante in f)")
canon, dcan = combo(pt=None, f=1.0)
sc = stats(canon)
print(f" {'variante':<22}{'ShFULL':>8}{'ShHOLD':>8}{'maxDD':>8}{'CAGR':>8}"
f"{'peggior sett':>14}{'usciti prima':>14}{'giorni':>8}")
print(f" {'canonico (scadenza)':<22}{sc['sh']:>8.2f}{sc['sh_h']:>8.2f}"
f"{sc['dd']*100:>7.1f}%{sc['cagr']*100:>7.1f}%{sc['worst']*100:>13.2f}%"
f"{'':>14}{7.0:>8.1f}")
variants = {}
for pt in PT_GRID:
v, dg = combo(pt=pt, f=1.0)
variants[pt] = v
s = stats(v)
early = np.mean([dg[a]["early_pct"] for a in ASSETS])
held = np.mean([dg[a]["days_held"] for a in ASSETS])
print(f" {'PT ' + str(int(pt*100)) + '% del credito':<22}{s['sh']:>8.2f}{s['sh_h']:>8.2f}"
f"{s['dd']*100:>7.1f}%{s['cagr']*100:>7.1f}%{s['worst']*100:>13.2f}%"
f"{early:>13.0f}%{held:>8.1f}")
# ---- (2) null del de-levering ---------------------------------------------
print("\n (2) NULL DEL DE-LEVERING — il canonico ridotto di size fa lo stesso lavoro?")
print(" (regola del progetto: un claim 'meno DD' parte REFUTED finche' non lo batte)")
for pt in PT_GRID:
for match in ("vol", "dd"):
r = delever_null(canon, variants[pt], match)
verdict = "batte il null" if r["batte"] else "REFUTED"
print(f" PT {int(pt*100):>2}% match {match:>3}: k={r['k']:.3f} "
f"Sh null {r['sh_null']:>5.2f} vs variante {r['sh_var']:>5.2f} "
f"{r['d_sh']:+.2f}) DD {r['dd_null']*100:.1f}% vs {r['dd_var']*100:.1f}% {verdict}")
# ---- (3) sensibilita' a f --------------------------------------------------
print("\n (3) BANDA f — f scala il PnL ma NON il trigger (f*debit <= 0.5*f*credito)")
print(f" {'f':>6}{'Sh canonico':>14}{'Sh PT50':>10}{'ΔSh':>8}"
f"{'DD canonico':>14}{'DD PT50':>10}")
for f in F_GRID:
c, _ = combo(pt=None, f=f)
v, _ = combo(pt=0.50, f=f)
s0, s1 = stats(c), stats(v)
print(f" {f:>6.2f}{s0['sh']:>14.2f}{s1['sh']:>10.2f}{s1['sh']-s0['sh']:>+8.2f}"
f"{s0['dd']*100:>13.1f}%{s1['dd']*100:>9.1f}%")
# ---- (4) redeploy ----------------------------------------------------------
print("\n (4) RIENTRO IMMEDIATO dopo l'uscita (strategia DIVERSA, non un overlay)")
rd, drd = combo(pt=0.50, f=1.0, redeploy=True)
s = stats(rd)
tr = sum(drd[a]["trades"] for a in ASSETS)
tr0 = sum(dcan[a]["trades"] for a in ASSETS)
print(f" PT50 + rientro: Sh {s['sh']:.2f} ShHOLD {s['sh_h']:.2f} DD {s['dd']*100:.1f}%"
f" CAGR {s['cagr']*100:.1f}% trade {tr} (canonico {tr0}, +{100*(tr/tr0-1):.0f}%)")
# ---- (4-bis) confronto APPAIATO per ingresso: il meccanismo ---------------
print("\n (4-bis) MECCANISMO — confronto appaiato PER INGRESSO")
print(" (le serie sono indicizzate sulla data di USCITA, che il PT cambia: un inner-join")
print(" fra le due confronterebbe solo le settimane senza uscita anticipata)")
for a in ASSETS:
R = paired_trades(a, pt=0.50)
W, L = R[R.hold > 0], R[R.hold <= 0]
print(f" {a}: {len(R)} trade, uscita anticipata {100*R.early.mean():.0f}%"
f" — scatta sul {100*W.early.mean():.0f}% dei VINCENTI e sul {100*L.early.mean():.0f}% dei PERDENTI")
print(f" vincenti {W.hold.mean()*100:+.3f}% -> {W.pt.mean()*100:+.3f}% ({100*(W.pt.mean()/W.hold.mean()-1):+.0f}%)"
f" perdenti {L.hold.mean()*100:+.3f}% -> {L.pt.mean()*100:+.3f}% ({100*(L.pt.mean()/L.hold.mean()-1):+.0f}%)")
# ---- (4-ter) ipotesi: funziona a scadenze piu' lunghe? --------------------
print("\n (4-ter) IPOTESI DA VERIFICARE: su 7g chi tocca +50% e' chi sarebbe scaduto")
print(" senza valore, quindi il PT dovrebbe pagare a scadenze LUNGHE (bite usava 18g).")
print(f" {'tenore':>8}{'Sh hold':>10}{'Sh PT50':>10}{'ΔSh':>8}{'DD hold':>10}{'DD PT50':>10}{'usciti':>9}")
for tn in TENOR_GRID:
c, _ = combo(pt=None, f=1.0, tenor=tn)
v, dg = combo(pt=0.50, f=1.0, tenor=tn)
s0, s1 = stats(c), stats(v)
early = np.mean([dg[a]["early_pct"] for a in ASSETS])
print(f" {tn:>6}g{s0['sh']:>10.2f}{s1['sh']:>10.2f}{s1['sh']-s0['sh']:>+8.2f}"
f"{s0['dd']*100:>9.1f}%{s1['dd']*100:>9.1f}%{early:>8.0f}%")
print(" -> IPOTESI REFUTATA se ΔSh resta negativo ovunque. ⚠ La colonna 'Sh hold' NON e'")
print(" un risultato: 6 celle scelte sul campione pieno; per crederci serve")
print(" study_family_honest + deflated-Sharpe, non questo sweep.")
# ---- (5) $3.000 -------------------------------------------------------------
print(f"\n (5) ESEGUIBILITA' A ${args.capital:,.0f} — contratti INTERI, collaterale = strike corto")
ex = executability(args.capital)
for a, r in ex.items():
stato = "ESEGUIBILE" if r["lots"] >= 1 else "FUORI"
print(f" {a}: spot ${r['spot']:,.0f} lotto minimo {r['unit']} "
f"collaterale/lotto ${r['collat']:,.0f} -> {r['lots']} lotti {stato}")
print(f" credito ${r['credit']:.2f}/lotto · larghezza ${r['width']:.2f} · "
f"fee round-trip ${r['fee_rt']:.2f} = {r['fee_pct_credit']:.1f}% del credito")
tot = sum(r["lots"] for r in ex.values())
print(f"\n Il sleeve e' 50/50 BTC+ETH: a ${args.capital:,.0f} "
f"{'diventa ETH-only' if ex['BTC']['lots'] == 0 and ex['ETH']['lots'] > 0 else 'non e collocabile'}.")
print(f" Al PESO DI BOOK (12%) l'allocazione sarebbe ${args.capital*0.12:,.0f} = "
f"{int(args.capital*0.12 // ex['ETH']['collat'])} lotti ETH.")
if tot == 0:
print(" -> a questo capitale lo sleeve NON e' eseguibile in nessuna forma.")
print()
return 0
if __name__ == "__main__":
raise SystemExit(main())
+178
View File
@@ -0,0 +1,178 @@
"""Test della misura del profit-take su VRP01 (scripts/research/r0730_vrp_profit_take.py).
Tre cose vanno congelate qui, e una sola e' il risultato:
* **La replica bit-exact.** Tutto il confronto poggia su una re-implementazione del sleeve:
se qualcuno tocca `_vrp_weekly_asset`, il test deve rompersi invece di far passare un delta
misurato fra due cose diverse.
* **La trappola dell'inner-join.** Le due varianti escono in date DIVERSE, quindi un
`join="inner"` fra le due serie confronta solo le settimane senza uscita anticipata e
risponde "identiche" con potenza zero. E' l'errore che ho commesso in sessione; il test lo
rende irripetibile.
* **Il verdetto** (PT50 non batte il canonico) con il **controllo positivo** obbligatorio sul
null del de-levering: un test che dice sempre REFUTED e' indistinguibile da uno rotto.
"""
from __future__ import annotations
import importlib.util
import sys
from pathlib import Path
import numpy as np
import pandas as pd
import pytest
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT))
_spec = importlib.util.spec_from_file_location(
"r0730_pt", ROOT / "scripts" / "research" / "r0730_vrp_profit_take.py")
PT = importlib.util.module_from_spec(_spec)
_spec.loader.exec_module(PT)
from src.portfolio.sleeves import VRP_CFG, _vrp_weekly_asset # noqa: E402
# ---------------------------------------------------------------------------
# cache: ogni corsa rilegge 7 anni di storia
# ---------------------------------------------------------------------------
@pytest.fixture(scope="module")
def canon():
return PT.combo(pt=None, f=1.0, real_fee=True)[0]
@pytest.fixture(scope="module")
def pt50():
return PT.combo(pt=0.50, f=1.0, real_fee=True)[0]
# ===========================================================================
# la fondazione: senza questa nessun delta significa niente
# ===========================================================================
@pytest.mark.parametrize("asset", ["BTC", "ETH"])
def test_la_replica_e_bit_exact_col_sleeve(asset):
mine, _ = PT.run_asset(asset, pt=None, f=VRP_CFG["f"], real_fee=False)
ref = _vrp_weekly_asset(asset)
j = pd.concat({"m": mine, "r": ref}, axis=1, join="inner")
assert len(j) == len(ref) == len(mine) # nessuna data persa da nessuna delle due
assert float((j["m"] - j["r"]).abs().max()) == 0.0
# ===========================================================================
# l'errore di metodo commesso in sessione, congelato
# ===========================================================================
def test_confrontare_per_data_di_uscita_da_una_risposta_falsa(canon, pt50):
"""Un inner-join fra canonico e PT tiene solo le settimane in cui il PT NON e' scattato,
dove per costruzione i due valori coincidono: la conclusione sarebbe 'nessuna differenza'."""
a = canon[canon != 0.0]
b = pt50[pt50 != 0.0]
j = pd.concat({"a": a, "b": b}, axis=1, join="inner")
assert len(j) < 0.9 * min(len(a), len(b)) # il join butta via la maggior parte dei dati
# ...e su cio' che resta le due sono IDENTICHE: la risposta sarebbe "nessuna differenza"
assert float((j["a"] - j["b"]).abs().max()) == 0.0
# mentre la differenza vera, misurata appaiata, e' enorme
assert abs(PT.stats(a)["sh"] - PT.stats(b)["sh"]) > 1.0
def test_il_confronto_appaiato_per_ingresso_vede_la_differenza():
"""La lente giusta: stesso ingresso, due esiti. Qui la differenza c'e' ed e' grande."""
R = PT.paired_trades("ETH", pt=0.50)
assert len(R) > 50
assert R["early"].mean() > 0.5 # il trigger e' raggiungibile
assert R["pt"].sum() < R["hold"].sum() # e costa
# ===========================================================================
# il meccanismo
# ===========================================================================
@pytest.mark.parametrize("asset", ["BTC", "ETH"])
def test_il_profit_take_scatta_sui_vincenti_non_sui_perdenti(asset):
"""E' il motivo per cui perde: tronca i trade che sarebbero finiti bene e non salva
quelli che finiscono male."""
R = PT.paired_trades(asset, pt=0.50)
W, L = R[R["hold"] > 0], R[R["hold"] <= 0]
assert W["early"].mean() > 0.75 # >75% dei vincenti escono prima
assert L["early"].mean() < 0.40 # <40% dei perdenti
assert W["early"].mean() > 2.0 * L["early"].mean()
assert W["pt"].mean() < W["hold"].mean() # i vincenti vengono troncati
# ===========================================================================
# il trigger non dipende da f — proprieta', non coincidenza
# ===========================================================================
def test_il_trigger_e_invariante_in_f():
"""`f*debit <= (1-pt)*f*credito` non contiene f: cambiare il prezzo del modello sposta il
PnL ma non QUANDO si esce. Se qualcuno applicasse f asimmetricamente, questo si rompe."""
a = PT.paired_trades("ETH", pt=0.50, f=1.00)
b = PT.paired_trades("ETH", pt=0.50, f=0.73)
assert len(a) == len(b)
assert (a["early"].values == b["early"].values).all()
# ===========================================================================
# fee: la forma del listino, non solo il livello
# ===========================================================================
def test_la_fee_di_gamba_usa_il_sottostante_quando_il_premio_e_alto():
# opzione cara: 12.5% del premio (12.5) e' piu' del 3bps del sottostante (0.57) -> vince il 3bps
assert PT.leg_fee(1900.0, 100.0) == pytest.approx(0.0003 * 1900.0)
def test_la_fee_di_gamba_e_tappata_al_premio_quando_l_opzione_e_quasi_senza_valore():
# opzione da 1 dollaro: il cap al 12.5% (0.125) morde, altrimenti si pagherebbe piu' del premio
assert PT.leg_fee(1900.0, 1.0) == pytest.approx(0.125)
def test_la_fee_non_e_mai_negativa_su_premio_negativo():
assert PT.leg_fee(1900.0, -5.0) == 0.0
# ===========================================================================
# il verdetto, e il controllo positivo che lo rende credibile
# ===========================================================================
def test_pt50_non_batte_il_canonico(canon, pt50):
for match in ("vol", "dd"):
r = PT.delever_null(canon, pt50, match)
assert not r["batte"], f"match={match}: il verdetto e' cambiato, va rivisto il diario"
assert r["d_sh"] < -0.5
def test_controllo_positivo_il_null_sa_dire_di_si(canon):
"""Obbligatorio: un null che risponde sempre REFUTED e' indistinguibile da uno rotto.
Una variante COSTRUITA migliore (stesso rendimento, meta' della volatilita' di rumore)
deve passare."""
better = canon.copy()
better[better < 0] *= 0.5 # perdite dimezzate = davvero meglio
r = PT.delever_null(canon, better, "vol")
assert r["batte"] and r["d_sh"] > 0
# ===========================================================================
# eseguibilita': a $3.000 la domanda viene prima del rendimento
# ===========================================================================
@pytest.fixture(scope="module")
def ex3k():
return PT.executability(3000.0)
def test_a_3000_le_opzioni_btc_sono_fuori(ex3k):
"""min_trade_amount 0.1 BTC = ~$6.000 di collaterale cash-secured per lotto."""
assert ex3k["BTC"]["lots"] == 0
assert ex3k["BTC"]["collat"] > 3000.0
def test_a_3000_eth_entra_per_un_solo_lotto(ex3k):
assert ex3k["ETH"]["lots"] == 1
assert ex3k["ETH"]["collat"] < 3000.0 < 2 * ex3k["ETH"]["collat"]
def test_al_peso_di_book_lo_sleeve_non_e_collocabile(ex3k):
"""VRP01 pesa il 12%: $360 su $3.000, sotto il collaterale di un solo lotto ETH.
E' il vincolo che decide, non lo Sharpe."""
assert int(3000.0 * 0.12 // ex3k["ETH"]["collat"]) == 0
def test_il_lotto_minimo_e_quello_letto_dal_venue():
"""Congelato dal listino Deribit del 2026-07-30: se cambia, i numeri di questo studio
cambiano e il test deve dirlo."""
assert PT.MIN_TRADE == {"BTC": 0.1, "ETH": 1.0}
assert PT.FEE_UNDERLYING == 0.0003 and PT.FEE_PREMIUM_CAP == 0.125