COLLAR01 (§71): il pavimento funziona, il tetto lo paga troppo — e cio' che vince e' VRP01
Chiesto dall'operatore in quattro battute: hold BTC long o short coperto in
opzioni, scadenza <=15gg, "ridurre la vincita ma bloccare la perdita" (=>
collar, non put protettiva), entrata gated da indicatori ("forte bull"), e
uscita dalle opzioni fra il 50% e il 75% del tempo.
PERCHE' SI POTEVA RIAPRIRE DOPO §46. §46 (tail-hedge) fu refutato sul beta
+0,076 del libro — "non si assicura un libro che nei crash e' gia' quasi
piatto". Qui il sottostante e' un hold di BTC, beta 1,0: quel motivo non si
applica. E §46 dichiarava non provata proprio la copertura gated su regime.
L'entrata non aggiunge un solo parametro: tsmom_blend media tre np.sign() su
(30,90,180) => valori in {-1,-1/3,+1/3,+1}, quindi "forte bull" = |blend|==1
(52,5% dei giorni) contro il confronto dichiarato |blend|>=1/3 (97,1%).
RISULTATI (lente lunga 2021-03 -> 2026-09, 5,44 anni; griglia 48 celle
dichiarata prima + 36 di estensione dichiarata):
- A1 CONFERMATA: il pavimento FUNZIONA, maxDD scende in 36/48 (§46: saliva
in 162/162). La meta' della domanda ha risposta positiva.
- A3 CONFERMATA: il de-levering lo fa meglio in 45/48. Δdrift/ΔmaxDD 7,90
(gate forte) / 1,98 (largo): 2-8 punti di drift per punto di DD.
- C9 in forma pura: il tetto taglia il 46,2% dei cicli VINCENTI, il pavimento
para il 6,7% dei PERDENTI — 7x piu' spesso sui vincenti: troncatura.
- M1: collar Sharpe 0,508 vs TP01 0,852; TP01+10% => +0,000 di Sharpe e
+2,32pp di maxDD.
IL FATTO CHE VALE PIU' DEL VERDETTO. Le 3 celle vincenti stavano tutte sul
BORDO; estesa la famiglia vince 35/36 nell'ANGOLO (dput 0,02 / dcall 0,50,
Sharpe 1,471) — e il limite di quell'angolo e' una COVERED CALL: la pendenza
porta fuori dalla domanda posta e dentro lo short-vol. E quel 1,471 e' il
prezzatore che si paga da solo: DVOL/RV-forward 1,320 a 7g (sopra nel 76,9%
dei giorni) => riprezzato alla vol vera l'angolo cade a 0,511, che e' VRP01
(0,47). Non una scoperta: VRP01 per una strada piu' lunga. §3 lo blocca.
USCITA ANTICIPATA: implementata (exit_frac) e COSTA. Cella onesta gate forte:
drift +9,48% (scadenza) -> +3,84% (50%), esito da VINCE a perde sotto 0,75.
Il meccanismo previsto c'e' (VRP residuo +3,38 -> +1,27pp) ma lo spread lo
travolge. Corregge l'applicazione di §46: "un roll anticipato non paga f"
vale per una copertura solo LONG; in un collar la gamba venduta va
RICOMPRATA, quindi si paga f sulla parte che a scadenza si regolava gratis.
L'asimmetria si INVERTE quando la struttura ha una gamba corta.
CONTROLLI DELL'APPARATO 3/3 (M15): pranzo gratis riconosciuto (maxDD
51,83%->36,76%, drift +10,04%->+35,69%), premio x10 rifiutato, zero-cost
finito. Cinque difetti miei catturati dai controlli, non a occhio: bisezione
zero-cost invertita (dava Sharpe -3,9), dcall=NaN nella cassa, C9 non
consapevole della direzione (S1>S0 non e' "vincente" per uno short), e due di
contabilita' che avrebbero ADULATO il collar (base che rollava lo spot
pagando ~3,6%/a di fee inesistenti; roll che chiudeva lo spot senza motivo).
Corregge anche un muro di §46: il tick da 5 USDC e' della famiglia USDC; la
catena che raccogliamo e' 100% inverse, quindi li' non si applica.
Regole nuove in CLAUDE.md: M29 (un edge da opzioni prezzate a modello si
riprezza alla vol REALIZZATA prima di crederci), M8 esteso (un argmax sul
BORDO e' una pendenza, non una cella), C4 esteso (il segno dell'asimmetria di
f dipende dal verso della gamba).
Libro, pesi, cron, config INVARIATI. Nessun ordine. Suite: 807 passati.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -17,7 +17,7 @@ chi la riapre deve battere il motivo, non ripetere la misura.
|
||||
| `docs/memory/40-produzione-e-deploy.md` | esecutore, tripwire, monitor, libro di bordo, PRIIPs/UCITS | tocchi cio' che gira con soldi veri |
|
||||
| `docs/memory/50-dati-e-feed.md` | difetti del dato trovati e riparati, catena opzioni | tocchi i feed |
|
||||
| `docs/memory/60-metodo-e-gate.md` | i gate codificati in `altlib.py` | valuti un candidato |
|
||||
| `docs/research/RESULTS-0822.md` | registro per filone §1-70 | vuoi il dettaglio di un filone |
|
||||
| `docs/research/RESULTS-0822.md` | registro per filone §1-71 | vuoi il dettaglio di un filone |
|
||||
| `docs/diary/` (123 voci) | la sessione originale, per data | vuoi il contesto completo |
|
||||
|
||||
---
|
||||
@@ -340,7 +340,7 @@ nei file di memoria.
|
||||
- **M5** **Null del de-levering (7 occorrenze):** ogni claim "meno drawdown" si testa a **iso-rischio** prima di crederci — e' il primo test, non l'ultimo. Se la variante e' una pura ri-scalatura il null e' **degenere** (`sh(k·base) ≡ sh(base)`): serve iso-peso sullo Sharpe.
|
||||
- **M6** Un diversificatore a **basso CAGR** si giudica a **iso-rischio**, mai a iso-nozionale.
|
||||
- **M7** Su offset/ancore appaiate la statistica e' la **mediana delle differenze**, non la differenza delle mediane. E **se si de-lucka una strategia va de-luckato anche il suo DEGRADO**.
|
||||
- **M8** Un **argmax dentro un plateau non e' una decisione**; e la mediana da sola puo' nascondere il fatto decisivo (p90 su, p10 giu' = si compra dipendenza dall'ancora, non Sharpe).
|
||||
- **M8** Un **argmax dentro un plateau non e' una decisione**; e la mediana da sola puo' nascondere il fatto decisivo (p90 su, p10 giu' = si compra dipendenza dall'ancora, non Sharpe). E un **argmax sul BORDO della griglia non e' una cella: e' una pendenza** — si segue fino al suo limite prima di leggerne il valore, perche' il limite puo' essere un oggetto diverso da quello che si stava studiando (COLLAR01: le 3 celle vincenti su 48 stavano tutte sul bordo, e il limite era una **covered call**, cioe' fuori dalla domanda posta).
|
||||
- **M9** Un contributo positivo si **scompone per anno** prima di crederci: "24/24 ancore positive" puo' essere **un anno solo**.
|
||||
- **M10** **Due punti non fanno una tendenza** nemmeno quando la meccanica sembra spiegarla.
|
||||
- **M11** Un **blocco dichiarato e' un'ipotesi, non un fatto**: si ri-verifica prima di costruirci sopra o di rimandare (3 occorrenze, ogni volta il blocco non esisteva).
|
||||
@@ -361,12 +361,13 @@ nei file di memoria.
|
||||
- **M26** **Non contare su una compensazione fra correzioni misurate separatamente** — la si misura, e in piu' di una **moneta** (un'interazione piccola cambia segno con la moneta).
|
||||
- **M27** Prima di dichiarare che un campo di un dataset e' sbagliato, **aprire il codice che lo produce**. E una **fonte normativa citata in un commento si verifica come un numero**.
|
||||
- **M28** Quando **due affermazioni della stessa memoria si contraddicono, la contraddizione e' informazione**: una delle due e' stata scritta guardando i dati.
|
||||
- **M29** 🚨 Un edge da **opzioni prezzate a modello** si **riprezza alla volatilita' REALIZZATA** prima di crederci: se sparisce, non era la struttura — era il **premio di varianza**, cioe' VRP01, che il progetto ha gia'. Su BTC il **DVOL sta il 32% sopra la RV-forward** a 7 giorni (**77% dei giorni**), quindi *qualunque* struttura che vende vol **stampa per costruzione del prezzatore**: in COLLAR01 il **72-100%** del drift della cella migliore era questo, e l'angolo cadeva da Sharpe **1,471 a 0,511** — che e' il **0,47** gia' pubblicato di VRP01. Vale anche al contrario: un edge che *compra* vol a modello e' sottostimato dallo stesso fattore.
|
||||
|
||||
### Sui costi e l'eseguibilita'
|
||||
- **C1** Il costo di un venue va modellato nella sua **FORMA** (fisso vs proporzionale), non solo nel livello: un pavimento fisso e' una **tassa regressiva** e due venue a pari "costo medio" danno esiti **opposti** al variare del capitale.
|
||||
- **C2** Un parametro d'esecuzione espresso in **valuta assoluta** ha effetto che dipende dal capitale: il controllo non e' lo Sharpe ma la **quota di tempo a mercato**.
|
||||
- **C3** Prima di misurare il rendimento a un capitale dato, misurare il **lotto minimo del venue** — e sapere **quale famiglia di strumenti** si sta guardando (inverse vs USDC-lineare: lotti 7,5× diversi).
|
||||
- **C4** Un **fattore di errore si giudica moltiplicato per il suo peso** (5,85 al 3% fa meno danno di 2,23 al 18%). Il `f` di una struttura multi-gamba **non e' il `f` di una sua gamba**.
|
||||
- **C4** Un **fattore di errore si giudica moltiplicato per il suo peso** (5,85 al 3% fa meno danno di 2,23 al 18%). Il `f` di una struttura multi-gamba **non e' il `f` di una sua gamba**. E **il segno dell'asimmetria di `f` dipende dal VERSO della gamba**: *"un roll anticipato non paga `f`"* (§46) vale per una copertura **solo LONG** — se c'e' una **gamba VENDUTA da ricomprare**, uscire prima paga `f` *esattamente* sulla parte che a scadenza si regolava gratis, e piu' spesso. **L'asimmetria si INVERTE** (COLLAR01: uscire al 50% del tempo costa 5,6 punti di drift col gate forte, 10,2 col largo).
|
||||
- **C5** **L'open interest NON misura la negoziabilita'** (su Deribit USDC sono quasi anti-correlati). Prima di credere a un rapporto estremo su un prezzo piccolo, **contare i tick**.
|
||||
- **C6** La negoziabilita' **sul conto reale** va verificata quando lo sleeve entra in **RICERCA**, non quando entra nel book (PRIIPs costo': 5 settimane di misure). *"Non posso comprare" ≠ "non posso vedere"*: il segnale puo' girare su una serie che non si puo' negoziare.
|
||||
- **C7** Si cerca per **ISIN, non per ticker**; si prende la linea nella **valuta del proprio saldo**; un costo di conversione e' proprieta' della **coppia strumento-conto**, non dello strumento.
|
||||
|
||||
@@ -0,0 +1,225 @@
|
||||
# 2026-09-01 — COLLAR01: il pavimento funziona, il tetto lo paga troppo, e ciò che vince è VRP01 travestito
|
||||
|
||||
*Scritto il 2026-09-01. Ogni numero è riprodotto da `scripts/research/r0901_btc_collar.py`.
|
||||
**Libro, pesi, cron, config INVARIATI. Nessun ordine.***
|
||||
|
||||
## 0. La domanda, come è arrivata
|
||||
|
||||
> *«crea una strategia di hold BTC in long o short con copertura con options (max 15gg)»*
|
||||
> *«opzione deve essere al max di 15gg»*
|
||||
> *«voglio ridurre la vincita, ma bloccare la perdita»*
|
||||
> *«ovviamente l'entrata deve essere gestita da una strategia confortata da indicatori (es. è in forte bull)»*
|
||||
> *«dalle opzioni dobbiamo uscire prima del termine (tra 50% e 75% del tempo)»*
|
||||
|
||||
Le tre precisazioni cambiano l'oggetto, e vanno lette insieme: **non è una put protettiva, è un
|
||||
COLLAR** (pavimento comprato, tetto venduto per finanziarlo), su un **hold gated da indicatori**,
|
||||
con **scadenza ≤15 giorni**.
|
||||
|
||||
## 1. Perché questo filone si poteva riaprire
|
||||
|
||||
§46 (TAIL-HEDGE, 2026-08-23) è **REFUTATO** e la memoria dice di battere il motivo, non di
|
||||
ripetere la misura. Il motivo di §46, verbatim:
|
||||
|
||||
> *il maxDD **SALE in 162/162 celle**, a ogni lente di f, perché il **beta del libro al sottostante
|
||||
> è +0,076**: non si assicura un libro che nei crash è già quasi piatto.*
|
||||
|
||||
**Quel motivo non si applica qui.** §46 assicurava il *libro* (TP01+SKH01, piatto il 28% dei
|
||||
giorni); qui il sottostante è un **hold di BTC, beta 1,0 per costruzione**. E §46 dichiara di non
|
||||
aver provato proprio questo: *«Nessuna copertura dinamica (gated su regime) è stata provata: la
|
||||
domanda era la statica»*.
|
||||
|
||||
Riusati e non rimisurati: metriche e `k_for_same_dd` da `r0823_tail_hedge`, il listino fee opzioni
|
||||
Deribit, la catena via `cblib.load_chain()`.
|
||||
|
||||
## 2. L'entrata, presa dal progetto invece che inventata
|
||||
|
||||
`trend_portfolio.tsmom_blend` media tre `np.sign()` sugli orizzonti (30, 90, 180), quindi assume
|
||||
**solo** i valori {−1, −1/3, +1/3, +1} (il bucket 2/3 non esiste — errore già corretto in §57).
|
||||
Questo dà a **"forte bull" una definizione che non aggiunge nemmeno un parametro nuovo**:
|
||||
|
||||
- **gate FORTE** = `|blend| == 1`, tutti e tre gli orizzonti concordi → a mercato **52,5%** dei giorni
|
||||
- **gate LARGO** = `|blend| ≥ 1/3`, confronto dichiarato → a mercato **97,1%** dei giorni
|
||||
|
||||
Il segno dà la direzione, quindi **long e short** sono entrambi coperti come chiesto. Si entra il
|
||||
giorno **dopo** il segnale (eseguibile, §8.1).
|
||||
|
||||
## 3. La calibrazione: quello che il modello non poteva assumere
|
||||
|
||||
161.964 quote a due lati su 95 giorni (2026-05-07 → 2026-09-01), scadenze 1-15 giorni.
|
||||
|
||||
**Lo skew, ed è il fatto strutturale del filone:**
|
||||
|
||||
| \|δ\| | IV put / ATM | IV call / ATM | la put costa |
|
||||
|---|---|---|---|
|
||||
| 0,10 | 1,249 | 0,959 | **+30%** della call |
|
||||
| 0,20 | 1,132 | 0,950 | **+19%** |
|
||||
| 0,30 | 1,072 | 0,960 | **+12%** |
|
||||
|
||||
**Un collar delta-simmetrico è un debito netto**: compro l'ala cara e vendo quella a buon mercato.
|
||||
Non è un dettaglio di prezzo, è la ragione per cui "gratis" non esiste a delta simmetrici.
|
||||
Spread (mezza forchetta / mid): put 1,9-8,3%, call 2,1-10,0% secondo il delta. Struttura a termine
|
||||
IV/DVOL30 ≈ 0,92-0,94 a 2-14 giorni.
|
||||
|
||||
📌 **A5 confermata, e corregge un muro di §46:** il tick da **5 USDC** che lì era «il secondo muro,
|
||||
strutturale» vale per la famiglia **USDC**. La catena che raccogliamo è **100% inverse**
|
||||
(`BTC-31JUL26-45000-P`), col tick in BTC. Il muro di §46 **non si applica a questo filone**.
|
||||
|
||||
## 4. Il risultato, in ordine di come uccide
|
||||
|
||||
Lente lunga 2021-03-24 → 2026-09-01 (1.988 giorni, 5,44 anni). BTC buy&hold nudo: Sharpe 0,413 ·
|
||||
maxDD 76,73% · drift +7,82%/a. Griglia dichiarata prima: 3 δput × 3 δcall × 2 tenor × 2 gate = **48
|
||||
celle**, più 12 varianti zero-cost dichiarate a parte.
|
||||
|
||||
### ✅ A1 CONFERMATA — il pavimento funziona davvero (e §46 è battuto sul suo motivo)
|
||||
|
||||
**maxDD scende in 36/48 celle.** Con beta 1,0 la put para davvero: è il risultato che §46 non
|
||||
poteva ottenere. *Questa metà della domanda dell'operatore ha risposta positiva.*
|
||||
|
||||
### ❌ A3 CONFERMATA — ma il de-levering lo fa meglio in 45/48 celle
|
||||
|
||||
| gate | base gated senza opzioni | |
|
||||
|---|---|---|
|
||||
| FORTE | Sharpe 0,442 · maxDD **51,83%** · drift **+10,04%**/a | ← il null da battere |
|
||||
| LARGO | Sharpe 0,578 · maxDD **71,94%** · drift **+17,37%**/a | |
|
||||
|
||||
Il null è **lo stesso hold gated, senza opzioni, scalato a iso-maxDD** — generato dallo stesso
|
||||
motore, non ridichiarato (P1). Il collar lo batte in **3 celle su 48**.
|
||||
|
||||
### ❌ A2 CONFERMATA — ogni punto di DD risparmiato costa 2-8 punti di drift
|
||||
|
||||
Δdrift/ΔmaxDD mediano: **7,90** col gate FORTE, **1,98** col LARGO. Il tetto costa più di quanto
|
||||
il pavimento renda, e non di poco.
|
||||
|
||||
### 🚨 C9 — non è protezione, è troncatura
|
||||
|
||||
Cella migliore, 196 cicli: **il tetto taglia nel 46,2% dei cicli vincenti, il pavimento para nel
|
||||
6,7% dei cicli perdenti.** Scatta **7 volte più spesso sui vincenti che sui perdenti** — la
|
||||
definizione letterale di C9. *La regola dell'operatore («ridurre la vincita, bloccare la perdita»)
|
||||
è implementata fedelmente: il problema è che su BTC quel baratto è pagato male.*
|
||||
|
||||
### ❌ M1 — dentro il libro non aggiunge nulla
|
||||
|
||||
Collar Sharpe 0,508 contro TP01 **0,852**, corr +0,486. TP01 + 10% di collar: Sharpe **+0,000** e
|
||||
maxDD **+2,32pp**. A 25%: **−0,065** di Sharpe e **+6,57pp** di maxDD. *Peggiora proprio la cosa
|
||||
che dovrebbe proteggere.*
|
||||
|
||||
## 5. 🚨 Il fatto che vale più del verdetto
|
||||
|
||||
**Tutte e 3 le celle vincenti stanno sul BORDO** della griglia (δput al minimo, δcall al massimo).
|
||||
M8: un argmax sul bordo non è una decisione. Ho esteso la famiglia (M4) con 36 trial dichiarati
|
||||
verso l'angolo — e lì **vince in 35/36 celle**, con il massimo *nell'angolo*:
|
||||
|
||||
> gate FORTE, 7 giorni, **δput 0,02 · δcall 0,50** → Sharpe **1,471** · maxDD **18,12%** · drift **+29,99%**/a
|
||||
|
||||
Il limite di quell'angolo è *nessun pavimento, tetto ATM*: **una covered call**. Cioè **la pendenza
|
||||
porta fuori da ciò che l'operatore ha chiesto e dentro lo short-vol.**
|
||||
|
||||
E quel Sharpe 1,471 non è una scoperta — **è il mio prezzatore che si paga da solo**:
|
||||
|
||||
| | DVOL / RV-forward | DVOL sta sopra |
|
||||
|---|---|---|
|
||||
| a 7 giorni | **1,320** | **76,9%** dei giorni |
|
||||
| a 14 giorni | **1,253** | **74,8%** dei giorni |
|
||||
|
||||
Riprezzando le opzioni alla **volatilità effettivamente realizzata** (diagnostica con look-ahead
|
||||
dichiarato — è il valore equo ex-post, non una strategia):
|
||||
|
||||
| cella | a DVOL | a vol realizzata | il VRP valeva |
|
||||
|---|---|---|---|
|
||||
| FORTE 7g δ0,02/0,50 | Sh **1,471** · +29,99% | Sh **0,511** · +8,45% | **+21,54 pp (72%)** |
|
||||
| LARGO 7g δ0,02/0,50 | Sh **1,141** · +32,17% | Sh **0,114** · **−1,10%** | **+33,26 pp (tutto)** |
|
||||
| FORTE 7g δ0,10/0,30 | VINCE | **perde** | +6,75 pp |
|
||||
| LARGO 14g δ0,10/0,30 | perde | perde | +8,02 pp |
|
||||
|
||||
**Il 72-100% dell'edge dell'angolo è il premio di varianza**, non la struttura. E ciò che
|
||||
sopravvive alla riprezzatura — **Sharpe 0,511** — è, entro il rumore, **il numero che il progetto
|
||||
ha già**: VRP01 a f=0,73 vale **Sharpe 0,47**. *Non ho trovato una strategia nuova: ho ri-scoperto
|
||||
VRP01 per una strada più lunga.* E §3 lo blocca comunque: **«niente short-vol da modello in
|
||||
deploy»** — questo è esattamente short-vol da modello.
|
||||
|
||||
## 5-bis. L'uscita anticipata: chiesta, implementata, e costa
|
||||
|
||||
L'operatore ha chiesto di **uscire dalle opzioni fra il 50% e il 75% del tempo**. Implementato come
|
||||
`exit_frac`, su tenor 14 giorni (⇒ uscita a 7 / 9 / 10 giorni). **La mia ipotesi a priori era che
|
||||
migliorasse**: uscire presto recupera valore temporale sulla put e rinuncia al theta più veloce
|
||||
sulla call, cioè proprio alla parte che stampava il premio di varianza. **Metà giusta, metà no.**
|
||||
|
||||
| cella `δ0,10/0,30`, gate FORTE | uscita | Sharpe | drift | null | esito | VRP residuo |
|
||||
|---|---|---|---|---|---|---|
|
||||
| a scadenza | 1,000 | 0,488 | **+9,48%** | +8,07% | VINCE | +3,38pp |
|
||||
| 75% | 0,750 | 0,457 | +8,59% | +7,72% | VINCE | +4,93pp |
|
||||
| 62,5% | 0,625 | 0,366 | +6,11% | +7,98% | **perde** | +2,51pp |
|
||||
| 50% | 0,500 | 0,278 | **+3,84%** | +8,04% | **perde** | **+1,27pp** |
|
||||
|
||||
**Il meccanismo previsto c'è** — il VRP residuo scende da +3,38 a +1,27pp, quindi uscire presto
|
||||
*davvero* rinuncia allo short-vol — **ma lo spread lo travolge**: −5,6 punti di drift sul gate
|
||||
FORTE, −10,2 sul LARGO (12,06% → 1,88%), e su `δ0,20/0,20` da −4,37% a −8,58%.
|
||||
|
||||
🚨 **E qui §46 va corretto nella sua applicazione.** §46 misurò: *«f si paga solo sulla parte di
|
||||
valore che converge a intrinseco, quindi un roll anticipato non lo paga»*. **Vero per una copertura
|
||||
SOLO LONG.** In un collar c'è una **gamba venduta da ricomprare**: uscire prima paga f *esattamente*
|
||||
sulla parte che a scadenza si sarebbe regolata gratis, e si fa ~2× più spesso per unità di tempo.
|
||||
**L'asimmetria di §46 si inverte quando la struttura ha una gamba corta.** È il risultato
|
||||
trasferibile di questa richiesta.
|
||||
|
||||
**Nella banda chiesta (50-75%) il verdetto si spacca:** a **0,75** la cella onesta passa ancora il
|
||||
null (di poco); a **0,625 e sotto** non lo passa più. Nessun `exit_frac` ribalta il verdetto del
|
||||
filone — lo peggiora.
|
||||
|
||||
## 6. I controlli dell'apparato (M15) — 3/3
|
||||
|
||||
§46 insegna che *«un controllo positivo rotto dichiara guasto l'apparato»*. Prima di credere a un
|
||||
verdetto negativo ho verificato che l'apparato sappia riconoscere un successo:
|
||||
|
||||
| controllo | atteso | misurato | |
|
||||
|---|---|---|---|
|
||||
| pavimento a **premio zero** (pranzo gratis) | deve VINCERE | maxDD 51,83%→**36,76%**, drift +10,04%→**+35,69%** | ✅ |
|
||||
| premio **×10** | deve PERDERE | drift **−33,96%**/a | ✅ |
|
||||
| zero-cost costruibile e finito | non NaN | drift −3,50%/a | ✅ |
|
||||
|
||||
## 7. Tre difetti miei, catturati dai controlli e non a occhio
|
||||
|
||||
1. **Bisezione dello zero-cost invertita** (`hi = md` dove serve `lo = md`): il premio cresce col
|
||||
delta, quindi se incasso troppo poco il tetto va *avvicinato*. Dava Sharpe −3,0/−3,9 e drift
|
||||
−53%/a — spazzatura che al primo giro avevo quasi pubblicato.
|
||||
2. **`dcall=NaN`** nella variante zero-cost propagava NaN nella cassa allo smontaggio anticipato.
|
||||
3. **C9 non consapevole della direzione:** usavo `S1 > S0` come proxy di "ciclo vincente", ma per
|
||||
un ciclo **short** un prezzo che sale è una **perdita**. Era la causa del risultato impossibile
|
||||
*«il pavimento para nello 0,0% dei cicli perdenti»* con una put a 10 delta.
|
||||
|
||||
E due difetti di contabilità trovati prima di misurare, che avrebbero **adulato il collar**:
|
||||
la base senza opzioni rollava lo spot ogni `tenor` giorni pagando ~3,6%/anno di fee inesistenti;
|
||||
e al roll delle opzioni chiudevo e riaprivo anche lo spot, che non ha motivo di muoversi.
|
||||
|
||||
## 8. Cosa NON ho misurato, dichiarato
|
||||
|
||||
- **La lente reale (quote vere, 2026-05→09) non è stata girata come backtest.** 123 giorni = ~8
|
||||
cicli non sovrapposti: sotto-potenziata per costruzione, e su una finestra in cui BTC è salito da
|
||||
~64,7k a ~77,5k — **avversa a un collar per costruzione**. La catena è servita a **calibrare**
|
||||
(skew, termine, spread), che è l'uso in cui 161.964 quote hanno potenza.
|
||||
- **Nessun DSR, nessun `study_family_honest`.** Non servono: il filone cade al **primo** gate (M5),
|
||||
e M2 si spende su ciò che il primo gate lascia in piedi.
|
||||
- **A6 e A7 non verificate** (lente reale non girata; il verso short è dentro i gate ma non
|
||||
separato). Restano previsioni non misurate, non risultati.
|
||||
- **Il funding dei perp non è nel motore**, come in ogni backtest del progetto: −2,16%/a di drift.
|
||||
Colpisce base e collar quasi allo stesso modo (stessa esposizione spot), quindi **non cambia il
|
||||
segno del confronto**, ma abbassa entrambi.
|
||||
|
||||
## 9. Verdetto
|
||||
|
||||
> **`IL PAVIMENTO FUNZIONA — E' IL TETTO CHE NON SI PUO' PAGARE. E CIO' CHE VINCE SUL BORDO NON E'
|
||||
> LA PROTEZIONE: E' IL PREMIO DI VARIANZA, CIOE' VRP01`** — **REFUTATO come chiesto.**
|
||||
|
||||
Con una postilla che è il vero risultato trasferibile: **la domanda «bloccare la perdita» ha
|
||||
risposta positiva sul maxDD (36/48) e negativa sul prezzo (45/48).** Su BTC il pavimento si compra
|
||||
meglio **tenendo meno BTC** che comprando una put e vendendo una call — e il de-levering, a
|
||||
differenza del collar, non tocca il rendimento nella coda destra dove BTC vive.
|
||||
|
||||
## 10. Cosa lo riaprirebbe
|
||||
|
||||
- Un **f di stress misurato su un crash catturato** — la stessa condizione che §3 pone allo
|
||||
short-vol. Con quello, l'angolo covered-call diventa discutibile invece che escluso.
|
||||
- Un sottostante **senza coda destra grassa**: il tetto costa perché BTC vive lì. Su un asset a
|
||||
distribuzione più simmetrica il baratto cambia di segno, e il conto va rifatto.
|
||||
- **Non** lo riapre un tenor diverso, un delta diverso o una griglia più fine: la pendenza è
|
||||
monotona verso l'angolo short-vol, e l'angolo è già misurato.
|
||||
@@ -1452,3 +1452,54 @@ una maschera a caso della stessa frequenza**.
|
||||
dall'aria seria. Se ne è accorta solo la riga `giorni in cui il LIBRO e' flat: 0 = 0.0%`.
|
||||
**REGOLA: stampare sempre la CARDINALITÀ di una maschera prima di usarla** — un filtro vuoto non
|
||||
solleva, produce il caso degenere e lo veste da risultato.
|
||||
|
||||
---
|
||||
|
||||
- **COLLAR01 "hold BTC gated dal trend + collar di opzioni <=15 giorni" — REFUTATO (2026-09-01)** —
|
||||
`scripts/research/r0901_btc_collar.py`, diario `2026-09-01-collar-btc.md`, registro §71.
|
||||
Chiesto dall'operatore in quattro battute: *hold BTC long o short coperto in opzioni* + *opzione
|
||||
max 15gg* + ***"ridurre la vincita, ma bloccare la perdita"*** (⇒ collar, non put protettiva) +
|
||||
*entrata gestita da indicatori (es. forte bull)*.
|
||||
🚨 **Si poteva riaprire dopo §46 perche' ne batte il MOTIVO, non perche' ne ripete la misura:**
|
||||
§46 cadde sul **beta +0,076 del libro** (*"non si assicura un libro che nei crash e' gia' quasi
|
||||
piatto"*), qui il sottostante e' un **hold di BTC, beta 1,0**. E §46 dichiarava non provata la
|
||||
copertura **gated su regime**, che e' questa.
|
||||
**L'entrata non ha aggiunto un solo parametro:** `tsmom_blend` media tre `np.sign()` su (30,90,180)
|
||||
⇒ valori in {−1,−1/3,+1/3,+1}, quindi *"forte bull"* = `|blend|==1` (52,5% dei giorni) contro il
|
||||
confronto dichiarato `|blend|>=1/3` (97,1%); il segno da' long **e** short.
|
||||
✅ **A1 CONFERMATA — il pavimento FUNZIONA: maxDD scende in 36/48 celle** (§46: saliva in 162/162).
|
||||
*La meta' della domanda dell'operatore ha risposta positiva.*
|
||||
❌ **Ma il de-levering lo fa meglio in 45/48**, e il baratto costa **2-8 punti di drift per punto
|
||||
di DD** (Δdrift/ΔmaxDD **7,90** gate forte, **1,98** largo).
|
||||
🚨 **C9 in forma pura: il tetto taglia il 46,2% dei cicli VINCENTI, il pavimento para il 6,7% dei
|
||||
PERDENTI** — 7x piu' spesso sui vincenti: *troncatura, non protezione*.
|
||||
❌ **M1:** collar Sharpe 0,508 vs TP01 **0,852**; TP01+10% ⇒ **+0,000** di Sharpe e **+2,32pp** di
|
||||
maxDD — peggiora proprio cio' che dovrebbe proteggere.
|
||||
🚨 **IL RISULTATO TRASFERIBILE STA NELL'ESTENSIONE, NON NELLA GRIGLIA.** Le 3 celle vincenti
|
||||
stavano tutte **sul bordo**; estesa la famiglia vince **35/36** con il massimo **nell'angolo**
|
||||
(δput 0,02 / δcall 0,50, Sharpe **1,471**) — e il limite di quell'angolo e' *nessun pavimento,
|
||||
tetto ATM* = **una covered call**: **la pendenza porta fuori dalla domanda posta e dentro lo
|
||||
short-vol**. E quel 1,471 era **il prezzatore che si paga da solo**: **DVOL/RV-forward 1,320 a 7g,
|
||||
sopra nel 76,9% dei giorni** ⇒ riprezzato alla vol vera l'angolo cade a **0,511** (VRP = **72%**
|
||||
del drift; col gate largo **il 100%**, drift **−1,10%**). E **0,511 e' VRP01** (ShFULL 0,47 a
|
||||
f=0,73): *non una scoperta, VRP01 ri-trovato per una strada piu' lunga*. §3 lo blocca comunque.
|
||||
🚨 **USCITA ANTICIPATA (chiesta: 50-75% del tempo) — implementata e COSTA**, e produce il secondo
|
||||
risultato trasferibile. Cella onesta, gate FORTE: drift **+9,48% (scadenza) -> +3,84% (uscita al
|
||||
50%)**, esito da VINCE a **perde sotto 0,75**; gate LARGO **12,06% -> 1,88%**. Il meccanismo
|
||||
previsto c'e' (il VRP residuo scende **+3,38 -> +1,27pp**: uscire presto rinuncia davvero allo
|
||||
short-vol) **ma lo spread lo travolge**. 📌 **Corregge l'APPLICAZIONE di §46:** *"un roll anticipato
|
||||
non paga f"* vale per una copertura **solo LONG**; in un collar la **gamba venduta va RICOMPRATA**,
|
||||
quindi uscire prima paga f *esattamente* sulla parte che a scadenza si regolava gratis, ~2x piu'
|
||||
spesso. **L'asimmetria di §46 si INVERTE quando la struttura ha una gamba corta.**
|
||||
✅ **Controlli dell'apparato 3/3** (la lezione di §46): pranzo gratis riconosciuto, premio x10
|
||||
rifiutato, zero-cost finito.
|
||||
⚠️ **Cinque difetti miei catturati dai controlli:** bisezione zero-cost **invertita** (dava Sharpe
|
||||
−3,9); `dcall=NaN` che propagava NaN nella cassa; **C9 non consapevole della direzione** (`S1>S0`
|
||||
non e' "vincente" per un ciclo short — dava l'impossibile *"pavimento 0,0%"*); e due di
|
||||
contabilita' che **avrebbero adulato il collar** (la base che rollava lo spot pagando ~3,6%/a di
|
||||
fee inesistenti, e il roll che chiudeva lo spot senza motivo).
|
||||
📌 **Corregge un muro di §46:** il **tick da 5 USDC** e' della famiglia **USDC**; la catena che
|
||||
raccogliamo e' **100% inverse** ⇒ quel muro **non si applica** a questo filone.
|
||||
**Cosa lo riapre:** un **f di stress su un crash catturato** (la condizione che §3 pone allo
|
||||
short-vol), o un sottostante **senza coda destra grassa**. **Non** lo riaprono tenor, delta o
|
||||
griglie piu' fini: la pendenza e' monotona verso l'angolo, e l'angolo e' gia' misurato.
|
||||
|
||||
@@ -17,6 +17,7 @@ null de-levering superato + eseguibilita' al capitale dichiarato.
|
||||
| 8 | VOL-SIZE | **LEAD RIDIMENSIONATO** — gate 22/12 **gia' fallito oggi** | dare a SKH01 una size per-trade regge a **23/23 ancore**, 8/8 anni, null di permutazione e trasferimento su V1 — ma vale **+0,07 di Sharpe di libro, un terzo della fortuna d'ancora del libro stesso (+0,196)**. Il vol-target di **libro** e' invece falsificato: compra peso SKH gia' respinto e peggiora l'eseguibilita' |
|
||||
| — | **XSR-REPRO** (integrita') | 🚨 **DIFETTO DI PRODUZIONE** | il numero 1.82 e' SPIEGATO e non era sbagliato (era su una **terza** lente, e su una barra non ancora chiusa) — ma cercandone la causa e' emerso che **`paper_xsr` registra ~41 minuti di mercato al giorno**, non un giorno. Tre gate pre-registrati leggono serie costruite cosi' |
|
||||
| 70 | **XSR-RENDITA** | **SCARTATO sotto la lente RENDITA** (non come sleeve) | il muro scende del 20,6% a iso-rischio, ma **mescolare i rendimenti di XSR01 da' lo stesso muro** (il meccanismo vale 0,8%, dentro la risoluzione MC) e un **conto remunerato allo stesso tasso lo eguaglia a vol 0 e senza secondo venue**. A drift zero il muro **sale**: si compra un drift scorrelato, non la scorrelazione. E l'haircut non pareggia un conto al 4% **nemmeno a haircut ZERO** |
|
||||
| 71 | **COLLAR01** | hold BTC gated dal trend, coperto da un collar (pavimento comprato, tetto venduto) a scadenza <=15g: "ridurre la vincita, bloccare la perdita" batte il de-levering? | **REFUTATO** — ✅ il pavimento FUNZIONA (maxDD scende in **36/48**, §46 battuto sul suo motivo: qui beta=1,0) ❌ ma costa **2-8 punti di drift per punto di DD** e il de-levering vince **45/48**. 🚨 Le 3 celle vincenti stanno sul **bordo**: estesa la famiglia vince 35/36 **nell'angolo covered-call**, e **il 72-100% di quell'edge e' il PREMIO DI VARIANZA** (DVOL sta **32% sopra** la RV-forward): riprezzato alla vol vera l'angolo cade da Sharpe **1,471 a 0,511** — che e' VRP01 (0,47), non una scoperta |
|
||||
| 5 | SKEW | **SCARTATO** (Q1, Q2) + **LEAD** (Q3, gate 2027-02-22) | il prezzo muove lo skew (t 3,1-9,4 su 8/8 test), **non il contrario** (max |t| in avanti 2,35 contro 2,08 atteso dal rumore). Ma Q3 e' grosso: **il f=0,73 di VRP01 e' per il 42% STRUTTURA A TERMINE e solo per il 25% skew** |
|
||||
| — | **HL-EXEC** (audit di fatto) | **3 falsificazioni misurate** | il pavimento vero e' **$10 (non $5)** e il taker **4,50 bps (non 5,0)** — ma il *"XS01 serve ~$20k"* e' **refutato del tutto** (nessuna soglia da min-order), il *"XSR01 ~$5.000"* e' **conservativo di 1,7x** (vero ~$3.000), e lo **slippage "rischio #1" di XSR01 e' refutato** con margine **21x** |
|
||||
| — | **SLIP-AUDIT** (audit di fatto) | **SCARTATO** = nessun costo nascosto a questa taglia | i backtest a 10 bps RT restano **conservativi di ~1,5 bps/lato**; nessuna evidenza di impatto sopravvive al null (p=0,274; estremi avversi **1/18 contro 4,7 attesi**). **Ma la misura ha una data di scadenza** |
|
||||
@@ -3799,3 +3800,90 @@ rendita non e' lo strumento**, e che il gate del 23/10 sta per decidere su una g
|
||||
capitale **non e' comprabile**.
|
||||
|
||||
**Con questa sono 70 filoni e 0 candidati promossi.**
|
||||
|
||||
---
|
||||
|
||||
## 71 — COLLAR01 (hold BTC gated dal trend, coperto da un collar a <=15 giorni)
|
||||
|
||||
`scripts/research/r0901_btc_collar.py`. **Libro, pesi, cron, config INVARIATI. Nessun ordine.**
|
||||
Domanda dell'operatore, con tre precisazioni in corso d'opera che ne cambiano l'oggetto: *"hold BTC
|
||||
long o short con copertura in opzioni"* + *"opzione max 15gg"* + *"ridurre la vincita, ma bloccare
|
||||
la perdita"* (⇒ **collar**, non put protettiva) + *"l'entrata gestita da indicatori (es. forte bull)"*.
|
||||
|
||||
🚨 **PERCHE' SI POTEVA RIAPRIRE DOPO §46.** §46 fu refutato perche' **il beta del libro al
|
||||
sottostante e' +0,076** — *"non si assicura un libro che nei crash e' gia' quasi piatto"*. Qui il
|
||||
sottostante e' un **hold di BTC nudo, beta 1,0**: il motivo non si applica. E §46 dichiara di non
|
||||
aver provato la copertura **gated su regime**, che e' esattamente questa.
|
||||
|
||||
**L'entrata non aggiunge parametri:** `tsmom_blend` media tre `np.sign()` su (30, 90, 180) ⇒ valori
|
||||
in {−1, −1/3, +1/3, +1}, quindi *"forte bull"* = `|blend|==1` (3/3 orizzonti, a mercato **52,5%**
|
||||
dei giorni) contro il confronto dichiarato `|blend|>=1/3` (**97,1%**). Il segno da' la direzione:
|
||||
long **e** short, come chiesto.
|
||||
|
||||
**CALIBRAZIONE dalla catena vera** (161.964 quote a due lati, 95 giorni, DTE 1-15). **Lo skew e' il
|
||||
fatto strutturale:** IV_put/ATM contro IV_call/ATM = **1,249 vs 0,959** a |δ|0,10 · 1,132 vs 0,950 a
|
||||
0,20 · 1,072 vs 0,960 a 0,30 ⇒ **un collar delta-simmetrico e' un DEBITO netto**, si compra l'ala
|
||||
cara e si vende quella a buon mercato. 📌 **Corregge un muro di §46:** il **tick da 5 USDC** e' della
|
||||
famiglia **USDC**; la catena che raccogliamo e' **100% inverse** ⇒ **quel muro non si applica qui**.
|
||||
|
||||
| esito | misura |
|
||||
|---|---|
|
||||
| ✅ **A1 — il pavimento FUNZIONA** | maxDD scende in **36/48** celle (§46: saliva in 162/162) |
|
||||
| ❌ **A3 — ma il de-levering lo fa meglio** | il collar batte il null a iso-maxDD in **3/48** |
|
||||
| ❌ **A2 — il prezzo del baratto** | Δdrift/ΔmaxDD mediano **7,90** (gate forte) / **1,98** (largo) |
|
||||
| 🚨 **C9 — troncatura, non protezione** | il tetto taglia il **46,2%** dei cicli **vincenti**, il pavimento para il **6,7%** dei **perdenti**: 7x piu' spesso sui vincenti |
|
||||
| ❌ **M1 — dentro il libro non aggiunge** | collar Sharpe 0,508 vs TP01 **0,852**; TP01+10% ⇒ Sharpe **+0,000** e maxDD **+2,32pp** |
|
||||
|
||||
🚨 **IL FATTO CHE VALE PIU' DEL VERDETTO — e che si vede solo estendendo la griglia (M4/M8).** Tutte
|
||||
e 3 le celle vincenti stanno sul **BORDO** (δput min, δcall max). Estesa la famiglia con 36 trial
|
||||
dichiarati verso l'angolo: **vince 35/36**, massimo **nell'angolo** (FORTE 7g, δput 0,02 / δcall
|
||||
0,50) = Sharpe **1,471**, maxDD 18,12%, drift **+29,99%/a**. Il limite di quell'angolo e' *nessun
|
||||
pavimento, tetto ATM* = **una covered call**: **la pendenza porta FUORI da cio' che l'operatore ha
|
||||
chiesto e DENTRO lo short-vol.**
|
||||
🚨 **E quel Sharpe e' il prezzatore che si paga da solo.** **DVOL / RV-forward mediana 1,320 a 7g**
|
||||
(DVOL sta sopra nel **76,9%** dei giorni), 1,253 a 14g. Riprezzando alla vol **realizzata**
|
||||
(look-ahead dichiarato, valore equo ex-post): l'angolo cade **1,471 -> 0,511** di Sharpe (VRP
|
||||
**+21,54pp = 72%** del drift) col gate FORTE, e **1,141 -> 0,114** col LARGO (VRP **+33,26pp = tutto**;
|
||||
drift **−1,10%**). Le celle con un pavimento VERO (δ0,10/0,30) passano da VINCE a **perde**.
|
||||
📌 **Cio' che sopravvive — Sharpe 0,511 — e' il numero che il progetto GIA' HA:** VRP01 a f=0,73 vale
|
||||
ShFULL **0,47**. *Non e' una strategia nuova: e' VRP01 ri-scoperto per una strada piu' lunga*, e §3
|
||||
lo blocca comunque (**"niente short-vol da modello in deploy"**).
|
||||
|
||||
🚨 **USCITA ANTICIPATA (chiesta dall'operatore: 50-75% del tempo) — IMPLEMENTATA, E COSTA.**
|
||||
`exit_frac` su tenor 14g (uscita a 7/9/10 giorni). Cella onesta δ0,10/0,30 gate FORTE: drift
|
||||
**+9,48% (scadenza) -> +8,59% (0,75) -> +6,11% (0,625) -> +3,84% (0,50)**, e l'esito passa da VINCE a
|
||||
**perde sotto 0,75**; gate LARGO **12,06% -> 1,88%**. ⚠️ **Il meccanismo previsto c'e'** — il VRP
|
||||
residuo scende da +3,38 a **+1,27pp**, quindi uscire presto rinuncia davvero allo short-vol — **ma lo
|
||||
spread lo travolge**. 📌 **E CORREGGE L'APPLICAZIONE DI §46:** *"f si paga solo sulla parte di valore
|
||||
che converge a intrinseco, quindi un roll anticipato non lo paga"* vale per una copertura **SOLO
|
||||
LONG**; in un collar c'e' una **gamba VENDUTA da RICOMPRARE**, e uscire prima paga f *esattamente*
|
||||
sulla parte che a scadenza si sarebbe regolata gratis, ~2x piu' spesso per unita' di tempo.
|
||||
**L'asimmetria di §46 si INVERTE quando la struttura ha una gamba corta.**
|
||||
|
||||
✅ **CONTROLLI DELL'APPARATO 3/3** (M15, la lezione di §46): pavimento a **premio zero** riconosciuto
|
||||
come vittoria (maxDD 51,83%→**36,76%**, drift +10,04%→**+35,69%**); premio **x10** rifiutato (drift
|
||||
**−33,96%/a**); zero-cost costruibile e finito.
|
||||
|
||||
⚠️ **CINQUE DIFETTI MIEI, tutti catturati dai controlli e non a occhio.** (i) **bisezione dello
|
||||
zero-cost invertita** — il premio cresce col delta, quindi a incasso insufficiente il tetto va
|
||||
*avvicinato*: dava Sharpe −3,0/−3,9 e drift −53%/a; (ii) `dcall=NaN` propagava NaN nella cassa allo
|
||||
smontaggio anticipato; (iii) **C9 non consapevole della direzione** — `S1>S0` non e' "vincente" per
|
||||
un ciclo **short**: era la causa dell'impossibile *"il pavimento para nello 0,0% dei perdenti"* con
|
||||
una put a 10 delta; (iv) la base senza opzioni **rollava lo spot** ogni tenor pagando ~3,6%/a di fee
|
||||
inesistenti (**avrebbe adulato il collar**); (v) al roll delle opzioni chiudevo e riaprivo **anche lo
|
||||
spot**, che non ha motivo di muoversi.
|
||||
|
||||
⚠️ **NON MISURATO, DICHIARATO:** la **lente reale** (quote vere, 123 giorni = ~8 cicli) non e' stata
|
||||
girata come backtest — sotto-potenziata, e su una finestra in cui BTC e' salito da ~64,7k a ~77,5k,
|
||||
**avversa a un collar per costruzione**; la catena e' servita a **calibrare**, dove 161.964 quote
|
||||
hanno potenza. Nessun DSR e nessun `study_family_honest`: il filone cade al **primo** gate (M5), e M2
|
||||
si spende su cio' che il primo gate lascia in piedi. A6/A7 restano previsioni non misurate. Il
|
||||
**funding** non e' nel motore (−2,16%/a) ma colpisce base e collar quasi allo stesso modo: abbassa
|
||||
entrambi, **non cambia il segno**.
|
||||
|
||||
**VERDETTO: `IL PAVIMENTO FUNZIONA — E' IL TETTO CHE NON SI PUO' PAGARE; E CIO' CHE VINCE SUL BORDO
|
||||
NON E' LA PROTEZIONE, E' IL PREMIO DI VARIANZA (VRP01)`** — REFUTATO.
|
||||
**Cosa lo riapre:** un **f di stress misurato su un crash catturato** (la stessa condizione che §3
|
||||
pone allo short-vol), o un sottostante **senza coda destra grassa** — il tetto costa perche' BTC vive
|
||||
li'. **Non** lo riaprono tenor, delta o griglie piu' fini: la pendenza e' monotona verso l'angolo, e
|
||||
l'angolo e' gia' misurato.
|
||||
|
||||
@@ -0,0 +1,715 @@
|
||||
"""COLLAR01 — hold BTC (long/short) con COLLAR di opzioni a <=15 giorni.
|
||||
|
||||
DOMANDA (posta dall'operatore il 2026-09-01):
|
||||
"crea una strategia di hold BTC in long o short con copertura con options (max 15gg)"
|
||||
+ due precisazioni in corso d'opera:
|
||||
"opzione deve essere al max di 15gg" -> vincolo sulla SCADENZA, non sul solo holding
|
||||
"voglio ridurre la vincita, ma bloccare la perdita" -> NON e' una put protettiva: e' un COLLAR
|
||||
(pavimento comprato, tetto venduto per finanziarlo).
|
||||
"l'entrata deve essere gestita da una strategia confortata da indicatori (es. forte bull)"
|
||||
"dalle opzioni dobbiamo uscire prima del termine (tra 50% e 75% del tempo)" -> `exit_frac`.
|
||||
|
||||
PERCHE' QUESTO FILONE NON E' §46 (TAIL-HEDGE, REFUTATO il 2026-08-23).
|
||||
§46 compro' put deep-OTM sul LIBRO (TP01+SKH01) e fu refutato *prima del prezzo*: il maxDD
|
||||
saliva in 162/162 celle perche' **il beta del libro al sottostante e' +0,076** — "non si
|
||||
assicura un libro che nei crash e' gia' quasi piatto".
|
||||
Qui il sottostante e' un **hold di BTC nudo, beta 1,0 per costruzione**: il motivo che uccise
|
||||
§46 NON si applica, ed e' l'unica ragione per cui questo filone puo' essere riaperto
|
||||
(CLAUDE.md: "chi la riapre deve battere il motivo, non ripetere la misura").
|
||||
§46 dichiara inoltre di NON aver provato la copertura dinamica e di aver misurato la sola
|
||||
statica: anche quella porta e' aperta, ma qui non la uso — il collar e' statico a griglia.
|
||||
|
||||
COSA E' GIA' NOTO E VIENE RIUSATO, NON RIMISURATO:
|
||||
- metriche e i due null di scala (`k_for_same_dd`, `k_for_same_drift`) da r0823_tail_hedge;
|
||||
- listino fee opzioni Deribit (taker 0,03% del sottostante, cap 12,5% del premio, consegna
|
||||
0,015%) dallo stesso;
|
||||
- la catena reale via `cblib.load_chain()` (archivio 2026-05-01 + raccolta propria 2026-07-30).
|
||||
|
||||
LE DUE LENTI, ed e' una scelta dichiarata:
|
||||
(A) LENTE LUNGA (decide): 2021-03-24 -> oggi, la finestra del DVOL — ~5,4 anni. Opzioni
|
||||
prezzate a modello (BS, r=0) con DVOL come ancora ATM, CORRETTA da skew e struttura a
|
||||
termine MISURATI SULLA CATENA VERA, e spread f misurato per ala. E' l'impianto di §46.
|
||||
(B) LENTE REALE (controlla il segno, NON decide): 2026-05-01 -> oggi, solo quote a due lati.
|
||||
123 giorni = ~8 cicli a 15 giorni: sotto-potenziata per costruzione, e per giunta su una
|
||||
finestra in cui BTC e' salito da ~64,7k a ~77,5k, che e' AVVERSA a un collar (il tetto si
|
||||
paga proprio li'). Si legge il segno, mai la taglia.
|
||||
|
||||
M5 (null del de-levering) E' IL PRIMO TEST, NON L'ULTIMO: ogni claim "meno drawdown" si misura
|
||||
contro "tieni semplicemente meno BTC" a iso-maxDD. Se il collar non batte quello, non e'
|
||||
protezione: e' una ri-scalatura pagata cara.
|
||||
C9 e' il secondo: "una regola d'uscita che scatta piu' spesso sui vincenti che sui perdenti non e'
|
||||
protezione, e' troncatura" — qui il tetto e' letteralmente quella regola.
|
||||
|
||||
NESSUN ORDINE. Libro, pesi, cron, config INVARIATI.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
from scipy.stats import norm
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
sys.path.insert(0, str(ROOT))
|
||||
sys.path.insert(0, str(ROOT / "scripts" / "research"))
|
||||
|
||||
import cblib # noqa: E402
|
||||
from src.data.downloader import load_data # noqa: E402
|
||||
|
||||
DPY = 365.25
|
||||
|
||||
# ---- listino Deribit opzioni (identico a r0823_tail_hedge, non ridichiarato a mano) -----------
|
||||
OPT_TAKER_UNDERLYING = 0.0003
|
||||
OPT_FEE_CAP_FRAC = 0.125
|
||||
OPT_DELIVERY_UNDERLYING = 0.00015
|
||||
|
||||
# ---- GRIGLIA DICHIARATA PRIMA DI GUARDARE I RISULTATI -----------------------------------------
|
||||
G_DPUT = (0.10, 0.20, 0.30) # |delta| della put comprata = il PAVIMENTO
|
||||
G_DCALL = (0.10, 0.20, 0.30) # delta della call venduta = il TETTO
|
||||
G_TENOR = (7, 14) # giorni a scadenza: <=15, vincolo dell'operatore
|
||||
N_CELLS = len(G_DPUT) * len(G_DCALL) * len(G_TENOR)
|
||||
|
||||
# varianti dichiarate a parte (non nella griglia, per non gonfiare i trial)
|
||||
V_ZEROCOST = "zero-cost" # tetto scelto per finanziare esattamente il pavimento
|
||||
|
||||
ATTESE = """
|
||||
ATTESE A PRIORI — scritte e stampate PRIMA di misurare (M12: un follow-up contiene una previsione).
|
||||
A1 Il maxDD SCENDE davvero, a differenza di §46: il beta e' 1,0 e il pavimento morde.
|
||||
(se A1 fallisse, il filone cade come §46 e per lo stesso motivo)
|
||||
A2 Il drift scende PIU' del maxDD, perche' il rendimento di BTC vive nella coda destra e il
|
||||
tetto la taglia. Previsione: rapporto Δdrift/ΔmaxDD > 1.
|
||||
A3 Il collar PERDE contro il null del de-levering a iso-maxDD in piu' della meta' delle celle.
|
||||
E' la previsione centrale del filone.
|
||||
A4 Lo skew rende il collar delta-simmetrico un DEBITO netto (put piu' cara della call).
|
||||
[gia' misurato prima di scrivere: +31% / +19% / +12% a |d| 0,10 / 0,20 / 0,30 -> CONFERMATA]
|
||||
A5 Il tick morde MENO che in §46: quella era la famiglia USDC (tick 5 USDC); la catena che
|
||||
raccogliamo e' 100% inverse, con tick in BTC.
|
||||
A6 Nella lente reale (BTC +20% in 123 giorni) il collar sembrera' pessimo: finestra avversa,
|
||||
non evidenza. Va letta come segno, non come taglia.
|
||||
A7 Il verso SHORT sara' peggiore del LONG: il drift di BTC e' positivo e lo short lo paga tutto,
|
||||
col collar che ne cappa il solo rimbalzo favorevole.
|
||||
"""
|
||||
|
||||
|
||||
# ==================================================================== 0. METRICHE E NULL (da §46)
|
||||
|
||||
def _sh(x) -> float:
|
||||
r = np.asarray(pd.Series(x).dropna().values, float)
|
||||
return float(np.mean(r) / np.std(r) * math.sqrt(DPY)) if len(r) > 2 and np.std(r) > 0 else 0.0
|
||||
|
||||
|
||||
def _dd(x) -> float:
|
||||
r = np.asarray(pd.Series(x).dropna().values, float)
|
||||
eq = np.cumprod(1.0 + r)
|
||||
pk = np.maximum.accumulate(eq)
|
||||
return float(np.max((pk - eq) / pk)) if len(eq) else 0.0
|
||||
|
||||
|
||||
def _drift(x) -> float:
|
||||
r = np.asarray(pd.Series(x).dropna().values, float)
|
||||
if len(r) < 2:
|
||||
return 0.0
|
||||
tot = float(np.prod(1.0 + r))
|
||||
return -1.0 if tot <= 0 else float(tot ** (DPY / len(r)) - 1.0)
|
||||
|
||||
|
||||
def k_for_same_dd(base: np.ndarray, target_dd: float) -> float:
|
||||
"""La scala costante che porta il maxDD del NUDO esattamente a quello del coperto (bisezione)."""
|
||||
lo, hi = 1e-4, 1.0
|
||||
if _dd(hi * base) <= target_dd:
|
||||
return 1.0
|
||||
for _ in range(60):
|
||||
mid = 0.5 * (lo + hi)
|
||||
if _dd(mid * base) > target_dd:
|
||||
hi = mid
|
||||
else:
|
||||
lo = mid
|
||||
return 0.5 * (lo + hi)
|
||||
|
||||
|
||||
# ==================================================================== 1. CALIBRAZIONE DALLA CATENA
|
||||
|
||||
_BANDS = (0.05, 0.10, 0.15, 0.20, 0.25, 0.30, 0.40, 0.50)
|
||||
|
||||
|
||||
def calibra_catena() -> dict:
|
||||
"""Skew, struttura a termine e spread f: MISURATI sulla catena vera, mai assunti.
|
||||
|
||||
Restituisce funzioni di correzione applicate al DVOL (che e' un ATM a 30 giorni) per ottenere
|
||||
l'IV di una gamba a delta e scadenza dati. Senza queste il modello prezzerebbe la put e la
|
||||
call allo STESSO vol: l'errore andrebbe tutto a favore del collar (A4).
|
||||
"""
|
||||
df = cblib.load_chain()
|
||||
b = df[df["asset"].eq("BTC")].copy()
|
||||
b["exp"] = pd.to_datetime(b["exp"], utc=True)
|
||||
b["ts"] = pd.to_datetime(b["ts"], utc=True)
|
||||
b["dte"] = (b["exp"] - b["ts"]).dt.total_seconds() / 86400.0
|
||||
q = b[(b["dte"] > 1) & (b["dte"] <= 15) & b["bid"].gt(0) & b["ask"].gt(0)
|
||||
& b["iv"].notna() & b["delta"].notna()].copy()
|
||||
q["ad"] = q["delta"].abs()
|
||||
|
||||
atm = q[q["ad"].between(0.45, 0.55)].groupby(["ts", "exp"])["iv"].mean().rename("iv_atm")
|
||||
q = q.join(atm, on=["ts", "exp"])
|
||||
q = q[q["iv_atm"].notna() & q["iv_atm"].gt(0)]
|
||||
|
||||
skew = {}
|
||||
for t, nm in (("P", "put"), ("C", "call")):
|
||||
xs, ys = [], []
|
||||
for d in _BANDS:
|
||||
lo, hi = d * 0.85, d * 1.15
|
||||
s = q[q["option_type"].eq(t) & q["ad"].between(lo, hi)]
|
||||
if len(s) >= 200:
|
||||
xs.append(d)
|
||||
ys.append(float((s["iv"] / s["iv_atm"]).median()))
|
||||
skew[nm] = (np.array(xs), np.array(ys))
|
||||
|
||||
# struttura a termine: IV ATM a DTE t contro il DVOL 30g dello stesso istante
|
||||
dv = cblib.dvol_series("BTC")
|
||||
a = q[q["ad"].between(0.45, 0.55)].copy()
|
||||
a["giorno"] = a["ts"].dt.floor("D")
|
||||
dvd = dv.copy()
|
||||
dvd.index = pd.to_datetime(dvd.index, utc=True).floor("D")
|
||||
dvd = dvd.groupby(level=0).last()
|
||||
a = a.join(dvd.rename("dvol"), on="giorno")
|
||||
a = a[a["dvol"].notna() & a["dvol"].gt(0)]
|
||||
a["ratio"] = (a["iv"] / 100.0) / (a["dvol"] / 100.0) if a["iv"].median() > 3 else a["iv"] / (a["dvol"] / 100.0)
|
||||
ts_x, ts_y = [], []
|
||||
for lo, hi in ((1, 4), (4, 8), (8, 12), (12, 15)):
|
||||
s = a[a["dte"].between(lo, hi)]
|
||||
if len(s) >= 200:
|
||||
ts_x.append(0.5 * (lo + hi))
|
||||
ts_y.append(float(s["ratio"].median()))
|
||||
|
||||
# spread relativo per ala e per banda di delta (mezza forchetta su mid) = il costo di f
|
||||
spread = {}
|
||||
for t, nm in (("P", "put"), ("C", "call")):
|
||||
xs, ys = [], []
|
||||
for d in _BANDS:
|
||||
lo, hi = d * 0.85, d * 1.15
|
||||
s = q[q["option_type"].eq(t) & q["ad"].between(lo, hi)]
|
||||
s = s[s["mid"].gt(0)]
|
||||
if len(s) >= 200:
|
||||
xs.append(d)
|
||||
ys.append(float((((s["ask"] - s["bid"]) / 2.0) / s["mid"]).median()))
|
||||
spread[nm] = (np.array(xs), np.array(ys))
|
||||
|
||||
return dict(skew=skew, term=(np.array(ts_x), np.array(ts_y)), spread=spread,
|
||||
n_quote=len(q), giorni=int(q["ts"].dt.date.nunique()),
|
||||
span=(q["ts"].min(), q["ts"].max()))
|
||||
|
||||
|
||||
def _interp(tab, x):
|
||||
xs, ys = tab
|
||||
if len(xs) == 0:
|
||||
return 1.0
|
||||
return float(np.interp(x, xs, ys))
|
||||
|
||||
|
||||
# ==================================================================== 2. PREZZATORE
|
||||
|
||||
def _d1(S, K, T, sig):
|
||||
return (math.log(S / K) + 0.5 * sig * sig * T) / (sig * math.sqrt(T))
|
||||
|
||||
|
||||
def bs_call(S, K, T, sig):
|
||||
if T <= 0 or sig <= 0:
|
||||
return max(S - K, 0.0)
|
||||
d1 = _d1(S, K, T, sig)
|
||||
return S * norm.cdf(d1) - K * norm.cdf(d1 - sig * math.sqrt(T))
|
||||
|
||||
|
||||
def bs_put(S, K, T, sig):
|
||||
if T <= 0 or sig <= 0:
|
||||
return max(K - S, 0.0)
|
||||
d1 = _d1(S, K, T, sig)
|
||||
return K * norm.cdf(-(d1 - sig * math.sqrt(T))) - S * norm.cdf(-d1)
|
||||
|
||||
|
||||
def strike_da_delta(S, T, sig_atm, cal, delta, tipo):
|
||||
"""Strike che realizza |delta| dato, risolvendo la circolarita' skew<->delta per punto fisso.
|
||||
|
||||
L'IV dipende dal delta (skew) e il delta dipende dallo strike che dipende dall'IV: due
|
||||
iterazioni bastano (verificato: la terza muove lo strike di <0,1%).
|
||||
"""
|
||||
sig = sig_atm * _interp(cal["skew"][tipo], delta)
|
||||
K = S
|
||||
for _ in range(3):
|
||||
z = norm.ppf(delta) if tipo == "call" else -norm.ppf(delta)
|
||||
K = S * math.exp(0.5 * sig * sig * T - z * sig * math.sqrt(T))
|
||||
sig = sig_atm * _interp(cal["skew"][tipo], delta)
|
||||
return K, sig
|
||||
|
||||
|
||||
def _fee(prem, S):
|
||||
return min(OPT_TAKER_UNDERLYING * S, OPT_FEE_CAP_FRAC * prem) if prem > 0 else 0.0
|
||||
|
||||
|
||||
# ==================================================================== 3. IL MOTORE DEL COLLAR
|
||||
|
||||
def serie_giornaliera() -> pd.DataFrame:
|
||||
"""BTC giornaliero + DVOL, sulla finestra in cui ESISTONO entrambi (2021-03 in poi)."""
|
||||
d = load_data("BTC", "1h")
|
||||
idx = pd.DatetimeIndex(pd.to_datetime(d["datetime"], utc=True))
|
||||
px = pd.Series(d["close"].values.astype(float), index=idx).resample("1D").last()
|
||||
dv = pd.read_parquet(ROOT / "data" / "raw" / "dvol_btc.parquet")
|
||||
iv = pd.Series(dv["close"].values.astype(float) / 100.0,
|
||||
index=pd.to_datetime(dv["timestamp"], unit="ms", utc=True)).resample("1D").last()
|
||||
P = pd.concat({"px": px, "iv": iv}, axis=1, join="inner").dropna()
|
||||
return P
|
||||
|
||||
|
||||
SPOT_FEE_SIDE = 0.00035 # Deribit taker reale dal 2026-08-01 (3,50 bps/lato), §8
|
||||
|
||||
|
||||
def gate_trend(P: pd.DataFrame, forte: bool) -> np.ndarray:
|
||||
"""L'ENTRATA, governata da indicatori — e presa dal progetto, non inventata qui.
|
||||
|
||||
`trend_portfolio.tsmom_blend` media tre `np.sign()` sugli orizzonti (30, 90, 180) giorni,
|
||||
quindi assume SOLO i valori {-1, -1/3, +1/3, +1} (il bucket 2/3 non esiste: e' un errore gia'
|
||||
corretto in §57). Questo da' a "forte bull" una definizione che non aggiunge NEMMENO UN
|
||||
parametro nuovo:
|
||||
forte=True -> |blend| == 1 : tutti e tre gli orizzonti concordi ("forte bull/bear")
|
||||
forte=False -> |blend| >= 1/3: almeno la maggioranza (confronto dichiarato)
|
||||
Il segno del blend da' la DIREZIONE (long o short), come chiesto.
|
||||
"""
|
||||
from src.strategies.trend_portfolio import tsmom_blend
|
||||
c = P["px"].values.astype(float)
|
||||
b = tsmom_blend(c, (30, 90, 180))
|
||||
d = np.sign(b)
|
||||
d[np.abs(b) < (0.99 if forte else 0.32)] = 0.0
|
||||
return np.concatenate([[0.0], d[:-1]]) # ESEGUIBILE: si entra il giorno DOPO il segnale
|
||||
|
||||
|
||||
def collar(P: pd.DataFrame, cal: dict, *, dput: float, dcall: float, tenor: int,
|
||||
dirs: np.ndarray, zerocost: bool = False, senza_opzioni: bool = False,
|
||||
prem_scale: float = 1.0, solo_pavimento: bool = False,
|
||||
exit_frac: float = 1.0) -> dict:
|
||||
"""Hold di BTC gated dal trend, coperto da un collar rollato ogni `tenor` giorni (<=15).
|
||||
|
||||
direzione +1 = LONG BTC -> compra PUT (pavimento), vende CALL (tetto)
|
||||
direzione -1 = SHORT BTC -> compra CALL (pavimento), vende PUT (tetto)
|
||||
`exit_frac` < 1 esce dalle opzioni PRIMA della scadenza (0,50-0,75 = richiesta dell'operatore
|
||||
del 2026-09-01). Non e' un dettaglio di rollo: cambia il segno di due cose opposte.
|
||||
- sulla gamba COMPRATA (il pavimento) uscire presto RECUPERA valore temporale: il theta di una
|
||||
long accelera verso la scadenza, e §46 aveva gia' misurato che *"f si paga solo sulla parte
|
||||
di valore che converge a intrinseco, quindi un roll anticipato non lo paga"*;
|
||||
- sulla gamba VENDUTA (il tetto) uscire presto RINUNCIA al theta piu' veloce, cioe' proprio
|
||||
alla parte che in questo filone stampava il premio di varianza.
|
||||
Il prezzo di entrambe e' che si paga lo SPREAD in uscita su due gambe, invece di regolare a
|
||||
intrinseco. Chi lo attiva compra meno short-vol e piu' protezione onesta: e' misurabile, ed e'
|
||||
misurato nella sezione dedicata.
|
||||
|
||||
`prem_scale` e `solo_pavimento` esistono per i CONTROLLI (M15): con premio 0 e solo il
|
||||
pavimento la struttura e' un pranzo gratis e DEVE vincere; con premio x10 DEVE perdere ovunque.
|
||||
Un apparato che non riconosce il pranzo gratis e' guasto, e §46 lo scopri' a proprie spese.
|
||||
|
||||
`senza_opzioni=True` produce il MEDESIMO gated hold senza collar: e' la BASE del null M5, e
|
||||
dev'essere generata dallo stesso motore o il confronto misurerebbe anche le differenze di
|
||||
contabilita' (P1: il null deriva dal codice sorvegliato, non lo ridichiara).
|
||||
|
||||
Marcatura GIORNALIERA a modello: il pavimento vincola il PAYOFF A SCADENZA, non il mark
|
||||
intermedio. Senza marcatura giornaliera il maxDD del collar sarebbe misurato solo ai roll e la
|
||||
protezione sembrerebbe migliore di quanto e'.
|
||||
"""
|
||||
S = P["px"].values.astype(float)
|
||||
A = P["iv"].values.astype(float)
|
||||
n = len(S)
|
||||
if n < tenor + 5:
|
||||
return {}
|
||||
|
||||
tipo_di = {+1: ("put", "call"), -1: ("call", "put")}
|
||||
nav = np.full(n, np.nan)
|
||||
V = 1.0 # capitale normalizzato
|
||||
cash, q, nopt = 1.0, 0.0, 0.0
|
||||
Kl = Ks = np.nan
|
||||
dc_vivo = 0.20 # delta del tetto VIVO: serve allo spread d'uscita anticipata
|
||||
scad = -1
|
||||
roll_at = -1 # quando si ESCE (<= scad): con exit_frac<1 e' PRIMA della scadenza
|
||||
cur = 0
|
||||
cicli = []
|
||||
dc_scelti = [] # delta del tetto effettivamente usato a ogni apertura
|
||||
apre = 0
|
||||
|
||||
def _mark(K, T, sig_atm, tipo, S_t):
|
||||
if T <= 0 or math.isnan(K):
|
||||
return 0.0 if math.isnan(K) else (max(K - S_t, 0.0) if tipo == "put" else max(S_t - K, 0.0))
|
||||
sig = sig_atm * _interp(cal["term"], max(T * DPY, 1.0))
|
||||
d1 = _d1(S_t, K, T, sig)
|
||||
dl = norm.cdf(d1) if tipo == "call" else 1.0 - norm.cdf(d1)
|
||||
sig *= _interp(cal["skew"][tipo], min(max(dl, 0.02), 0.60))
|
||||
return bs_put(S_t, K, T, sig) if tipo == "put" else bs_call(S_t, K, T, sig)
|
||||
|
||||
def _chiudi(i, S_t, anche_spot=True):
|
||||
"""Regola le opzioni vive; liquida lo spot SOLO se richiesto.
|
||||
|
||||
⚠️ Il flag esiste per un difetto che falserebbe tutto: al ROLL delle opzioni lo spot non
|
||||
ha motivo di muoversi. Chiuderlo e riaprirlo pagherebbe 2x3,5 bps ogni `tenor` giorni
|
||||
(a 7 giorni: ~3,6%/anno) di fee che nella realta' non si pagano.
|
||||
"""
|
||||
nonlocal cash, q, nopt, Kl, Ks, cur
|
||||
if nopt > 0 and not senza_opzioni:
|
||||
tl, ts = tipo_di[cur]
|
||||
T_rem = max(scad - i, 0) / DPY
|
||||
vl = _mark(Kl, T_rem, A[i], tl, S_t)
|
||||
vs = 0.0 if solo_pavimento else _mark(Ks, T_rem, A[i], ts, S_t)
|
||||
if T_rem > 0: # smontaggio anticipato: pago lo spread in USCITA
|
||||
vl *= 1.0 - _interp(cal["spread"][tl], dput)
|
||||
vs *= 1.0 + _interp(cal["spread"][ts], dc_vivo)
|
||||
else:
|
||||
for x in (vl, vs):
|
||||
if x > 0:
|
||||
cash -= nopt * OPT_DELIVERY_UNDERLYING * S_t
|
||||
cash += nopt * (vl - vs)
|
||||
nopt, Kl, Ks = 0.0, np.nan, np.nan
|
||||
if anche_spot and q != 0.0:
|
||||
cash += q * S_t - abs(q) * S_t * SPOT_FEE_SIDE
|
||||
q = 0.0
|
||||
|
||||
for i in range(n):
|
||||
S_t = S[i]
|
||||
d = int(dirs[i])
|
||||
fine_ciclo = (i >= roll_at) and cur != 0 and not senza_opzioni
|
||||
solo_roll = fine_ciclo and d == cur
|
||||
if d != cur or fine_ciclo:
|
||||
if cur != 0:
|
||||
if nopt > 0 and i >= scad:
|
||||
tl, ts = tipo_di[cur]
|
||||
il_ = max(Kl - S_t, 0.0) if tl == "put" else max(S_t - Kl, 0.0)
|
||||
is_ = 0.0 if solo_pavimento else (
|
||||
max(Ks - S_t, 0.0) if ts == "put" else max(S_t - Ks, 0.0))
|
||||
cicli.append(dict(i0=apre, i1=i, S0=S[apre], S1=S_t, Kl=Kl, Ks=Ks,
|
||||
dirn=cur, cap=is_ > 0, floor=il_ > 0,
|
||||
scaduto=bool(fine_ciclo)))
|
||||
_chiudi(i, S_t, anche_spot=not solo_roll)
|
||||
if not solo_roll:
|
||||
V = cash
|
||||
if not solo_roll:
|
||||
cur = d
|
||||
if cur != 0:
|
||||
if not solo_roll:
|
||||
q = cur * V / S_t
|
||||
cash = V - q * S_t - abs(q) * S_t * SPOT_FEE_SIDE
|
||||
apre, scad = i, i + tenor
|
||||
roll_at = i + max(1, int(round(exit_frac * tenor)))
|
||||
if not senza_opzioni:
|
||||
tl, ts = tipo_di[cur]
|
||||
T = tenor / DPY
|
||||
Kl, sl = strike_da_delta(S_t, T, A[i], cal, dput, tl)
|
||||
pl = (bs_put if tl == "put" else bs_call)(S_t, Kl, T, sl)
|
||||
pl *= 1.0 + _interp(cal["spread"][tl], dput) # compro all'ASK
|
||||
pl *= prem_scale
|
||||
if zerocost:
|
||||
lo, hi = 0.02, 0.60
|
||||
for _ in range(40):
|
||||
md = 0.5 * (lo + hi)
|
||||
Kx, sx = strike_da_delta(S_t, T, A[i], cal, md, ts)
|
||||
px_ = (bs_put if ts == "put" else bs_call)(S_t, Kx, T, sx)
|
||||
px_ *= 1.0 - _interp(cal["spread"][ts], md) # vendo al BID
|
||||
# il premio CRESCE col delta: se incasso meno di quanto pago,
|
||||
# il tetto va avvicinato (delta PIU' ALTO) -> lo = md.
|
||||
if px_ < pl:
|
||||
lo = md
|
||||
else:
|
||||
hi = md
|
||||
dc_use = 0.5 * (lo + hi)
|
||||
else:
|
||||
dc_use = dcall
|
||||
dc_vivo = dc_use
|
||||
dc_scelti.append(float(dc_use))
|
||||
Ks, ss = strike_da_delta(S_t, T, A[i], cal, dc_use, ts)
|
||||
ps = (bs_put if ts == "put" else bs_call)(S_t, Ks, T, ss)
|
||||
ps *= (1.0 - _interp(cal["spread"][ts], dc_use)) * prem_scale
|
||||
if solo_pavimento:
|
||||
ps, Ks = 0.0, np.inf if ts == "call" else 0.0
|
||||
nopt = abs(q)
|
||||
cash += nopt * (ps - pl) - nopt * (_fee(pl, S_t) + _fee(ps, S_t))
|
||||
vl = vs = 0.0
|
||||
if nopt > 0 and not senza_opzioni:
|
||||
tl, ts = tipo_di[cur]
|
||||
T_rem = max(scad - i, 0) / DPY
|
||||
vl = _mark(Kl, T_rem, A[i], tl, S_t)
|
||||
vs = 0.0 if solo_pavimento else _mark(Ks, T_rem, A[i], ts, S_t)
|
||||
nav[i] = cash + q * S_t + nopt * (vl - vs)
|
||||
|
||||
v = pd.Series(nav, index=P.index).dropna()
|
||||
r = (v / v.shift(1) - 1.0).dropna()
|
||||
return dict(ret=r, nav=v, cicli=cicli, dc_scelti=dc_scelti)
|
||||
|
||||
|
||||
# ==================================================================== 4. MAIN
|
||||
|
||||
def _riga(nome, r) -> dict:
|
||||
return dict(nome=nome, sh=_sh(r), dd=_dd(r), drift=_drift(r))
|
||||
|
||||
|
||||
def main() -> None:
|
||||
print("=" * 96)
|
||||
print(" COLLAR01 — hold BTC gated dal trend, coperto da collar di opzioni a <=15 giorni")
|
||||
print("=" * 96)
|
||||
print(ATTESE)
|
||||
|
||||
cal = calibra_catena()
|
||||
print(f" CALIBRAZIONE dalla catena VERA: {cal['n_quote']:,} quote a due lati su "
|
||||
f"{cal['giorni']} giorni, {cal['span'][0].date()} -> {cal['span'][1].date()}")
|
||||
for nm in ("put", "call"):
|
||||
xs, ys = cal["skew"][nm]
|
||||
print(f" skew {nm:5s} IV/ATM : " + " ".join(f"d{x:.2f}={y:.3f}" for x, y in zip(xs, ys)))
|
||||
xs, ys = cal["term"]
|
||||
print(f" termine IV/DVOL30 : " + " ".join(f"{x:.0f}g={y:.3f}" for x, y in zip(xs, ys)))
|
||||
for nm in ("put", "call"):
|
||||
xs, ys = cal["spread"][nm]
|
||||
print(f" spread {nm:5s} (mezza forchetta/mid): "
|
||||
+ " ".join(f"d{x:.2f}={y:.3f}" for x, y in zip(xs, ys)))
|
||||
|
||||
P = serie_giornaliera()
|
||||
print(f"\n LENTE LUNGA: {P.index[0].date()} -> {P.index[-1].date()} "
|
||||
f"({len(P)} giorni, {len(P)/DPY:.2f} anni)")
|
||||
|
||||
bh = (P["px"] / P["px"].shift(1) - 1.0).dropna()
|
||||
print(f" BTC buy&hold nudo: Sharpe {_sh(bh):.3f} · maxDD {100*_dd(bh):.2f}% · "
|
||||
f"drift {100*_drift(bh):+.2f}%/a")
|
||||
|
||||
righe = []
|
||||
for forte in (True, False):
|
||||
dirs = gate_trend(P, forte)
|
||||
nome_g = "FORTE (3/3 orizzonti)" if forte else "LARGO (>=2/3)"
|
||||
quota = 100.0 * np.mean(dirs != 0)
|
||||
qlong = 100.0 * np.mean(dirs > 0)
|
||||
base = collar(P, cal, dput=0.2, dcall=0.2, tenor=7, dirs=dirs, senza_opzioni=True)
|
||||
rb = base["ret"]
|
||||
print(f"\n ── GATE {nome_g}: a mercato {quota:.1f}% dei giorni "
|
||||
f"(long {qlong:.1f}%, short {quota-qlong:.1f}%)")
|
||||
print(f" BASE gated SENZA opzioni: Sharpe {_sh(rb):.3f} · maxDD {100*_dd(rb):.2f}% · "
|
||||
f"drift {100*_drift(rb):+.2f}%/a ← e' il NULL da battere (M5)")
|
||||
for tenor in G_TENOR:
|
||||
for dp in G_DPUT:
|
||||
for dc in G_DCALL:
|
||||
out = collar(P, cal, dput=dp, dcall=dc, tenor=tenor, dirs=dirs)
|
||||
if not out:
|
||||
continue
|
||||
r = out["ret"]
|
||||
dd_c, dr_c = _dd(r), _drift(r)
|
||||
k = k_for_same_dd(rb.values, dd_c)
|
||||
dr_null = _drift(k * rb.values)
|
||||
righe.append(dict(gate=nome_g, forte=forte, tenor=tenor, dp=dp, dc=dc,
|
||||
sh=_sh(r), dd=dd_c, drift=dr_c, k=k, drift_null=dr_null,
|
||||
vince=(dr_c > dr_null) and (dd_c < _dd(rb)),
|
||||
dd_scende=dd_c < _dd(rb), cicli=out["cicli"], ret=r))
|
||||
# varianti zero-cost, dichiarate FUORI griglia
|
||||
for tenor in G_TENOR:
|
||||
for dp in G_DPUT:
|
||||
out = collar(P, cal, dput=dp, dcall=np.nan, tenor=tenor, dirs=dirs, zerocost=True)
|
||||
if not out:
|
||||
continue
|
||||
r = out["ret"]
|
||||
dd_c, dr_c = _dd(r), _drift(r)
|
||||
k = k_for_same_dd(rb.values, dd_c)
|
||||
righe.append(dict(gate=nome_g, forte=forte, tenor=tenor, dp=dp, dc="zc",
|
||||
sh=_sh(r), dd=dd_c, drift=dr_c, k=k,
|
||||
drift_null=_drift(k * rb.values),
|
||||
vince=(dr_c > _drift(k * rb.values)) and (dd_c < _dd(rb)),
|
||||
dd_scende=dd_c < _dd(rb), cicli=out["cicli"], ret=r))
|
||||
R = pd.DataFrame([x for x in righe if x["gate"] == nome_g])
|
||||
print(f"\n {'ten':>4} {'dput':>5} {'dcall':>6} {'Sharpe':>7} {'maxDD':>7} "
|
||||
f"{'drift':>8} {'k null':>7} {'drift null':>10} esito")
|
||||
for _, x in R.iterrows():
|
||||
dcs = x["dc"] if isinstance(x["dc"], str) else f"{x['dc']:.2f}"
|
||||
print(f" {x['tenor']:>4} {x['dp']:>5.2f} {dcs:>6} {x['sh']:>7.3f} "
|
||||
f"{100*x['dd']:>6.2f}% {100*x['drift']:>7.2f}% {x['k']:>7.3f} "
|
||||
f"{100*x['drift_null']:>9.2f}% "
|
||||
f"{'VINCE' if x['vince'] else ('dd↓ ma perde' if x['dd_scende'] else 'dd↑')}")
|
||||
|
||||
R = pd.DataFrame(righe)
|
||||
n_dd = int(R["dd_scende"].sum())
|
||||
n_win = int(R["vince"].sum())
|
||||
print("\n" + "=" * 96)
|
||||
print(f" A1 maxDD SCENDE in {n_dd}/{len(R)} celle "
|
||||
f"({'CONFERMATA' if n_dd > len(R)//2 else 'REFUTATA'})")
|
||||
print(f" A3 il collar BATTE il de-levering in {n_win}/{len(R)} celle "
|
||||
f"({'REFUTATA (il collar vince)' if n_win > len(R)//2 else 'CONFERMATA (il collar perde)'})")
|
||||
|
||||
# ---- A2: rapporto Δdrift/ΔmaxDD ----------------------------------------------------------
|
||||
for forte in (True, False):
|
||||
sub = R[R["forte"] == forte]
|
||||
if not len(sub):
|
||||
continue
|
||||
dirs = gate_trend(P, forte)
|
||||
rb = collar(P, cal, dput=0.2, dcall=0.2, tenor=7, dirs=dirs, senza_opzioni=True)["ret"]
|
||||
d0, g0 = _dd(rb), _drift(rb)
|
||||
rap = ((g0 - sub["drift"]) / np.maximum(d0 - sub["dd"], 1e-9)).median()
|
||||
print(f" A2 gate {'FORTE' if forte else 'LARGO'}: Δdrift/ΔmaxDD mediano {rap:.2f} "
|
||||
f"({'CONFERMATA' if rap > 1 else 'REFUTATA'}: >1 = il tetto costa piu' di quanto il "
|
||||
f"pavimento renda)")
|
||||
|
||||
# ---- C9: il tetto scatta piu' sui vincenti o sui perdenti? -------------------------------
|
||||
best = R.sort_values("sh", ascending=False).iloc[0]
|
||||
cic = pd.DataFrame(best["cicli"])
|
||||
if len(cic):
|
||||
# ⚠️ "vincente" dipende dalla DIREZIONE: per un ciclo SHORT un prezzo che sale e' una
|
||||
# PERDITA. Usare S1>S0 come proxy di vittoria e' il difetto che dava "pavimento 0,0%".
|
||||
cic["vinc"] = cic["dirn"] * (cic["S1"] - cic["S0"]) > 0
|
||||
cap_w = 100.0 * cic[cic["vinc"]]["cap"].mean()
|
||||
flo_l = 100.0 * cic[~cic["vinc"]]["floor"].mean()
|
||||
print(f"\n C9 cella migliore (gate {best['gate']}, {best['tenor']}g, dput {best['dp']}, "
|
||||
f"dcall {best['dc']}), {len(cic)} cicli:")
|
||||
print(f" il TETTO taglia nel {cap_w:.1f}% dei cicli VINCENTI · "
|
||||
f"il PAVIMENTO para nel {flo_l:.1f}% dei cicli PERDENTI")
|
||||
print(f" -> {'TRONCATURA' if cap_w > flo_l else 'PROTEZIONE'} "
|
||||
f"(C9: se scatta piu' sui vincenti che sui perdenti, non e' protezione)")
|
||||
|
||||
# ---- M1: il confronto che decide se merita uno slot --------------------------------------
|
||||
try:
|
||||
from src.portfolio.sleeves import _tp01_returns
|
||||
tp = _tp01_returns()
|
||||
tp.index = pd.to_datetime(tp.index, utc=True)
|
||||
tpd = (1.0 + tp).resample("1D").prod() - 1.0
|
||||
j = pd.concat({"c": best["ret"], "tp": tpd}, axis=1, join="inner").dropna()
|
||||
print(f"\n M1 vs TP01 sulla finestra comune ({len(j)} giorni): "
|
||||
f"collar Sharpe {_sh(j['c']):.3f} · TP01 {_sh(j['tp']):.3f} · "
|
||||
f"corr {j['c'].corr(j['tp']):+.3f}")
|
||||
for w in (0.10, 0.25):
|
||||
mix = (1 - w) * j["tp"] + w * j["c"]
|
||||
print(f" TP01 + {100*w:.0f}% collar: Sharpe {_sh(mix):.3f} "
|
||||
f"({_sh(mix)-_sh(j['tp']):+.3f}) · maxDD {100*_dd(mix):.2f}% "
|
||||
f"({100*(_dd(mix)-_dd(j['tp'])):+.2f}pp)")
|
||||
except Exception as e: # pragma: no cover
|
||||
print(f"\n M1 non calcolato: {type(e).__name__}: {e}")
|
||||
|
||||
# ---- ESTENSIONE AL BORDO (M4: si riapre la FAMIGLIA, M8: un argmax sul bordo non decide) --
|
||||
print("\n" + "=" * 96)
|
||||
print(" ESTENSIONE DICHIARATA — le celle vincenti stanno tutte sul BORDO (dput min, dcall max).")
|
||||
print(" Sondo dove punta la pendenza. Il limite dput->0 / dcall->0,50 e' la COVERED CALL pura:")
|
||||
print(" nessun pavimento, tetto ATM. Trial aggiunti: 2 gate x 2 tenor x 3 x 3 = 36 (dichiarati).")
|
||||
ext = []
|
||||
for forte in (True, False):
|
||||
dirs = gate_trend(P, forte)
|
||||
rb = collar(P, cal, dput=0.2, dcall=0.2, tenor=7, dirs=dirs, senza_opzioni=True)["ret"]
|
||||
for tenor in (7, 14):
|
||||
for dp in (0.02, 0.05, 0.10):
|
||||
for dc in (0.30, 0.40, 0.50):
|
||||
r = collar(P, cal, dput=dp, dcall=dc, tenor=tenor, dirs=dirs)["ret"]
|
||||
k = k_for_same_dd(rb.values, _dd(r))
|
||||
ext.append(dict(forte=forte, tenor=tenor, dp=dp, dc=dc, sh=_sh(r),
|
||||
dd=_dd(r), drift=_drift(r),
|
||||
vince=(_drift(r) > _drift(k * rb.values)) and (_dd(r) < _dd(rb))))
|
||||
E = pd.DataFrame(ext)
|
||||
top = E.sort_values("sh", ascending=False).iloc[0]
|
||||
print(f" -> VINCE in {int(E['vince'].sum())}/{len(E)} celle, e il massimo e' NELL'ANGOLO: "
|
||||
f"gate {'FORTE' if top['forte'] else 'LARGO'} {top['tenor']}g dput {top['dp']:.2f} "
|
||||
f"dcall {top['dc']:.2f} -> Sharpe {top['sh']:.3f}, maxDD {100*top['dd']:.2f}%, "
|
||||
f"drift {100*top['drift']:+.2f}%/a")
|
||||
print(" ⚠️ Il pavimento non e' cio' che vince: vince il TETTO. La pendenza porta FUORI da cio'")
|
||||
print(" che l'operatore ha chiesto ('bloccare la perdita') e DENTRO lo short-vol.")
|
||||
|
||||
# ---- LA DIAGNOSTICA CHE DECIDE: quanto dell'edge e' il PREMIO DI VARIANZA? ----------------
|
||||
lr = np.log(P["px"] / P["px"].shift(1))
|
||||
print("\n RIPREZZATURA ALLA VOLATILITA' REALIZZATA — diagnostica con LOOK-AHEAD DICHIARATO,")
|
||||
print(" non e' una strategia: e' il valore equo ex-post. Se l'edge sparisce, era IV-RV.")
|
||||
for h in (7, 14):
|
||||
fw = (lr[::-1].rolling(h).std()[::-1] * math.sqrt(DPY)).bfill().ffill()
|
||||
j = pd.concat({"iv": P["iv"], "fw": fw}, axis=1).dropna()
|
||||
print(f" DVOL / RV-forward a {h}g: mediana {(j['iv']/j['fw']).median():.3f} · "
|
||||
f"DVOL sopra nel {100*(j['iv']>j['fw']).mean():.1f}% dei giorni")
|
||||
for tenor in (7, 14):
|
||||
fw = (lr[::-1].rolling(tenor).std()[::-1] * math.sqrt(DPY)).bfill().ffill()
|
||||
Pf = P.copy()
|
||||
Pf["iv"] = fw.reindex(P.index).values
|
||||
for forte in (True, False):
|
||||
dirs = gate_trend(P, forte)
|
||||
rb = collar(P, cal, dput=0.2, dcall=0.2, tenor=7, dirs=dirs, senza_opzioni=True)["ret"]
|
||||
for dp, dc in ((0.02, 0.50), (0.10, 0.30)):
|
||||
a = collar(P, cal, dput=dp, dcall=dc, tenor=tenor, dirs=dirs)["ret"]
|
||||
b = collar(Pf, cal, dput=dp, dcall=dc, tenor=tenor, dirs=dirs)["ret"]
|
||||
ka, kb = k_for_same_dd(rb.values, _dd(a)), k_for_same_dd(rb.values, _dd(b))
|
||||
wa = _drift(a) > _drift(ka * rb.values) and _dd(a) < _dd(rb)
|
||||
wb = _drift(b) > _drift(kb * rb.values) and _dd(b) < _dd(rb)
|
||||
print(f" {'FORTE' if forte else 'LARGO'} {tenor:>2}g dput {dp:.2f} dcall {dc:.2f}: "
|
||||
f"a DVOL Sh {_sh(a):6.3f} drift {100*_drift(a):+7.2f}% {'VINCE' if wa else 'perde'}"
|
||||
f" | a RV Sh {_sh(b):6.3f} drift {100*_drift(b):+7.2f}% {'VINCE' if wb else 'perde'}"
|
||||
f" | VRP {100*(_drift(a)-_drift(b)):+6.2f} pp")
|
||||
|
||||
# ---- USCITA ANTICIPATA (richiesta dell'operatore: fra il 50% e il 75% del tempo) ---------
|
||||
print("\n" + "=" * 96)
|
||||
print(" USCITA ANTICIPATA DALLE OPZIONI — exit_frac 0,50 / 0,625 / 0,75 contro la scadenza.")
|
||||
print(" §46 misuro' che 'f si paga solo sulla parte di valore che converge a intrinseco,")
|
||||
print(" quindi un roll anticipato NON lo paga'. ⚠️ Vale per una copertura SOLO LONG: in un")
|
||||
print(" collar c'e' una gamba VENDUTA da RICOMPRARE, e uscire prima paga f esattamente sulla")
|
||||
print(" parte che a scadenza si sarebbe regolata gratis. L'asimmetria si INVERTE.")
|
||||
lr2 = np.log(P["px"] / P["px"].shift(1))
|
||||
for forte in (True, False):
|
||||
dirs = gate_trend(P, forte)
|
||||
rb = collar(P, cal, dput=0.2, dcall=0.2, tenor=7, dirs=dirs, senza_opzioni=True)["ret"]
|
||||
print(f"\n gate {'FORTE' if forte else 'LARGO'} (base drift {100*_drift(rb):+.2f}%/a):")
|
||||
for dp, dc in ((0.10, 0.30), (0.20, 0.20), (0.02, 0.50)):
|
||||
for ef in (1.00, 0.75, 0.625, 0.50):
|
||||
a = collar(P, cal, dput=dp, dcall=dc, tenor=14, dirs=dirs, exit_frac=ef)["ret"]
|
||||
hh = max(1, int(round(ef * 14)))
|
||||
fw = (lr2[::-1].rolling(hh).std()[::-1] * math.sqrt(DPY)).bfill().ffill()
|
||||
Pf = P.copy()
|
||||
Pf["iv"] = fw.reindex(P.index).values
|
||||
b = collar(Pf, cal, dput=dp, dcall=dc, tenor=14, dirs=dirs, exit_frac=ef)["ret"]
|
||||
k = k_for_same_dd(rb.values, _dd(a))
|
||||
w = (_drift(a) > _drift(k * rb.values)) and (_dd(a) < _dd(rb))
|
||||
print(f" d{dp:.2f}/{dc:.2f} exit {ef:.3f}: Sh {_sh(a):6.3f} DD {100*_dd(a):6.2f}% "
|
||||
f"drift {100*_drift(a):+7.2f}% null {100*_drift(k*rb.values):+6.2f}% "
|
||||
f"{'VINCE' if w else 'perde'} | a RV drift {100*_drift(b):+7.2f}% "
|
||||
f"VRP {100*(_drift(a)-_drift(b)):+6.2f}pp")
|
||||
|
||||
# ---- CONTROLLI DELL'APPARATO (M15) — un rilevatore che non li passa non misura nulla ----
|
||||
print("\n CONTROLLI DELL'APPARATO:")
|
||||
dirs = gate_trend(P, True)
|
||||
rb = collar(P, cal, dput=0.2, dcall=0.2, tenor=7, dirs=dirs, senza_opzioni=True)["ret"]
|
||||
esiti = []
|
||||
g = collar(P, cal, dput=0.20, dcall=0.20, tenor=14, dirs=dirs,
|
||||
prem_scale=0.0, solo_pavimento=True)["ret"]
|
||||
ok_g = (_dd(g) < _dd(rb)) and (_drift(g) > _drift(rb))
|
||||
esiti.append(ok_g)
|
||||
print(f" pranzo gratis (pavimento a premio ZERO): maxDD {100*_dd(rb):.2f}%->{100*_dd(g):.2f}% "
|
||||
f"drift {100*_drift(rb):+.2f}%->{100*_drift(g):+.2f}% {'OK' if ok_g else 'FALLITO'}")
|
||||
c = collar(P, cal, dput=0.20, dcall=0.20, tenor=14, dirs=dirs, prem_scale=10.0)["ret"]
|
||||
ok_c = _drift(c) < _drift(rb)
|
||||
esiti.append(ok_c)
|
||||
print(f" premio x10 (deve perdere): drift {100*_drift(c):+.2f}%/a "
|
||||
f"{'OK' if ok_c else 'FALLITO'}")
|
||||
zc = collar(P, cal, dput=0.20, dcall=np.nan, tenor=14, dirs=dirs, zerocost=True)
|
||||
prem_res = np.nan
|
||||
ok_z = bool(len(zc.get("cicli", [])) > 0) and np.isfinite(_drift(zc["ret"]))
|
||||
esiti.append(ok_z)
|
||||
print(f" zero-cost costruibile e finito: drift {100*_drift(zc['ret']):+.2f}%/a "
|
||||
f"{'OK' if ok_z else 'FALLITO'}")
|
||||
if not all(esiti):
|
||||
print(" 🚨 APPARATO GUASTO: il verdetto sotto NON e' leggibile (§46: un controllo "
|
||||
"positivo rotto dichiara guasto l'apparato)")
|
||||
|
||||
# ---- VERDETTO A RUNTIME (N11) ------------------------------------------------------------
|
||||
print("\n" + "=" * 96)
|
||||
# il verdetto deve tenere conto della diagnostica VRP: il bordo vince, ma di che cosa?
|
||||
dirs_f = gate_trend(P, True)
|
||||
rb_f = collar(P, cal, dput=0.2, dcall=0.2, tenor=7, dirs=dirs_f, senza_opzioni=True)["ret"]
|
||||
fw7 = (np.log(P["px"] / P["px"].shift(1))[::-1].rolling(7).std()[::-1]
|
||||
* math.sqrt(DPY)).bfill().ffill()
|
||||
Pf7 = P.copy()
|
||||
Pf7["iv"] = fw7.reindex(P.index).values
|
||||
ang_dvol = collar(P, cal, dput=0.02, dcall=0.50, tenor=7, dirs=dirs_f)["ret"]
|
||||
ang_rv = collar(Pf7, cal, dput=0.02, dcall=0.50, tenor=7, dirs=dirs_f)["ret"]
|
||||
quota_vrp = (_drift(ang_dvol) - _drift(ang_rv)) / max(_drift(ang_dvol), 1e-9)
|
||||
|
||||
if n_dd == 0:
|
||||
v = "IL PAVIMENTO NON RIDUCE IL maxDD — cade come §46 e per lo stesso motivo"
|
||||
elif n_win <= len(R) // 8:
|
||||
v = (f"IL PAVIMENTO FUNZIONA (a differenza di §46: maxDD scende in {n_dd}/{len(R)}) MA IL "
|
||||
f"TETTO LO PAGA TROPPO: solo {n_win}/{len(R)} celle battono il de-levering. "
|
||||
f"E cio' che vince sul bordo NON e' il pavimento: e' il premio di varianza "
|
||||
f"({100*quota_vrp:.0f}% del drift dell'angolo sparisce riprezzando alla vol vera) "
|
||||
f"— cioe' il meccanismo di VRP01, non una scoperta, e §3 lo blocca in deploy")
|
||||
else:
|
||||
v = f"REGGE IL PRIMO GATE: {n_win}/{len(R)} celle battono il de-levering — serve M2/DSR"
|
||||
print(f" VERDETTO (calcolato a runtime): {v}")
|
||||
print("=" * 96)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,124 @@
|
||||
"""Guardie su COLLAR01 (§71): i difetti che l'apparato ha gia' pagato non devono tornare.
|
||||
|
||||
Non testano il VERDETTO (quello e' una misura, e cambia col dato): testano che il MOTORE sappia
|
||||
riconoscere un successo e non regali soldi al collar per errore di contabilita'. E' la lezione di
|
||||
§46 — *"un controllo positivo rotto dichiara guasto l'apparato"* — trasformata in test.
|
||||
|
||||
Calibrazione SINTETICA (non la catena vera): il test dev'essere veloce e hermetico. Quello che
|
||||
verifica e' l'algebra del motore, non i parametri di mercato.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
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 / "scripts" / "research"))
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def C():
|
||||
import r0901_btc_collar as mod
|
||||
return mod
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def cal():
|
||||
"""Skew/termine/spread piatti e neutri: isola l'algebra dai parametri di mercato."""
|
||||
d = np.array([0.02, 0.10, 0.20, 0.30, 0.50])
|
||||
return dict(
|
||||
skew={"put": (d, np.ones_like(d)), "call": (d, np.ones_like(d))},
|
||||
term=(np.array([1.0, 30.0]), np.array([1.0, 1.0])),
|
||||
spread={"put": (d, np.zeros_like(d)), "call": (d, np.zeros_like(d))},
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def P():
|
||||
"""Serie deterministica di 800 giorni con un trend e un crollo: abbastanza per il gate a 180g."""
|
||||
rng = np.random.default_rng(7)
|
||||
n = 800
|
||||
r = rng.normal(0.0015, 0.03, n)
|
||||
r[600:640] = -0.045 # un crollo vero, dove il pavimento deve mordere
|
||||
px = 30000.0 * np.exp(np.cumsum(r))
|
||||
idx = pd.date_range("2022-01-01", periods=n, freq="D", tz="UTC")
|
||||
return pd.DataFrame({"px": px, "iv": np.full(n, 0.60)}, index=idx)
|
||||
|
||||
|
||||
def test_pranzo_gratis_e_riconosciuto(C, P, cal):
|
||||
"""CONTROLLO POSITIVO (M15). Un pavimento a premio ZERO e senza tetto e' denaro regalato:
|
||||
DEVE abbassare il maxDD E alzare il drift. Se questo test fallisce, ogni verdetto NEGATIVO
|
||||
del filone e' illeggibile — non e' il collar che perde, e' l'apparato che e' rotto."""
|
||||
dirs = C.gate_trend(P, True)
|
||||
base = C.collar(P, cal, dput=0.2, dcall=0.2, tenor=14, dirs=dirs, senza_opzioni=True)["ret"]
|
||||
free = C.collar(P, cal, dput=0.2, dcall=0.2, tenor=14, dirs=dirs,
|
||||
prem_scale=0.0, solo_pavimento=True)["ret"]
|
||||
assert C._dd(free) < C._dd(base), "un pavimento gratis non abbassa il maxDD: apparato guasto"
|
||||
assert C._drift(free) > C._drift(base), "un pavimento gratis non alza il drift: apparato guasto"
|
||||
|
||||
|
||||
def test_premio_esagerato_deve_perdere(C, P, cal):
|
||||
"""CONTROLLO NEGATIVO: a premio x10 la struttura deve perdere contro la base."""
|
||||
dirs = C.gate_trend(P, True)
|
||||
base = C.collar(P, cal, dput=0.2, dcall=0.2, tenor=14, dirs=dirs, senza_opzioni=True)["ret"]
|
||||
caro = C.collar(P, cal, dput=0.2, dcall=0.2, tenor=14, dirs=dirs, prem_scale=10.0)["ret"]
|
||||
assert C._drift(caro) < C._drift(base)
|
||||
|
||||
|
||||
def test_nav_short_parte_positivo(C, P, cal):
|
||||
"""Difetto reale trovato il 2026-09-01: scritto come `direzione*S_t + ...` il NAV di uno SHORT
|
||||
partirebbe NEGATIVO. Lo short non e' un'attivita' che vale -S: e' capitale a garanzia."""
|
||||
dirs = np.where(np.arange(len(P)) >= 200, -1.0, 0.0)
|
||||
out = C.collar(P, cal, dput=0.2, dcall=0.2, tenor=14, dirs=dirs)
|
||||
assert (out["nav"] > 0).all(), "NAV non positivo in direzione short"
|
||||
|
||||
|
||||
def test_zerocost_bisezione_non_invertita(C, P, cal):
|
||||
"""Difetto reale: la bisezione dello zero-cost era INVERTITA (dava Sharpe -3,9 e drift -53%/a).
|
||||
|
||||
L'invariante e' sulla BISEZIONE, non sul drift: il premio cresce col delta, quindi per
|
||||
finanziare una put PIU' CARA il tetto va AVVICINATO => il delta scelto per il tetto deve
|
||||
CRESCERE col delta del pavimento. (Il drift NON e' monotono e non va usato come proxy: un
|
||||
pavimento piu' stretto aiuta nei crolli e danneggia nei rally.)
|
||||
"""
|
||||
dirs = C.gate_trend(P, True)
|
||||
scelti = []
|
||||
for dp in (0.05, 0.15, 0.30):
|
||||
o = C.collar(P, cal, dput=dp, dcall=np.nan, tenor=14, dirs=dirs, zerocost=True)
|
||||
assert o["dc_scelti"], "lo zero-cost non ha aperto nessuna struttura"
|
||||
assert np.isfinite(C._drift(o["ret"])), "zero-cost non finito (NaN nella cassa)"
|
||||
scelti.append(float(np.median(o["dc_scelti"])))
|
||||
assert scelti[0] < scelti[1] < scelti[2], f"bisezione invertita: dc scelti {scelti}"
|
||||
|
||||
|
||||
def test_base_senza_opzioni_non_paga_roll_fantasma(C, P, cal):
|
||||
"""Difetto reale: la base del null rollava lo SPOT ogni `tenor` giorni pagando fee che nella
|
||||
realta' non esistono (~3,6%/anno a 7 giorni) — avrebbe ADULATO il collar. La base non deve
|
||||
dipendere dal tenor, che senza opzioni non ha significato."""
|
||||
dirs = C.gate_trend(P, True)
|
||||
a = C.collar(P, cal, dput=0.2, dcall=0.2, tenor=7, dirs=dirs, senza_opzioni=True)["ret"]
|
||||
b = C.collar(P, cal, dput=0.2, dcall=0.2, tenor=14, dirs=dirs, senza_opzioni=True)["ret"]
|
||||
assert C._drift(a) == pytest.approx(C._drift(b), abs=1e-9), "la base dipende dal tenor"
|
||||
|
||||
|
||||
def test_gate_forte_e_piu_selettivo_del_largo(C, P):
|
||||
"""`tsmom_blend` assume solo {-1,-1/3,+1/3,+1}: il gate FORTE (3/3 concordi) deve essere un
|
||||
SOTTOINSIEME stretto del LARGO, o "forte bull" non sta selezionando nulla."""
|
||||
f = C.gate_trend(P, True)
|
||||
l = C.gate_trend(P, False)
|
||||
assert np.mean(f != 0) < np.mean(l != 0)
|
||||
assert np.all((f == 0) | (f == l)), "il gate forte non e' un sottoinsieme del largo"
|
||||
|
||||
|
||||
def test_entrata_non_guarda_il_futuro(C, P):
|
||||
"""L'entrata e' il giorno DOPO il segnale (§8.1): la direzione del giorno i non puo' dipendere
|
||||
dal prezzo del giorno i."""
|
||||
d0 = C.gate_trend(P, True)
|
||||
P2 = P.copy()
|
||||
P2.iloc[-1, P2.columns.get_loc("px")] *= 1.5 # muovo SOLO l'ultimo prezzo
|
||||
d1 = C.gate_trend(P2, True)
|
||||
assert np.array_equal(d0, d1), "la direzione reagisce a un prezzo che non ha ancora visto"
|
||||
Reference in New Issue
Block a user