Quant500 Academy

3. Dependencias entre tareas y fallos parciales

Nivel 9: Orquestación y calendario

Una estrategia en producción no es una tarea, son cinco encadenadas: descargar datos, calcular señales, dimensionar posiciones, ejecutar y conciliar. Cuando las cinco funcionan, la cadena es invisible. El día que la primera devuelve la mitad de los datos y la segunda calcula igual, la cadena decide tu dinero. Este módulo va de eso: de qué hace el sistema cuando un eslabón falla a mitad. No hablamos aquí de reintentar una llamada concreta con espera creciente, sino de qué le pasa a la tarea entera y a todas las que dependen de ella.

La cadena y sus dependencias reales

Esa penúltima frase es la clave del módulo: terminar no es lo mismo que terminar bien, y el sistema tiene que distinguirlo sin ambigüedad.

Declarar la dependencia, no confiar en el orden

Poner las llamadas una detrás de otra en un archivo no declara nada: si la primera lanza una excepción que alguien capturó por comodidad, la segunda corre igual. Declarar una dependencia significa que una tarea solo arranca si el estado registrado de sus predecesoras es correcto, y que ese estado sobrevive al proceso.

from dataclasses import dataclass

OK, FALLO, OMITIDA = "OK", "FALLO", "OMITIDA"

@dataclass(frozen=True)
class Tarea:
    nombre: str
    depende_de: tuple = ()

def ejecutar_flujo(tareas, funciones):
    estado = {}
    for t in tareas:
        if any(estado.get(d) != OK for d in t.depende_de):
            estado[t.nombre] = OMITIDA          # no se intenta siquiera
            continue
        try:
            funciones[t.nombre]()
            estado[t.nombre] = OK
        except Exception:
            estado[t.nombre] = FALLO            # y arrastra a sus dependientes
    return estado

Lo importante de este bucle no es lo que hace, sino lo que no hace: no hay ninguna ruta por la que ejecutar arranque si dimensionar quedó en FALLO. La omisión se propaga sola por el grafo y queda registrada, que es distinto de no haber ocurrido.

Conviene además fijar el número de intentos y el punto en el que la tarea se rinde. Algo que se reintenta indefinidamente no es resistente: es una tarea que nunca termina y que impide que el flujo llegue a la conciliación, que es justo lo que necesitas leer cuando algo va mal. Tres intentos y un fallo declarado valen más que un bucle sin final.

Datos incompletos: la señal que parece buena

El fallo más caro no es el ruidoso, es el silencioso. El proveedor responde con código 200 y devuelve 380 de los 500 valores del universo. La señal se calcula, sale un número, y ese número está calculado sobre un universo distinto del que definiste. Si tu estrategia ordena por percentil, faltar 120 valores mueve todos los cortes; si compara contra la media del universo, la media es otra.

Por eso la comprobación de completitud no es una advertencia en el registro: es un fallo duro de la tarea de datos.

class DatosIncompletos(Exception):
    pass

def validar_descarga(df, universo_esperado, sesion):
    faltan = set(universo_esperado) - set(df["simbolo"].unique())
    if faltan:
        raise DatosIncompletos("faltan " + str(len(faltan)) + " valores del universo")
    viejos = df[df["fecha"] < sesion]
    if not viejos.empty:
        raise DatosIncompletos("hay filas con fecha anterior a la sesion de hoy")
    return df

Rellenar los huecos con el precio de ayer parece prudente y es lo contrario: convierte un fallo visible en una señal falsa que nadie va a revisar.

Reintentar o saltarse: no son intercambiables

Reintentar una tarea significa: creo que el fallo es transitorio y que volver a intentarlo puede dar un resultado correcto. Saltarse una tarea significa: acepto seguir sin ella. Son decisiones distintas y solo la primera es casi siempre segura.

Hay una asimetría que conviene tener presente: casi todos los errores de este nivel llevan a operar de más, no de menos. Los datos incompletos generan órdenes, y la señal calculada sobre un universo recortado también. Un sistema que se apaga ante la duda se equivoca en la dirección barata.

La regla que resume el módulo entero: ante la duda, no operar. Un día sin operar cuesta un día de rendimiento esperado; un día operando con datos incompletos puede costar la cartera entera, y además ensucia el registro con el que mañana intentarás entender qué pasó.

def clasificar(exc):
    if isinstance(exc, (TimeoutError, ConnectionError)):
        return "reintentable"
    if isinstance(exc, DatosIncompletos):
        return "fatal"          # nunca se reintenta a ciegas ni se ignora
    return "fatal"              # lo desconocido se trata como grave, no como leve

Escenario trabajado

Escenario trabajado, con cifras de ejemplo. Un universo de 500 valores; el proveedor entrega 380 y responde con éxito. Sin validación, la estrategia calcula sus deciles sobre 380 nombres: los cortes se desplazan y 41 valores cambian de decil, de los cuales 12 pasan de no estar en cartera a estar en el decil comprador. El sistema ejecuta doce entradas que la estrategia real nunca habría pedido, y la conciliación del día siguiente las encuentra en cartera sin explicación.

Con la validación, la tarea de datos queda en FALLO, señales y ejecución quedan en OMITIDA, la conciliación corre igual y confirma que la cartera sigue como ayer. El coste es un día sin ajustar. Formación, no asesoramiento financiero.

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.