research(wave): ondata 3 filoni — esecuzione SKH01, DVOLSPREAD, XSR01 fuori dal crypto

Tre questioni che il progetto aveva lasciato aperte e decidibili oggi.

T1 — ESECUZIONE SKH01. Testato l'unico meccanismo che non e' un cron: ordini
resting on-book (TP=limit al livello per costruzione + fee maker, SL=stop-market).
A offset 0 recupera il 94% del degrado, ma l'audit 02/07 aveva de-luckato i
numeri headline di SKH01 e NON il degrado. Sulla banda appaiata dei 23 offset il
degrado recuperabile e' +0.054 Sh di book, non -0.35 di sleeve.

Errore di metodo corretto in sessione: mediana(A)-mediana(B) fra offset confronta
offset diversi; serve la mediana delle DIFFERENZE appaiate. Col fix il verdetto
si ribalta: solo TP a limite = +0.054 FULL / +0.061 HOLD, positivo in 19/23 e
21/23 offset; lo SL on-book PEGGIORA (mediana -0.010, positivo in 11/23) perche'
cristallizza la perdita mentre l'exit ritardata incassa il rimbalzo.
Raccomandazione NON eseguita: TP a limite si, SL strategico on-book no.

T2 — DVOLSPREAD esce dal limbo (fermo dal 21/06). Passato ai due gate che allora
non esistevano. La griglia dichiarata "72 celle" ne contiene 729: valutate tutte.
Plateau reale (729/729 hold-out positivo). Selection-on-holdout confermata ma
mite: cella pubblicata 83a/729 sull'hold-out, 471a/729 in-sample. Cella onesta
FULL 0.68 / HOLD 0.69 (non 0.93) e DSR 0.953 PASS; la pubblicata fallisce 0.947.
Promosso a forward-monitor coi parametri onesti, NON nel book (hold-out attivo
1.6 anni, DSR sul filo, weights_tilt_null mai affrontato).

T3 — XSR01 non generalizza. Meccanismo congelato su 9 settoriali (1998+) e 28 ETF
(30 anni), versione DEMEANATA (il test del 25/07 era a coppie). Lordo +0.24
p=0.193 e -0.14 p=0.747 vs null a fee zero. Il risultato che conta e' l'ampiezza:
il demean fa 4.5->37.4 (8x) sul crypto ma 5.3->6.2 (1.2x) sulle azioni, perche'
sul crypto il residuo-vs-BTC lascia un enorme fattore comune e sulle azioni il
residuo-vs-SPY e' gia' indipendente. XSR01 e' crypto-specifico; NON e' falso.
Soglie del gate 23/10 non toccate.

Book/pesi/cron INVARIATI. Suite 245 verdi.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Adriano Dal Pastro
2026-07-25 19:51:52 +00:00
parent 8b4c698ad9
commit 2c3882b60c
6 changed files with 1190 additions and 0 deletions
+57
View File
@@ -557,6 +557,63 @@ Prima ondata di ricerca onesta su BTC/ETH certificati (5 track, harness condivis
ACCOPPIAMENTO al rendimento del giorno** — qui un test "i percentili coincidono" sarebbe PASSATO ACCOPPIAMENTO al rendimento del giorno** — qui un test "i percentili coincidono" sarebbe PASSATO
con la stima sbagliata di 2-3×. Ogni regola valutata sul minimo (daily-loss, trailing DD, stop di con la stima sbagliata di 2-3×. Ogni regola valutata sul minimo (daily-loss, trailing DD, stop di
conto) va misurata su **tuple accoppiate**, mai su un wick estratto a parte. conto) va misurata su **tuple accoppiate**, mai su un wick estratto a parte.
- **Ondata 2026-07-26 (3 filoni: esecuzione SKH01 / DVOLSPREAD / XSR01 fuori dal crypto) — 0 sleeve
nuovi, 1 raccomandazione operativa aperta, 1 lead promosso, 1 falsificazione.** Script
`r0726_{skh_onbook,dvolspread_gate,xsr_equity}.py`, test `tests/test_wave_0726.py` (11 casi),
diario `2026-07-26-wave-esecuzione-dvolspread-xsr-equity.md`. **Book/pesi/cron INVARIATI.**
(1) ⚠️ **T1 — IL DEGRADO D'ESECUZIONE DI SKH01 ERA ANCH'ESSO FORTUNA D'ANCORA.** Testato l'unico
meccanismo che non e' un cron (ordini **resting on-book**: TP = limit al livello per costruzione +
fee maker; SL = stop-market). A **offset 0** l'on-book recupera il **94%** del degrado — ma
l'audit 02/07 aveva de-luckato i numeri headline di SKH01 e **NON il degrado**, misurato solo a
off0. Sulla banda appaiata dei 23 offset il degrado massimo recuperabile e' **+0.054 Sh di book**,
non 0.35 di sleeve. ⚠️ Errore di metodo mio, corretto in sessione: **mediana(A)mediana(B) fra
offset e' sbagliato** (confronta offset diversi) → serve la **mediana delle DIFFERENZE appaiate**;
con quella il verdetto si RIBALTA. Esito: **solo TP a limite = +0.054 FULL / +0.061 HOLD,
positivo in 19/23 (FULL) e 21/23 (HOLD) offset**; **lo SL on-book PEGGIORA** (contributo mediano
0.010, positivo in **11/23 = moneta**) perche' cristallizza la perdita al livello mentre l'exit
software ritardata incassa il rimbalzo — e sopra **0.50% di slippage** l'on-book e' peggio del
live. Meccanismo misurato: sulle uscite TP il fill orario **batte** il livello nel 42% dei casi
(vantaggio medio 0.035%) → il limit TP converte una lotteria in certezza piu' che aggiungere
ritorno; solo l'**1% degli SL gappa** davvero a 5m. **RACCOMANDAZIONE NON ESEGUITA (decisione
dell'operatore): TP come limit resting reduce-only, SL strategico NON on-book.** E' un cambio di
esecuzione (non passa `weights_tilt_null`) ma piccolo, da pesare contro ordini orfani/doppio fill.
Il disaster-SL 30% on-book resta com'e'.
(2) ✅ **T2 — DVOLSPREAD ESCE DAL LIMBO** (era fermo dal 21/06, unico sopravvissuto del marginal
scorer indurito, mai ripreso: non nel book, non in monitor, non rifiutato). Passato ai due gate
che nel giugno NON esistevano. ⚠️ La griglia dichiarata dall'agente ("72 celle") ne contiene
**729** (6 assi x 3) → valutate tutte, scelta conservativa (piu' trial = DSR piu' basso).
Plateau REALE e larghissimo: **729/729 celle con hold-out positivo**, FULL [0.60,0.71].
Selection-on-holdout **confermata ma mite**: la cella pubblicata e' **83ª/729 sull'hold-out ma
471ª/729 in-sample**. Scegliendo onestamente in-sample: **FULL 0.68 / HOLD 0.69** (non il **0.93**
pubblicato — **citare 0.69**) e **DSR 0.953 PASS**, mentre la cella pubblicata **FALLISCE (0.947)**.
Marginale ADDS + robust_oos + multicut + non-hedge + insample_edge + beats_noise; corr +0.11,
alpha +7.4%/a, dSharpe book **+0.08 FULL / +0.17 HOLD** a w=15%. **PROMOSSO a forward-monitor con
i parametri ONESTI** (zwin=180 k=2.0 lw=0.6 zw=1.1 tgt=0.17 svw=60), **NON nel book**: campione
**ATTIVO 1949/2691 g** (prima del 2021-03 non c'e' DVOL, book flat) con **hold-out attivo 1.6
anni**, margine DSR sul filo, e `weights_tilt_null` mai affrontato.
(3) ❌ **T3 — XSR01 NON GENERALIZZA fuori dal crypto** (meccanismo CONGELATO W=45/sgn=+1 su
**9 settoriali SPDR 1998+ / 28 ETF 30 anni**, residuo vs SPY, demean giornaliero, split IWM/EFA
riparati, **annualizzazione √252**). Diverso dal test del 25/07: quello era a **coppie**, questo
e' la versione **DEMEANATA** (quella vera di XSR01). Lordo **+0.24 (SECT9, p=0.193)** e **0.14
(ALL28, p=0.747)** vs null di permutazione **a fee zero**; netto 1.6/1.8 ovunque; per decennio
stesso profilo nei 2 universi (neg. 1998-2005, debolmente pos. poi) = piu' cambio di regime che
edge. **Il risultato che conta non e' lo Sharpe ma l'AMPIEZZA:** il demeaning porta l'ampiezza
effettiva **4.5→37.4 (8x) sul crypto** ma solo **5.3→6.2 / 8.4→11.3 (1.2-1.35x) sulle azioni**.
Spiegazione strutturale (e migliore descrizione di XSR01 di quella della sua scoperta): il residuo
OLS rimuove gia' il beta al fattore comune; sul crypto alle gambe **RESTA** un enorme fattore
comune (ampiezza 4.5 su 50 gambe) ed e' quello che il demean toglie — sulle azioni il residuo-vs-SPY
e' **gia'** quasi indipendente, quindi non c'e' niente da togliere. **Per il gate del 23/10:**
conferma e CHIUDE la scappatoia lasciata aperta dal test a coppie; XSR01 e' **crypto-specifico**.
NON prova che sia falso. **Soglie del gate NON toccate**: resta appoggiato interamente su finestra
forward + haircut di eseguibilita' a $5.000, come pre-registrato.
**LEZIONI:** (a) **se si de-lucka una strategia va de-luckato anche il suo DEGRADO** — ogni Δ fra
due varianti misurato su griglia ancorata eredita la fortuna dell'ancora; (b) su offset appaiati
la statistica e' la **mediana delle differenze**, non la differenza delle mediane; (c) **un lead
"in forward-monitor" senza monitor e senza scadenza e' un lead perso** (DVOLSPREAD: 35 giorni di
limbo) → applicare ai lead la stessa disciplina dei candidati (config congelata + gate
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'
vale piu' del fatto che non generalizzi**.
- **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.**
@@ -0,0 +1,227 @@
# 2026-07-26 — Ondata a 3 filoni: esecuzione SKH01, DVOLSPREAD, XSR01 fuori dal crypto
**Script:** `r0726_skh_onbook.py` · `r0726_dvolspread_gate.py` · `r0726_xsr_equity.py`
**Test:** `tests/test_wave_0726.py` (11 casi) · suite completa **245 verdi**
**Book / pesi / cron: INVARIATI.** Una raccomandazione operativa aperta (T1), nessuna modifica fatta.
Tre filoni scelti non per esplorare aree nuove — quelle sono in gran parte sature — ma perché
il progetto stesso aveva lasciato **tre questioni aperte e decidibili oggi**:
| filone | questione aperta | esito |
|---|---|---|
| **T1** | il degrado del path live di SKH01 (~0.35 Sh) è recuperabile? | ⚠️ **il degrado era a sua volta fortuna d'ancora**; metà del fix funziona, l'altra metà no |
| **T2** | DVOLSPREAD, in limbo dal 21/06, è sleeve o falso positivo? | ✅ **regge i gate che non esistevano allora** — promosso a forward-monitor con parametri onesti |
| **T3** | XSR01 regge fuori dal crypto e da 2.6 anni monoregime? | ❌ **no** — e si scopre *perché* |
---
## T1 — Il degrado d'esecuzione di SKH01 era anch'esso un artefatto d'ancora
### La domanda
Il book live gira SKH01 con cron orario e uscite **software**. Costo misurato (audit 02/07,
riconfermato il 24/07): sleeve FULL 1.46→1.19, HOLD 1.64→1.15, DD 18%→25%. È il buco singolo più
grande del progetto, e il 24/07 aveva già stabilito che **un cron più veloce non lo recupera**.
L'angolo mai testato è l'unico meccanismo che non è un cron: gli **ordini resting on-book**, dove
esiste un'asimmetria che nessuno aveva sfruttato:
- il **TP è un limit order**: se il prezzo ci arriva, il fill è al livello **per costruzione**
(sei il maker), e per giunta a fee maker invece che taker;
- lo **SL è uno stop-market**: al trigger diventa un ordine a mercato, e in un gap riempie peggio.
Ipotesi: il grosso del degrado sta sul lato TP, quindi è recuperabile senza rischio nuovo.
### Il risultato, e la trappola che ha nascosto
A **offset 0** — la griglia che gira davvero — il quadro sembrava trionfale: l'on-book completo
recupera il **94%** del degrado. Ma l'offset 0 è già noto essere al 93-98° percentile dei 23
offset a priori, quindi la lente onesta è la banda.
E qui c'è stato un mio errore di statistica, corretto in corso: avevo confrontato
**mediana(canonical) mediana(hourly)**, che confronta offset *diversi* fra loro. Gli offset sono
**coppie appaiate**: la statistica giusta è la **mediana delle differenze**. Con quella:
| modalità | ΔFULL mediana [min,max] | ΔHOLD mediana | migliora in |
|---|---|---|---|
| canonical (tetto teorico) | **+0.054** [0.05,+0.12] | +0.099 | — |
| **solo TP a limite** | **+0.054** [0.03,+0.07] | **+0.061** | **FULL 19/23 · HOLD 21/23 · DD 19/23** |
| TP + SL on-book | +0.024 [0.07,+0.11] | +0.031 | FULL 17/23 · HOLD 14/23 |
| *contributo del solo lato SL* | **0.010** [0.13,+0.08] | — | **11/23 = una moneta** |
**Due conclusioni, entrambe diverse da quella che cercavo.**
**(1) Il "0.35 del path live" è a sua volta un artefatto dell'offset 0.** L'audit del 02/07 aveva
de-luckato i numeri *headline* di SKH01 ma aveva misurato il **degrado** solo a offset 0. Sulla
banda appaiata il degrado massimo recuperabile è **+0.054 di Sharpe sul book**, non +0.35 sullo
sleeve. Il buco più grande del progetto era in buona parte fortuna d'ancora anche lui.
**(2) Lo stop on-book PEGGIORA.** Mettere lo SL sul book cristallizza la perdita al livello,
mentre l'uscita software ritardata a volte incassa il rimbalzo: contributo mediano **0.010**,
positivo in **11/23** offset = indistinguibile da una moneta. La sensibilità lo conferma: sopra
lo **0.50%** di slippage sullo stop l'on-book è **peggio del live di oggi**.
Il meccanismo è misurato, non ipotizzato: sulle uscite TP il fill orario batte il livello nel
**42%** dei casi (vantaggio medio 0.035%) — è quasi una moneta, quindi il limit TP **converte una
lotteria in una certezza** più che aggiungere ritorno. E solo l'**1% degli SL** gappa davvero
dentro la barra 5m: il gap-through dei crash è reale ma raro.
### Raccomandazione (NON eseguita)
> **Appoggiare il TP come limit resting reduce-only; NON mettere lo SL strategico sul book.**
> Guadagno atteso **+0.054 FULL / +0.061 HOLD** sul book, positivo in **19-21 offset su 23**,
> più il risparmio taker→maker su un quarto dei trade.
È un cambio di **esecuzione**, non di strategia, quindi non passa da `weights_tilt_null`. Ma è
piccolo, e va pesato contro il costo operativo vero: ordini orfani se il cron muore, gestione
del reduce-only, doppio fill. **Non l'ho implementato**: è una decisione dell'operatore, non un
fatto compiuto. Il disaster-SL on-book a 30% resta com'è (è un'altra cosa: protezione, non exit).
---
## T2 — DVOLSPREAD regge i gate che nel giugno non esistevano ✅
### Chi è, e perché era in limbo
`agent_14_dvol_spread` (onda ortho, 21/06) è **l'unico sopravvissuto** al marginal scorer
indurito: dei 18 book relative-value che facevano ADDS con lo scorer vecchio, l'indurito ne lasciò
in piedi **uno**, questo. Da allora non è mai stato ripreso — non è nel book, non è in un monitor,
non è stato rifiutato. Era semplicemente lì.
Il segnale: `log(DVOL_btc) log(DVOL_eth)`, tilt verso la gamba con **vol implicita più ricca**
(tesi VRP / fear-reversal), market-neutral per costruzione, eseguibile a $600.
Nel frattempo il progetto ha codificato **due gate che il 21/06 non c'erano**, ed erano proprio
quelli che colpivano le sue riserve dichiarate:
1. **`deflated_sharpe`** (29/06) — l'agente dichiara *da sé* uno sweep di celle. Nel giugno
"72/72 celle ADDS" si leggeva come robustezza; il DSR dice che sono anche 72 tentativi.
2. **`select_cell_insample`** (29/06) — il plateau dell'agente è descritto in termini di
**`uplift_hold`**, cioè la cella è stata guardata **sull'hold-out**. È la firma esatta che il
gate selection-on-holdout fu scritto per catturare.
### Esito
⚠️ **Prima osservazione: la griglia dichiarata è sbagliata.** Il docstring dice "72-cell sweep", ma
il plateau che descrive è 6 assi × 3 valori = **729** combinazioni. Le ho valutate tutte e 729 —
scelta **conservativa**, perché più trial abbassano il DSR.
**Il plateau è reale e larghissimo:** FULL in [0.60, 0.71], HOLD in [0.68, 0.97], e
**729/729 celle con hold-out positivo**. Non è una cella fortunata.
**La selection-on-holdout c'è, ma è mite.** La cella pubblicata è **83ª/729 sull'hold-out** ma
**471ª/729 in-sample** — la firma è inequivocabile. Solo che, essendo il plateau così piatto,
scegliere onestamente la cella in-sample costa poco:
| cella | FULL | IS | HOLD | DSR (729 trial) |
|---|---|---|---|---|
| scelta **IN-SAMPLE** (onesta) | 0.68 | 0.70 | **0.69** | **0.953 PASS** |
| scelta sull'hold-out (proibita) | 0.68 | 0.58 | 0.97 | — |
| **pubblicata dall'agente** | 0.66 | 0.57 | 0.93 | 0.947 **FAIL** |
Cioè: **l'hold-out onesto è 0.69, non il 0.93 pubblicato** — un haircut reale — ma resta
chiaramente positivo. E la cella onesta **passa il deflated-Sharpe (0.953)** mentre quella
pubblicata lo **fallisce (0.947)**, il che è di per sé una conferma che il gate fa il suo lavoro.
Marginale vs TP01 sulla cella onesta: **ADDS**, `robust_oos` ✓, `multicut_persistent` ✓,
`is_hedge` ✗, `has_insample_edge` ✓, `beats_noise_null` ✓; corr +0.11, alpha annua **+7.4%**,
beta a TP01 +0.117, jackknife_min_uplift **+0.069**; dSharpe sul book **+0.08 FULL / +0.17 HOLD**
a w=15%, **+0.11 / +0.25** a w=25%.
### Verdetto: promosso, ma non nel book
**DVOLSPREAD esce dal limbo e diventa un candidato in forward-monitor con parametri ONESTI**
(`zwin=180, k=2.0, lw=0.6, zw=1.1, tgt=0.17, svw=60` — la cella in-sample), non quelli pubblicati.
**Perché NON entra nel book, dichiarato:**
- **campione ATTIVO 1949 giorni su 2691** (72%): prima del 2021-03 non esiste DVOL e il book è
correttamente flat. L'**hold-out attivo è 1.6 anni**;
- **margine DSR sul filo** (0.953 vs soglia 0.95) — un passaggio, non un'assoluzione;
- non ha ancora affrontato `weights_tilt_null`, che ogni cambio di pesi deve superare;
- il numero headline che circolava (HOLD 0.93) era gonfiato dalla selezione: **va citato 0.69**.
---
## T3 — XSR01 non generalizza fuori dal crypto ❌ (e si capisce perché)
### La domanda
XSR01 ha superato tutto ciò che gli è stato chiesto, ma la sua debolezza numero uno è dichiarata:
**2.6 anni monoregime, con l'edge crescente nel tempo** (Sharpe 2024 1.03 / 2025 1.98 / 2026 3.11).
Con 2.6 anni non si distingue un meccanismo da un regime fortunato. L'unico modo di guadagnare
potenza senza aspettare è portare il meccanismo **congelato** su un pannello lungo decenni.
**Non è il test del 25/07.** `r0725_statarb_eq.py` aveva già scartato le azioni, ma testava il
meccanismo a **coppie**. XSR01 è la versione **DEMEANATA cross-sezionalmente**, ed è proprio il
demeaning ad aver creato l'edge (ampiezza effettiva 4.5 → 37.4). Testare il demean sulle azioni è
una domanda diversa, mai posta.
### Esito
Meccanismo congelato (W=45, sgn=+1, residuo OLS causale vs **SPY**, z-score, tanh, vol-target 20%,
demean giornaliero), split IWM/EFA riparati in lettura, annualizzazione a **√252** (giorni di borsa):
| universo | variante | ampiezza eff. | Sharpe **LORDA** | Sharpe netta | maxDD |
|---|---|---|---|---|---|
| **SECT9** (9 settoriali, 1998+, 26 anni) | coppie | 5.3 | 0.05 | 1.67 | 91% |
| **SECT9** | **DEMEAN (= XSR01)** | **6.2** | **+0.24** | 1.61 | 89% |
| **ALL28** (tutti gli ETF, 30 anni) | coppie | 8.4 | 0.20 | 1.61 | 92% |
| **ALL28** | **DEMEAN** | **11.3** | **0.14** | 1.82 | 91% |
| *XSR01 crypto (25/07, riferimento)* | *DEMEAN* | ***37.4*** | ***+2.70*** | *+1.82* | *2.6%* |
Null di permutazione cross-sezionale **a fee zero** (la lezione XSR01: permutare un segnale ne fa
esplodere il turnover, quindi a fee piena il null perderebbe per *costo* e regalerebbe un p-value
trionfale e falso): SECT9 candidato +0.24 vs null medio 0.00, **p = 0.193**; ALL28 0.14, **p = 0.747**.
Per decennio (DEMEAN, lordo): SECT9 0.88 / +0.49 / +0.29 / +0.74 · ALL28 0.88 / +0.46 / +0.38 / +0.05.
Stesso profilo nei due universi: fortemente negativo 1998-2005, debolmente positivo poi — il che
somiglia più a un cambio di regime che a un edge.
### Il risultato più interessante non è il Sharpe, è l'ampiezza
**Il demeaning non fa sulle azioni quello che fa sul crypto.** Sul crypto porta l'ampiezza
effettiva da 4.5 a 37.4 (**8×**); sulle azioni da 5.3 a 6.2 e da 8.4 a 11.3 (**1.2-1.35×**).
Il perché è strutturale, e spiega XSR01 meglio di quanto lo spiegasse la sua scoperta: il residuo
OLS **rimuove già** il beta al fattore comune. Sul crypto, dopo aver residualizzato su BTC, alle
gambe **resta** un enorme fattore comune (ampiezza 4.5 su 50 gambe = quasi una gamba sola) — ed è
quello che il demeaning toglie. Sulle azioni il residuo-vs-SPY è **già** quasi indipendente
(5.3 su 9 gambe), quindi il demeaning non ha molto da togliere e non c'è guadagno.
### Cosa significa per il gate del 2026-10-23
Il 25/07 aveva già scritto che *«il gate non può appoggiarsi all'argomento fenomeno universale»*.
Questo test **lo conferma e chiude la scappatoia** che il test a coppie lasciava aperta: nemmeno
la versione demeanata — quella vera di XSR01 — funziona sulle azioni.
**Onestà su cosa NON prova:** non prova che XSR01 sia falso. Prova che è **crypto-specifico**, e
suggerisce quale sia il meccanismo (l'esistenza di un fattore comune residuo che sopravvive alla
residualizzazione, cosa che le azioni non hanno). Il gate del 23/10 resta **interamente** appoggiato
sulla finestra forward e sull'haircut di eseguibilità a $5.000, come pre-registrato.
Nessuna soglia toccata.
---
## Lezioni da portare avanti
1. **Se si de-lucka una strategia, va de-luckato anche il suo DEGRADO.** L'audit del 02/07 ha
corretto i numeri headline di SKH01 per la fortuna d'ancora ma ha misurato il costo
d'esecuzione a offset 0 — e quel costo si è rivelato **fortunato quanto i numeri che
correggeva**. Ogni Δ fra due varianti misurato su una griglia ancorata eredita la fortuna
dell'ancora.
2. **Su una griglia di offset appaiati, la statistica è la mediana delle DIFFERENZE, non la
differenza delle mediane.** La seconda confronta offset diversi fra loro e qui ribaltava il
verdetto (faceva sembrare l'on-book completo migliore del solo TP, mentre è il contrario).
3. **Un lead lasciato "in forward-monitor" senza monitor e senza data di scadenza è un lead
perso.** DVOLSPREAD è stato in limbo 35 giorni. La disciplina che il progetto applica ai
candidati nuovi (config congelata + gate pre-registrato + cron) non era stata applicata a lui.
4. **Il claim di multiple-testing di un agente va ricontato, non creduto.** Il docstring diceva
72 celle; la griglia descritta ne contiene 729. Ricontare è conservativo e costa poco.
5. **Quando un meccanismo non generalizza, chiedersi PERCHÉ vale più del fatto che non generalizzi.**
Il fallimento di XSR01 sulle azioni ha prodotto la spiegazione migliore di come funziona sul
crypto: il demeaning vale quanto vale il fattore comune **residuo** dopo la residualizzazione,
ed è una proprietà dell'asset class, non del segnale.
+202
View File
@@ -0,0 +1,202 @@
#!/usr/bin/env python
"""r0726_dvolspread_gate — T2 dell'ondata 2026-07-26: chiudere il lead DVOLSPREAD, in limbo
dal 2026-06-21.
CHI E'. `agent_14_dvol_spread` (onda ortho, 2026-06-21) e' **l'unico sopravvissuto** al marginal
scorer indurito: dei 18 book relative-value che facevano "ADDS" con lo scorer vecchio, l'indurito
ne ha lasciato in piedi **1**, questo — "l'unico con edge in-sample reale". Fu lasciato in
forward-monitor con due riserve dichiarate: *multiple-testing* e *storia DVOL corta*. Da allora
non e' mai stato ripreso: non e' nel book, non e' in un paper monitor, non e' stato rifiutato.
E' semplicemente rimasto li'.
PERCHE' RIPRENDERLO ORA. Perche' nel frattempo il progetto ha codificato **due gate che il 21/06
non esistevano**, e sono esattamente quelli che colpiscono la sua riserva:
1. **`deflated_sharpe`** (29/06). Il docstring dell'agente dichiara *da se'* uno **sweep di 72
celle** su ZWIN/LW/ZW/TGT/SVW. 72 trial sono precisamente cio' che il DSR sconta. Nel giugno
"72/72 celle ADDS" veniva letto come prova di robustezza; il DSR dice che e' anche 72 tentativi.
2. **`select_cell_insample` / `study_family_honest`** (29/06). Il plateau dell'agente e' descritto
in termini di **`uplift_hold`** ("ogni vicino resta ADDS con uplift_hold in [0.34,0.39]"),
cioe' la cella e' stata guardata SULL'HOLD-OUT. E' la firma che il gate SELECTION-ON-HOLDOUT
e' stato scritto per catturare — lo stesso difetto che uccise il lead ERM intraday.
DOMANDA SECCA: scegliendo la cella con il solo in-sample, e scontando i 72 trial, DVOLSPREAD
resta uno sleeve o era un falso positivo sopravvissuto solo perche' i gate non c'erano ancora?
Terza verifica, indipendente dalle prime due: la **storia DVOL** parte 2021-03, quindi il book e'
FLAT per meta' del campione. Si misura quanto e' corto il campione ATTIVO (non quello nominale),
perche' e' su quello che vanno letti tutti i numeri.
Uso: `uv run python scripts/research/r0726_dvolspread_gate.py`
"""
from __future__ import annotations
import sys
from pathlib import Path
import numpy as np
import pandas as pd
ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT))
sys.path.insert(0, str(ROOT / "scripts" / "research" / "alt"))
sys.path.insert(0, str(ROOT / "scripts" / "research" / "ortho"))
import altlib as al # noqa: E402
import ortholib as ol # noqa: E402
# griglia DICHIARATA dall'agente stesso (docstring PLATEAU): 72 celle
GRID = dict(zwin=[120, 150, 180], tanh_k=[1.5, 1.8, 2.0],
lw=[0.3, 0.45, 0.6], zw=[0.9, 1.0, 1.1],
tgt=[0.11, 0.13, 0.17], svw=[40, 50, 60])
CHOSEN = dict(zwin=150, tanh_k=1.8, lw=0.45, zw=1.0, tgt=0.13, svw=50)
LEG_CAP = 0.5
def make_book(zwin, tanh_k, lw, zw, tgt, svw):
"""Fabbrica il book dell'agente a parametri arbitrari (stessa formula, nessuna variante)."""
def _book(btc, eth):
import warnings
bc = btc["close"].values.astype(float)
ec = eth["close"].values.astype(float)
rb = ol.simple_returns(bc)
re = ol.simple_returns(ec)
db = al.dvol(btc, "BTC")
de = al.dvol(eth, "ETH")
with np.errstate(invalid="ignore", divide="ignore"):
ldvr = np.log(db) - np.log(de)
ldvr = np.where(np.isfinite(ldvr), ldvr, np.nan)
level = np.tanh(tanh_k * np.nan_to_num(ldvr, nan=0.0))
zdev = np.tanh(tanh_k * np.nan_to_num(ol.zscore(ldvr, zwin), nan=0.0))
g_dir = np.where(np.isfinite(ldvr), lw * level + zw * zdev, 0.0)
with warnings.catch_warnings():
warnings.simplefilter("ignore", category=RuntimeWarning)
spvol = ol.realized_vol(re - rb, svw, 365.25)
scal = np.where((spvol > 0) & np.isfinite(spvol), tgt / spvol, 0.0)
g = np.nan_to_num(np.clip(g_dir * scal, -LEG_CAP, LEG_CAP), nan=0.0)
return g, -g
return _book
_EVAL_CACHE: dict = {}
def evaluate(**kw) -> dict:
"""Valutazione col MOTORE ORIGINALE dell'agente (ortholib.eval_book): stesso shift, stesse
fee su entrambe le gambe, stessa serie daily. Riusarlo — invece di riscriverlo — e' cio' che
rende questo un RE-TEST dello stesso oggetto e non di una sua approssimazione."""
key = tuple(sorted(kw.items()))
if key not in _EVAL_CACHE:
_EVAL_CACHE[key] = ol.eval_book(make_book(**kw))
return _EVAL_CACHE[key]
def daily_returns(**kw) -> pd.Series:
return evaluate(**kw)["daily"].dropna()
def sh(s: pd.Series, lo=None, hi=None) -> float:
x = s.dropna()
if lo is not None:
x = x[x.index >= lo]
if hi is not None:
x = x[x.index < hi]
return float(x.mean() / x.std() * np.sqrt(365.25)) if len(x) > 30 and x.std() > 0 else 0.0
HOLDOUT = "2025-01-01"
def main() -> None:
print("=" * 100)
print(" T2 — DVOLSPREAD: il lead del 21/06 passa i gate del 29/06?")
print("=" * 100)
base = daily_returns(**CHOSEN)
act = base[base != 0.0]
print(f"\n campione NOMINALE: {len(base)} giorni ({base.index[0].date()} -> {base.index[-1].date()})")
print(f" campione ATTIVO : {len(act)} giorni ({len(act)/len(base):.0%}) — "
f"prima del 2021-03 non c'e' DVOL e il book e' FLAT")
print(f" attivo pre-holdout {len(act[act.index < HOLDOUT])} g | "
f"attivo hold-out {len(act[act.index >= HOLDOUT])} g "
f"({len(act[act.index >= HOLDOUT])/365:.1f} anni)")
# ------------------------------------------------------- griglia dichiarata dall'agente
print("\n" + "-" * 100)
print(" A. LA GRIGLIA CHE L'AGENTE DICHIARA DA SE' (72 celle: ZWIN x K x LW x ZW x TGT x SVW)")
print("-" * 100)
import itertools
keys = list(GRID)
cells = []
for combo in itertools.product(*[GRID[k] for k in keys]):
kw = dict(zip(keys, combo))
r = daily_returns(**kw)
cells.append(dict(kw=kw, full=sh(r), is_=sh(r, hi=HOLDOUT), hold=sh(r, lo=HOLDOUT)))
df = pd.DataFrame([{**c["kw"], "full": c["full"], "is": c["is_"], "hold": c["hold"]}
for c in cells])
print(f" celle valutate: {len(df)}")
print(f" Sharpe FULL : min {df['full'].min():.2f} | mediana {df['full'].median():.2f} | "
f"max {df['full'].max():.2f}")
print(f" Sharpe HOLD : min {df['hold'].min():.2f} | mediana {df['hold'].median():.2f} | "
f"max {df['hold'].max():.2f}")
print(f" celle con HOLD > 0: {int((df['hold'] > 0).sum())}/{len(df)}")
# ------------------------------------------------------- selezione IN-SAMPLE vs hold-out
print("\n" + "-" * 100)
print(" B. SELECTION-ON-HOLDOUT — la cella scelta in-sample e' la stessa scelta sull'hold-out?")
print("-" * 100)
i_is = int(df["is"].idxmax())
i_ho = int(df["hold"].idxmax())
i_ch = int(df[[*CHOSEN]].eq(pd.Series(CHOSEN)).all(axis=1).idxmax())
for lab, i in (("cella scelta IN-SAMPLE (onesta)", i_is),
("cella scelta sull'HOLD-OUT (proibita)", i_ho),
("cella PUBBLICATA dall'agente", i_ch)):
row = df.loc[i]
pars = {k: row[k] for k in CHOSEN}
print(f" {lab:>40}: FULL {row['full']:>5.2f} IS {row['is']:>5.2f} "
f"HOLD {row['hold']:>5.2f} {pars}")
rank_ch_hold = int((df["hold"] > df.loc[i_ch, "hold"]).sum()) + 1
rank_ch_is = int((df["is"] > df.loc[i_ch, "is"]).sum()) + 1
print(f"\n rango della cella PUBBLICATA: {rank_ch_hold}/{len(df)} sull'HOLD-OUT, "
f"{rank_ch_is}/{len(df)} sull'IN-SAMPLE")
print(" (se e' molto meglio classificata sull'hold-out che in-sample -> selezione-sull'hold-out)")
# ------------------------------------------------------- deflated Sharpe sui 72 trial
print("\n" + "-" * 100)
print(" C. DEFLATED SHARPE sui trial DICHIARATI DALL'AGENTE STESSO")
print("-" * 100)
r_is = daily_returns(**{k: df.loc[i_is, k] for k in CHOSEN})
for lab, i, r in (("cella IN-SAMPLE", i_is, r_is),
("cella PUBBLICATA", i_ch, base)):
dsr, sr0 = al.deflated_sharpe(sh(r), list(df["full"].values), r.dropna().values)
verdict = "PASS" if (np.isfinite(dsr) and dsr >= 0.95) else "FAIL"
print(f" {lab:>20}: Sharpe FULL {sh(r):>5.2f} | max atteso sotto il null "
f"{sr0:>5.2f} | DSR {dsr:>6.3f} -> {verdict}")
# ------------------------------------------------------- marginale vs TP01
print("\n" + "-" * 100)
print(" D. MARGINALE vs TP01 (scorer indurito) — sulla cella scelta IN-SAMPLE")
print("-" * 100)
tp = al.tp01_baseline_daily()
for lab, r, kwc in (("cella IN-SAMPLE", r_is, {k: df.loc[i_is, k] for k in CHOSEN}),
("cella PUBBLICATA", base, CHOSEN)):
J = pd.concat({"tp": tp, "c": r}, axis=1, join="inner").fillna(0.0)
corr = float(J["tp"].corr(J["c"]))
for w in (0.15, 0.25):
bl = (1 - w) * J["tp"] + w * J["c"]
print(f" {lab:>20} w={w:.0%}: corr {corr:>+5.2f} | "
f"dSharpe FULL {sh(bl) - sh(J['tp']):>+5.2f} | "
f"dSharpe HOLD {sh(bl, lo=HOLDOUT) - sh(J['tp'], lo=HOLDOUT):>+5.2f}")
mg = al.marginal_vs_tp01(r)
print(f" {'':>20} verdetto {mg['marginal_verdict']} | robust_oos={mg['robust_oos']} | "
f"multicut={mg['multicut_persistent']} | is_hedge={mg['is_hedge']} | "
f"insample_edge={mg['has_insample_edge']} | beats_noise={mg['beats_noise_null']}")
print(f" {'':>20} jackknife_min_uplift {mg['jackknife_min_uplift']:+.3f} | "
f"alpha_ann {mg['alpha_ann']:+.1%} | beta_TP01 {mg['beta_to_tp01']:+.3f} | "
f"uplift TP01-up {mg['uplift_tp01_up']:+.3f} / TP01-down {mg['uplift_tp01_down']:+.3f}")
print("\n" + "=" * 100)
if __name__ == "__main__":
main()
+346
View File
@@ -0,0 +1,346 @@
#!/usr/bin/env python
"""r0726_skh_onbook — T1 dell'ondata 2026-07-26: il -0.35 Sharpe del path live di SKH01
e' recuperabile con ORDINI RESTING ON-BOOK invece dell'exit software del cron?
CONTESTO. Il book live Deribit (TP01 0.75 + SKH01 0.25) gira SKH01 col cron ORARIO e uscite
SOFTWARE: il codice guarda il prezzo una volta all'ora e, se il livello SL/TP e' stato superato,
chiude a mercato. Costo misurato (audit 2026-07-02, riconfermato il 24/07): alla lente 50/50 il
book passa FULL 1.46 -> 1.19, HOLD 1.64 -> 1.15, DD 18% -> 25%. E' il **buco singolo piu' grande
del progetto** (~ -0.35 di Sharpe), e il 24/07 ha gia' stabilito che **un cron piu' veloce non lo
recupera** ("nessun cron riporta l'exit software sul livello SL dentro una barra che gappa";
allineare la cadenza a 230m vale ~ +0.01/+0.02 Sharpe = rumore).
L'ANGOLO MAI TESTATO: l'unico meccanismo che non e' un cron — gli **ordini resting on-book**.
E li' c'e' un'ASIMMETRIA che nessuno ha sfruttato:
* il **TP e' un LIMIT order** appoggiato sul book al livello. Se il prezzo ci arriva, il fill
e' AL LIVELLO **per costruzione** (sei tu il maker: il prezzo non puo' attraversare il tuo
limite senza eseguirlo). -> il lato TP e' **integralmente recuperabile**, e per giunta a
fee MAKER invece che taker.
* lo **SL e' uno STOP-MARKET**: al trigger diventa un ordine a mercato. In condizioni normali
riempie a ~pochi bps dal livello; **in un gap riempie molto peggio**. -> il lato SL e'
recuperabile solo IN PARTE, e il gap resta (com'e' giusto: e' un rischio vero, non un
artefatto di modello).
Da cui l'ipotesi da falsificare: **il grosso del -0.35 e' sul lato TP, quindi e' recuperabile
senza prendersi nessun rischio nuovo.** Se invece il grosso e' sul lato SL, la conclusione del
24/07 ("il degrado e' strutturale") si estende anche agli ordini on-book e non c'e' niente da fare.
MODALITA' SIMULATE (stesse trade boundaries, cambia SOLO il prezzo d'esecuzione)
canonical : entry a close[i], exit AL LIVELLO -> il BACKTEST (riferimento ottimista)
hourly : entry/exit al close 5m del prossimo multiplo orario -> il PATH LIVE DI OGGI
onbook : entry al prossimo multiplo orario (l'ingresso resta una decisione del cron:
e' un segnale a chiusura di barra, non si puo' appoggiare in anticipo), ma:
- TP -> fill AL LIVELLO, fee MAKER (limit resting)
- SL -> stop-market: fill al livello se la barra 5m del trigger lo ATTRAVERSA;
fill all'OPEN della barra 5m se la barra ci GAPPA dentro (open gia'
oltre il livello), in entrambi i casi meno SLIPPAGE dichiarato
- time-> resta al prossimo multiplo orario (e' il cron che decide)
onbook_tp : contro-fattuale che ISOLA il lato TP (TP on-book, SL e time come oggi) -> dice
quanta parte del recupero viene dal lato che NON aggiunge rischio.
ONESTA'
* la rilevazione del gap e' a risoluzione **5m** (il gap vero e' piu' fine) -> il fill SL
modellato e' un OTTIMISMO residuo su quel lato; il lato TP invece non ha questo problema
(un limit resting non ha bisogno di risoluzione: o il prezzo tocca il livello o no).
* si ignora la POSIZIONE IN CODA sul limit TP: a $300 di nozionale su BTC perp e' una
semplificazione benigna (size irrilevante rispetto al book), ma e' dichiarata.
* risultati riportati sull'off 0 E sulla **banda dei 23 offset** a priori: il progetto ha gia'
stabilito che l'off 0 e' al 93-98 pctl (best-of-23) -> la mediana di banda e' la lente onesta.
* niente file di produzione toccato. Se il risultato regge, la modifica al live e' una
RACCOMANDAZIONE, non un fatto compiuto.
Uso: `uv run python scripts/research/r0726_skh_onbook.py` (~5-10 min: 23 offset x 2 asset)
"""
from __future__ import annotations
import sys
from pathlib import Path
import numpy as np
import pandas as pd
ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT))
sys.path.insert(0, str(ROOT / "scripts" / "research"))
sys.path.insert(0, str(ROOT / "scripts" / "research" / "alt"))
import r0702_anchor_skh01 as r02 # noqa: E402
from src.portfolio.portfolio import metrics # noqa: E402
MSH = 3_600_000
MS_LTF = r02.MS_LTF
FEE_TAKER_SIDE = 0.0005 # Deribit taker ~0.05%/lato (il RT 0.10% del progetto)
FEE_MAKER_SIDE = 0.0000 # limit resting: maker (Deribit futures ~0, a volte rebate)
SL_SLIP = 0.0005 # slippage dichiarata sullo stop-market (5 bps)
HOLDOUT = "2025-01-01"
# --------------------------------------------------------------------------- simulazione
def sim_equity_exec(ltf: pd.DataFrame, ent: list, mode: str,
ts5_close: np.ndarray, o5: np.ndarray, h5: np.ndarray,
l5: np.ndarray, c5: np.ndarray) -> np.ndarray:
"""Come r0702.sim_equity ma con le modalita' d'esecuzione ON-BOOK.
Le trade boundaries (barra d'ingresso, barra di trigger, non-overlap) sono IDENTICHE al
canonico in ogni modalita': cambia solo il prezzo a cui si esegue e la fee del lato.
"""
c = ltf["close"].values.astype(float)
h = ltf["high"].values.astype(float)
lo = ltf["low"].values.astype(float)
n = len(c)
close_ts = ltf["timestamp"].values.astype(np.int64) + MS_LTF
def px_hour(t: int) -> float:
hb = ((t + MSH - 1) // MSH) * MSH
j = np.searchsorted(ts5_close, hb, side="left")
return c5[min(j, len(c5) - 1)]
def stop_fill(t_start: int, t_end: int, level: float, direction: int) -> float:
"""Prezzo di fill di uno STOP-MARKET on-book fra due timestamp, a risoluzione 5m.
Trova la prima barra 5m che tocca il livello: se ci GAPPA dentro (open gia' oltre)
il fill e' l'open (il gap e' un rischio vero, non un artefatto); altrimenti e' il
livello. In entrambi i casi meno la slippage dichiarata.
"""
a = int(np.searchsorted(ts5_close, t_start, side="left"))
b = int(np.searchsorted(ts5_close, t_end, side="right"))
for j in range(a, min(b + 1, len(c5))):
touched = (l5[j] <= level) if direction == 1 else (h5[j] >= level)
if touched:
gapped = (o5[j] < level) if direction == 1 else (o5[j] > level)
px = o5[j] if gapped else level
return px * (1.0 - SL_SLIP * direction)
return level * (1.0 - SL_SLIP * direction) # non trovato a 5m: cap al livello
initial = 1000.0
capital = initial
equity = np.full(n, capital, dtype=float)
busy_until = -1
stats = dict(sl=0, tp=0, time=0, gap_sl=0)
for i in range(n):
e = ent[i] if i < len(ent) else None
if e is None or e.get("dir", 0) == 0 or i <= busy_until:
equity[i] = capital
continue
direction = int(e["dir"])
tp = e.get("tp"); sl = e.get("sl")
max_bars = int(e.get("max_bars") or 24)
entry = c[i] if mode == "canonical" else px_hour(close_ts[i])
fee_entry = FEE_TAKER_SIDE
# --- detection identica al canonico (SL prioritario)
exit_idx = min(i + max_bars, n - 1)
exit_lvl = c[exit_idx]
kind = "time"
for j in range(i + 1, min(i + max_bars + 1, n)):
hit_sl = sl is not None and (
(direction == 1 and lo[j] <= sl) or (direction == -1 and h[j] >= sl))
hit_tp = tp is not None and (
(direction == 1 and h[j] >= tp) or (direction == -1 and lo[j] <= tp))
if hit_sl:
exit_lvl, exit_idx, kind = sl, j, "sl"
break
if hit_tp:
exit_lvl, exit_idx, kind = tp, j, "tp"
break
exit_lvl, exit_idx = c[j], j
stats[kind] += 1
# --- prezzo d'uscita per modalita'
if mode == "canonical":
exit_price, fee_exit = exit_lvl, FEE_TAKER_SIDE
elif mode == "hourly":
exit_price, fee_exit = px_hour(close_ts[exit_idx]), FEE_TAKER_SIDE
elif mode in ("onbook", "onbook_tp"):
if kind == "tp":
exit_price, fee_exit = tp, FEE_MAKER_SIDE # limit resting: al livello
elif kind == "sl" and mode == "onbook":
t0 = int(ltf["timestamp"].values[exit_idx])
exit_price = stop_fill(t0, t0 + MS_LTF, sl, direction)
fee_exit = FEE_TAKER_SIDE
if (direction == 1 and exit_price < sl * (1 - SL_SLIP) - 1e-9) or \
(direction == -1 and exit_price > sl * (1 + SL_SLIP) + 1e-9):
stats["gap_sl"] += 1
else: # sl in onbook_tp, o time
exit_price, fee_exit = px_hour(close_ts[exit_idx]), FEE_TAKER_SIDE
else:
raise ValueError(mode)
gross = (exit_price - entry) / entry * direction
net = gross - (fee_entry + fee_exit)
capital = max(capital + capital * net, 1.0)
equity[i:exit_idx + 1] = capital
busy_until = exit_idx
return equity, stats
def skh_series(off: int, mode: str) -> tuple[pd.Series, dict]:
"""Rendimenti daily dello sleeve SKH01 (50/50 BTC+ETH) a un offset e una modalita'."""
out = {}
agg = dict(sl=0, tp=0, time=0, gap_sl=0)
for asset in ("BTC", "ETH"):
_, _, ltf, ent = r02.run_asset(asset, off)
df5 = r02.get5m(asset)
ts5 = df5["timestamp"].values.astype(np.int64) + 300_000
eq, st = sim_equity_exec(ltf, ent, mode, ts5,
df5["open"].values.astype(float),
df5["high"].values.astype(float),
df5["low"].values.astype(float),
df5["close"].values.astype(float))
for k in agg:
agg[k] += st[k]
idx = pd.to_datetime(ltf["timestamp"].values + MS_LTF, unit="ms", utc=True)
s = pd.Series(eq, index=idx).resample("1D").last().ffill()
out[asset] = s.pct_change().fillna(0.0)
J = pd.concat(out, axis=1, join="inner").fillna(0.0)
return pd.Series(0.5 * J["BTC"].values + 0.5 * J["ETH"].values, index=J.index), agg
def book(skh: pd.Series, w: float) -> pd.Series:
tp = r02.al.tp01_baseline_daily()
tp = tp.dropna()
if tp.index.tz is None:
tp.index = tp.index.tz_localize("UTC")
tp.index = tp.index.floor("D")
s = skh.copy()
s.index = s.index.floor("D")
J = pd.concat({"tp": tp, "skh": s}, axis=1, join="inner").fillna(0.0)
return pd.Series((1 - w) * J["tp"].values + w * J["skh"].values, index=J.index)
def sh3(s: pd.Series) -> tuple[float, float, float, float]:
m_all = metrics(s)
m_is = metrics(s[s.index < HOLDOUT])
m_ho = metrics(s[s.index >= HOLDOUT])
return m_all["sharpe"], m_is["sharpe"], m_ho["sharpe"], m_all["maxdd"]
MODES = ("canonical", "hourly", "onbook_tp", "onbook")
LABEL = {"canonical": "canonical (backtest, fill-al-livello)",
"hourly": "hourly (PATH LIVE DI OGGI)",
"onbook_tp": "onbook_tp (solo TP a limite)",
"onbook": "onbook (TP limite + SL stop-market)"}
def main() -> None:
global SL_SLIP
print("=" * 100)
print(" T1 — SKH01: il degrado del path live e' recuperabile con ordini RESTING ON-BOOK?")
print("=" * 100)
print(f"\n fee: taker {FEE_TAKER_SIDE:.4%}/lato, maker {FEE_MAKER_SIDE:.4%}/lato | "
f"slippage stop-market {SL_SLIP:.2%} | hold-out {HOLDOUT}")
# ------------------------------------------------ offset 0 (la griglia che gira davvero)
print("\n" + "-" * 100)
print(" A. OFFSET 0 — la griglia che gira davvero nel live")
print("-" * 100)
ser0 = {}
print(f" {'modalita':>40} {'SKH standalone':>28} {'book 75/25':>26}")
print(f" {'':>40} {'FULL':>6} {'IS':>6} {'HOLD':>6} {'DD':>6} "
f"{'FULL':>6} {'IS':>6} {'HOLD':>6} {'DD':>6}")
stats0 = {}
for m in MODES:
s, st = skh_series(0, m)
ser0[m] = s
stats0[m] = st
a = sh3(s)
b = sh3(book(s, 0.25))
print(f" {LABEL[m]:>40} {a[0]:>6.2f} {a[1]:>6.2f} {a[2]:>6.2f} {a[3]:>5.1%} "
f"{b[0]:>6.2f} {b[1]:>6.2f} {b[2]:>6.2f} {b[3]:>5.1%}")
st = stats0["onbook"]
tot = st["sl"] + st["tp"] + st["time"]
print(f"\n composizione delle uscite (off 0, BTC+ETH): {tot} trade — "
f"SL {st['sl']} ({st['sl']/tot:.0%}) | TP {st['tp']} ({st['tp']/tot:.0%}) | "
f"time {st['time']} ({st['time']/tot:.0%})")
print(f" di cui SL con GAP dentro la barra 5m (fill peggiore del livello): "
f"{st['gap_sl']} = {st['gap_sl']/max(st['sl'],1):.0%} degli SL")
# ------------------------------------------------ scomposizione del recupero
print("\n" + "-" * 100)
print(" B. DOVE VIVE IL DEGRADO — scomposizione del gap canonical -> live")
print("-" * 100)
b_can, b_hr = sh3(book(ser0["canonical"], 0.25)), sh3(book(ser0["hourly"], 0.25))
b_tp, b_ob = sh3(book(ser0["onbook_tp"], 0.25)), sh3(book(ser0["onbook"], 0.25))
gap_tot = b_can[0] - b_hr[0]
print(f" book 75/25, Sharpe FULL: canonical {b_can[0]:.3f} -> live {b_hr[0]:.3f} "
f"(degrado {gap_tot:+.3f})")
if abs(gap_tot) > 1e-9:
print(f" recupero col solo TP a limite : {b_tp[0]-b_hr[0]:+.3f} "
f"= {(b_tp[0]-b_hr[0])/gap_tot:>5.0%} del degrado [NESSUN rischio nuovo]")
print(f" recupero TP + SL on-book : {b_ob[0]-b_hr[0]:+.3f} "
f"= {(b_ob[0]-b_hr[0])/gap_tot:>5.0%} del degrado")
print(f" residuo NON recuperabile : {b_can[0]-b_ob[0]:+.3f} "
f"= {(b_can[0]-b_ob[0])/gap_tot:>5.0%} del degrado [gap + entry del cron]")
# ------------------------------------------------ banda dei 23 offset (la lente onesta)
print("\n" + "-" * 100)
print(" C. BANDA DEI 23 OFFSET — la lente de-luckata (off 0 e' al 93-98 pctl, audit 02/07)")
print("-" * 100)
rows = {m: [] for m in MODES}
for off in r02.OFFSETS:
for m in MODES:
s, _ = skh_series(off, m)
rows[m].append(sh3(book(s, 0.25)))
print(f" {'modalita':>40} {'FULL med':>9} {'IS med':>8} {'HOLD med':>9} "
f"{'DD med':>7} {'HOLD min':>9} {'HOLD max':>9}")
med = {}
A = {m: np.array(rows[m]) for m in MODES}
for m in MODES:
med[m] = np.median(A[m], axis=0)
print(f" {LABEL[m]:>40} {med[m][0]:>9.2f} {med[m][1]:>8.2f} {med[m][2]:>9.2f} "
f"{med[m][3]:>6.1%} {A[m][:, 2].min():>9.2f} {A[m][:, 2].max():>9.2f}")
# ⚠️ gli offset sono coppie APPAIATE: la statistica giusta e' la mediana delle DIFFERENZE
# per-offset, non la differenza delle mediane (che confronta offset diversi fra loro).
print("\n differenze APPAIATE per-offset (mediana [min, max] sui 23 offset):")
print(f" {'':>40} {'dFULL':>22} {'dHOLD':>22} {'dDD':>20}")
for m in ("canonical", "onbook_tp", "onbook"):
d0 = A[m][:, 0] - A["hourly"][:, 0]
d2 = A[m][:, 2] - A["hourly"][:, 2]
d3 = A[m][:, 3] - A["hourly"][:, 3]
print(f" {LABEL[m]:>40} "
f"{np.median(d0):>+7.3f} [{d0.min():+.2f},{d0.max():+.2f}] "
f"{np.median(d2):>+7.3f} [{d2.min():+.2f},{d2.max():+.2f}] "
f"{np.median(d3):>+6.2%} [{d3.min():+.1%},{d3.max():+.1%}]")
dmax = np.median(A["canonical"][:, 0] - A["hourly"][:, 0])
dob = np.median(A["onbook"][:, 0] - A["hourly"][:, 0])
dtp = np.median(A["onbook_tp"][:, 0] - A["hourly"][:, 0])
print(f"\n degrado recuperabile al massimo (canonical-hourly, mediana appaiata): {dmax:+.3f} FULL")
if abs(dmax) > 1e-9:
print(f" recuperato dal solo TP a limite: {dtp:+.3f} = {dtp/dmax:>5.0%}")
print(f" recuperato da TP + SL on-book : {dob:+.3f} = {dob/dmax:>5.0%}")
print("\n in quanti dei 23 offset la modalita' MIGLIORA il path live di oggi?")
for m in ("onbook_tp", "onbook"):
nf = int((A[m][:, 0] - A["hourly"][:, 0] > 0).sum())
nh = int((A[m][:, 2] - A["hourly"][:, 2] > 0).sum())
nd = int((A[m][:, 3] - A["hourly"][:, 3] < 0).sum())
print(f" {LABEL[m]:>40}: FULL {nf}/23 HOLD {nh}/23 DD {nd}/23")
# il lato SL da solo = differenza fra onbook (TP+SL) e onbook_tp (solo TP)
dsl = A["onbook"][:, 0] - A["onbook_tp"][:, 0]
print(f"\n contributo del SOLO lato SL on-book (onbook - onbook_tp), FULL: "
f"mediana {np.median(dsl):+.3f} [{dsl.min():+.2f},{dsl.max():+.2f}], "
f"positivo in {int((dsl > 0).sum())}/23 offset")
# ------------------------------------------------ robustezza alla slippage assunta
print("\n" + "-" * 100)
print(" D. SENSIBILITA' ALLA SLIPPAGE ASSUNTA sullo stop-market (off 0, book 75/25)")
print("-" * 100)
keep = SL_SLIP
print(f" {'slippage':>10} {'FULL':>7} {'HOLD':>7} {'DD':>7} (live di oggi: "
f"FULL {b_hr[0]:.2f} / HOLD {b_hr[2]:.2f})")
for slip in (0.0000, 0.0005, 0.0020, 0.0050, 0.0100):
SL_SLIP = slip
s, _ = skh_series(0, "onbook")
r = sh3(book(s, 0.25))
flag = "" if r[0] > b_hr[0] else " <- peggio del live"
print(f" {slip:>9.2%} {r[0]:>7.2f} {r[2]:>7.2f} {r[3]:>6.1%}{flag}")
SL_SLIP = keep
print("\n" + "=" * 100)
if __name__ == "__main__":
main()
+214
View File
@@ -0,0 +1,214 @@
#!/usr/bin/env python
"""r0726_xsr_equity — T3 dell'ondata 2026-07-26: XSR01 regge fuori dal crypto e fuori dai
2.6 anni monoregime? Test di FALSIFICAZIONE su 26+ anni di ETF settoriali.
PERCHE'. XSR01 (candidato in forward-monitor dal 25/07, gate pre-registrato al 2026-10-23) ha
superato tutto quello che gli e' stato chiesto — marginale ADDS, deflated-Sharpe 0.985, corr ~0
a tutti e 5 gli sleeve, tre null a fee-neutrale — MA la sua debolezza numero uno e' dichiarata:
"storia 2.6 anni MONOREGIME e CRESCENTE (Sharpe 2024 1.03 / 2025 1.98 / 2026 3.11):
l'edge e' recente."
Con 2.6 anni non si distingue un meccanismo reale da un regime fortunato. L'unico modo di
guadagnare potenza statistica senza aspettare anni e' portare il meccanismo CONGELATO su un
pannello **diverso per asset class e lungo decenni**, e vedere se sopravvive. Se regge su 26 anni
di settoriali US, l'ipotesi "e' un artefatto del bear alt 2024-2026" e' molto piu' difficile da
sostenere. Se non regge, il gate del 23/10 deve saperlo.
NON E' IL TEST DEL 25/07. `r0725_statarb_eq.py` ha gia' portato il meccanismo sulle azioni ed e'
stato SCARTATO (paniere Sharpe -1.00 netta, -0.17 lorda) — ma quello era il meccanismo a **COPPIE**
(12 coppie a priori, ampiezza effettiva 9.9). XSR01 non e' un paniere di coppie: e' la versione
**DEMEANATA cross-sezionalmente**, ed e' proprio il demeaning che ha cambiato tutto (ampiezza
effettiva 4.5 -> 37.4) annullando algebricamente la gamba comune. Testare il demean su equity e'
quindi una domanda DIVERSA, mai posta. Il risultato lordo del 25/07 (-0.17, cioe' il residuo
REVERTE debolmente sulle azioni mentre CONTINUA sul crypto) e' l'ipotesi nulla di questo test.
DISEGNO
* meccanismo CONGELATO importato da r0725_statarb_multi (W=45, sgn=+1, residuo OLS causale su
un fattore comune, z-score, tanh, vol-target 20%, cap 2x) — nessuna reimplementazione;
* demeaning cross-sezionale giornaliero, identico a r0725_statarb_basket_gate;
* fattore comune = **SPY** (analogo di BTC: il fattore che il demeaning deve annullare);
* due universi, entrambi a priori:
SECT9 = i 9 settoriali SPDR classici (1998+, ~26 anni) — cross-section OMOGENEA, il
parallelo piu' stretto degli alt (un solo fattore comune forte);
ALL28 = tutti gli ETF su disco tranne SPY — parallelo del "ALL50" di XSR01;
* split NON aggiustati riparati in lettura (eqlib.load_eq -> src/data/eq_splits) — senza questo
IWM/EFA avvelenerebbero il pannello (difetto trovato il 25/07);
* LORDO **e** netto: la lettura del 25/07 e' che sulle azioni il segno vive nel lordo e il
turnover se lo mangia. Un venue a PAVIMENTO FISSO (IB) e' anche peggio di una fee
proporzionale a questa taglia (lezione 25/07) -> il netto proporzionale e' gia' generoso;
* null di permutazione cross-sezionale **A FEE ZERO** (lezione XSR01: permutare un segnale ne
fa esplodere il turnover, quindi un null a fee piena perderebbe per COSTO invece che per
assenza d'informazione, e regalerebbe un p-value trionfale e falso);
* decomposizione per DECENNIO: 26 anni servono a questo, non a fare una media unica.
Uso: `uv run python scripts/research/r0726_xsr_equity.py`
"""
from __future__ import annotations
import sys
from pathlib import Path
import numpy as np
import pandas as pd
ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT))
sys.path.insert(0, str(ROOT / "scripts" / "research"))
from eqlib import SECTORS_CLASSIC, load_eq # noqa: E402
from r0725_statarb_multi import (FEE_LEG, SGN, W, _dd, _sh, signal) # noqa: E402
RAW = ROOT / "data" / "raw"
BASE = "SPY"
MIN_BARS = 400
N_PERM = 300
BLOCK = 20
SEED = 20260726
ANN = np.sqrt(252.0) # ⚠️ giorni di BORSA, non 365 (lezione 25/07 sull'annualizzazione)
def all_symbols() -> list[str]:
out = []
for p in sorted(RAW.glob("eq_*_1d.parquet")):
s = p.stem.replace("eq_", "").replace("_1d", "").upper()
if s != BASE:
out.append(s)
return out
def _px(sym: str) -> pd.Series:
s = load_eq(sym)["close"].astype(float)
return s[~s.index.duplicated(keep="last")].sort_index()
def build(cols: list[str]) -> tuple[pd.DataFrame, pd.DataFrame]:
"""Posizioni e spread per ogni gamba, col meccanismo congelato vs SPY."""
base = _px(BASE)
P, S = {}, {}
for sym in cols:
try:
t = _px(sym)
except FileNotFoundError:
continue
ix = base.index.intersection(t.index)
if len(ix) < MIN_BARS:
continue
p, s = signal(base[ix], t[ix])
P[sym] = pd.Series(p, index=ix)
S[sym] = pd.Series(s, index=ix)
return (pd.concat(P, axis=1, sort=True).sort_index(),
pd.concat(S, axis=1, sort=True).sort_index())
def pnl_fee(pos: np.ndarray, spread: np.ndarray, fee_leg: float) -> np.ndarray:
held = np.concatenate([[0.0], pos[:-1]])
turn = np.abs(np.diff(held, prepend=0.0))
return held * spread - 2.0 * fee_leg * turn
def basket(P: pd.DataFrame, S: pd.DataFrame, demean: bool, fee_leg: float) -> pd.Series:
Q = P.sub(P.mean(axis=1), axis=0) if demean else P
out = {c: pd.Series(pnl_fee(Q[c].to_numpy(float), S[c].to_numpy(float), fee_leg), index=Q.index)
for c in Q.columns}
return pd.concat(out, axis=1, sort=True).mean(axis=1, skipna=True).dropna()
def eff_breadth(P: pd.DataFrame, S: pd.DataFrame, demean: bool) -> float:
Q = P.sub(P.mean(axis=1), axis=0) if demean else P
M = pd.concat({c: pd.Series(pnl_fee(Q[c].to_numpy(float), S[c].to_numpy(float), 0.0),
index=Q.index) for c in Q.columns}, axis=1)
C = M.corr().values
n = C.shape[0]
off = (C.sum() - np.trace(C)) / (n * (n - 1))
return float(n / (1.0 + (n - 1) * off))
def sh(r) -> float:
r = np.asarray(r, float)
r = r[np.isfinite(r)]
return float(r.mean() / r.std() * ANN) if len(r) > 30 and r.std() > 0 else 0.0
def main() -> None:
print("=" * 100)
print(" T3 — XSR01 fuori dal crypto: il meccanismo congelato su 26 anni di ETF settoriali")
print("=" * 100)
print(f"\n meccanismo: W={W}, sgn={SGN:+d}, residuo OLS causale vs {BASE}, z-score, tanh,")
print(" vol-target 20%, cap 2x, demean cross-sezionale giornaliero [CONGELATO, importato]")
print(f" annualizzazione: sqrt(252) = giorni di BORSA (NON 365)")
universes = {
"SECT9 9 settoriali SPDR (1998+)": SECTORS_CLASSIC,
"ALL28 tutti gli ETF su disco": all_symbols(),
}
store = {}
for name, cols in universes.items():
P, S = build(cols)
store[name] = (P, S)
print("\n" + "-" * 100)
print(f" {name}{P.shape[1]} gambe, {len(P)} barre, "
f"{P.index[0].date()} -> {P.index[-1].date()}")
print("-" * 100)
print(f" {'variante':>28} {'ampiezza eff.':>14} {'Sharpe LORDA':>13} "
f"{'Sharpe NETTA':>13} {'maxDD':>8} {'ret/anno':>9}")
for lab, dm in (("coppie (no demean)", False), ("DEMEAN (= XSR01)", True)):
g = basket(P, S, dm, 0.0)
n = basket(P, S, dm, FEE_LEG)
eb = eff_breadth(P, S, dm)
cagr = float(np.nanmean(n.values)) * 252.0
print(f" {lab:>28} {eb:>14.1f} {sh(g):>13.2f} {sh(n):>13.2f} "
f"{_dd(n.values):>7.1%} {cagr:>8.1%}")
# ------------------------------------------------------------------ per decennio
print("\n" + "-" * 100)
print(" A. PER DECENNIO — 26 anni servono a questo (variante DEMEAN, la stessa di XSR01)")
print("-" * 100)
print(f" {'universo':>34} {'periodo':>14} {'anni':>5} {'Sh LORDA':>9} {'Sh NETTA':>9} {'giorni':>7}")
for name, (P, S) in store.items():
g = basket(P, S, True, 0.0)
n = basket(P, S, True, FEE_LEG)
for lo, hi in (("1998", "2005"), ("2005", "2012"), ("2012", "2019"), ("2019", "2027")):
kg = (g.index >= lo) & (g.index < hi)
kn = (n.index >= lo) & (n.index < hi)
if kg.sum() < 250:
continue
print(f" {name.split()[0]:>34} {lo + '-' + hi:>14} {kg.sum() / 252:>5.1f} "
f"{sh(g[kg]):>9.2f} {sh(n[kn]):>9.2f} {int(kg.sum()):>7d}")
# ------------------------------------------------------------------ null a fee ZERO
print("\n" + "-" * 100)
print(" B. NULL DI PERMUTAZIONE CROSS-SEZIONALE — a FEE ZERO (lezione XSR01)")
print("-" * 100)
print(" Permutare un segnale ne fa esplodere il turnover: a fee piena il null perderebbe")
print(" per COSTO invece che per assenza d'informazione -> p-value trionfale e falso.")
rng = np.random.default_rng(SEED)
print(f"\n {'universo':>34} {'candidato LORDO':>16} {'null medio':>11} {'null p95':>9} "
f"{'null max':>9} {'p-value':>8}")
for name, (P, S) in store.items():
cand = sh(basket(P, S, True, 0.0))
Pv = P.to_numpy(float)
draws = []
for _ in range(N_PERM):
idx = rng.permutation(Pv.shape[1])
Pp = pd.DataFrame(Pv[:, idx], index=P.index, columns=P.columns)
draws.append(sh(basket(Pp, S, True, 0.0)))
d = np.array(draws)
pval = float((d >= cand).mean())
print(f" {name.split()[0]:>34} {cand:>16.2f} {d.mean():>11.2f} "
f"{np.percentile(d, 95):>9.2f} {d.max():>9.2f} {pval:>8.3f}")
# ------------------------------------------------------------------ confronto col crypto
print("\n" + "-" * 100)
print(" C. CONFRONTO DIRETTO col candidato crypto (numeri del 25/07, non ricalcolati)")
print("-" * 100)
print(" XSR01 (50 alt HL, 2.6 anni): ampiezza eff. 37.4 | LORDA 2.70 | NETTA 1.82 | DD -2.6%")
print(" STATARB-EQ a COPPIE (25/07): ampiezza eff. 9.9 | LORDA -0.17 | NETTA -1.00")
print("\n La domanda del test: il DEMEAN — che sul crypto ha fatto 4.5 -> 37.4 di ampiezza e")
print(" ha creato l'edge — fa la stessa cosa sulle azioni, o li' il residuo resta reverting?")
print("\n" + "=" * 100)
if __name__ == "__main__":
main()
+144
View File
@@ -0,0 +1,144 @@
"""Test dell'ondata 2026-07-26 (T1 esecuzione SKH01, T2 DVOLSPREAD, T3 XSR01 su equity).
Blindano le proprieta' da cui dipendono le conclusioni — non i numeri (che si muovono col feed),
ma le INVARIANTI che, se si rompessero, renderebbero i numeri privi di significato.
"""
from __future__ import annotations
import sys
from pathlib import Path
import numpy as np
import pytest
ROOT = Path(__file__).resolve().parents[1]
for p in (ROOT, ROOT / "scripts" / "research", ROOT / "scripts" / "research" / "alt",
ROOT / "scripts" / "research" / "ortho"):
sys.path.insert(0, str(p))
pytest.importorskip("pandas")
# ============================================================ T1 — esecuzione SKH01
def test_t1_le_modalita_condividono_le_stesse_trade_boundaries():
"""Il confronto fra modalita' d'esecuzione ha senso SOLO se i trade sono gli stessi:
cambia il prezzo di fill, non quali trade si fanno. Se le boundaries divergessero
staremmo confrontando due strategie diverse e non due esecuzioni della stessa."""
import r0702_anchor_skh01 as r02
import r0726_skh_onbook as ob
_, _, ltf, ent = r02.run_asset("BTC", 0)
df5 = r02.get5m("BTC")
args = (df5["timestamp"].values.astype(np.int64) + 300_000,
df5["open"].values.astype(float), df5["high"].values.astype(float),
df5["low"].values.astype(float), df5["close"].values.astype(float))
stats = {}
for m in ("canonical", "hourly", "onbook", "onbook_tp"):
_, st = ob.sim_equity_exec(ltf, ent, m, *args)
stats[m] = (st["sl"], st["tp"], st["time"])
assert len(set(stats.values())) == 1, stats
def test_t1_canonical_riproduce_il_backtest_ufficiale():
"""La modalita' 'canonical' deve coincidere col backtest dello sleeve: e' l'ancora
di tutto il confronto. Tolleranza larga (il feed cresce ogni giorno)."""
import r0726_skh_onbook as ob
s, _ = ob.skh_series(0, "canonical")
full, _, hold, dd = ob.sh3(s)
assert 1.2 < full < 1.7, full # audit 02/07: ~1.46
assert 1.3 < hold < 1.9, hold # audit 02/07: ~1.64
assert 0.12 < dd < 0.24, dd # audit 02/07: ~18.1%
def test_t1_il_limit_tp_non_puo_riempire_peggio_del_livello():
"""Un limit resting o riempie AL livello o non riempie. Se il modello desse un prezzo
peggiore del livello sul lato TP, starebbe modellando un ordine a mercato."""
import r0702_anchor_skh01 as r02
import r0726_skh_onbook as ob
_, _, ltf, ent = r02.run_asset("BTC", 0)
df5 = r02.get5m("BTC")
eq_tp, _ = ob.sim_equity_exec(
ltf, ent, "onbook_tp", df5["timestamp"].values.astype(np.int64) + 300_000,
df5["open"].values.astype(float), df5["high"].values.astype(float),
df5["low"].values.astype(float), df5["close"].values.astype(float))
assert np.all(np.isfinite(eq_tp)) and eq_tp[-1] > 0
def test_t1_slippage_piu_alta_non_migliora_lo_stop():
"""Monotonia elementare: piu' slippage sullo stop-market non puo' dare un book migliore.
Se si rompe, il segno dello slippage e' invertito da qualche parte."""
import r0726_skh_onbook as ob
keep = ob.SL_SLIP
try:
ob.SL_SLIP = 0.0
lo = ob.sh3(ob.book(ob.skh_series(0, "onbook")[0], 0.25))[0]
ob.SL_SLIP = 0.02
hi = ob.sh3(ob.book(ob.skh_series(0, "onbook")[0], 0.25))[0]
finally:
ob.SL_SLIP = keep
assert lo > hi, (lo, hi)
# ============================================================ T2 — DVOLSPREAD
def test_t2_il_book_e_market_neutral_per_costruzione():
"""w_eth == -w_btc: e' l'unica ragione per cui il lead e' a bassa correlazione con TP01.
Se si rompesse, la bassa correlazione sarebbe un caso e non una proprieta'."""
import r0726_dvolspread_gate as g
ev = g.evaluate(**g.CHOSEN)
assert abs(ev["net_beta"]) < 0.02, ev["net_beta"]
def test_t2_book_flat_prima_che_esista_il_dvol():
"""Prima del 2021-03 non c'e' DVOL: il book DEVE essere flat, altrimenti sta usando
un valore inventato (e il campione 'attivo' sarebbe una bugia)."""
import pandas as pd
import r0726_dvolspread_gate as g
r = g.daily_returns(**g.CHOSEN)
pre = r[r.index < pd.Timestamp("2021-03-01", tz="UTC")]
assert len(pre) > 100
assert float(np.abs(pre.values).max()) < 1e-12
def test_t2_eseguibile_al_capitale_reale():
"""Il lead vale solo se sta dentro il cap di $300/asset a $600 di capitale."""
import r0726_dvolspread_gate as g
ev = g.evaluate(**g.CHOSEN)
assert ev["executable"], (ev["max_leg_frac"], ev["gross_lev"])
# ============================================================ T3 — XSR01 su equity
def test_t3_usa_il_meccanismo_congelato_non_una_copia():
"""Il test di falsificazione vale solo se gira LO STESSO meccanismo del candidato:
se qualcuno cambiasse W o il segno, il test smetterebbe di dire qualcosa su XSR01."""
from r0725_statarb_multi import SGN, W
assert (W, SGN) == (45, +1)
def test_t3_il_demean_azzera_la_posizione_media():
"""Il demeaning cross-sezionale deve rendere la posizione media giornaliera nulla:
e' cio' che elimina algebricamente la gamba comune. Senza, non e' XSR01."""
import r0726_xsr_equity as xe
from eqlib import SECTORS_CLASSIC
P, _ = xe.build(SECTORS_CLASSIC)
Q = P.sub(P.mean(axis=1), axis=0)
assert float(np.abs(np.nan_to_num(Q.mean(axis=1).values)).max()) < 1e-12
def test_t3_lo_split_e_riparato_in_lettura():
"""IWM ha uno split non aggiustato il 2005-06-09 (difetto trovato il 25/07). Se il
pannello non lo riparasse, il test su 26 anni sarebbe avvelenato."""
import pandas as pd
from eqlib import load_eq
d = load_eq("IWM")
r = d["close"].pct_change()
around = r[(r.index >= pd.Timestamp("2005-06-01", tz="UTC"))
& (r.index <= pd.Timestamp("2005-06-20", tz="UTC"))]
assert float(around.min()) > -0.20, float(around.min())
def test_t3_annualizzazione_su_giorni_di_borsa():
"""Lezione 25/07: una serie su ~252 barre/anno annualizzata a 365 gonfia lo Sharpe di 1.2x."""
import r0726_xsr_equity as xe
assert abs(xe.ANN - np.sqrt(252.0)) < 1e-9