Cómo reajustar con seguridad un modelo multifactor: Regime, Shadow y Weight Drift
Cuando un factor ya muestra un IC positivo, el siguiente paso más peligroso puede ser subirle el peso de inmediato.
En el experimento anterior ordené primero la validación del factor: congelar el signal-time snapshot y después mirar Bucket Spread, Rank IC y outcomes en varios horizontes. Eso responde a una pregunta básica: ¿este factor contiene realmente información de ranking?
Al pasar a un sistema que reajusta pesos automáticamente aparece otra pregunta: ¿por qué un factor que se vio más fuerte en los últimos 20 periods debería poder reescribir el active model de inmediato?
Es fácil confundir ambas cosas.
Un factor candidato puede contener información real y, aun así, estar sobreestimado en la última ventana. También puede haber cambiado de verdad el regime y que demasiadas protecciones hagan que el modelo tarde demasiado en reaccionar. La adaptación no termina cuando transformamos un IC de research en un nuevo vector de weights. El problema difícil es el cambio de estado del modelo.
No usé antiguos pesos de producción ni thresholds guardados en Notion como prueba. Construí un experimento sintético y controlado de regime switching. Como conozco el ground truth, sé exactamente cuándo cambia de verdad la relación entre factores y puedo comparar tres activation policies:
- activar cada nuevo weight estimate de inmediato;
- activar cada estimate, pero limitar el cambio máximo por factor;
- ejecutar primero el candidate en shadow, promoverlo solo tras varias victorias consecutivas frente a active y aplicar también un delta cap al promotion.
El experimento usa 12 random seeds fijos. Cada seed tiene 240 periods consecutivos, 200 synthetic assets por period y 4 factors. El regime cambia explícitamente en los periods 80 y 160. Cada 5 periods se crea un candidate usando solo los 20 periods anteriores; la evaluación se hace únicamente sobre los 5 periods siguientes.
La frontera temporal es deliberadamente estricta. La documentación de TimeSeriesSplit de scikit-learn explica por qué un cross-validation normal no es válido para datos ordenados en el tiempo cuando puede terminar entrenando con observaciones futuras y evaluando sobre el pasado.
Si el candidate weight ya ha visto el futuro, hablar después de shadow, regime o drift budget no arregla nada.
Cuánto cambia el resultado entre reajustar rápido y reajustar con estabilidad
Mantengo el estimador de candidate weights intencionadamente simple: dentro del trailing window calculo el Rank IC transversal de cada factor, recorto los valores negativos a cero y normalizo la evidencia positiva restante para obtener candidate weights.
Lo que cambia es la activation policy.
Registré:
- combined-score Rank IC en el future block;
- turnover L1 acumulado;
- mayor weight jump individual;
- número de promotions;
- proporción de promotions en las que el nuevo profile pierde frente al active anterior en el siguiente evaluation block;
- cuánto tarda, después de un regime switch real, en pasar el mayor peso del active profile al factor más fuerte del nuevo regime.
A esta última medida la llamo adaptation lag. No es una métrica estándar de la industria financiera; es una definición específica del experimento para medir cuánto tarda el modelo en reconocer un cambio de regime conocido.
Promedios de los 12 seeds:
| Policy | Cap | Shadow confirmations | Mean OOS Rank IC | L1 turnover | Max weight jump | False promotion rate | Adaptation lag |
|---|---|---|---|---|---|---|---|
| Immediate | — | — | 0.2199 | 7.305 | 0.333 | 39.0% | 11.67 |
| Delta capped | 5% | — | 0.2092 | 5.591 | 0.050 | 27.8% | 22.08 |
| Shadow first | 5% | 1 | 0.2001 | 4.197 | 0.050 | 20.6% | 29.79 |
| Shadow first | 5% | 2 | 0.1882 | 2.409 | 0.050 | 15.0% | 52.29 |
| Shadow first | 5% | 3 | 0.1850 | 1.659 | 0.050 | 10.2% | 75.83 |
| Delta capped | 10% | — | 0.2172 | 6.884 | 0.100 | 36.2% | 14.58 |
| Shadow first | 10% | 1 | 0.2077 | 5.015 | 0.100 | 27.6% | 22.71 |
| Shadow first | 10% | 2 | 0.1937 | 3.472 | 0.100 | 13.7% | 37.08 |
| Shadow first | 10% | 3 | 0.1869 | 2.411 | 0.100 | 11.2% | 47.71 |
La tabla no produce un único “parámetro más seguro”.
Immediate obtiene el mean OOS Rank IC más alto y se adapta más rápido al cambio de regime, con un adaptation lag medio de solo 11.67 periods. El coste también es evidente: tiene el turnover acumulado más alto, cambia pesos en prácticamente los 44 checkpoints y su false promotion rate ronda el 39%.
Con un delta cap real del 5%, el mean OOS Rank IC baja de 0.2199 a 0.2092 y la adaptación se ralentiza. A cambio, el L1 turnover cae de 7.305 a 5.591 y la false promotion rate de 39.0% a 27.8%.
Shadow añade otra capa. Con cap del 5% y tres victorias shadow consecutivas, la false promotion rate cae a aproximadamente 10.2% y el turnover a 1.659. Parece muy estable, hasta que se mira el adaptation lag: 75.83 periods.
Si el único objetivo fuera “menos promotions equivocadas”, la última configuración sería tentadora. Pero tras un regime switch real podríamos haber protegido tanto el modelo que casi deja de reaccionar.
Lo que me deja claro el experimento es que un guardrail no es una capa de seguridad gratuita: pasa a formar parte del propio modelo.
Los shadow tests de AWS SageMaker usan un patrón de producción parecido: una nueva variant puede recibir requests replicados de producción sin devolver su response al caller, para compararla con la production variant antes de decidir si se promueve. Es un patrón útil para un sistema cuantitativo, pero la documentación de SageMaker no dice cuántas victorias consecutivas deben bastar ni qué weight cap debe usar un modelo de trading.
Esos thresholds tienen que salir de los propios outcomes, del coste de turnover y de la tolerancia a reaccionar tarde ante un cambio de regime.
Regime no es una etiqueta: cambia qué parte del pasado conviene creer
Una ilusión frecuente en el reweighting dinámico es pensar que más muestras históricas siempre hacen la estimación más segura.
Si la relación subyacente es estable, más observaciones pueden ayudar. Pero el comportamiento de un factor puede depender del regime. Mezclar en un mismo trailing window datos muy antiguos de un entorno distinto puede aumentar el tamaño muestral y, al mismo tiempo, diluir la relación que importa ahora.
El artículo de Shu y Mulvey publicado en 2025 en Journal of Portfolio Management, Dynamic Factor Allocation Leveraging Regime-Switching Signals, estudia precisamente dynamic allocation usando inferencia de regime específica por factor. Mi experimento no reproduce ese modelo y sus resultados no validan mi sistema, pero sí confirman que el factor allocation condicionado por regime es una línea de investigación real.
En un sistema de reajuste automático prefiero tratar market / regime como contexto de evaluación, no como una etiqueta añadida al final.
Para el mismo momentum factor, el contexto puede pensarse así:
momentum evidence
+ market
+ regime
+ session / universe
+ data quality
↓
candidate evidence
No hace falta separar todo en muchos buckets pequeños. También se pueden usar enfoques de hierarchical shrinkage u otras formas de pooling. Lo importante es no traducir “funcionó en el histórico completo” directamente a “hay que subir el peso ahora”.
La guía actual de model risk management de la Reserva Federal de EE. UU. trata de gobierno de modelos bancarios, no de estrategias de trading. Pero contiene un principio trasladable: ongoing monitoring debe revisar si cambios en data relevance, exposures, activities o market conditions hacen que el modelo deje de comportarse como se esperaba, y el outcomes analysis puede justificar adjustment o redevelopment.
Ese marco también es útil en quant. Un weight vector no es una constante eterna después de un training, ni una variable desechable que deba recalcularse sin control en cada window. Necesita version, context y condiciones explícitas de cambio.
Por eso el artículo anterior de validación de factores va antes que este. Primero hay que decidir si la evidencia del factor merece confianza; después, cómo se permite cambiar el candidate weight. Si se invierte el orden, el modelo puede acabar reaccionando con enorme precisión al ruido.
Un cap del 5% casi terminó convertido en 9.1% después de normalize
La parte más útil del experimento no fue la tabla, sino un error de implementación.
La primera versión del delta cap parecía completamente natural:
target weight
↓
clip de cada factor a old ± 5%
↓
normalize para que los pesos sumen 1
A primera vista, después de limitar a ±5%, normalize solo debería corregir la suma.
Al ejecutar el experimento, un cap nominal del 5% produjo un observed max weight jump medio de aproximadamente 9.1%.
La razón es sencilla: clip solo restringe las coordenadas antes de normalizar. Al volver a escalar todos los pesos, algunas dimensiones pueden alejarse otra vez del valor anterior.
Por eso este código aparentemente seguro:
clipped = np.clip(target, old - cap, old + cap)
weights = clipped / clipped.sum()
no garantiza:
abs(weights[i] - old[i]) <= cap
Un bug así puede ser más peligroso que equivocarse en un decimal del IC. El dashboard puede seguir mostrando cap=0.05 mientras el modelo se está moviendo mucho más.
La implementación final usa una bounded simplex projection. El nuevo vector debe cumplir simultáneamente tres restricciones: pesos no negativos, suma igual a 1 y cada coordenada dentro de old ± cap.
sum(w) = 1
0 <= w_i <= 1
old_i - cap <= w_i <= old_i + cap
La proyección usa una pequeña búsqueda binaria:
lower = np.maximum(0.0, old - cap)
upper = np.minimum(1.0, old + cap)
lo = np.min(target - upper) - 1.0
hi = np.max(target - lower) + 1.0
for _ in range(80):
lam = (lo + hi) / 2.0
w = np.clip(target - lam, lower, upper)
if w.sum() > 1.0:
lo = lam
else:
hi = lam
Después de corregirlo, el observed max jump final es 0.05 con cap del 5% y 0.10 con cap del 10%, salvo epsilon de coma flotante.
Desde entonces veo el drift budget de otra forma: no debería existir solo en configuración.
Si el sistema contiene:
max_weight_delta: 0.05
también debería comprobarse en runtime:
max(abs(new_weight - old_weight)) <= max_weight_delta + epsilon
Si no, hay un parámetro, no una restricción real.
Shadow no intenta predecir mejor; separa la conclusión de research del production state
Ahora prefiero separar el ciclo de vida de un factor adaptativo en cuatro capas:
observe
↓
candidate
↓
shadow
↓
active
Observe recoge evidencia y no altera el live scoring.
Candidate significa que research ya produjo un weight profile que merece comparación, pero todavía no es production state.
Shadow compara candidate y active en paralelo sobre los mismos future outcomes. Usar datos más recientes no da al candidate derecho automático a deployment.
Promotion es el verdadero cambio de estado.
Al separar estas capas aparecen distinciones útiles:
mejora el factor IC
≠ candidate es necesariamente mejor
candidate gana una ventana reciente
≠ hay que hacer promotion
promotion está permitido
≠ los pesos pueden moverse sin límites
Mi definición de false promotion aquí es deliberadamente estrecha: si se promueve un nuevo profile y en el siguiente future evaluation block pierde frente al active anterior, cuento una false promotion.
No es la única definición posible, pero basta para mostrar el problema. Un candidate que se ve fuerte en el último estimation window puede perder justo después de activarse.
Más shadow confirmations reducen esos casos, pero también retrasan la reacción ante un cambio real de regime.
Por eso no fijaría de entrada:
3 victorias seguidas -> deploy
Primero guardaría:
candidate_version
active_version
market / regime
evidence_window
future_eval_window
weight_delta
turnover
shadow_outcome
promotion_decision
y luego usaría outcomes ya cerrados para medir qué intercambian realmente confirmation count y drift budget.
En antiguas notas de Notion tenía ideas de dos o tres confirmaciones y límites de 5%, 8% o 12–15%. Este experimento no demuestra que ninguno de esos valores sea correcto. Solo demuestra que estos controles cambian de forma material el comportamiento del modelo y que el tradeoff es observable.
Trataría el reweighting automático como un proceso de cambio de modelo con restricciones
Llevado a diseño de sistema, el bucle mínimo no es complicado:
1. congelar los active weights actuales
2. estimar factor evidence solo con datos pasados
3. adjuntar market / regime context
4. generar candidate weights
5. aplicar hard constraint / drift check
6. ejecutar candidate en shadow
7. esperar a que maduren future outcomes
8. comparar active y shadow
9. crear una nueva active version solo si se cumple la promotion rule
10. persistir todo el decision trail
El punto 9 no debería reducirse a if ic > threshold.
Como mínimo quiero saber:
- cuánto tiempo lleva ganando el candidate;
- si todas esas victorias vienen del mismo regime;
- qué weight jump causaría el promotion;
- cuánto turnover se ha acumulado recientemente;
- si la diferencia entre candidate y active viene de un factor o del profile completo;
- cuánto adaptation lag acepto si todavía no hago promotion.
“Recent Rank IC = 0.08” no responde a ninguna de esas preguntas.
Incluso trataría cada promotion decision como un objeto de outcome tracking. Cuando el sistema diga “cambia los pesos ahora”, volvería después a revisar: ¿qué habría pasado sin cambiar? ¿Y esperando una ventana más?
Solo con suficientes registros tendría sentido decidir con evidencia propia si shadow confirmation debe ser 1, 2 o 3, o si el delta cap debe acercarse a 5% o 10%.
En ese punto ya no se parece tanto a parameter tuning.
Un adaptive weighting system debe aprender no solo qué factor funciona ahora, sino también si sus propias reglas de cambio de modelo han ayudado históricamente.
Ahí termina este experimento sintético. Immediate reacciona rápido pero genera mucho churn. Shadow es más prudente, pero puede volverse tan lento que pierda el cambio de regime. Un delta cap limita el tamaño de cada movimiento, pero no sustituye la validación con future outcomes.
No voy a copiar un número de esta tabla directamente a production config.
El siguiente paso útil es dejar estos guardrails funcionando un tiempo en shadow sobre outcomes reales ya cerrados: registrar cada candidate que el sistema quiso promover y comprobar después si siguió ganando. Cuando haya suficiente evidencia real, el sistema podrá empezar a decidir cuántas false promotions puede tolerar y cuánto adaptation lag está dispuesto a pagar por estabilidad.