Quant500 Academy

4. La orden como máquina de estados

Nivel 8: Estado y ejecución fiable

Una orden no es un evento, es un proceso con vida propia. Tratarla como un booleano ("mandada, sí o no") es el error que produce las posiciones fantasma. Modelarla como una máquina de estados te obliga a escribir de antemano todo lo que puede pasarle, y a que el código diga "no" cuando ocurra algo imposible en vez de aceptarlo en silencio.

Los estados y sus transiciones

Siete estados bastan para la inmensa mayoría de los sistemas:

La ventaja de escribir las transiciones en una tabla, y no repartidas en condicionales por todo el código, es que la tabla se lee de un vistazo y se revisa en cinco minutos. Cuando alguien tenga que añadir un estado nuevo, verá exactamente desde dónde se puede llegar a él y a dónde puede ir.

Que los estados terminales sean terminales no es una obviedad, es una defensa. Un evento retrasado que intente devolver una orden EJECUTADA a PARCIAL debe rebotar contra una excepción, porque significa que algo llegó desordenado y que aplicarlo corrompería tu contabilidad.

TRANSICIONES = {
    "CREADA":   {"ENVIADA", "RECHAZADA"},
    "ENVIADA":  {"ACEPTADA", "RECHAZADA", "PARCIAL", "EJECUTADA"},
    "ACEPTADA": {"PARCIAL", "EJECUTADA", "CANCELADA", "RECHAZADA"},
    "PARCIAL":  {"PARCIAL", "EJECUTADA", "CANCELADA"},
    "EJECUTADA": set(), "CANCELADA": set(), "RECHAZADA": set(),
}

class TransicionIlegal(Exception):
    pass

def transitar(actual, nuevo):
    if nuevo not in TRANSICIONES[actual]:
        raise TransicionIlegal(actual + " -> " + nuevo)
    return nuevo

Las ejecuciones parciales cambian la aritmética

Pides 500 títulos y te llenan 180. Tu posición ya no es la que el modelo cree, y la orden sigue viva por los 320 restantes. Dos reglas que evitan el 90% de los errores de contabilidad:

Y ahora el caso que arruina la tarde: cancelas una orden parcialmente ejecutada. La cancelación solo elimina la parte pendiente; lo ya ejecutado es tuyo para siempre. Si tu código trata la cancelación como "esta orden no ha pasado" y pone la posición a cero, acabas de crear una posición invisible de 180 títulos. Cancelar nunca deshace: solo detiene.

def aplicar_evento(fila, evento):
    # filled_qty llega ACUMULADO: se asigna, jamas se suma.
    if evento.filled_qty < fila["ejecutada"]:
        raise ValueError("evento atrasado, se descarta")
    fila["ejecutada"] = evento.filled_qty
    fila["pendiente"] = fila["cantidad"] - evento.filled_qty

    if fila["pendiente"] == 0:
        destino = "EJECUTADA"
    elif evento.estado == "canceled":
        destino = "CANCELADA"   # conservamos lo ya ejecutado
    elif evento.filled_qty > 0:
        destino = "PARCIAL"
    else:
        destino = "ACEPTADA"

    if destino != fila["estado"]:            # repetir el mismo evento no debe romper nada
        fila["estado"] = transitar(fila["estado"], destino)
    return fila

Eventos repetidos, atrasados y perdidos

Los avisos de ejecución llegan por un canal que no es fiable: puede repetir un mensaje, entregarlos fuera de orden o dejar de emitir sin cerrar la conexión. Por eso la función anterior hace dos cosas que parecen paranoia y no lo son. Descarta cualquier evento cuya cantidad acumulada sea menor que la ya registrada, porque solo puede ser un mensaje atrasado. Y como asigna en vez de sumar, procesar dos veces el mismo evento es inofensivo: es la misma idea de idempotencia del módulo dos, aplicada ahora a la entrada de datos en lugar de a la salida de órdenes.

Lo que ninguna de las dos defensas cubre es el evento que nunca llega. Contra eso solo hay una cosa: no confiar únicamente en el canal de avisos y consultar por REST el estado de toda orden que lleve demasiado tiempo sin moverse.

Órdenes huérfanas al arrancar

Una orden huérfana es la que existe en un lado y no en el otro. Al arrancar te vas a encontrar tres familias, y cada una tiene su respuesta:

Regla de arranque: ninguna señal nueva se ejecuta hasta que todas las órdenes en estado no terminal se han resuelto. Un robot que empieza a operar con órdenes dudosas abiertas está apostando dinero a que su caché tenía razón, y ya sabes quién manda.

Escenario trabajado: la cancelación de las 17:20

Escenario construido para el curso. El sistema tiene una venta de 500 títulos que lleva media hora en el libro. A las 17:20 decide cancelarla para reemplazarla por otra más agresiva. En ese momento ya se habían ejecutado 180. El código marca CANCELADA y, al no distinguir estados, deja la posición del símbolo en 500 largos, cuando en realidad quedan 320. La orden de reemplazo se calcula sobre 500 y vende 500: 180 títulos de más, una posición corta de 180 que ninguna estrategia pidió y que nadie mira, porque el sistema cree estar plano. Con la máquina de estados anterior, la cancelación conserva la cantidad ejecutada, la posición baja a 320 y la orden de reemplazo sale por 320.

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.