research: leave-one-out del book DE-LUCKATO sulle ancore — la classifica si ribalta

Domanda: "quale sleeve terrei?". Il leave-one-out ovvio e' misurato all'ancora canonica
di tutti e cinque gli sleeve, quindi NON e' credibile: un LOO e' un Delta, ed eredita la
fortuna d'ancora come ogni Delta (lezione 26/07, altlib.anchor_luck_delta).

Metodo: 2000 estrazioni uniformi indipendenti sullo spazio congiunto 24x10x7x23x5 =
193.200 configurazioni; book completo + 5 LOO alla STESSA configurazione; statistica =
mediana delle differenze appaiate. Sanity bit-exact 5/5 (max|dif| = 0.0) contro gli
sleeve di produzione. Quattro repliche ancorate riusate dagli audit 02/07-03/07, solo
la fase di GTAA01 e' nuova (ed e' l'unica coperta da test dedicato).

RISULTATI
- Livello del book: la stima a occhio del 02/07 era ottimista su 3/3 metriche.
  FULL 2.222 (97.0 pctl) -> 1.95 | HOLD 2.364 (99.6 pctl) -> 1.54 [1.11, 1.91] |
  maxDD 6.07% (12.0 pctl) -> 6.85%. Solo 9 estrazioni su 2000 battono l'hold-out canonico.
  La somma delle fortune marginali NON e' la mediana congiunta: sbagliava di +0.82 di Sharpe.
- SKH01 non e' il motore del book: l'ancora regala 2/3 del FULL, 70% dell'hold-out, 80%
  della protezione DD -> de-luckato e' il meno affidabile dei cinque.
- GTAA01 e' l'unico positivo nel 100% delle estrazioni su tutte e tre le metriche e il
  miglior protettore di DD. Ma attribuzione != eseguibilita': sotto $3k resta non deployabile.
- TP01 e' il maggior contributore (+0.390 FULL, 2000/2000) e il canonico lo SOTTOSTIMAVA.
  Il suo hold-out negativo non e' artefatto d'ancora (negativo nel 99.1%): e' la firma
  dell'assicurazione. Uno sleeve difensivo si giudica sul sinistro, non sul premio.
- XS01: protezione DD esattamente zero (positiva nel 43% = moneta) -> diversificatore di
  rendimento, non di rischio. VRP01: 2a conferma di zero fortuna (canonico all'1.8 pctl).

Corretto in sessione: un "pctl 100%" era arrotondamento di 99.55% con %.0f — un percentile
a 0 decimali mente esattamente agli estremi, che sono l'unico posto dove lo si legge.

Book, pesi, cron, config INVARIATI. Non e' un gate sui pesi (resta weights_tilt_null) e
non e' evidenza out-of-sample: e' attribuzione, de-luckata.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Adriano Dal Pastro
2026-07-26 13:23:36 +00:00
parent 7453a29699
commit b9b2ef1b26
4 changed files with 817 additions and 5 deletions
+54 -5
View File
@@ -84,11 +84,23 @@ Prima ondata di ricerca onesta su BTC/ETH certificati (5 track, harness condivis
Test `tests/test_gtaa_sleeve.py`; diario `2026-07-01-strategy-wave-6threads.md` (addendum GTAA). Test `tests/test_gtaa_sleeve.py`; diario `2026-07-01-strategy-wave-6threads.md` (addendum GTAA).
Report `scripts/portfolio/run_portfolio.py`. Sleeve a date d'inizio diverse → outer-join con pesi Report `scripts/portfolio/run_portfolio.py`. Sleeve a date d'inizio diverse → outer-join con pesi
rinormalizzati (TP01/SKH01/GTAA dal 2019*, VRP dal 2021, XS dal 2024; *GTAA troncato all'era book). rinormalizzati (TP01/SKH01/GTAA dal 2019*, VRP dal 2021, XS dal 2024; *GTAA troncato all'era book).
⚠️ **ANCHOR-LUCK del book (2026-07-02):** l'HOLD 2.46 è calcolato con TUTTI gli sleeve ancorati ⚠️ **ANCHOR-LUCK del book — MISURATO 2026-07-26 (la stima del 02/07 era ottimista su 3/3).**
alla loro ancora canonica, che per TP01/XS01/SKH01 è al top della rispettiva banda (eredità di I numeri canonici sono calcolati con TUTTI e cinque gli sleeve alla loro ancora canonica. Il
fortuna ~+0.10/+0.17/+0.5 HOLD). **Stima de-luckata onesta: HOLD ~1.9-2.1, FULL ~2.0-2.2, DD ~6% 02/07 la correzione fu **stimata a occhio** sommando le fortune marginali (HOLD ~1.9-2.1, FULL
invariato** — il book resta positivo e diversificato a ogni ancora testata, ma 2.46 è un massimo ~2.0-2.2, "DD ~6% invariato"); il 26/07 è stata **misurata sullo spazio congiunto** (24×10×7×23×5
di configurazioni d'ancora, non la stima centrale. Diario `2026-07-02-anchor-audit-xs01-skh01.md`. = 193.200 configurazioni, 2000 estrazioni uniformi indipendenti, `r0726_loo_deluck.py`):
| | canonico | pctl | **stima onesta = mediana** | banda p10-p90 |
|---|---|---|---|---|
| Sharpe FULL | +2.222 | 97.0° | **+1.95** | [1.81, 2.12] |
| Sharpe HOLD-OUT | +2.364 | **99.6°** | **+1.54** | [1.11, 1.91] |
| maxDD FULL | 6.07% | 12.0° | **6.85%** | [5.99, 7.94] |
**Solo 9 estrazioni su 2000 battono l'hold-out canonico.** Il book resta positivo e diversificato
a ogni ancora, ma **i numeri da citare sono 1.95 / 1.54 / 6.85%**, non i canonici.
⚠️ **Lezione: la somma delle fortune marginali NON è la mediana congiunta** — sbagliava di +0.82
di Sharpe hold-out, più di quasi tutti gli uplift per cui in questo progetto si è discusso se
ammettere uno sleeve. Quando serve la banda di un AGGREGATO si campiona lo spazio congiunto; gli
audit uno-sleeve-alla-volta (02/07, 03/07) restano validi per giudicare *quello* sleeve.
Diari `2026-07-02-anchor-audit-xs01-skh01.md` (originale) e `2026-07-26-loo-deluck.md` (misura).
- **SKH01-V2-DD "Skyhook" — DIVERSIFICATORE quasi-ortogonale (research)** — `src/strategies/skyhook.SKH01_V2_DD`, - **SKH01-V2-DD "Skyhook" — DIVERSIFICATORE quasi-ortogonale (research)** — `src/strategies/skyhook.SKH01_V2_DD`,
sleeve `src/portfolio/sleeves._skyhook_returns`. Sistema dual-TF (segnale 690m / exec 230m) regime sleeve `src/portfolio/sleeves._skyhook_returns`. Sistema dual-TF (segnale 690m / exec 230m) regime
(BuzVola/BuzVolume tipo-Chande) AND pattern (Donchian breakout), NON trend-follower, L/S. Vincitrice (BuzVola/BuzVolume tipo-Chande) AND pattern (Donchian breakout), NON trend-follower, L/S. Vincitrice
@@ -746,6 +758,43 @@ Prima ondata di ricerca onesta su BTC/ETH certificati (5 track, harness condivis
pre-registrato + cron); (d) il claim di multiple-testing di un agente va **ricontato**, non pre-registrato + cron); (d) il claim di multiple-testing di un agente va **ricontato**, non
creduto (72 dichiarate, 729 reali); (e) quando un meccanismo non generalizza, **chiedersi PERCHE' creduto (72 dichiarate, 729 reali); (e) quando un meccanismo non generalizza, **chiedersi PERCHE'
vale piu' del fatto che non generalizzi**. vale piu' del fatto che non generalizzi**.
- ⚠️ **LEAVE-ONE-OUT DEL BOOK DE-LUCKATO (2026-07-26, 3° filone del giorno) — la classifica dei
contributi si RIBALTA su 2 metriche su 3, e la stima de-luckata del book era ottimista.**
Domanda: *"quale sleeve terrei?"*. Script `scripts/research/r0726_loo_deluck.py`, test
`tests/test_loo_deluck.py` (9), diario `2026-07-26-loo-deluck.md`. **Book/pesi/cron/config
INVARIATI** (non è un gate sui pesi: quello resta `weights_tilt_null`).
(1) **Il metodo:** un leave-one-out **è un Δ**, quindi eredita la fortuna d'ancora come ogni Δ
(lezione 26/07). Con 4 sleeve su 5 ancorati il problema è congiunto → 2000 estrazioni uniformi
indipendenti sullo spazio 24×10×7×23×5, book completo + 5 LOO **alla stessa configurazione**,
statistica = **mediana delle differenze appaiate**. Sanity bit-exact 5/5 (`max|dif| = 0.0`) sulle
repliche ancorate — quattro riusate dagli audit 02/07-03/07, solo la fase di GTAA01 è nuova.
(2) **Contributi de-luckati** (mediana, e **frazione di estrazioni positive** — che conta più
della mediana): **TP01 +0.390 FULL (100%)** = quasi 3× il secondo, e il canonico lo
**SOTTOSTIMAVA** (+0.280 al 5.8° pctl); SKH01 +0.142 (91%), XS01 +0.125 (99.9%), VRP01 +0.122
(100%), GTAA01 +0.115 (100%).
(3) **Due giudizi ribaltati rispetto alla lente canonica.** **SKH01 non è il motore del book**:
l'ancora regala **2/3 del FULL** (+0.43→+0.14), **70% dell'hold-out** (+0.65→+0.19), **80% della
protezione DD** (+1.64pp→+0.32pp) → de-luckato è **il meno affidabile dei cinque**. **GTAA01 è
l'unico positivo nel 100% delle estrazioni su TUTTE E TRE le metriche** e il miglior protettore
di DD (+2.0pp, doppio del secondo), senza fortuna da restituire (canonico *sotto* la mediana su
FULL e DD). ⚠️ Ma **attribuzione ≠ eseguibilità**: sotto `GTAA_MIN_CAPITAL` $3k resta non
deployabile, e la nota del 25/07 (€0.50/g sopra il risk-free con maxDD 10% a $10k) è intatta.
(4) **TP01: hold-out negativo 0.200, e NON è artefatto d'ancora** (negativo nel **99.1%** delle
configurazioni) — mentre sul FULL è il maggior contributore. È la firma dell'assicurazione,
misurata: paga premio negli anni senza incendio, riprende tutto sul campione che contiene il 2022.
**Regola: uno sleeve difensivo si giudica sul sinistro, non sul premio** — l'hold-out negativo
non è un argomento per ridurlo. **XS01: protezione DD ESATTAMENTE ZERO** (mediana 0.000, positiva
nel 43% = moneta) → è un diversificatore di **rendimento, non di rischio**; conta perché sul
canale funded il vincolo binding è il DD (25/07 §4). **VRP01: 2ª conferma di zero fortuna**
(canonico al 1.8° pctl sul FULL = numeri di ammissione *conservativi*).
(5) **Contrappeso dovuto a SKH01:** il de-luck lo penalizza sul path **backtest**, ma il path
**live** è misurato migliore su entrambi i lati (ingressi +0.38, uscite +0.081) → **vale meno dei
suoi numeri di ammissione e più di questa tabella**; quantificarlo resta il follow-up bloccato
(serve la versione vol-targeted del path live).
⚠️ **Errore di stampa catturato:** il primo output dava `pctl 100%` per l'hold-out del book —
arrotondamento di 99.55% con `%.0f`. **Un percentile stampato a 0 decimali mente esattamente agli
estremi, che sono l'unico posto dove lo si legge.** Verificato a mano (9 estrazioni su 2000
battono la canonica), formato portato a 1 decimale.
- **Soffitto strutturale BTC/ETH-direzionale ~1.3** superato SOLO espandendo a un meccanismo diverso: - **Soffitto strutturale BTC/ETH-direzionale ~1.3** superato SOLO espandendo a un meccanismo diverso:
cross-sectional su universo Hyperliquid certificato (XS01) → portafoglio Sharpe ~1.55. cross-sectional su universo Hyperliquid certificato (XS01) → portafoglio Sharpe ~1.55.
- **Sweep "strategie alternative" (2026-06-20) — 104 ipotesi / 153 agenti / NIENTE di nuovo regge.** - **Sweep "strategie alternative" (2026-06-20) — 104 ipotesi / 153 agenti / NIENTE di nuovo regge.**
+225
View File
@@ -0,0 +1,225 @@
# 2026-07-26 — "Quale strategia terrei?" — leave-one-out del book DE-LUCKATO sulle ancore
**Richiesta:** *"dimmi qual strategia pensi di tenere"*.
**Script:** `scripts/research/r0726_loo_deluck.py`**test:** `tests/test_loo_deluck.py` (9).
**Book, pesi, cron, config: INVARIATI.** Nessuna decisione di allocazione cambia.
Cambia invece un **numero documentato in CLAUDE.md** (§4) e cambiano **due giudizi che avevo
dato un'ora prima nella stessa sessione** (§3).
---
## 0. Perche' la risposta ovvia non vale
La domanda "quanto vale ogni sleeve al suo posto nel book" ha una misura ovvia: togli lo sleeve,
guarda quanto cala il book. L'ho fatta, e alla prima lettura diceva questo:
```
BOOK COMPLETO FULL +2.222 HOLD +2.364 maxDD 6.07%
tolto dFULL dHOLD dDD
TP01 (33%) +0.280 -0.219 +0.24pp
XS01 (15%) +0.142 +0.640 +0.25pp
VRP01 (12%) +0.087 +0.064 +1.08pp
SKH01 (20%) +0.432 +0.652 +1.64pp
GTAA01 (20%) +0.120 +0.244 +1.70pp
```
Letta cosi', SKH01 e' il motore del book e TP01 e' un peso morto sull'hold-out.
**Entrambe le letture sono sbagliate**, e il motivo era gia' scritto nel progetto il giorno prima:
> *se si de-lucka una strategia va de-luckato anche il suo DEGRADO* — ogni Δ fra due varianti
> misurato a un'ancora sola eredita la fortuna di quell'ancora.
> (lezione 26/07, codificata in `altlib.anchor_luck_delta`)
Un leave-one-out **e' esattamente un Δ fra due varianti**. E qui il problema e' al quadrato: 4
sleeve su 5 sono ancorati, e per 3 di loro l'ancora canonica era gia' stata misurata come
fortunata (TP01 92° pctl su 24 ancore, SKH01 93-98° su 23 offset, XS01 fase 0 al 15° pctl di DD).
VRP01 e' l'unico senza firma — la sua fase canonica e' la **peggiore** delle 7.
---
## 1. Il metodo: estrazioni CONGIUNTE, non una griglia per sleeve
Gli audit del 02/07 e del 03/07 hanno de-luckato **uno sleeve alla volta**, tenendo gli altri alla
loro ancora canonica. Va bene per giudicare quello sleeve; non basta per giudicare il **book**,
perche' i numeri del book sono calcolati con tutti e cinque alla canonica insieme.
Spazio di luck congiunto: **24 (TP01, ancore orarie) x 10 (XS01, fasi H=10) x 7 (VRP01, fasi
settimanali) x 23 (SKH01, offset di griglia 230m/690m) x 5 (GTAA01, fasi della cadenza
settimanale) = 193.200 configurazioni.** Non enumerabile: si campiona.
**2000 estrazioni uniformi e indipendenti** (seed 20260726). Uniformi-indipendenti e' il null
onesto: non avevamo nessuna ragione a priori di preferire un'ancora a un'altra — se ce l'avessimo
avuta, sarebbe stata parte della strategia.
Per ogni estrazione: book completo + i 5 leave-one-out, **tutti alla stessa configurazione
d'ancora**. La statistica e' la **mediana delle differenze appaiate**, mai la differenza delle
mediane (l'errore che il 26/07 aveva ribaltato il verdetto sul recupero on-book di SKH01: le due
mediane cadono su ancore diverse).
**Repliche riusate, non riscritte.** I quattro generatori ancorati vengono dagli audit che li
hanno gia' verificati bit-exact (`r0702_tp01_offset.daily_off`, `r0702_anchor_xs01.xs_phase`,
`r0703_vrpimp_anchor.combo_weekly`, `r0702_anchor_skh01.skh_port`). Riscriverli avrebbe voluto
dire rifare gli stessi errori di convenzione. L'unico pezzo nuovo e' la fase di GTAA01
(`i % REBAL_EVERY == ph` invece di `== 0`), ed e' l'unico coperto da test dedicato.
**Sanity obbligatorio prima di tutto** — ogni replica ancorata deve riprodurre lo sleeve di
**produzione** alla sua ancora canonica, altrimenti sto misurando un'altra strategia:
```
TP01 ancora 0 max|dif| = 0.0e+00 (n=2692) OK
XS01 ancora 0 max|dif| = 0.0e+00 (n=938) OK
VRP01 ancora 0 max|dif| = 0.0e+00 (n=1884) OK
SKH01 ancora 0 max|dif| = 0.0e+00 (n=2690) OK
GTAA01 ancora 0 max|dif| = 0.0e+00 (n=2690) OK
```
Costo totale: 69 serie, ~8s di precalcolo, 34s per le 2000 estrazioni.
---
## 2. Il livello del book: i numeri pubblicati stanno in cima alla banda
```
canonico pctl MEDIANA p10 p90 fortuna
Sharpe FULL +2.222 97.0% +1.946 +1.807 +2.116 +0.276
Sharpe HOLD-OUT +2.364 99.6% +1.542 +1.109 +1.910 +0.822
maxDD FULL 6.07% 12.0% 6.85% 5.99% 7.94% -0.78%
```
**L'hold-out canonico sta al 99.6° percentile dello spazio d'ancora: solo 9 estrazioni su 2000
fanno meglio.** La stima onesta e' **1.54**, con banda [1.11, 1.91].
⚠️ **Un "100%" che ho dovuto correggere.** La prima stampa dava `pctl 100%` — arrotondamento di
99.55% con un `%.0f`. In un artefatto di ricerca un 100% stampato significa "e' il massimo, punto"
e avrebbe fatto concludere a una sessione futura qualcosa di piu' forte del vero. Verificato a
mano (max fra le 2000 = +2.577, a `TP01=0 / XS01=7 / VRP01=1 / SKH01=0 / GTAA01=3`), formato
portato a 1 decimale. *Regola pratica: un percentile stampato senza decimali mente esattamente
agli estremi, che sono l'unico posto dove lo si legge.*
---
## 3. Il leave-one-out de-luckato — e due giudizi miei ribaltati
```
dSharpe FULL canonico pctl MEDIANA >0 in
TP01 33% +0.280 5.8% +0.390 100.0%
SKH01 20% +0.432 99.0% +0.142 90.8%
XS01 15% +0.142 76.8% +0.125 99.9%
VRP01 12% +0.087 1.8% +0.122 100.0%
GTAA01 20% +0.120 63.5% +0.115 100.0%
dSharpe HOLD-OUT canonico pctl MEDIANA >0 in
XS01 15% +0.640 63.4% +0.497 96.2%
GTAA01 20% +0.244 17.3% +0.267 100.0%
SKH01 20% +0.652 96.0% +0.190 85.5%
VRP01 12% +0.064 47.4% +0.065 84.2%
TP01 33% -0.219 39.5% -0.200 0.9%
dMaxDD (pp, >0 = protegge) pctl MEDIANA >0 in
GTAA01 20% +1.695pp 23.8% +1.999pp 100.0%
VRP01 12% +1.075pp 55.6% +1.056pp 88.3%
SKH01 20% +1.636pp 89.3% +0.322pp 64.0%
TP01 33% +0.236pp 48.4% +0.274pp 62.1%
XS01 15% +0.251pp 65.0% +0.000pp 43.0%
```
**SKH01 non e' il motore del book.** Del suo contributo apparente, l'ancora canonica regala **2/3
sul FULL** (+0.43 → +0.14), **70% sull'hold-out** (+0.65 → +0.19), **80% sulla protezione DD**
(+1.64pp → +0.32pp). De-luckato e' indistinguibile dagli altri sul FULL ed e' **il meno affidabile
dei cinque**: l'unico sotto il 91% di estrazioni positive su tutte e tre le metriche (91/86/64%).
**GTAA01 non e' il primo da tagliare** (lo avevo detto un'ora prima, sulla lente canonica). E'
**l'unico sleeve positivo nel 100% delle estrazioni su tutte e tre le metriche** e il miglior
protettore di DD del book, al doppio del secondo. Non ha fortuna da restituire: il suo canonico
sta *sotto* la mediana su FULL e DD. ⚠️ L'altra gamba di quel giudizio resta pero' intatta e
indipendente: a $10k comprare ~€0.50/giorno sopra il risk-free con un maxDD del 10% e' caro, e
sotto `GTAA_MIN_CAPITAL` = $3k lo sleeve non e' deployabile. **Attribuzione ≠ eseguibilita'.**
**TP01 e' il maggior contributore, e il canonico lo SOTTOSTIMAVA** (+0.280 al 5.8° pctl → +0.390
de-luckato, positivo in 2000/2000, quasi 3x il secondo). Il suo hold-out negativo **non e'
artefatto d'ancora**: e' negativo nel **99.1%** delle configurazioni. Le due cose insieme sono la
storia dell'assicurazione, finalmente misurata invece che asserita — sul campione pieno (che
contiene il 2022) e' il contributore piu' grande; nella finestra 2025-26 e' stato fermo mentre gli
altri guadagnavano, e questo costa 0.20. **Giudicarlo sulla finestra in cui non e' servito
significa venderlo esattamente prima che serva.**
**VRP01** conferma per la seconda volta di essere l'unico senza firma di fortuna: canonico al
**1.8° pctl** sul FULL, cioe' i numeri di ammissione erano *conservativi*. Secondo protettore di DD.
**XS01** tiene il primo posto sull'hold-out anche de-luckato (+0.497), ma la sua protezione di DD
e' **esattamente zero** (mediana 0.000, positiva nel 43% = moneta). E' un diversificatore di
**rendimento, non di rischio** — e conta, perche' sul canale funded il vincolo binding e' il DD,
non il CAGR (25/07 §4).
**Correlazioni:** stabili sotto de-luck, tutte quasi-nulle. La sola sopra 0.1 e' TP01-GTAA01
(mediana +0.189, canonica +0.242).
### Il contrappeso che SKH01 ha diritto ad avere
Questo de-luck penalizza SKH01 sul path **backtest** (chiusura di bin). Ma le due misure del 26/07
dicono che il **path live e' migliore del backtest su entrambi i lati**: ingressi +0.38 di Sharpe
mediano, uscite +0.081 di Sharpe FULL di book. Applicare il de-luck senza quel contrappeso lo
penalizzerebbe due volte. Verdetto onesto: **SKH01 vale meno dei suoi numeri di ammissione e piu'
di questa tabella.** Quanto esattamente resta il follow-up gia' dichiarato e bloccato (serve la
versione vol-targeted del path live di SKH01).
---
## 4. Correzione a CLAUDE.md
La stima del 02/07 era **a occhio** — sommava le fortune misurate sleeve-per-sleeve. Oggi e'
**misurata congiuntamente**, ed era ottimista su tutte e tre le metriche:
| | stimato 02/07 | misurato 26/07 |
|---|---|---|
| HOLD-OUT | ~1.9-2.1 | **1.54** [1.11, 1.91] |
| FULL | ~2.0-2.2 | **1.95** [1.81, 2.12] |
| maxDD | "~6% invariato" | **6.85%** [5.99, 7.94] |
L'errore non e' stato usare una stima: e' stato che una stima per somma di effetti marginali
**non e' la mediana congiunta**, e la differenza (+0.82 di hold-out) e' piu' grande di quasi
tutti gli uplift per cui in questo progetto si e' discusso se ammettere uno sleeve.
---
## 5. Cosa NON e' questa misura
- **Non e' evidenza out-of-sample.** Il book e' valutato sugli stessi anni in cui XS01 e SKH01
sono stati scelti e affinati. E' attribuzione, de-luckata.
- **Non e' un gate sui pesi.** Ogni proposta di cambio pesi passa da `weights_tilt_null`. Qui non
si propone nulla: i pesi restano 33/15/12/20/20.
- **Non e' una lente di eseguibilita'.** Dice quanto uno sleeve contribuisce al book modellato,
non se lo si puo' comprare a $600 (GTAA01 sotto $3k e XS01 sotto ~$20k non si possono).
---
## 6. Risposta alla domanda
**TP01**, se ne va tenuta una. Maggior contributore de-luckato (+0.390, 2000/2000), l'unico
eseguibile davvero oggi a $600 senza haircut, e l'unico il cui valore dichiarato — il taglio del
DD ~6x contro il buy&hold — **non e' ancorato** e regge a tutte le ancore testate. Tutto il resto
dei suoi numeri e' gia' stato declassato (hold-out 0.31 → ~0.05); quello no.
Le altre: **SKH01** si tiene al 25% e non un euro di piu'; **GTAA01** si tiene nel book modello
(miglior protettore di DD) ma non nel live finche' non ci sono $3k su IB; **VRP01** si tiene come
*metro* — e' l'unico i cui numeri sono onesti per costruzione; **XS01** non e' da tenere oggi, e'
da accendere quando arriva il capitale (~$20k, o un conto funded, dove vale piu' di tutte).
Il book live che gira — **TP01 75 / SKH01 25** — e' gia' questa risposta.
---
## 7. Regole trasferibili
1. **Un leave-one-out e' un Δ, quindi va de-luckato come ogni Δ.** L'attribuzione all'ancora
canonica ha invertito la classifica su 2 metriche su 3.
2. **De-luckare uno sleeve alla volta non de-lucka il book.** La fortuna del book e' un fenomeno
*congiunto*: la somma delle fortune marginali ha sbagliato di +0.82 di Sharpe hold-out. Quando
serve la banda di un aggregato, si campiona lo spazio congiunto.
3. **Un percentile stampato a 0 decimali mente agli estremi**, che sono l'unico posto dove serve.
4. **La frazione di estrazioni positive dice piu' della mediana.** Fra +0.142 (SKH01, positivo nel
91%) e +0.115 (GTAA01, positivo nel 100%) la mediana preferisce il primo e l'affidabilita' il
secondo — ed e' la seconda a decidere se ci si mette del capitale.
5. **Uno sleeve difensivo si giudica sul sinistro, non sul premio.** L'hold-out negativo di TP01 e'
robusto (99.1% delle ancore) e *non* e' un argomento per ridurlo.
+373
View File
@@ -0,0 +1,373 @@
#!/usr/bin/env python
"""r0726_loo_deluck.py — LEAVE-ONE-OUT del book 5-sleeve DE-LUCKATO sulle ancore.
DOMANDA: "quale sleeve terrei?" cioe' quanto vale OGNI sleeve alla sua posizione attuale
nel book. La risposta ovvia (togli lo sleeve, guarda quanto cala il book) e' misurata
all'ANCORA CANONICA di tutti e cinque, e per questo NON e' credibile: ogni Δ misurato su
griglia ancorata eredita la fortuna di quell'ancora (lezione codificata il 26/07 in
`altlib.anchor_luck_delta`; corollario: *se si de-lucka una strategia va de-luckato anche
il suo DEGRADO*). E qui il problema e' al quadrato — 4 sleeve su 5 sono ancorati, e per 3
di loro l'ancora canonica e' gia' stata misurata come fortunata (TP01 92° pctl su 24 ancore,
SKH01 93-98° su 23 offset, XS01 fase 0 al 15° pctl di DD; VRP01 e' l'unico senza firma, la
sua fase canonica e' la PEGGIORE delle 7).
COSA FA (nessun file di produzione toccato, nessun peso cambiato):
0. SANITY OBBLIGATORIO ogni replica parametrizzata sull'ancora deve riprodurre
BIT-EXACT lo sleeve di produzione alla sua ancora canonica. Senza questo il resto
misura un'altra strategia.
1. Precalcola le 69 serie: TP01 x24 ancore orarie, XS01 x10 fasi, VRP01 x7 fasi,
SKH01 x23 offset di griglia, GTAA01 x5 fasi di cadenza settimanale.
2. ESTRAZIONI CONGIUNTE: N configurazioni d'ancora, una per sleeve, uniformi e
INDIPENDENTI (il null onesto: non avevamo ragione di preferire nessuna ancora).
Lo spazio pieno e' 24x10x7x23x5 = 193.200 -> si campiona.
3. Per ogni estrazione: book completo + i 5 book leave-one-out (combine_outer, pesi
rinormalizzati per-riga come in produzione) -> Δ APPAIATI per sleeve.
La statistica e' la MEDIANA DELLE DIFFERENZE APPAIATE, mai la differenza delle mediane
(l'errore ribaltante catturato il 26/07): le due mediane cadono su ancore diverse.
4. Output: per sleeve, Δ canonico + il suo percentile nella banda, Δ mediano de-luckato,
banda [p10,p90], frazione di estrazioni in cui il contributo e' positivo.
Piu' il livello del book de-luckato (FULL/HOLD/DD mediani) — che aggiorna con una
MISURA la stima a occhio in CLAUDE.md ("HOLD ~1.9-2.1, FULL ~2.0-2.2").
COSA NON E': non e' un gate sui pesi (quello resta `weights_tilt_null`), e non e' evidenza
out-of-sample il book e' misurato sugli stessi anni su cui XS01/SKH01 sono stati scelti e
affinati. E' attribuzione, de-luckata: dice quanto di cio' che vediamo e' struttura e quanto
e' l'ancora che ci e' capitata.
uv run python scripts/research/r0726_loo_deluck.py [--draws N]
"""
from __future__ import annotations
import argparse
import pickle
import sys
import time
from pathlib import Path
import numpy as np
import pandas as pd
ROOT = Path("/opt/docker/PythagorasGoal")
sys.path.insert(0, str(ROOT))
sys.path.insert(0, str(ROOT / "scripts" / "research"))
sys.path.insert(0, str(ROOT / "scripts" / "research" / "alt"))
from src.portfolio.portfolio import HOLDOUT, combine_outer, metrics, to_daily # noqa: E402
from src.portfolio import gtaa as G # noqa: E402
from src.portfolio.sleeves import (GTAA_DEFAULT_CAPITAL, VRP_CFG, # noqa: E402
_gtaa_daily_returns, _skyhook_returns,
_tp01_returns, _vrp_combo_returns,
_xsec_returns, active_sleeves)
# repliche ancorate GIA' verificate bit-exact nei rispettivi audit (02/07, 03/07):
# riusate, non riscritte — riscriverle significherebbe rifare gli stessi errori di convenzione.
import r0702_anchor_skh01 as A_SKH # noqa: E402 skh_port(off)
import r0702_anchor_xs01 as A_XS # noqa: E402 xs_phase(phase)
import r0702_tp01_offset as A_TP # noqa: E402 daily_off(asset, h)
import r0703_vrpimp_anchor as A_VRP # noqa: E402 combo_weekly(phase, f) + to_daily_lumped
from src.strategies.trend_portfolio import CANONICAL, TrendPortfolio, simple_returns # noqa: E402
SEED = 20260726
# cache rigenerabile delle 69 serie d'ancora (~8s a ricostruirla): gitignored via `*.pkl`.
# `--force` la ignora. Path stabile nel repo, non legato alla sessione che l'ha scritta.
CACHE = ROOT / "data" / "_cache" / "anchor_series.pkl"
WEIGHTS = {"TP01_trend_1d": 0.33, "XS01_xsec_hl": 0.15, "VRP01_shortvol": 0.12,
"SKH01_skyhook": 0.20, "GTAA01_eq_trend": 0.20}
SHORT = {"TP01_trend_1d": "TP01", "XS01_xsec_hl": "XS01", "VRP01_shortvol": "VRP01",
"SKH01_skyhook": "SKH01", "GTAA01_eq_trend": "GTAA01"}
# ===========================================================================
# Generatori ancorati — uno per sleeve. Ogni serie esce GIA' normalizzata a
# giornaliera con `to_daily`, come fa Sleeve.daily() in produzione.
# ===========================================================================
def tp01_at(h: int) -> pd.Series:
"""TP01 all'ancora oraria h. Replica ESATTA di sleeves._tp01_returns con il daily
risampionato a offset h invece che a 00:00 UTC."""
tp = TrendPortfolio(**CANONICAL)
series = {}
for a in ("BTC", "ETH"):
df = A_TP.daily_off(a, h)
r = simple_returns(df["close"].values.astype(float))
tgt = tp.target_series(df)
held = np.zeros(len(tgt))
held[1:] = tgt[:-1]
net = held * r - tp.fee_side * np.abs(np.diff(held, prepend=0.0))
net[0] = 0.0
series[a] = pd.Series(np.clip(net, -0.99, None), index=pd.to_datetime(df["datetime"]))
J = pd.concat(series, axis=1, join="inner").fillna(0.0)
return to_daily(pd.Series(0.5 * J["BTC"].values + 0.5 * J["ETH"].values, index=J.index))
def xs01_at(p: int) -> pd.Series:
return to_daily(A_XS.xs_phase(p))
def vrp01_at(p: int) -> pd.Series:
return to_daily(A_VRP.to_daily_lumped(A_VRP.combo_weekly(p, f=VRP_CFG["f"])))
def skh01_at(off: int) -> pd.Series:
return to_daily(A_SKH.skh_port(off))
def _gtaa_leg_phase(sym: str, cap_leg: float, band_usd: float, every: int, ph: int) -> pd.Series:
"""Replica di gtaa._gated_returns con la cadenza in fase `ph` (i % every == ph)."""
close = G._close(sym)
ex = np.nan_to_num(np.asarray(G._exposure(close).values, float))
px = close.values.astype(float)
ret = np.zeros(len(px))
ret[1:] = px[1:] / px[:-1] - 1.0
held = np.empty(len(ex))
comm = np.zeros(len(ex))
cur = 0.0
for i in range(len(ex)):
if i % every == ph:
notional = abs(ex[i] - cur) * cap_leg
if notional >= max(band_usd, G.IB_MIN_TRADE_USD):
comm[i] = G.ib_commission(notional, px[i]) / cap_leg
cur = ex[i]
held[i] = cur
pos = np.zeros(len(held))
pos[1:] = held[:-1]
net = pos * ret - comm
net[0] = 0.0
return pd.Series(net, index=close.index)
def gtaa01_at(ph: int) -> pd.Series:
"""GTAA01 alla fase `ph` della cadenza settimanale (REBAL_EVERY=5 barre di borsa).
Replica ESATTA di sleeves._gtaa_daily_returns, incluse le convenzioni di calendario."""
cap_leg = float(GTAA_DEFAULT_CAPITAL) / len(G.EQ_UNIVERSE)
cols = {a: _gtaa_leg_phase(a, cap_leg, G.REBAL_BAND_USD, G.REBAL_EVERY, ph)
for a in G.EQ_UNIVERSE}
r = pd.concat(cols, axis=1, sort=True).sort_index().mean(axis=1, skipna=True).dropna()
r = r.dropna().sort_index()
if r.index.tz is None:
r.index = r.index.tz_localize("UTC")
days = pd.date_range(r.index.min().normalize(), r.index.max().normalize(), freq="1D", tz="UTC")
r = r.reindex(days).fillna(0.0)
from src.portfolio.sleeves import GTAA_BOOK_ACTIVATION
return to_daily(r[r.index >= GTAA_BOOK_ACTIVATION])
# nome -> (generatore, ancore a priori, ancora canonica, sleeve di produzione)
SPEC = {
"TP01_trend_1d": (tp01_at, tuple(range(24)), 0, _tp01_returns),
"XS01_xsec_hl": (xs01_at, tuple(range(10)), 0, _xsec_returns),
"VRP01_shortvol": (vrp01_at, tuple(range(7)), 0, _vrp_combo_returns),
"SKH01_skyhook": (skh01_at, tuple(range(0, 690, 30)), 0, _skyhook_returns),
"GTAA01_eq_trend": (gtaa01_at, tuple(range(5)), 0, _gtaa_daily_returns),
}
# ===========================================================================
# PART 0 — sanity: la replica ancorata == sleeve di produzione all'ancora canonica
# ===========================================================================
def sanity() -> None:
print("[SANITY] ogni replica ancorata deve riprodurre lo sleeve di produzione "
"alla sua ancora canonica")
for name, (gen, _anchors, canon, prod_fn) in SPEC.items():
mine = gen(canon)
ref = to_daily(prod_fn())
J = pd.concat({"m": mine, "r": ref}, axis=1, join="outer")
assert len(mine) == len(ref), (
f"{name}: lunghezze diverse ({len(mine)} vs {len(ref)}) — non e' la stessa serie")
dmax = float(np.nanmax(np.abs(J["m"].values - J["r"].values)))
assert dmax < 1e-12, f"SANITY FAIL {name}: max|dif| = {dmax:.3e}"
print(f" {SHORT[name]:<7} ancora {canon:>3} max|dif| = {dmax:.1e} "
f"(n={len(mine)}) OK")
# ===========================================================================
# PART 1 — precalcolo delle 69 serie
# ===========================================================================
def build_all(force: bool = False) -> dict:
if CACHE.exists() and not force:
with CACHE.open("rb") as fh:
cached = pickle.load(fh)
if set(cached) == set(SPEC) and all(len(cached[n]) == len(SPEC[n][1]) for n in SPEC):
print(f"[CACHE] serie d'ancora lette da {CACHE.name}")
return cached
out = {}
for name, (gen, anchors, _c, _p) in SPEC.items():
t0 = time.time()
out[name] = {a: gen(a) for a in anchors}
print(f" {SHORT[name]:<7} {len(anchors):>3} ancore in {time.time()-t0:5.1f}s")
CACHE.parent.mkdir(parents=True, exist_ok=True)
with CACHE.open("wb") as fh:
pickle.dump(out, fh)
return out
# ===========================================================================
# PART 2 — book + leave-one-out a una configurazione d'ancora
# ===========================================================================
def book_stats(cols: dict) -> tuple[float, float, float]:
"""(Sharpe FULL, Sharpe HOLD-OUT, maxDD FULL) del book a pesi WEIGHTS ristretti a `cols`."""
w = {k: WEIGHTS[k] for k in cols}
full = combine_outer(cols, w)
mf = metrics(full)
mh = metrics(full[full.index >= HOLDOUT])
return mf["sharpe"], mh["sharpe"], mf["maxdd"]
def one_config(series: dict, cfg: dict) -> dict:
"""Un'estrazione d'ancora -> book completo + 5 leave-one-out, tutti alla STESSA ancora."""
cols = {n: series[n][cfg[n]] for n in SPEC}
bf, bh, bd = book_stats(cols)
res = {"book": (bf, bh, bd)}
for n in SPEC:
sub = {k: v for k, v in cols.items() if k != n}
f, h, d = book_stats(sub)
# Δ = contributo dello sleeve: >0 su Sharpe = togliendolo il book peggiora
# >0 su DD = togliendolo il drawdown peggiora
res[n] = (bf - f, bh - h, d - bd)
return res
def paired_deltas(draws: list[dict], name: str, j: int) -> np.ndarray:
"""Vettore dei Δ APPAIATI di uno sleeve su una metrica (j: 0=FULL, 1=HOLD, 2=maxDD).
Ogni elemento e' (book con X) - (book senza X) alla STESSA configurazione d'ancora. La
statistica corretta e' la mediana di QUESTO vettore — mai la differenza fra la mediana dei
book-con e quella dei book-senza, perche' le due mediane cadono su ancore DIVERSE e il
verdetto puo' ribaltarsi (errore catturato il 26/07 sul recupero on-book di SKH01, codificato
in `altlib.anchor_luck_delta`). Isolata qui apposta per essere testabile.
"""
return np.array([d[name][j] for d in draws], float)
def pctl_of(arr: np.ndarray, v: float) -> float:
return float((arr < v).mean() * 100.0)
def band(arr: np.ndarray) -> tuple[float, float, float]:
return float(np.median(arr)), float(np.percentile(arr, 10)), float(np.percentile(arr, 90))
def main() -> None:
ap = argparse.ArgumentParser()
ap.add_argument("--draws", type=int, default=600)
ap.add_argument("--force", action="store_true", help="ricalcola le serie ignorando la cache")
args = ap.parse_args()
t_start = time.time()
print("=" * 100)
print(" r0726 — LEAVE-ONE-OUT del book 5-sleeve, DE-LUCKATO sulle ancore")
print("=" * 100)
sanity()
print(f"\n[PRECALCOLO] {sum(len(v[1]) for v in SPEC.values())} serie d'ancora "
f"(spazio congiunto {np.prod([len(v[1]) for v in SPEC.values()]):,})")
S = build_all(force=args.force)
# --- configurazione canonica (quella di tutti i numeri del progetto) ---
canon_cfg = {n: SPEC[n][2] for n in SPEC}
canon = one_config(S, canon_cfg)
# --- estrazioni congiunte uniformi indipendenti ---
rng = np.random.default_rng(SEED)
print(f"\n[ESTRAZIONI] {args.draws} configurazioni d'ancora congiunte, "
f"uniformi indipendenti (seed {SEED})")
t0 = time.time()
draws = []
for _ in range(args.draws):
cfg = {n: int(rng.choice(SPEC[n][1])) for n in SPEC}
draws.append(one_config(S, cfg))
print(f" fatte in {time.time()-t0:.0f}s")
# ------------------------------------------------------------------
# Livello del book
# ------------------------------------------------------------------
B = np.array([d["book"] for d in draws], float)
print("\n" + "=" * 100)
print(" (1) LIVELLO DEL BOOK — quanto dei numeri pubblicati e' l'ancora che ci e' capitata")
print("=" * 100)
print(f" {'':<22}{'canonico':>10}{'pctl':>7}{'MEDIANA':>10}{'p10':>9}{'p90':>9}"
f"{'fortuna':>10}")
for j, lab in enumerate(["Sharpe FULL", "Sharpe HOLD-OUT", "maxDD FULL"]):
med, lo, hi = band(B[:, j])
can = canon["book"][j]
fmt = (lambda x: f"{x*100:8.2f}%") if j == 2 else (lambda x: f"{x:+8.3f}")
# pctl a 1 decimale: un "100%" arrotondato da 99.55% e' esattamente il numero che
# inganna una lettura futura (qui 9 estrazioni su 2000 battono la canonica).
print(f" {lab:<22}{fmt(can)}{pctl_of(B[:, j], can):6.1f}%{fmt(med)}"
f"{fmt(lo)}{fmt(hi)}{fmt(can - med)}")
print("\n (fortuna = canonico - mediana. Su maxDD un valore NEGATIVO e' fortuna:")
print(" il DD canonico e' piu' basso del tipico.)")
# ------------------------------------------------------------------
# Leave-one-out de-luckato
# ------------------------------------------------------------------
print("\n" + "=" * 100)
print(" (2) LEAVE-ONE-OUT DE-LUCKATO — contributo di ogni sleeve, mediana delle")
print(" DIFFERENZE APPAIATE (book con X) - (book senza X) alla STESSA ancora")
print("=" * 100)
order = sorted(SPEC, key=lambda n: -float(np.median(paired_deltas(draws, n, 0))))
for j, lab in enumerate(["dSharpe FULL", "dSharpe HOLD-OUT", "dMaxDD (pp, >0 = protegge)"]):
print(f"\n --- {lab} ---")
print(f" {'sleeve':<9}{'w':>6}{'canonico':>11}{'pctl':>7}{'MEDIANA':>11}"
f"{'p10':>10}{'p90':>10}{'>0 in':>9}")
for n in order:
arr = paired_deltas(draws, n, j)
med, lo, hi = band(arr)
can = canon[n][j]
k = 100.0 if j == 2 else 1.0
u = "pp" if j == 2 else ""
print(f" {SHORT[n]:<9}{WEIGHTS[n]*100:5.0f}%{can*k:+10.3f}{u}"
f"{pctl_of(arr, can):6.1f}%{med*k:+10.3f}{u}{lo*k:+9.3f}{u}{hi*k:+9.3f}{u}"
f"{(arr > 0).mean()*100:7.1f}%")
# ------------------------------------------------------------------
# Quanto sposta il de-luck ogni contributo
# ------------------------------------------------------------------
print("\n" + "=" * 100)
print(" (3) FORTUNA PER SLEEVE — canonico meno mediana, sul suo stesso contributo")
print("=" * 100)
print(f" {'sleeve':<9}{'dFULL':>22}{'dHOLD':>22}{'dDD':>22}")
print(f" {'':<9}{'can':>8}{'med':>7}{'luck':>7}{'can':>8}{'med':>7}{'luck':>7}"
f"{'can':>8}{'med':>7}{'luck':>7}")
for n in order:
row = f" {SHORT[n]:<9}"
for j in range(3):
arr = paired_deltas(draws, n, j)
med = float(np.median(arr))
can = canon[n][j]
k = 100.0 if j == 2 else 1.0
row += f"{can*k:+8.2f}{med*k:+7.2f}{(can-med)*k:+7.2f}"
print(row)
# ------------------------------------------------------------------
# Correlazioni: struttura o ancora?
# ------------------------------------------------------------------
print("\n" + "=" * 100)
print(" (4) CORRELAZIONI FRA SLEEVE — banda sulle estrazioni (finestra comune)")
print("=" * 100)
rng2 = np.random.default_rng(SEED + 1)
pairs = [(a, b) for i, a in enumerate(SPEC) for b in list(SPEC)[i + 1:]]
acc = {p: [] for p in pairs}
for _ in range(min(120, args.draws)):
cfg = {n: int(rng2.choice(SPEC[n][1])) for n in SPEC}
D = pd.concat({n: S[n][cfg[n]] for n in SPEC}, axis=1, join="inner").fillna(0.0)
C = D.corr()
for a, b in pairs:
acc[(a, b)].append(float(C.loc[a, b]))
Dc = pd.concat({n: S[n][SPEC[n][2]] for n in SPEC}, axis=1, join="inner").fillna(0.0).corr()
print(f" {'coppia':<16}{'canonica':>10}{'MEDIANA':>10}{'p10':>9}{'p90':>9}")
for a, b in sorted(pairs, key=lambda p: -abs(np.median(acc[p]))):
arr = np.array(acc[(a, b)], float)
med, lo, hi = band(arr)
print(f" {SHORT[a]+'-'+SHORT[b]:<16}{Dc.loc[a, b]:+9.3f}{med:+10.3f}"
f"{lo:+9.3f}{hi:+9.3f}")
print(f"\nFatto in {time.time()-t_start:.0f}s.")
print("NB: attribuzione de-luckata, NON evidenza out-of-sample — il book e' misurato")
print(" sugli stessi anni su cui XS01 e SKH01 sono stati scelti e affinati.")
if __name__ == "__main__":
main()
+165
View File
@@ -0,0 +1,165 @@
"""Test per r0726_loo_deluck.py — leave-one-out del book de-luckato sulle ancore.
Copre i due punti dove questo studio puo' rompersi in silenzio:
(a) la REPLICA di fase di GTAA01 l'unico generatore ancorato scritto nuovo (gli altri
quattro sono riusati dagli audit 02/07 e 03/07, gia' verificati bit-exact li');
(b) la STATISTICA APPAIATA l'invariante che il 26/07 ha ribaltato un verdetto: la mediana
delle differenze appaiate NON e' la differenza delle mediane.
I test su (b) girano su strutture sintetiche: sono logica, non dati, e devono restare veloci.
"""
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))
sys.path.insert(0, str(ROOT / "scripts" / "research"))
sys.path.insert(0, str(ROOT / "scripts" / "research" / "alt"))
import r0726_loo_deluck as R # noqa: E402
# ===========================================================================
# (a) replica di fase di GTAA01
# ===========================================================================
def test_gtaa_fase_canonica_replica_lo_sleeve_di_produzione():
"""La fase 0 deve riprodurre BIT-EXACT _gtaa_daily_returns(). Senza questo tutto lo
studio misura una strategia diversa da quella del book."""
from src.portfolio.portfolio import to_daily
from src.portfolio.sleeves import _gtaa_daily_returns
mine = R.gtaa01_at(0)
ref = to_daily(_gtaa_daily_returns())
assert len(mine) == len(ref)
assert mine.index.equals(ref.index)
assert float(np.max(np.abs(mine.values - ref.values))) == 0.0
def test_gtaa_le_fasi_sono_davvero_diverse():
"""Controllo POSITIVO: se `ph` non entrasse nel calcolo, il test sopra passerebbe lo stesso
e la banda d'ancora di GTAA01 sarebbe cinque copie della stessa serie (fortuna misurata 0
per costruzione). Almeno una fase deve differire dalla canonica."""
base = R.gtaa01_at(0)
diffs = []
for ph in (1, 2, 3, 4):
other = R.gtaa01_at(ph)
J = pd.concat({"a": base, "b": other}, axis=1, join="inner")
diffs.append(float(np.max(np.abs(J["a"].values - J["b"].values))))
assert max(diffs) > 1e-9, f"le 5 fasi di GTAA01 sono identiche: {diffs}"
def test_gtaa_fase_fuori_range_non_ribilancia_mai():
"""`i % 5 == 7` non e' mai vero -> la gamba resta a esposizione 0 per sempre. Serve a
dimostrare che e' davvero la CADENZA a essere parametrizzata (e non un no-op)."""
close = R.G._close("SPY")
leg = R._gtaa_leg_phase("SPY", 1000.0, R.G.REBAL_BAND_USD, R.G.REBAL_EVERY, 7)
assert len(leg) == len(close)
assert float(np.max(np.abs(leg.values))) == 0.0
# ===========================================================================
# (b) statistica appaiata
# ===========================================================================
def _fake_draws(book_vals, loo_vals, name="TP01_trend_1d"):
"""Costruisce la struttura che one_config() produce, con Δ = book - loo per estrazione."""
return [{"book": (b, 0.0, 0.0), name: (b - l, 0.0, 0.0)}
for b, l in zip(book_vals, loo_vals)]
def test_mediana_appaiata_non_e_differenza_delle_mediane():
"""L'INVARIANTE del 26/07, con numeri costruiti perche' i due metodi abbiano segno OPPOSTO.
book : [1.0, 2.0, 3.0] -> mediana 2.0
senza : [0.5, 2.5, 2.9] -> mediana 2.5
differenza delle mediane = -0.5 (direbbe: lo sleeve fa MALE)
mediana delle differenze appaiate = mediana([+0.5, -0.5, +0.1]) = +0.1 (fa BENE)
"""
book = [1.0, 2.0, 3.0]
loo = [0.5, 2.5, 2.9]
draws = _fake_draws(book, loo)
appaiata = float(np.median(R.paired_deltas(draws, "TP01_trend_1d", 0)))
ingenua = float(np.median(book) - np.median(loo))
assert appaiata == pytest.approx(0.1)
assert ingenua == pytest.approx(-0.5)
assert np.sign(appaiata) != np.sign(ingenua), "il caso di test non dimostra piu' il ribaltamento"
def test_paired_deltas_preserva_ordine_e_metrica():
draws = [{"book": (0, 0, 0), "X": (1.0, 10.0, 100.0)},
{"book": (0, 0, 0), "X": (2.0, 20.0, 200.0)}]
assert list(R.paired_deltas(draws, "X", 0)) == [1.0, 2.0]
assert list(R.paired_deltas(draws, "X", 1)) == [10.0, 20.0]
assert list(R.paired_deltas(draws, "X", 2)) == [100.0, 200.0]
# ===========================================================================
# (c) semantica del leave-one-out sul book
# ===========================================================================
def _flat_series(vals, start="2020-01-01"):
idx = pd.date_range(start, periods=len(vals), freq="1D", tz="UTC")
return pd.Series(np.asarray(vals, float), index=idx)
def test_one_config_e_appaiato_per_costruzione():
"""res[n] deve essere il Δ calcolato alla STESSA configurazione, non a due configurazioni
diverse. Si verifica ricalcolando a mano il book completo e quello senza uno sleeve."""
rng = np.random.default_rng(7)
n = 400
series = {name: {0: _flat_series(rng.normal(0.0004, 0.01, n)), 1: _flat_series(rng.normal(0.0004, 0.01, n))}
for name in R.SPEC}
cfg = {name: (i % 2) for i, name in enumerate(R.SPEC)}
res = R.one_config(series, cfg)
cols = {n_: series[n_][cfg[n_]] for n_ in R.SPEC}
bf, bh, bd = R.book_stats(cols)
assert res["book"] == (bf, bh, bd)
for name in R.SPEC:
sub = {k: v for k, v in cols.items() if k != name}
f, h, d = R.book_stats(sub)
assert res[name][0] == pytest.approx(bf - f)
assert res[name][1] == pytest.approx(bh - h)
# sul maxDD il segno e' INVERTITO apposta: >0 = togliendolo il drawdown PEGGIORA
assert res[name][2] == pytest.approx(d - bd)
def test_sleeve_costante_a_zero_non_contribuisce_al_sharpe():
"""Controllo positivo sul segno: uno sleeve che non fa nulla deve dare Δ ~0 sul Sharpe,
non un contributo positivo per magia di rinormalizzazione."""
rng = np.random.default_rng(11)
n = 500
names = list(R.SPEC)
series = {nm: {0: _flat_series(rng.normal(0.0005, 0.008, n))} for nm in names}
dead = names[-1]
series[dead] = {0: _flat_series(np.zeros(n))}
cfg = {nm: 0 for nm in names}
res = R.one_config(series, cfg)
# lo sleeve morto abbassa il Sharpe del book (diluisce): il suo Δ dev'essere <= 0
assert res[dead][0] <= 1e-9
def test_book_stats_usa_i_pesi_di_produzione():
"""I pesi devono essere quelli del book attivo, e la somma dei cinque deve fare 1.0:
se qualcuno cambia active_sleeves() e non questo modulo, il test lo dice."""
from src.portfolio.sleeves import active_sleeves
live = {s.name: round(s.weight, 6) for s in active_sleeves()}
assert live == R.WEIGHTS, f"pesi disallineati da active_sleeves(): {live} vs {R.WEIGHTS}"
assert sum(R.WEIGHTS.values()) == pytest.approx(1.0)
def test_spec_copre_tutti_gli_sleeve_attivi_con_ancora_canonica_valida():
from src.portfolio.sleeves import active_sleeves
assert set(R.SPEC) == {s.name for s in active_sleeves()}
for name, (_gen, anchors, canon, _prod) in R.SPEC.items():
assert canon in anchors, f"{name}: ancora canonica {canon} fuori dalla griglia"
assert len(anchors) >= 5, f"{name}: griglia d'ancora troppo piccola per una banda"