Audit del lavoro sulle correlazioni — dieci errori trovati
Ricalcolo indipendente delle correlazioni, dei residui e delle dipendenze di coda usate nello studio sulle coperture.
Misurato e sopravvissuto
Coprire una coppia FX con un'altra coppia dovrebbe eliminare una quota sostanziale del rischio.
Dipendenza nelle code
I crash congiunti superano nettamente il riferimento gaussiano.
Come e' stato misurato
Rendimenti non sovrapposti di 24 strumenti dal 2010 al 2026, fattori valutari ai minimi quadrati, residui e code con ricalcolo indipendente.
Verdetto stretto
Nel perimetro valutario testato quasi tutta la varianza e' fattoriale: due coppie spesso ricreano una terza esposizione pagando due spread.
- Misura rischio rimosso, non rendimento rimosso; storia e contratti broker non congelati.
- Le eccezioni principali sono oro duplicato e residuo BTC/ETH.
Artefatti registrati: 3 presenti · 1 esterni · 1 mancanti
Quello che regge, verificato per strade indipendenti
| controllo | esito |
|---|---|
| ρ, σ e coda di GBPJPY↔EURJPY a 4h, ricalcolate con un codice completamente diverso | ρ +0,7579 contro +0,7579 · rapporto σ 1,1004 contro 1,1004 · coda 53,9% contro 54% — coincidono alla quarta cifra |
| il metro gaussiano della coda | 0,052 a ρ=0 (deve dare 0,05) · confermato da una simulazione con altro seme a ρ=0, 0,5, 0,9 |
| i rendimenti sono davvero NON sovrapposti | autocorrelazione a ritardo 1: −0,018 / −0,025 / −0,009 → casi efficaci 99,9% del nominale |
| ho letto giuste le formule delle features | ricalcolate dalle 7 medie salvate: cluster ρ +1,0000, velocita' +0,9988, accelerazione +0,9947 · il 99% dei giorni entro lo 0,22% |
I sette difetti
1 · ⚠⚠ Il rapporto di copertura era invertito su 414 coppie su 750
Il banco calcolava h = ρ·σa/σb nell'ordine di caricamento; la tabella riordinava in alfabetico dopo, tenendo lo stesso numero. Il valore cambiava significato senza che niente lo dicesse.
GBPJPY↔EURJPY a 4h · ρ = 0,7579
salvato: 0,834 (= ρ·σGBPJPY/σEURJPY)
giusto: 0,689 (= ρ·σEURJPY/σGBPJPY, perche' la riga ha a=EURJPY)
→ una copertura piu' grande del 21%
Chiuso alla radice: l'ordine si fissa prima di calcolare (cols = sorted(...)), e il caricamento ora rifiuta una coppia non ordinata invece di riordinarla.
Il giro sbagliato non e' stato cancellato: e' marcato valido = False col motivo scritto dentro, e _ultimo() non lo sceglie mai. Un errore da cui si impara vale piu' di una tabella pulita.
2 · ⚠⚠ «0 cambiamenti» che erano 2.402
Nel banco della storia le date passavano da pandas come datetime64[us] e venivano divise a mano per una costante in nanosecondi: tutti i giorni finivano nel 1970, cioe' in un periodo solo, e il test di omogeneita' non partiva mai. Stampava 0 con p<0,05 per ognuna delle quaranta grandezze, che si legge come «niente e' cambiato».
E' lo stesso errore gia' corretto in correlazioni_notizie.py il mattino stesso, e rifatto nel modulo nuovo. Adesso l'unita' si chiede esplicitamente (astype("datetime64[m]")) e c'e' una guardia: se una serie lunga si riduce a un periodo solo, il banco si ferma invece di stampare uno zero.
3 · ⚠ Lo zero esatto letto come «valore mancante»
"e_cambiata": bool(testa and (testa.q_omogeneita or 1) < 0.05) # sbagliato
In Python 0.0 or 1 fa 1. Le coppie con q esattamente zero — cioe' i cambiamenti piu' certi di tutti — risultavano non cambiate. EURUSD↔GBPUSD, che passa da +0,67 a +0,84 con q=0, usciva e_cambiata: False.
Misurato: il banco contava 1.127 cambiamenti, la tabella ne ha 2.402. Mancavano i 1.275 piu' sicuri.
4 · ⚠ Una difesa applicata a meta'
La difesa contro la copertura disomogenea del calendario (partire dal 2013, perche' prima si registrano 53-235 giorni l'anno invece di ~285) era nello strato giornaliero e non in quello a cinque minuti. Non una scelta: una dimenticanza. Misurato:
2010-2012 (calendario rado) rapporto 1,000 su 9.069 confronti
2013-2026 (copertura piena) rapporto 1,111 su 123.642 confronti
Il primo numero non dice «allora le notizie non muovevano il mercato»: dice che non sappiamo quali giorni fossero quieti, perche' le notizie di quei giorni non sono state registrate. Tenerli dentro abbassava il risultato a 1,103 mescolando misura e ignoranza. La cifra pubblicata passa da 1,10 a 1,11.
5 · ⚠ Due strati su periodi diversi senza dirlo
Il banco della storia leggeva la propria data di partenza ma i prezzi arrivavano da un altro modulo con la sua: la prova col 2023 dava rotture datate 2019. Ora le due date sono legate e c'e' una riga che lo verifica.
6 · ⚠⚠ Avevo scritto una cosa falsa sulle nostre features
Nel rapporto sulle notizie ho scritto che volume e amihud sono grandezze giornaliere ricopiate nelle righe a cinque minuti, e che cluster/velocita'/accelerazione/volatilita' «si aggiornano ogni 29-37 minuti» — come se fossero intraday con un aggiornamento lento.
Non e' vero. Leggendo il sorgente dell'indicatore:
struct D1Data { double med[]; double cols[][7]; int bars; };
cluster = scarto medio delle 7 MEDIE MOBILI GIORNALIERE dalla loro mediana
velocita = mediana su 7 di (MA[j] − MA[j+5]) / MA[j+5]
accelerazione = mediana su 7 della differenza seconda
volatilita = mediana su 7 della dev.std dei rendimenti delle MA su 14 barre
Tutte e quattro leggono g_d1.cols[j][m]: sono giornaliere per costruzione. Nelle righe M5 non c'e' una versione a cinque minuti, c'e' lo stesso valore giornaliero che si muove perche' la barra D1 di oggi e' ancora aperta. Sono sei grandezze giornaliere su tredici, non due.
Averle davvero a cinque minuti si puo', ma e' una costruzione, non un download: significa applicare le stesse quattro formule a una scala di medie mobili su M5. Le formule sono ora verificate (punto sotto), quindi la costruzione e' fattibile e controllabile. Manca solo il volume: l'esportatore M5 scrive datetime;open;high;low;close;spread_broker e non il tick_volume.
7 · ⚠ «Correlazione nella coda», misura truccata
La prima versione prendeva i casi con |A| grande e calcolava ρ li'. Quel numero sale sempre, anche fra due normali senza nessuna dipendenza di coda: selezionare sugli estremi allarga la varianza e alza meccanicamente la correlazione (con ρ=0,5 usciva ~0,8). Sostituita con la probabilita' di crollare insieme, confrontata con quella di due normali con la stessa ρ. Trovato prima di pubblicare, ma nato dalla stessa fretta.
8 · ⚠⚠⚠ Gli orizzonti erano scelti, non misurati — e sbagliati da tutte e due le parti
La griglia era 60 / 240 / 480 / 1440 minuti. L'ho scelta guardando le celle dell'Orologio, che dura 4 e 8 ore. Poi ho misurato quanto teniamo davvero le posizioni, su 17.320 accoppiate nel libro nero piu' 1.066 nella tabella signals — l'unica che copre anche i motori che non passano dall'Orologio:
| motore | mediana | 90° percentile |
|---|---|---|
| dock (la mano) | 6 minuti | 95 minuti |
| manuale / Traders | 14 minuti | 528 minuti |
| trend | 6 minuti | 537 minuti |
| Orologio 4h | 240 minuti (esatta) | — |
| Orologio 8h | 480 minuti (esatta) | — |
| reversione | 604 minuti | 10.076 minuti (7 giorni) |
| crypto | 7.200 minuti (5 giorni) | 27.360 minuti (19 giorni) |
Sotto: la mano di Dock chiude in sei minuti e il 46% delle posizioni dell'app dura meno di un quarto d'ora. Misurare la copertura a un'ora vuol dire misurarla su un orizzonte dieci volte piu' lungo di quello che si tiene.
Sopra: il 44% del libro dura piu' di ventiquattr'ore — 16,3% da 1 a 3 giorni, 12,3% da 3 a 7, 15,5% oltre la settimana. La tavola si fermava a un giorno.
La griglia nuova ha dodici orizzonti — 5, 15, 30, 60, 240, 480, 720, 1440, 2880, 7200, 10.080, 20.160 — e ognuno corrisponde a qualcosa che teniamo.
9 · ⚠⚠ E il filtro sui buchi avrebbe rifiutato TUTTI gli orizzonti lunghi
Il controllo pretendeva il 90% dei passi di calendario dentro la finestra. Giusto per quattro ore, dove un weekend in mezzo e' un'anomalia. Impossibile da soddisfare per cinque giorni, che un fine settimana lo contengono per forza: la tavola sarebbe uscita vuota sopra le ventiquattr'ore, e sarebbe sembrato «non ci sono dati».
Ora la soglia si tara sui dati: quante barre ha di solito una finestra di questa lunghezza che parte in questo giorno della settimana. Verificato: a 7200′ si passa da zero finestre a ~400.
10 · ⚠ Una modifica persa da un pkill troppo largo
Fermando i giri con pkill -f correlazioni_incroci ho ucciso la mia stessa shell — la riga di comando conteneva quella stringa. La modifica agli orizzonti che veniva dopo non e' mai stata scritta, e il giro successivo e' ripartito con la griglia vecchia stampando comunque un risultato completo e plausibile. Trovato confrontando gli orizzonti nel JSON con quelli che credevo di aver messo.
Due cose verificate che invece reggono, e vale la pena averle provate
La fase della griglia non conta. La cella EURJPY entra alle 17, la mia griglia a 8 ore usa 00/08/16. Misurando tutte e otto le fasi su GBPJPY↔EURJPY: ρ da 0,7332 a 0,7563 (circa due errori standard) e copertura da 0,6245 a 0,6395 — 2% di scarto. Alle ore vere della cella da' 0,7563 contro 0,7483. Trascurabile, e adesso e' dimostrato invece che supposto.
I rendimenti sono indipendenti a OGNI orizzonte, non solo a quelli comodi: autocorrelazione a ritardo uno fra −0,046 e +0,018 da 5 minuti a 2 settimane, casi efficaci al 99,6-100% del nominale.
Cosa cambia nei numeri gia' dati
| prima | dopo | |
|---|---|---|
| notizie, rapporto evento/gemella | 1,10 | 1,11 (solo anni a copertura piena) |
| correlazione col peso della notizia | +0,134 | +0,137 |
| copertura GBPJPY↔EURJPY | 0,83 | 0,69 |
| cambiamenti di correlazione datati | «0» | 2.402 con q<0,05 |
| grandezze giornaliere su 13 | 2 | 6 |
| orizzonti della tavola coperture | 4 (60→1440′) | 12 (5′→2 settimane) |
| quota del libro coperta dagli orizzonti | 56% | ~100% |
Tutto il resto — le correlazioni, la quota spiegata dalle valute (96-99,8%), la dipendenza di coda in eccesso, «resta in mano» — regge invariato.
La regola che ne esce
Quattro dei dieci difetti (2, 3, 5, 10) hanno la stessa faccia: un guasto che stampa un risultato invece di un errore. Zero cambiamenti, zero finestre agganciate, un residuo vuoto. Sono i piu' pericolosi perche' si leggono come informazione. Da oggi i banchi hanno guardie che si fermano: se nessuna finestra si aggancia, se una serie lunga cade in un periodo solo, se una coppia arriva non ordinata, se un orizzonte non raggiunge il numero minimo di finestre.
E una regola nuova che non avevo: gli orizzonti, le soglie e le finestre si misurano dal comportamento vero del sistema, non si scelgono guardando il motore che si ha in mente. Quattro orizzonti scelti sull'Orologio lasciavano fuori la meta' del libro, e nessun controllo se ne sarebbe accorto — perche' la tavola usciva piena.