Cómo validar factores cuantitativos con Bucket Spread y Rank IC
Al construir un modelo de ranking o puntuación de acciones, lo más fácil es añadir otro factor.
Momentum, RSI, fuerza relativa, distancia al máximo de 52 semanas, volatilidad, volumen. Si algo parece tener relación en un backtest, es muy tentador añadir otra columna y darle un peso. La pregunta difícil viene después: ¿ese factor realmente ordena los rendimientos futuros o solo quedó bien en una ventana concreta?
Al ordenar mi propio flujo de investigación cuantitativa volví a ver lo fácil que es dejarse llevar por la tasa de aciertos o por un único horizonte de forward return. Un factor puede verse bien a 5 días y comportarse de otra forma a 1 o 3 días. Y si evaluamos las mismas señales todos los días, el número de filas crece rápido aunque los forward returns se solapen mucho. La cantidad real de información independiente puede ser bastante menor de lo que sugiere el tamaño de la tabla.
Por eso no reutilicé los antiguos valores de IC o spread de mis notas internas. Hice una reproducción pública pequeña desde cero. Los datos son precios de cierre ajustados de cinco ETF tomados de un repositorio público: sector_prices.csv. Fijé el archivo al blob SHA 0a442a9260c2ad64e6cd8c01af49328eb7619367, usé SPY, XLK, XLF, XLE y XLV, construí varios factores de precio simples de 20 días y medí forward returns a 1D / 3D / 5D.
El resultado no fue un bonito “mejor factor”. Eso fue precisamente lo útil.
En momentum de 20 días, el High-Low median spread fue positivo a 1D y 5D en la muestra con anchors espaciados, pero ligeramente negativo a 3D. El factor de baja volatilidad tampoco mantuvo la misma dirección entre horizontes. Y, más interesante todavía, un factor de “fuerza relativa de 20 días frente a SPY” terminó produciendo exactamente el mismo ranking transversal que el momentum de 20 días sin ajustar.
Esas irregularidades se parecen mucho más a una investigación real que un informe donde todo aparece en verde.
Congelar el valor del factor en el momento de la señal
En validación de factores, hoy me importa más la frontera temporal que la fórmula.
Supongamos que un día el sistema puntúa un conjunto de acciones y registra momentum_20d. Lo que realmente hay que guardar es el valor que el sistema veía en el momento de la decisión. Después, cuando los outcomes de 1D, 3D y 5D ya están cerrados, se comprueba si ese valor tenía capacidad de ranking.
Es decir:
decision / snapshot -> factor value at t -> realized return at t + N
No esperar tres días y recalcular el “factor histórico” con los datos y el código de hoy.
Las dos cosas se parecen, pero no son equivalentes. Un proveedor puede revisar datos históricos. Los ajustes por dividendos o splits pueden cambiar. El universo de activos puede cambiar. La definición del factor también. Incluso la sección transversal usada para normalizar puede no ser la misma. Si recalculamos después, corremos el riesgo de validar “el pasado reconstruido con las reglas actuales” en lugar de lo que el sistema realmente vio.
Alphalens sigue una estructura parecida: alinea factor values con forward returns posteriores, analiza resultados por quantile y calcula un Information Coefficient. Su documentación también señala un riesgo concreto de look-ahead: si se usa el propio rendimiento futuro para calcular un z-score y filtrar valores atípicos, la limpieza ya está usando información futura.
Si algún día dejo que mi sistema de investigación en producción ajuste pesos automáticamente, quiero que esta frontera sea más estricta que en un notebook normal: una vez que un factor snapshot entra en outcome tracking, una versión posterior del modelo no debería poder sobrescribirlo.
Esta reproducción pública no usa mi base de datos de producción. Construye signal-time factors y forward returns a partir del mismo archivo histórico fijado. Eso basta para verificar el flujo de cálculo, pero no demuestra que un sistema de producción esté libre de problemas point-in-time. Son afirmaciones distintas.
Bucket Spread y Rank IC responden preguntas diferentes
Si un factor se usa sobre todo para ordenar activos, no me gusta empezar preguntando “¿cuál es la tasa de aciertos?”.
La tasa de aciertos comprime demasiada información en un booleano. +0,01 % y +8 % cuentan igual como “acierto”. El puesto 1 y el puesto 5 también pueden acabar dentro de la misma categoría. Sirve para describir resultados de estrategia, pero no responde directamente si valores más altos del factor se asocian con mejores resultados futuros.
Aquí uso dos medidas básicas.
La primera es Bucket Spread. En cada signal date, se ordena la sección transversal por el factor, se divide en Low / Mid / High y se comparan los rendimientos posteriores de High y Low:
Bucket Spread = median(return_high) - median(return_low)
Si un factor de “más alto es mejor” contiene información de ranking, cabría esperar que High supere a Low. Pero un spread positivo solo dice que los extremos se separaron en esta muestra. No implica automáticamente significancia estadística, no dice que sobreviva a costes de transacción y tampoco garantiza que el bucket intermedio sea monotónico.
La segunda medida es Rank IC. Uso Spearman rank correlation entre el ranking del factor y el ranking del rendimiento futuro:
Rank IC = Spearman(factor_rank, forward_return_rank)
spearmanr de SciPy es una medida no paramétrica de asociación monotónica entre -1 y 1. Para muchos factores me interesa más saber si el orden contiene información que suponer una relación lineal entre el valor del factor y el rendimiento futuro.
El cálculo central del experimento es deliberadamente pequeño:
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()
)
El script real está fijado como evidencia en Content Lab y el comando ejecutado terminó con exit code 0. No pretendía crear otro framework cuantitativo, sino reducir la validación al núcleo suficiente para que la lógica no quede escondida dentro de una estrategia compleja.
Cambia el horizonte y empieza a cambiar la historia
Para reducir algo el solapamiento de forward returns, la vista principal toma un anchor date cada cinco sesiones. Son 14 fechas y cinco ETF por fecha, así que cada combinación factor × horizon contiene 70 observaciones transversales.
El spread de la tabla es High bucket median - Low bucket median, en puntos porcentuales. Rank IC es la media del Spearman IC transversal en las fechas 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 |
Lo último que quiero hacer con esta tabla es rodear el número más grande y declarar el factor “válido”.
En 20D Momentum, si solo miramos 5D, un spread de +0.667 pp y un mean Rank IC de +0.214 invitan a subir el peso. Pero a 3D el spread es -0.071 pp y el Rank IC está casi en cero. No aparece una estructura limpia de “cuanto más largo el horizonte, mejor”.
Low Volatility es todavía menos estable. El median spread a 3D es positivo, pero a 5D pasa a -0.224 pp y el mean Rank IC cae a -0.207. Si solo enseñara la columna 3D parecería un factor positivo interesante. En 5D la historia cambia.
Ese es el valor práctico del multi-horizon outcome tracking. No se trata primero de mezclar 1D, 3D y 5D en una puntuación ponderada más bonita, sino de revelar hasta qué punto la conclusión depende del horizon.
También mantuve una muestra diaria con observaciones solapadas. Tiene 66 signal dates y 330 filas factor/outcome, por lo que el tamaño parece mucho mayor.
El problema es que los forward returns de 3D y 5D se solapan mucho.
Y el resultado cambia: el 3D median spread de 20D Momentum es aproximadamente +0.578 pp en la muestra daily-overlapping, pero -0.071 pp en la muestra con un anchor cada cinco sesiones.
No uso esa diferencia para decidir cuál es “la verdad”. Catorce anchors siguen siendo pocos y separar cinco días no elimina toda dependencia. La lección más útil es otra: si la conclusión de un factor puede cambiar de signo al variar de forma razonable el muestreo, un informe no debería esconder esa fragilidad detrás de una gran cifra total de filas.
Antes de añadir factores, comprobar si repiten la misma información de ranking
Hubo un resultado que al principio me hizo pensar que tenía un bug.
Probé a la vez:
momentum_20d
relative_strength_20d_vs_spy
El segundo está definido como:
asset_20d_return - SPY_20d_return
Al terminar, todos los bucket, spread y Rank IC eran idénticos.
El código estaba bien.
En una fecha concreta, el rendimiento de 20 días de SPY es la misma constante para todos los activos:
RS_i = Momentum_i - C_t
Restar el mismo C_t a todos los activos de la sección transversal no cambia el orden. Si Bucket y Rank IC dependen solo del rank transversal, ambas definiciones contienen la misma información de ranking en esta construcción.
Para mí, eso fue más útil que ver otro “IC = 0.21”.
Un modelo multifactor puede acumular falsa complejidad muy rápido: nombres distintos, fórmulas diferentes a simple vista, pero la misma información direccional. Dar pesos independientes a dos factores muy redundantes no aporta dos evidencias. Solo permite que la misma evidencia vote dos veces.
Esto no significa que “relative strength sea igual a momentum” en general. Frente a un sector, un benchmark dinámico, residual returns, ajuste por rolling beta o un lookback distinto, el ranking puede cambiar. La equivalencia aquí viene de una construcción muy específica: restar el mismo SPY 20D return a todos los activos en la misma fecha.
Por eso, antes de ajustar pesos, quiero comparar factor ranks, correlaciones y bucket assignments. La pregunta es si el nuevo factor añade información o solo añade una etiqueta.
Un único IC positivo no debería cambiar pesos en producción
Si el sistema de investigación terminará ajustando un modelo automáticamente, prefiero dejar distancia entre “factor descubierto” y “factor que afecta a producción”.
Un candidato puede empezar en observe: registrar el valor en el momento de la señal, pero no participar en la decisión. Cuando los outcomes estén cerrados, mirar Bucket Spread, Rank IC, coverage y distribución de la muestra en varios horizontes.
Si sigue siendo interesante, pasa a shadow. Shadow puede generar pesos candidatos o decisiones hipotéticas, pero todavía no modifica el active model.
Como mínimo quiero revisar:
- tamaño de muestra y non-null coverage suficientes;
- una estructura razonable Low / Mid / High, no solo extremos que se separan por casualidad;
- comportamiento 1D / 3D / 5D que pueda explicarse en lugar de direcciones contradictorias;
- si la dirección sobrevive en otro periodo o regime;
- si el factor es muy redundante con los existentes;
- si cambiar muestreo o deduplicación cambia la conclusión;
- cuántos factores, ventanas y parámetros se probaron durante la investigación.
El último punto se olvida fácilmente. La investigación de factores suele convertirse en una gran colección de tests de hipótesis. Si probamos 50 indicadores, seis lookbacks y cuatro horizons y elegimos el mejor resultado, aumenta la probabilidad de encontrar algo atractivo incluso cuando la señal real es débil. Harvey, Liu y Zhu tratan directamente este problema de multiple testing en … and the Cross-Section of Expected Returns.
Por eso no considero Bucket Spread y Rank IC una licencia para admitir el factor. Son más bien diagnósticos básicos. Permiten ver si el orden contiene información, si la dirección es estable y si aparecen horizon flips o redundancia. Pero entre eso y “este factor se puede operar con dinero real” siguen faltando estabilidad, costes de transacción, validación out-of-sample, régimen, exposiciones de cartera y control de riesgo.
Esta reproducción pública usa solo cinco ETF y 14 fechas anchor espaciadas. No demuestra ningún alpha de producción. Lo que sí demuestra es que la cadena de validación puede reproducirse de forma pequeña y que, cuando se muestran horizon, muestreo y rank redundancy, muchas conclusiones que parecían limpias se vuelven bastante menos seguras.
Esa incertidumbre merece conservarse.
Si un factor merece entrar en un modelo, prefiero que sea porque ha sobrevivido a bastante tiempo de outcome tracking y a varias comprobaciones incómodas, no porque un único informe de investigación haya producido un número bonito.