Lección 7.3.3 · 70 min
Monitorización del modelo en vivo y desactivación automática
Un modelo no avisa cuando deja de funcionar: sigue dando predicciones con la misma seguridad. Esta lección monta el monitor de un modelo en producción sobre un año de datos en el que la ventaja desaparece el 16 de junio: comprueba las entradas cada mañana, compara el IC con lo que prometía la validación, reduce y apaga con una política graduada, y lo comunica al ejecutor con un permiso que caduca si el monitor se calla.
- Módulo
- 7.3 · MLOps para modelos de trading
- Duración
- 70 min
Lecciones del nivel 7
7.1 · Ingeniería de software para trading
7.2 · Infraestructura y despliegue
7.3 · MLOps para modelos de trading
- 7.3.1 Registro de experimentos, modelos y datos: MLflow, DVC y una ejecución reproducida de principio a fin
- 7.3.2 Reentrenamiento programado, validación automática y champion/challenger
- 7.3.3 Monitorización del modelo en vivo y desactivación automática
- 7.3.4 Servir modelos: ONNX, inferencia en CPU o GPU y el coste de los LLM en producción
7.4 · Regulación, ética y fiscalidad
- 7.4.1 MiFID II: clientes, idoneidad y trading algorítmico
- 7.4.2 ESMA y CFD: apalancamiento, cierre y saldo negativo
- 7.4.3 PRIIPs, KID y ETFs estadounidenses
- 7.4.4 MiCA después del transitorio: comprobar un proveedor en los registros
- 7.4.5 Abuso de mercado: spoofing, layering y el bot que los imita sin querer
- 7.4.6 Fiscalidad del trading en España
- 7.4.7 Ética: datos personales, inteligencia artificial y la venta de señales
7.5 · Carrera profesional quant
Un modelo no avisa cuando muere
Las piezas ya están en lecciones anteriores. La 4.2.5 enseñó el kill switch de una estrategia: un CUSUM sobre sus rendimientos y un interruptor operativo con rearme manual. La 5.10.2 midió la deriva de datos (PSI, KS, validación adversaria) y la de concepto (media móvil del IC, CUSUM, Page-Hinkley) en un laboratorio. La 7.2.3 montó las métricas, las alertas y los latidos de un sistema. Esta lección las junta alrededor de un modelo en producción: qué se mira cada mañana antes de dejarle operar, con qué se compara su rendimiento, quién decide reducirlo o apagarlo y cómo llega esa decisión al proceso que envía las órdenes.
Fuente Mermaid
flowchart TB M[Modelo en vivo] --> O[Monitor] E[IC con retraso] --> O O --> K[Permiso en Redis] O --> G[Métricas] O --> A[Auditoría] K --> X[Ejecutor]
El monitor es un proceso aparte, como el motor de riesgo de la lección 6.4.5: no entrena ni predice, solo mira y decide. Su única salida hacia la operativa es un permiso: el multiplicador de tamaño con el que el ejecutor puede operar el modelo hoy.
Lo que se puede vigilar de un modelo se ordena por cuándo se sabe:
| Capa | Ejemplos | Cuándo se sabe | Qué decide |
|---|---|---|---|
| Proceso | latido, latencia, errores | al momento | alertas del sistema (7.2.3) |
| Entradas | cobertura, valores congelados, rangos | antes de operar | pausa del día |
| Deriva | PSI y KS de features y predicciones (5.10.2) | antes de operar | aviso y diagnóstico |
| Resultado | IC, acierto, calibración | al cerrar la etiqueta | reducir o apagar |
| Negocio | P&L, slippage (4.5.3) | al cierre | revisión |
La fila de resultados es la que importa y la que llega tarde. Un modelo que predice el rendimiento de mañana se puede evaluar pasado mañana; uno que predice el de dentro de veinte sesiones, dentro de un mes. Las capas de arriba no dicen si el modelo gana dinero, pero llegan a tiempo para impedir que opere con datos rotos.
El laboratorio: un año y medio con la respuesta conocida
Cincuenta activos y un modelo cuyas predicciones están correlacionadas con lo que el mercado paga. Hasta el 16 de junio de 2025 el rendimiento del día siguiente depende de esa componente; desde ese día, no. Es la muerte de la ventaja de la 4.2.5, aplicada al modelo en vez de a la estrategia. Encima, tres incidentes de la tubería: dos días con las features congeladas en marzo y un día de abril con un 35 % de predicciones vacías. El periodo de julio a diciembre de 2024 sirve de referencia: el modelo ya estaba en producción y se conoce su IC en vivo.
import numpy as np
import pandas as pd
rng = np.random.default_rng(702)
fechas = pd.bdate_range("2024-07-01", "2025-12-31")
T, N = len(fechas), 50
MUERTE = pd.Timestamp("2025-06-16") # la ventaja desaparece
S = rng.standard_normal((T, N)) # lo que el mercado paga
P = S + rng.standard_normal((T, N)) # predicción del modelo
alfa = np.where(fechas < MUERTE, 0.12, 0.0)
Z = alfa[:, None] * S + rng.standard_normal((T, N))
R = 0.01 * Z # rendimiento de t a t+1
INCIDENTES = { # fallos de la tubería
"2025-03-11": "features congeladas",
"2025-03-12": "features congeladas",
"2025-04-22": "35 % de predicciones vacías",
}
i = fechas.get_loc(pd.Timestamp("2025-03-10"))
P[i + 1] = P[i + 2] = P[i] # el proveedor no actualiza
j = fechas.get_loc(pd.Timestamp("2025-04-22"))
P[j, rng.choice(N, 17, replace=False)] = np.nan
def ic_dia(p, r):
"""IC de Spearman de un día, solo con predicciones válidas."""
m = np.isfinite(p)
a = p[m].argsort().argsort() - (m.sum() - 1) / 2
b = r[m].argsort().argsort() - (m.sum() - 1) / 2
return (a * b).sum() / np.sqrt((a * a).sum() * (b * b).sum())
ic = pd.Series([ic_dia(p, r) for p, r in zip(P, R)], index=fechas)
antes = ic["2025":MUERTE].iloc[:-1]
print("IC medio: 2.º semestre de 2024",
round(ic[:"2024-12"].mean(), 3), "| 2025 hasta la muerte",
round(antes.mean(), 3), "| después", round(ic[MUERTE:].mean(), 3))
print("desviación típica del IC diario:",
round(ic[:"2024-12"].std(), 3))IC medio: 2.º semestre de 2024 0.084 | 2025 hasta la muerte 0.071 | después -0.028
desviación típica del IC diario: 0.128El IC medio es de 0,084 en el segundo semestre de 2024, de 0,071 en 2025 hasta la muerte y de −0,028 después. La desviación típica del IC diario, 0,128, es la de cualquier modelo con 50 activos (): un solo día no dice casi nada.
La primera regla del monitor es no usar lo que todavía no se sabe. Antes de la apertura del 12 de marzo, las predicciones de hoy ya están, pero el último IC conocido es el del 10: el rendimiento del 11 al 12 aún no ha cerrado.
def lo_que_se_sabe(dia):
"""Antes de la apertura de `dia`: predicciones de hoy, y los IC
hasta el de anteayer (su rendimiento cerró ayer)."""
k = fechas.get_loc(dia)
return P[k], ic.iloc[:k - 1]
hoy = pd.Timestamp("2025-03-12")
pred, ics = lo_que_se_sabe(hoy)
print("hoy:", hoy.date(), "| último IC conocido:", ics.index[-1].date())
print("predicciones iguales a las de ayer:",
f"{np.mean(pred == P[fechas.get_loc(hoy) - 1]):.0%}")hoy: 2025-03-12 | último IC conocido: 2025-03-10
predicciones iguales a las de ayer: 100%Pausa: cuando lo que falla es la tubería
Las comprobaciones de entrada se hacen cada mañana sobre las predicciones del día, antes de que lleguen al ejecutor. Son baratas, no necesitan esperar a ningún resultado y separan un tipo de fallo que no hay que confundir con la pérdida de ventaja:
def operativas(k, min_cobertura=0.9, max_repetidas=0.5):
"""Lista de problemas de las predicciones del día k (vacía si
no hay ninguno)."""
p, ayer = P[k], P[k - 1]
problemas = []
cobertura = np.isfinite(p).mean()
if cobertura < min_cobertura:
problemas.append(f"cobertura {cobertura:.0%}")
repetidas = np.mean(p == ayer)
if repetidas > max_repetidas:
problemas.append(f"repetidas {repetidas:.0%}")
return problemas
inicio = fechas.get_loc(pd.Timestamp("2025-01-02"))
for k in range(inicio, T):
if p := operativas(k):
print(fechas[k].date(), p)2025-03-11 ['repetidas 100%']
2025-03-12 ['repetidas 100%']
2025-04-22 ['cobertura 66%']Las tres, exactamente los tres incidentes plantados: el 11 y el 12 de marzo todas las predicciones son iguales a las del día anterior (el proveedor no actualizó las features y el modelo, determinista, repite su salida), y el 22 de abril solo un 66 % de los activos tiene predicción. Ningún otro día de 2025 dispara nada.
La respuesta es una pausa: ese día el modelo no opera, y al día siguiente, si las comprobaciones pasan, vuelve solo. No es la desactivación del modelo. Un modelo sano con datos congelados no ha perdido su ventaja; apagarlo y esperar a que alguien lo rearme castiga al modelo por un fallo de la tubería, y la tubería es lo que hay que arreglar (con su alerta). Al revés también es un error: tratar una pérdida de ventaja como un fallo pasajero que se recupera solo.
Hay más comprobaciones de este tipo: rangos de cada feature, que la distribución de las predicciones no cambie de
forma (el PSI de la 5.10.2 aplicado a la salida del modelo), que la versión del modelo cargada sea la del alias
champion (lección 7.3.2). Todas comparten la propiedad que las hace útiles: se saben antes de operar.
Lo esperado y lo observado
Para juzgar el IC en vivo hace falta saber qué se esperaba. Ese número sale de la validación del modelo (el IC del walk-forward o del challenger en la lección 7.3.2), rebajado: el IC de una validación es el del mejor de varios intentos, y en vivo suele ser menor (lección 5.4.4). La lección usa 0,06. La desviación típica del IC diario se mide en vivo, en el periodo de referencia.
Con las dos cosas, la comparación es un z de la media del IC de las últimas sesiones:
Con el IC esperado y la desviación típica del IC diario. Un modelo que rinde lo esperado tiene alrededor de cero; uno muerto, con IC medio cero, se va a : −2,1 con 21 sesiones y −3,7 con 63. Es la diferencia con la 5.10.2, que vigilaba si el IC cambiaba: aquí se pregunta si el modelo cumple lo que se prometió al desplegarlo.
MU0 = 0.06 # IC esperado: el de la validación, con descuento
SIGMA = ic[:"2024-12"].std() # del IC diario, medido en vivo
def z_ic(ics, n):
"""z de la media de los n últimos IC frente a lo esperado."""
x = ics.iloc[-n:]
return (x.mean() - MU0) / (SIGMA / np.sqrt(len(x)))
def zs(dia):
_, ics = lo_que_se_sabe(dia)
return tuple(round(float(z_ic(ics, n)), 2) for n in (21, 63))
for d in ("2025-03-03", "2025-05-02", "2025-07-01", "2025-09-01"):
print(d, "z21, z63 =", zs(pd.Timestamp(d)))2025-03-03 z21, z63 = (0.53, 1.19)
2025-05-02 z21, z63 = (2.23, 0.73)
2025-07-01 z21, z63 = (-1.07, 1.07)
2025-09-01 z21, z63 = (-1.83, -3.49)En marzo y mayo, el modelo va bien (el 2 de mayo, incluso mejor de lo esperado en la ventana corta: +2,23). El 1 de julio, dos semanas después de la muerte, la ventana larga todavía marca +1,07: está llena de días buenos. El 1 de septiembre las dos están por debajo de −1,8, y la larga en −3,49.
Una política graduada
Con dos ventanas y dos acciones posibles (reducir el tamaño y apagar) se construye una máquina de estados pequeña:
Fuente Mermaid
stateDiagram-v2 [*] --> ACTIVO ACTIVO --> VIGILANCIA: z21 < -2 VIGILANCIA --> ACTIVO: z21 >= -2 ACTIVO --> REDUCIDO: z63 < -2 VIGILANCIA --> REDUCIDO: z63 < -2 REDUCIDO --> ACTIVO: z63 > -1 REDUCIDO --> DESACTIVADO: z63 < -3 DESACTIVADO --> ACTIVO: rearme manual
Cuatro decisiones de diseño:
- La ventana corta solo avisa. Con 21 sesiones el error de la media del IC es de 0,028, casi la mitad del IC esperado: un modelo sano baja de −2 con frecuencia. Sirve para mirar antes, no para actuar.
- La acción barata necesita menos evidencia que la cara. Reducir a la mitad se deshace solo y cuesta poco si era una falsa alarma; apagar exige rearme y deja el modelo fuera hasta que alguien lo revise.
- Histéresis. De REDUCIDO no se vuelve a ACTIVO al cruzar −2 hacia arriba, sino al pasar de −1. Sin ella, un que ronda el umbral haría que el tamaño cambiase cada día, con sus costes.
- Rearme manual, como en la 4.2.5 y en el artículo 12 del Reglamento Delegado (UE) 2017/589 que cita esa lección: lo que apaga un modelo es algo que nadie entiende todavía.
from dataclasses import dataclass, field
MULT = {"ACTIVO": 1.0, "VIGILANCIA": 1.0, "REDUCIDO": 0.5,
"DESACTIVADO": 0.0}
@dataclass
class Politica:
z_vigilar: float = -2.0 # ventana de 21: solo avisa
z_reducir: float = -2.0 # ventana de 63: mitad de tamaño
z_apagar: float = -3.0 # ventana de 63: a cero
z_volver: float = -1.0 # histéresis para volver a ACTIVO
estado: str = "ACTIVO"
cambios: list = field(default_factory=list)
def decidir(self, dia, z21, z63):
e = self.estado
if e == "DESACTIVADO":
return e # solo una persona lo rearma
if z63 < self.z_apagar:
e = "DESACTIVADO"
elif z63 < self.z_reducir:
e = "REDUCIDO"
elif e == "REDUCIDO" and z63 < self.z_volver:
e = "REDUCIDO" # no vuelve hasta z63 > -1
elif z21 < self.z_vigilar:
e = "VIGILANCIA"
else:
e = "ACTIVO"
if e != self.estado:
self.cambios.append((dia.date(), self.estado, e,
round(z21, 2) + 0.0,
round(z63, 2) + 0.0))
self.estado = e
return e
def ejecutar_anio(pol):
filas = []
for k in range(inicio, T):
dia = fechas[k]
_, ics = lo_que_se_sabe(dia)
estado = pol.decidir(dia, z_ic(ics, 21), z_ic(ics, 63))
pausa = operativas(k)
filas.append({"fecha": dia, "estado": estado,
"pausa": bool(pausa),
"mult": 0.0 if pausa else MULT[estado]})
return pd.DataFrame(filas).set_index("fecha")
pol = Politica()
diario = ejecutar_anio(pol)
for c in pol.cambios:
print(*c)
print("días en pausa:", int(diario["pausa"].sum()))2025-04-04 ACTIVO VIGILANCIA -2.07 0.0
2025-04-07 VIGILANCIA ACTIVO -1.86 -0.15
2025-07-15 ACTIVO VIGILANCIA -2.03 -0.26
2025-07-30 VIGILANCIA REDUCIDO -3.28 -2.11
2025-08-06 REDUCIDO DESACTIVADO -3.46 -3.01
días en pausa: 3La figura de la cabecera sigue las dos ventanas todo el año. Antes de la muerte, una sola alarma: la ventana corta toca −2,07 el viernes 4 de abril y el lunes ya ha vuelto. Es una falsa alarma y la política la trata como tal, porque la ventana larga está en 0. Después, la corta avisa el 15 de julio, la larga cruza −2 el 30 de julio (32 sesiones después de la muerte) y −3 el 6 de agosto (37 sesiones). Los tres días de pausa son los tres incidentes.
¿Merece la pena? La cartera del modelo compra los diez activos con mejor predicción y vende los diez peores cada día, con un coste de 5 pb por pata:
def cartera(k, n=10):
"""Rendimiento de comprar los n mejores y vender los n peores."""
ok = np.where(np.isfinite(P[k]))[0]
orden = ok[np.argsort(P[k, ok])]
return R[k, orden[-n:]].mean() - R[k, orden[:n]].mean()
rc = pd.Series([cartera(k) for k in range(inicio, T)],
index=fechas[inicio:])
con = rc * diario["mult"]
COSTE = 0.0005 # 5 pb por rotación completa
for nombre, serie, m in (("sin política", rc, 1.0),
("con política", con, diario["mult"])):
neto = serie - COSTE * 2 * m # dos patas, rotación diaria
print(f"{nombre:13} antes {neto[:MUERTE].iloc[:-1].sum():+.1%}"
f" · después {neto[MUERTE:].sum():+.1%}")sin política antes +13.3% · después -19.0%
con política antes +15.0% · después -4.5%Desde la muerte hasta fin de año, la cartera pierde un 19,0 % sin la política y un 4,5 % con ella (sumas de rendimientos diarios). Antes de la muerte la política no cuesta nada: un 15,0 % frente a un 13,3 %, porque las tres pausas evitaron operar con datos rotos y sus costes. Una estrategia muerta no gana cero: pierde sus costes de transacción cada día, y el monitor es lo que pone fin a eso.
Los umbrales −2 y −3 no salen de una tabla: se eligen mirando lo que cuestan. Dos números por umbral, los mismos de la 4.2.5: cuántas veces actúa la política en un año en que el modelo está sano y cuánto tarda en actuar cuando muere. Se simulan miles de años con IC diarios normales de media y desviación , y miles de muertes:
def simular(mu, dias, n, pol_kw, semilla=0):
"""n trayectorias de IC diario normales; devuelve el primer día
en REDUCIDO y en DESACTIVADO (o dias si no llega)."""
g = np.random.default_rng(semilla)
x = mu[None, :] + SIGMA * g.standard_normal((n, len(mu)))
c = np.cumsum(np.pad(x, ((0, 0), (1, 0))), axis=1)
m63 = (c[:, 63:] - c[:, :-63]) / 63 # medias móviles
z63 = (m63 - MU0) / (SIGMA / np.sqrt(63))
red = np.full(n, dias); apa = np.full(n, dias)
for t in range(z63.shape[1]):
nuevo_r = (z63[:, t] < pol_kw["z_reducir"]) & (red == dias)
red[nuevo_r] = t
nuevo_a = (z63[:, t] < pol_kw["z_apagar"]) & (apa == dias)
apa[nuevo_a] = t
return red, apa
filas = {}
for zr, za in ((-1.5, -2.5), (-2.0, -3.0), (-2.5, -3.5)):
kw = {"z_reducir": zr, "z_apagar": za}
sano = np.full(252 + 63, MU0) # un año sano
r0, a0 = simular(sano, 252, 4000, kw)
muerto = np.r_[np.full(63, MU0), np.zeros(252)] # muere en 63
r1, a1 = simular(muerto, 252, 4000, kw, semilla=1)
filas[f"{zr} / {za}"] = {
"P_reducir_sano": (r0 < 252).mean(),
"P_apagar_sano": (a0 < 252).mean(),
"reducir_mediana": np.median(r1),
"apagar_mediana": np.median(a1),
"apagar_p90": np.quantile(a1, 0.9)}
pd.DataFrame(filas).T.round(3) P_reducir_sano P_apagar_sano reducir_mediana apagar_mediana apagar_p90
-1.5 / -2.5 0.665 0.145 23.0 40.0 61.0
-2.0 / -3.0 0.364 0.041 31.0 48.0 75.0
-2.5 / -3.5 0.145 0.008 40.0 57.0 108.0| Umbrales (reducir / apagar) | Lo que gana | Lo que cuesta |
|---|---|---|
| −1,5 / −2,5 | apaga en 40 sesiones de mediana | un año sano reduce con probabilidad 0,67 y apaga con 0,15 |
| −2 / −3 | apaga en 48 sesiones de mediana | reduce con 0,36 y apaga con 0,04 |
| −2,5 / −3,5 | un año sano casi nunca apaga (0,008) | apaga en 57 sesiones de mediana y 108 en el percentil 90 |
La política de la lección es la del medio. Que un año sano reduzca alguna vez con probabilidad 0,36 parece mucho, y es aceptable porque reducir es la acción barata. Lo que tiene que ser raro es apagar. Y hay un límite que ningún umbral evita: con un IC esperado de 0,06 y 50 activos, un modelo muerto necesita semanas para distinguirse de uno sano. Para detectar antes hay que tener más información por día (más activos, un IC esperado mayor) o aceptar más falsas alarmas.
El permiso para operar
La decisión del monitor tiene que llegar al ejecutor, que es otro proceso. La forma más simple y robusta es un permiso en un almacén compartido: el monitor lo escribe cada mañana con su estado, su multiplicador y una hora de caducidad; el ejecutor lo lee antes de cada orden. Tres propiedades:
- A prueba de fallos. Si no hay permiso, si ha caducado o si el almacén no responde, el multiplicador es cero.
- Caducidad. El permiso vale 26 horas. Si el monitor se cae, no hace falta que nadie se dé cuenta: al día siguiente el permiso ha caducado y el modelo deja de operar. Es la versión del latido de la lección 7.2.3 que actúa en vez de avisar.
- Una sola dirección. El monitor escribe y el ejecutor lee. El ejecutor no puede renovar ni cambiar el permiso.
import json
import fakeredis
import redis
servidor = fakeredis.FakeServer()
kv = fakeredis.FakeRedis(server=servidor, decode_responses=True)
CLAVE = "permiso:senal-n7"
VALIDEZ = 26 * 3600 # el monitor lo renueva cada mañana
def publicar(kv, ahora, estado, mult, motivo=""):
"""El monitor escribe el permiso del día (lo único que hace)."""
kv.hset(CLAVE, mapping={"estado": estado, "mult": mult,
"valido_hasta": ahora + VALIDEZ,
"motivo": motivo})
def multiplicador(kv, ahora):
"""El ejecutor pregunta antes de cada orden. Ante la duda, 0."""
try:
p = kv.hgetall(CLAVE)
except redis.exceptions.RedisError:
return 0.0, "Redis no responde"
if not p:
return 0.0, "no hay permiso"
if ahora > float(p["valido_hasta"]):
return 0.0, "permiso caducado"
return float(p["mult"]), p["estado"]
t0 = 1_750_000_000 # reloj inyectado (segundos)
publicar(kv, t0, "REDUCIDO", 0.5, "z63 < -2")
print(multiplicador(kv, t0 + 3600))
print(multiplicador(kv, t0 + 30 * 3600)) # el monitor no renovó
servidor.connected = False # Redis cae
print(multiplicador(kv, t0 + 3600))
servidor.connected = True(0.5, 'REDUCIDO')
(0.0, 'permiso caducado')
(0.0, 'Redis no responde')En la celda, Redis lo simula fakeredis con el mismo cliente redis-py, y el reloj se inyecta (como en la 4.2.5) para
que la caducidad se pueda probar sin esperar 26 horas. En producción, la caducidad la puede poner el propio Redis
(SET con EX, o EXPIRE), de forma que la clave desaparece sola; el ejecutor la sigue tratando igual, porque
«no hay permiso» y «permiso caducado» llevan al mismo cero.
Cada cambio de estado queda en el registro de auditoría con los números que lo provocaron, en JSON con las claves ordenadas (lección 7.2.3):
import io
import structlog
buf = io.StringIO() # en producción, un fichero JSONL
structlog.configure(
processors=[structlog.processors.JSONRenderer(sort_keys=True)],
logger_factory=structlog.PrintLoggerFactory(file=buf))
log = structlog.get_logger().bind(modelo="senal-n7", version=3)
for dia, antes, despues, z21, z63 in pol.cambios:
log.warning("cambio_estado", fecha=str(dia), antes=antes,
despues=despues, z21=z21, z63=z63)
print(*buf.getvalue().splitlines()[-2:], sep="\n"){"antes": "VIGILANCIA", "despues": "REDUCIDO", "event": "cambio_estado", "fecha": "2025-07-30", "modelo": "senal-n7", "version": 3, "z21": -3.28, "z63": -2.11}
{"antes": "REDUCIDO", "despues": "DESACTIVADO", "event": "cambio_estado", "fecha": "2025-08-06", "modelo": "senal-n7", "version": 3, "z21": -3.46, "z63": -3.01}Y las métricas del modelo se publican para Prometheus junto a las del sistema. La 7.2.3 explica cómo se recogen y cómo se escriben las reglas; aquí basta con lo específico del modelo: el de cada ventana, el multiplicador vigente y la edad del último resultado, que avisa de que el monitor se ha quedado ciego:
from prometheus_client import (CollectorRegistry, Gauge,
generate_latest)
reg = CollectorRegistry()
g_z = Gauge("modelo_ic_z", "z del IC frente a lo esperado",
["modelo", "ventana"], registry=reg)
g_estado = Gauge("modelo_multiplicador", "Tamaño permitido (0 a 1)",
["modelo"], registry=reg)
g_edad = Gauge("modelo_ultimo_ic_dias",
"Sesiones desde el último IC conocido",
["modelo"], registry=reg)
dia = pd.Timestamp("2025-07-30")
z21, z63 = zs(dia)
g_z.labels("senal-n7", "21").set(z21)
g_z.labels("senal-n7", "63").set(z63)
g_estado.labels("senal-n7").set(diario.loc[dia, "mult"])
g_edad.labels("senal-n7").set(2)
print("\n".join(l for l in generate_latest(reg).decode().splitlines()
if not l.startswith("#")))modelo_ic_z{modelo="senal-n7",ventana="21"} -3.28
modelo_ic_z{modelo="senal-n7",ventana="63"} -2.11
modelo_multiplicador{modelo="senal-n7"} 0.5
modelo_ultimo_ic_dias{modelo="senal-n7"} 2.0import yaml
REGLAS = """
groups:
- name: modelos
rules:
- alert: ModeloRendimientoBajo
expr: modelo_ic_z{ventana="63"} < -2
labels: {severidad: aviso}
- alert: ModeloDesactivado
expr: modelo_multiplicador == 0
labels: {severidad: critica}
- alert: ModeloSinResultados
expr: modelo_ultimo_ic_dias > 3
labels: {severidad: critica}
"""
for r in yaml.safe_load(REGLAS)["groups"][0]["rules"]:
print(f'{r["alert"]:22} {r["expr"]:34} {r["labels"]["severidad"]}')ModeloRendimientoBajo modelo_ic_z{ventana="63"} < -2 aviso
ModeloDesactivado modelo_multiplicador == 0 critica
ModeloSinResultados modelo_ultimo_ic_dias > 3 criticaDespués de apagar
Un modelo desactivado no vuelve porque su suba. Vuelve cuando alguien entiende qué ha pasado:
- Diagnosticar. ¿Cambiaron las features (PSI, validación adversaria de la 5.10.2) o solo su relación con el resultado? ¿Hubo un cambio en la tubería o en el mercado (un cambio de régimen, una regla nueva, una estrategia que se ha masificado)?
- Decidir qué opera mientras tanto. Nada (multiplicador cero), el modelo anterior (el alias
anteriorde la 7.3.2, si su receta sigue siendo válida) o una versión más conservadora. - Volver por la puerta de entrada. Un modelo corregido o reentrenado entra como challenger en el ciclo de la 7.3.2, con sus puertas y su sombra. El rearme manual de este monitor solo se usa cuando la desactivación fue un error del propio monitor (por ejemplo, un IC esperado mal puesto).
Ejercicios
- Repite la calibración con la política −2 / −3 suponiendo que el IC real del modelo sano es 0,05 o 0,04 en lugar de 0,06 (el monitor sigue esperando 0,06). ¿Qué pasa con las falsas alarmas?
Solución
kw = {"z_reducir": -2.0, "z_apagar": -3.0}
for mu_real in (0.06, 0.05, 0.04):
r, a = simular(np.full(252 + 63, mu_real), 252, 4000, kw)
print(f"IC real {mu_real:.2f}: P(reducir) {(r < 252).mean():.2f}"
f" · P(apagar) {(a < 252).mean():.2f} en un año sano")IC real 0.06: P(reducir) 0.36 · P(apagar) 0.04 en un año sano
IC real 0.05: P(reducir) 0.73 · P(apagar) 0.18 en un año sano
IC real 0.04: P(reducir) 0.95 · P(apagar) 0.51 en un año sanoCon un IC real de 0,05, un año sano reduce alguna vez con probabilidad 0,73 y apaga con 0,18; con 0,04, con 0,95 y 0,51: la mitad de los años sanos terminarían con el modelo apagado. La política no distingue un modelo muerto de uno que rinde menos de lo prometido, porque las dos cosas son lo mismo para ella: un IC por debajo del esperado.
Hay dos lecturas, y las dos son útiles. La primera: el IC esperado tiene que ser prudente desde el principio (el de la validación con un descuento claro). La segunda: un modelo que rinde un tercio menos de lo que se validó también es una noticia que alguien debe revisar; quizá no haya que apagarlo, pero sí recalcular el tamaño de la posición y el IC esperado con los primeros meses en vivo, y dejarlo escrito.
- Añade a la política la regla de la alerta
ModeloSinResultados: si el último IC conocido tiene más de cinco sesiones, el estado es como mucho REDUCIDO. Pruébala con una parada de la tubería de precios: el último IC que llega es el del 17 de febrero.
Solución
def decidir_con_edad(pol, dia, ics, max_edad=5):
"""Edad: sesiones desde el último IC conocido, hoy incluido (2
en un día normal). Con más de max_edad, el monitor está ciego:
como mucho, REDUCIDO."""
edad = len(fechas[(fechas > ics.index[-1]) & (fechas <= dia)])
estado = pol.decidir(dia, z_ic(ics, 21), z_ic(ics, 63))
if edad > max_edad and estado in ("ACTIVO", "VIGILANCIA"):
return "REDUCIDO", edad
return estado, edad
p2 = Politica()
_, ics = lo_que_se_sabe(pd.Timestamp("2025-02-27"))
ics_parados = ics[:"2025-02-17"] # los precios dejan de llegar
for d in ("2025-02-19", "2025-02-27"):
print(d, decidir_con_edad(p2, pd.Timestamp(d), ics_parados))2025-02-19 ('ACTIVO', 2)
2025-02-27 ('REDUCIDO', 8)El 19 de febrero el último IC conocido sigue siendo el del 17 (edad 2, la normal) y el estado es ACTIVO. El 27, con ocho sesiones sin resultados, la regla lo lleva a REDUCIDO aunque el , calculado con IC viejos, siga diciendo que todo va bien. Si la parada continúa, una segunda regla (por ejemplo, más de diez sesiones) debería llevarlo a cero.
La alerta ModeloSinResultados salta antes (con edad mayor que 3): primero se avisa a una persona, y si el problema
dura, la política actúa sola. Es el mismo principio del permiso con caducidad: un control que se queda sin
información no puede seguir autorizando como si la tuviera.
- Un compañero propone que el modelo desactivado se rearme solo cuando vuelva por encima de −1. ¿Qué le responderías?
Solución
Que el de un modelo apagado sigue moviéndose por azar, y un modelo muerto puede pasar por encima de −1 durante unos días: con IC verdadero cero, la media de 63 sesiones tiene una desviación típica de 0,016 y su oscila alrededor de −3,7 con desviación 1, así que cruzar −1 es raro pero no imposible en un año, y cada cruce lo volvería a encender para apagarlo semanas después, con pérdidas entre medias. Además, el solo mide el IC: no dice si la causa de la desactivación (una tubería rota, un cambio de régimen) se ha resuelto. Y si el modelo no opera, su IC se puede seguir calculando en sombra, pero eso es exactamente lo que hace el ciclo de la 7.3.2 con un challenger, con puertas y con una persona que decide. La vuelta automática deshace lo que el rearme manual protege.
Respuesta plegada en cada pregunta
Autoevaluación
El 11 y el 12 de marzo las predicciones del modelo son idénticas a las del 10. ¿Qué hace la política de la lección?
- ADesactiva el modelo hasta que lo rearme una persona
- BPausa esos dos días (multiplicador 0) y vuelve sola a operar en cuanto las predicciones cambian
- CNada: el IC no ha bajado
- DReduce el tamaño a la mitad durante un mes
Ver respuesta
Respuesta B. Pausa esos dos días (multiplicador 0) y vuelve sola a operar en cuanto las predicciones cambian
Unas predicciones congeladas son un fallo de la tubería, no del modelo: se ven antes de operar y se arreglan arreglando la tubería. La pausa no toca el estado estadístico del modelo ni exige rearme; apagar el modelo por un fallo de datos lo dejaría parado sin motivo.
¿Por qué la política solo avisa (VIGILANCIA) con la ventana de 21 sesiones y deja reducir y apagar a la de 63?
- APorque la ventana de 21 sesiones no se puede calcular hasta febrero
- BPorque Prometheus no admite ventanas cortas
- CPorque la de 63 reacciona antes
- DPorque con 21 sesiones el error de la media del IC es casi el doble: un modelo sano cruza −2 con frecuencia, como el 4 de abril en la lección (−2,07)
Ver respuesta
Respuesta D. Porque con 21 sesiones el error de la media del IC es casi el doble: un modelo sano cruza −2 con frecuencia, como el 4 de abril en la lección (−2,07)
El error estándar de la media es σ/√n: con σ = 0,128, 0,028 en 21 sesiones frente a 0,016 en 63. La ventana corta es útil para mirar antes, no para actuar: en el laboratorio, la única alarma antes de la muerte de la ventaja viene de ella y dura un fin de semana.
La ventaja desaparece el 16 de junio. ¿Cuándo actúa la política de la lección?
- AReduce a la mitad el 30 de julio (z63 = −2,11) y apaga el 6 de agosto (z63 = −3,01)
- BApaga el 17 de junio
- CNo actúa en todo 2025
- DApaga el 15 de julio, cuando la ventana de 21 sesiones baja de −2
Ver respuesta
Respuesta A. Reduce a la mitad el 30 de julio (z63 = −2,11) y apaga el 6 de agosto (z63 = −3,01)
La media de 63 sesiones tarda en llenarse de días sin ventaja: 37 sesiones en apagar, algo menos que la mediana de 48 de la calibración. Más deprisa solo se puede con umbrales que, en un año sano, apagarían el modelo muchas más veces.
El ejecutor lee el permiso en Redis antes de cada orden y Redis no responde. ¿Qué devuelve
multiplicador?- AEl último valor que leyó, guardado en memoria
- B1, para no perder oportunidades
- C0: el permiso es a prueba de fallos, y sin respuesta no se opera
- DLanza una excepción y el ejecutor decide
Ver respuesta
Respuesta C. 0: el permiso es a prueba de fallos, y sin respuesta no se opera
Un control que falla abierto deja de controlar justo cuando más falta hace. Si el monitor no escribe (el permiso caduca) o el almacén no responde, el ejecutor no puede saber si el modelo sigue autorizado, y lo seguro es no operar.
En la calibración, con umbrales −2 y −3, un año sano reduce el tamaño alguna vez con probabilidad 0,36. ¿Es un problema?
- ASí: hay que subir el umbral hasta que no reduzca nunca
- BNo necesariamente: reducir es barato y reversible (vuelve con z63 > −1); lo que tiene que ser raro es apagar, que en un año sano pasa con probabilidad 0,04
- CSí: significa que el modelo está mal
- DNo: las falsas alarmas no cuestan nada
Ver respuesta
Respuesta B. No necesariamente: reducir es barato y reversible (vuelve con z63 > −1); lo que tiene que ser raro es apagar, que en un año sano pasa con probabilidad 0,04
La política es graduada para eso: la acción barata se dispara con menos evidencia y la cara con más. Un umbral de reducir tan exigente como el de apagar perdería la ventaja de tener dos escalones.
Ideas clave
- Lo que se vigila de un modelo se ordena por cuándo se sabe: las entradas (cobertura, valores congelados) antes de operar; los resultados (IC, acierto), con el retraso de la etiqueta.
- Un fallo de la tubería pausa el día y se recupera solo; una pérdida de ventaja reduce y apaga, y solo una persona rearma. Mezclarlos apaga modelos sanos o deja operar modelos muertos.
- El rendimiento se compara con lo que prometía la validación: z de la media del IC frente al esperado en 21 y 63 sesiones. La ventana corta avisa; la larga actúa.
- En el laboratorio la ventaja muere el 16 de junio; el monitor reduce el 30 de julio y apaga el 6 de agosto. Después de la muerte, la cartera pierde un 19,0 % sin la política y un 4,5 % con ella.
- Los umbrales se eligen por simulación: con −2 y −3, un año sano reduce alguna vez con probabilidad 0,36 y apaga con 0,04; un modelo muerto se apaga en una mediana de 48 sesiones. Si el IC esperado está inflado, las falsas alarmas se disparan.
- La decisión llega al ejecutor como un permiso con caducidad en Redis: sin permiso, caducado o sin respuesta, el multiplicador es cero.