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:
Adriano Dal Pastro
2026-09-01 17:30:18 +00:00
parent 5bcf212923
commit 05816c49f9
6 changed files with 1207 additions and 3 deletions
+4 -3
View File
@@ -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/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/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/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 | | `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. - **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. - **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**. - **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**. - **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. - **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). - **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). - **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**. - **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. - **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' ### 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. - **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**. - **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). - **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**. - **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. - **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. - **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.
+225
View File
@@ -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.
+51
View File
@@ -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%`. 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 **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. 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.
+88
View File
@@ -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' | | 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' | | — | **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** | | 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** | | 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** | | — | **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** | | — | **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**. capitale **non e' comprabile**.
**Con questa sono 70 filoni e 0 candidati promossi.** **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.
+715
View File
@@ -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()
+124
View File
@@ -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"