Come validare fattori quantitativi con Bucket Spread e Rank IC
Quando si costruisce un modello di ranking o scoring azionario, la cosa più facile è aggiungere un altro fattore.
Momentum, RSI, forza relativa, distanza dal massimo a 52 settimane, volatilità, volume. Se qualcosa sembra avere una relazione nel backtest, è facile aggiungere una colonna e assegnarle un peso. La domanda difficile arriva dopo: questo fattore sta davvero ordinando i rendimenti futuri oppure è sembrato buono solo in una finestra specifica?
Riordinando il mio workflow di ricerca quantitativa mi sono accorto ancora una volta di quanto sia facile farsi trascinare dal win rate o da un singolo orizzonte di forward return. Un fattore può sembrare buono a 5 giorni e comportarsi diversamente a 1 o 3 giorni. Se valutiamo gli stessi segnali ogni giorno, il numero di righe cresce rapidamente, ma i forward returns si sovrappongono molto. La quantità di informazione indipendente può essere molto inferiore a quella suggerita dal conteggio delle righe.
Per questo non ho riutilizzato vecchi valori di IC o spread presi dalle note interne. Ho rifatto una piccola riproduzione pubblica. I dati sono prezzi di chiusura rettificati di cinque ETF presenti in un repository pubblico: sector_prices.csv. Ho fissato il file al blob SHA 0a442a9260c2ad64e6cd8c01af49328eb7619367, usato SPY, XLK, XLF, XLE e XLV, costruito alcuni semplici fattori di prezzo a 20 giorni e misurato i forward returns a 1D / 3D / 5D.
Il risultato non è stato un bel “fattore migliore”. Ed è proprio questo l’aspetto utile.
Per il momentum a 20 giorni, l’High-Low median spread era positivo a 1D e 5D nel campione con anchor diradati, ma leggermente negativo a 3D. Anche il fattore di bassa volatilità cambiava direzione a seconda dell’orizzonte. Ancora più interessante: un fattore di “forza relativa a 20 giorni rispetto a SPY” ha prodotto esattamente lo stesso ranking cross-sectional del momentum a 20 giorni non corretto.
Queste irregolarità sono molto più vicine alla ricerca reale di un report in cui tutto è verde.
Congelare il valore del fattore al momento del segnale
Nella validazione dei fattori, oggi considero il confine temporale più importante della formula.
Supponiamo che in un certo giorno il sistema assegni un punteggio a un gruppo di azioni e registri momentum_20d. Il dato da salvare davvero è il valore che il sistema vedeva nel momento della decisione. Poi, quando gli outcome a 1D, 3D e 5D sono maturi, si verifica se quel valore aveva capacità di ranking.
In altre parole:
decision / snapshot -> factor value at t -> realized return at t + N
Non aspettare tre giorni e ricalcolare il “fattore storico” usando i dati e il codice di oggi.
Le due cose sembrano simili, ma non sono equivalenti. Un provider può correggere dati storici. Gli aggiustamenti per dividendi o split possono cambiare. L’universo può cambiare. Anche la definizione del fattore. La sezione trasversale usata per la normalizzazione può non essere più la stessa. Ricalcolando dopo rischiamo di validare “il passato ricostruito con le regole di oggi” invece di ciò che il sistema vedeva davvero.
Alphalens segue una struttura simile: allinea factor values e forward returns successivi, analizza i rendimenti per quantile e calcola un Information Coefficient. La documentazione segnala anche un rischio concreto di look-ahead: se si usa il rendimento futuro stesso per calcolare uno z-score e filtrare gli outlier, il processo di pulizia sta già usando informazione futura.
Se un giorno il mio sistema di ricerca in produzione regolerà automaticamente i pesi, voglio che questo confine sia più rigido di quello di un normale notebook: una volta entrato nell’outcome tracking, un factor snapshot non dovrebbe poter essere sovrascritto da una versione successiva del modello.
Questa riproduzione pubblica non usa il database di produzione. Costruisce signal-time factor e forward return dallo stesso file storico fissato. È sufficiente per verificare il flusso di calcolo, ma non dimostra che un sistema di produzione sia privo di problemi point-in-time. Sono due affermazioni separate.
Bucket Spread e Rank IC rispondono a domande diverse
Se un fattore viene usato soprattutto per ordinare gli asset, non mi piace partire chiedendo “qual è il win rate?”.
Il win rate comprime troppa informazione in un booleano. +0,01% e +8% contano entrambi come vittorie. Il primo e il quinto posto possono finire nella stessa categoria. È utile per descrivere il risultato di una strategia, ma non risponde direttamente alla domanda se valori più alti del fattore corrispondano a risultati futuri migliori.
Qui uso due misure semplici.
La prima è Bucket Spread. In ogni signal date si ordina la sezione trasversale per il fattore, la si divide in Low / Mid / High e si confrontano i rendimenti successivi dei gruppi High e Low:
Bucket Spread = median(return_high) - median(return_low)
Se un fattore “più alto è meglio” contiene davvero informazione di ranking, ci si aspetta che High sia più forte di Low. Ma uno spread positivo dice soltanto che gli estremi si sono separati in questo campione. Non implica automaticamente significatività statistica, non dice che il vantaggio sopravviva ai costi di transazione e non garantisce monotonicità nel bucket intermedio.
La seconda misura è Rank IC. Uso Spearman rank correlation tra il ranking del fattore e il ranking del rendimento futuro:
Rank IC = Spearman(factor_rank, forward_return_rank)
spearmanr di SciPy è una misura non parametrica di associazione monotona tra -1 e 1. Per molti fattori mi interessa di più sapere se l’ordine contiene informazione che assumere una relazione lineare tra valore del fattore e rendimento futuro.
Il calcolo centrale dell’esperimento è volutamente piccolo:
mom20 = px / px.shift(20) - 1
fwd = px.shift(-horizon) / px - 1
x = mom20.loc[dt].dropna()
y = fwd.loc[dt].dropna()
common = x.index.intersection(y.index)
bucket = pd.qcut(
x.loc[common].rank(method="first"),
3,
labels=["low", "mid", "high"],
)
rho, _ = spearmanr(
x.loc[common].values,
y.loc[common].values,
)
spread = (
y.loc[common][bucket == "high"].median()
- y.loc[common][bucket == "low"].median()
)
Lo script realmente eseguito è fissato come evidenza nel Content Lab e il comando è terminato con exit code 0. Non volevo costruire un altro framework quant, ma ridurre la validazione al nucleo necessario affinché la logica non sparisse dentro una strategia complessa.
Cambiando l’orizzonte, cambia anche la storia
Per ridurre un po’ la sovrapposizione dei forward returns, la vista principale usa un anchor date ogni cinque sedute. Sono 14 date e cinque ETF per data, quindi 70 osservazioni cross-sectional per ogni combinazione factor × horizon.
Lo spread nella tabella è High bucket median - Low bucket median, in punti percentuali. Rank IC è la media dello Spearman IC cross-sectional sulle date anchor.
| Factor | Horizon | N | Median Spread | Mean Rank IC |
|---|---|---|---|---|
| 20D Momentum | 1D | 70 | +0.583 pp | +0.179 |
| 20D Momentum | 3D | 70 | -0.071 pp | +0.014 |
| 20D Momentum | 5D | 70 | +0.667 pp | +0.214 |
| Distance to 20D High | 1D | 70 | +0.249 pp | +0.089 |
| Distance to 20D High | 3D | 70 | +0.259 pp | +0.151 |
| Distance to 20D High | 5D | 70 | +0.529 pp | +0.252 |
| Low Volatility 20D | 1D | 70 | -0.029 pp | +0.029 |
| Low Volatility 20D | 3D | 70 | +0.296 pp | +0.014 |
| Low Volatility 20D | 5D | 70 | -0.224 pp | -0.207 |
| Relative Strength 20D vs SPY | 1D | 70 | +0.583 pp | +0.179 |
| Relative Strength 20D vs SPY | 3D | 70 | -0.071 pp | +0.014 |
| Relative Strength 20D vs SPY | 5D | 70 | +0.667 pp | +0.214 |
L’ultima cosa che voglio fare con questa tabella è cerchiare il numero più grande e dichiarare il fattore “valido”.
Per 20D Momentum, guardando solo 5D, spread +0.667 pp e mean Rank IC +0.214 fanno venire voglia di aumentare il peso. Ma a 3D lo spread è -0.071 pp e il Rank IC quasi zero. Non c’è una struttura pulita “più lungo è l’orizzonte, migliore è il fattore”.
Low Volatility è ancora più instabile. Il median spread a 3D è positivo, ma a 5D passa a -0.224 pp e il mean Rank IC scende a -0.207. Mostrando solo 3D sembrerebbe un fattore positivo interessante. A 5D la storia cambia.
Questo è il valore pratico del multi-horizon outcome tracking. Non serve prima di tutto a combinare 1D, 3D e 5D in un punteggio pesato più bello, ma a mostrare quanto la conclusione sia sensibile all’horizon.
Ho mantenuto anche una versione con campionamento giornaliero sovrapposto. Ha 66 signal dates e 330 righe factor/outcome, quindi il campione sembra molto più grande.
Il problema è che i forward returns a 3D e 5D si sovrappongono fortemente.
E il risultato cambia davvero: il 3D median spread di 20D Momentum è circa +0.578 pp nel campione daily-overlapping, ma -0.071 pp nel campione con un anchor ogni cinque sedute.
Non uso questa differenza per stabilire quale sia “la verità”. Quattordici anchor sono ancora pochi e distanziare di cinque giorni non elimina ogni dipendenza. La lezione utile è un’altra: se la conclusione di un fattore può cambiare segno modificando in modo ragionevole il campionamento, un report non dovrebbe nascondere questa fragilità dietro un grande numero totale di righe.
Prima di aggiungere fattori, controllare se ripetono la stessa informazione di ranking
Un risultato all’inizio mi ha fatto pensare a un bug.
Avevo costruito insieme:
momentum_20d
relative_strength_20d_vs_spy
Il secondo è:
asset_20d_return - SPY_20d_return
A fine analisi, tutti i bucket, spread e Rank IC erano identici.
Il codice era corretto.
In una data specifica, il rendimento a 20 giorni di SPY è la stessa costante per ogni asset:
RS_i = Momentum_i - C_t
Sottrarre lo stesso C_t a tutti gli asset della sezione trasversale non cambia l’ordine. Se Bucket e Rank IC dipendono solo dal rank cross-sectional, queste due definizioni contengono la stessa informazione di ranking in questa costruzione.
Per me questa osservazione è stata più utile di vedere un altro “IC = 0.21”.
Un modello multifattore può accumulare falsa complessità molto rapidamente: nomi diversi, formule dall’aspetto diverso, ma la stessa informazione direzionale. Dare pesi indipendenti a due fattori fortemente ridondanti non crea due prove. Fa votare due volte la stessa prova.
Questo non significa che “relative strength sia uguale a momentum” in generale. Rispetto a un settore, a un benchmark dinamico, usando residual returns, aggiustamento rolling beta o un lookback diverso, il ranking può cambiare. L’equivalenza qui deriva da una costruzione molto specifica: sottrarre lo stesso SPY 20D return a tutti gli asset nella stessa data.
Prima di regolare i pesi, quindi, voglio confrontare factor ranks, correlazioni e bucket assignments. Il punto è capire se il nuovo fattore aggiunge davvero informazione o soltanto un nome.
Un solo IC positivo non dovrebbe cambiare i pesi live
Se il sistema di ricerca dovrà un giorno regolare il modello in automatico, preferisco lasciare distanza tra “fattore scoperto” e “fattore che influenza la produzione”.
Un candidato può iniziare in observe: si registra il valore al momento del segnale ma non lo si usa nelle decisioni. Quando gli outcome sono maturi, si osservano Bucket Spread, Rank IC, coverage e distribuzione del campione su più orizzonti.
Se resta interessante, passa a shadow. Shadow può generare pesi candidati o decisioni ipotetiche, ma non modifica ancora l’active model.
Come minimo voglio controllare:
- dimensione del campione e non-null coverage sufficienti;
- una struttura Low / Mid / High sensata, non solo estremi separati per caso;
- comportamento 1D / 3D / 5D spiegabile invece di direzioni contraddittorie;
- se la direzione sopravvive in un altro periodo o regime;
- ridondanza con i fattori esistenti;
- se campionamento o deduplicazione cambiano la conclusione;
- quanti fattori, finestre e parametri sono stati provati durante la ricerca.
L’ultimo punto è facile da dimenticare. La ricerca sui fattori diventa spesso una grande collezione di test d’ipotesi. Se si provano 50 indicatori, sei lookback e quattro horizons scegliendo poi il risultato più bello, aumenta la probabilità di trovare qualcosa di attraente anche quando il segnale sottostante è debole. Harvey, Liu e Zhu discutono direttamente questo problema di multiple testing in … and the Cross-Section of Expected Returns.
Per questo non considero Bucket Spread e Rank IC un lasciapassare per il fattore. Sono più simili a diagnostica di base. Mostrano se l’ordine contiene informazione, quanto è stabile la direzione e se esistono horizon flip o ridondanza. Ma tra questo e “il fattore è tradabile con denaro reale” restano stabilità, costi di transazione, validazione out-of-sample, dipendenza dal regime, esposizioni di portafoglio e controlli di rischio.
Anche questa riproduzione pubblica usa solo cinque ETF e 14 date anchor diradate. Non dimostra alcun alpha di produzione. Dimostra che la catena di validazione può essere riprodotta in forma piccola e che, rendendo visibili horizon, campionamento e rank redundancy, molte conclusioni apparentemente pulite diventano molto meno certe.
Quell’incertezza vale la pena conservarla.
Se un fattore merita di entrare in un modello, preferisco che sia perché ha superato un lungo periodo di outcome tracking e diverse verifiche scomode, non perché un singolo report di ricerca ha prodotto un bel numero.