Quant500 Academy

3. Controles automáticos: los datos se validan solos

Nivel 4: Limpieza y validación

Todo lo anterior tiene un problema práctico: se hace una vez, el día que montas el proyecto, y después nadie vuelve a mirarlo. Seis meses más tarde el proveedor cambia el formato de una columna, un festivo nuevo entra en el calendario, o la descarga nocturna trae la mitad del universo, y nada se entera. La solución no es más disciplina personal: es convertir la limpieza en código que corre antes de cada uso y que detiene el proceso cuando algo no cuadra.

Un contrato, no una revisión a ojo

Un control automático es un contrato explícito sobre cómo tienen que ser los datos para que el resto del sistema tenga permiso de usarlos. La lista mínima, que cubre la inmensa mayoría de los incidentes reales:

Escribirlo de forma que falle a gritos

class DatosInvalidos(Exception):
    pass


def validar(df, universo_esperado, cobertura_min=0.95):
    fallos = []

    faltan = {"apertura", "maximo", "minimo", "cierre", "volumen"} - set(df.columns)
    if faltan:
        fallos.append("faltan columnas: " + ", ".join(sorted(faltan)))

    if not df.index.is_monotonic_increasing:
        fallos.append("el indice de fechas no esta ordenado")
    if not df.index.is_unique:
        repes = df.index[df.index.duplicated()].unique()[:5]
        fallos.append("fechas duplicadas: " + str(list(repes)))

    if (df[["apertura", "maximo", "minimo", "cierre"]] <= 0).any().any():
        fallos.append("hay precios nulos o negativos")

    cobertura = df.groupby(level=0)["cierre"].count() / universo_esperado
    flojas = cobertura[cobertura < cobertura_min]
    if len(flojas):
        fallos.append("cobertura baja en " + str(len(flojas)) + " sesiones, "
                      "minima " + str(round(flojas.min(), 3)))

    if fallos:
        raise DatosInvalidos(" | ".join(fallos))
    return df

Tres decisiones de diseño que no son cosméticas. Primero, se acumulan todos los fallos y se lanzan juntos: si abortas en el primero, arreglas uno, vuelves a ejecutar y descubres el siguiente, y así seis veces. Segundo, el mensaje lleva los datos concretos, qué fechas están duplicadas y cuál es la cobertura mínima, porque un error que solo dice que algo va mal obliga a repetir la investigación entera. Y tercero, se lanza una excepción, no se emite un aviso. Un aviso se pierde entre la salida del proceso nocturno y todo sigue como si nada, entregando datos malos con aspecto de buenos.

El histórico no debería cambiar, y hay que comprobarlo

El control más olvidado es también el que más incidentes silenciosos detecta: comparar la descarga de hoy con la de ayer en el tramo que las dos comparten. Ese tramo es pasado; salvo revisión legítima del proveedor, tiene que ser idéntico.

def comparar_con_anterior(nuevo, anterior, tolerancia=1e-9):
    comun = nuevo.index.intersection(anterior.index)
    a, b = nuevo.loc[comun, "cierre"], anterior.loc[comun, "cierre"]

    cambiadas = comun[(a - b).abs() > tolerancia * b.abs().clip(lower=1)]
    desaparecidas = anterior.index.difference(nuevo.index)

    if len(cambiadas) or len(desaparecidas):
        raise DatosInvalidos(
            "el historico ha cambiado: " + str(len(cambiadas)) + " cierres "
            "distintos y " + str(len(desaparecidas)) + " filas desaparecidas"
        )

Que salte esta comprobación no significa siempre que haya un error: los proveedores revisan datos por motivos legítimos, y las empresas reformulan cuentas ante el regulador. Significa que alguien tiene que decidir, y que la decisión debe quedar escrita. Lo inaceptable es que el pasado cambie y nadie se dé cuenta, porque entonces dos backtests idénticos dan resultados distintos y se pierden semanas buscando el fallo en la estrategia.

Dónde se enganchan estos controles

Van en el punto exacto en que los datos cruzan de una etapa a la siguiente: al terminar la descarga, antes de escribir en el almacén, y otra vez al cargarlos para un backtest. Duplicar la validación en la carga parece redundante y no lo es: protege de que alguien haya tocado los ficheros por fuera del proceso, que es un incidente mucho más común de lo que nadie admite.

El comando que orquesta la descarga debe terminar con un código de salida distinto de cero cuando la validación falla, para que el programador de tareas lo trate como lo que es, un fallo, y avise. Y hay una decisión de diseño que conviene tomar pronto: cuando el control salta, los datos nuevos no sustituyen a los antiguos. Se escriben aparte, en cuarentena, y el sistema sigue funcionando con la última versión válida hasta que alguien resuelva la incidencia. Es la diferencia entre una noche mala y una semana operando a ciegas.

Un aviso sobre la tentación de aflojar umbrales: cuando un control salta a menudo por motivos legítimos, la reacción instintiva es subir la tolerancia hasta que deje de molestar. Eso convierte el control en decoración. Lo correcto es partirlo en dos, uno estricto que detiene el proceso y otro informativo que solo cuenta, y revisar el informativo cada cierto tiempo.

Ejemplo trabajado: la cobertura que se cae despacio

Un universo de 480 valores con un umbral del 95% exige al menos 456 valores con dato cada sesión. Un día llegan 447, y la comprobación falla por nueve valores. Vista de una en una, la incidencia parece menor. Vista como serie, es otra cosa: si la cobertura viene bajando del 99% al 93% a lo largo de tres semanas, no es un incidente puntual, es una fuga, típicamente identificadores que han dejado de resolverse tras un cambio de nombre. El control que se ejecuta cada día detecta la tendencia; la revisión manual, que ocurre cuando alguien tiene tiempo, no la detecta nunca.

El resto de Quant500

Lo que estás aprendiendo, aplicado al índice todos los días:

La cartera del modelo, cada semana en tu correo

Cada sábado, su semana en un minuto: cómo fue, lo mejor y lo peor, y qué directivos compraron con su dinero. Y el día que el modelo se rehace, qué entra y qué sale. Es una simulación, no un consejo. Como mucho un correo a la semana, sin publicidad y sin cuenta; te das de baja con un clic. Lo hace una persona: Pol Gracia.

Cartera modelo con resultados simulados, no una recomendación personalizada. Tu correo se usa solo para esto: privacidad.

Detrás de la Academia y del resto de Quant500 hay una sola persona: Pol Gracia Serrahima.