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