Quant500 Academy

1. El estado es el problema: qué guardar, dónde y por qué

Nivel 8: Estado y ejecución fiable

Un backtest no tiene estado. Arranca, recorre un vector de precios y termina. Si lo matas a la mitad lo vuelves a lanzar y no ha pasado nada. Un sistema en producción es justo lo contrario: existe para acumular estado a lo largo del tiempo, y ese estado vale dinero. Si lo pierdes no pierdes una ejecución, pierdes la capacidad de saber qué tienes en el mercado.

De qué está hecho el estado

Cuando decimos "el estado" de un sistema de trading estamos hablando de cinco cosas distintas, y conviene no mezclarlas porque cada una se guarda y se recupera de forma diferente:

Por qué la memoria del proceso no vale

Un proceso se muere. No es una hipótesis remota: se muere porque despliegas una versión nueva, porque el sistema operativo lo mata al quedarse sin memoria, porque una excepción no capturada sube hasta arriba, porque el servidor se reinicia por un parche del kernel, o porque alguien lo para con la mano. Y aquí está lo importante: el mercado no se entera de que tu Python ha muerto. La posición sigue abierta, las órdenes siguen en el libro y las ejecuciones siguen produciéndose mientras tu proceso no existe.

Un diccionario en memoria tiene además un defecto peor que desaparecer: miente en silencio. Mientras el proceso vive, esa variable parece verdad absoluta, y nadie comprueba nunca si sigue coincidiendo con la realidad.

El bróker es la fuente de verdad; tu base de datos es una caché

Esta es la regla que ordena todo el nivel: el bróker es la fuente de verdad de las posiciones y de las órdenes; tu base de datos es una caché y un diario de intenciones. Ante cualquier discrepancia gana el bróker, siempre, sin discusión. Tu trabajo no es tener razón, es detectar la diferencia y dejar de operar hasta entenderla. El proceso formal de comparación y ajuste tiene nombre propio, se llama conciliación, y se trata a fondo en el nivel 10; aquí basta con que asumas quién manda.

Ahora bien, hay información que el bróker no tiene y que solo existe en tu lado:

De ahí sale la regla práctica: pregunta siempre al bróker las posiciones, las órdenes abiertas y el efectivo; guarda en disco todo aquello que el bróker no pueda devolverte nunca.

Escribir en disco sin dejar el fichero a medias

Abrir un fichero en modo escritura lo vacía de inmediato. Si el proceso muere entre ese vaciado y el final del volcado, te queda un fichero truncado o vacío, y al arrancar el sistema no encuentra estado corrupto: encuentra ninguno. La solución es no escribir nunca encima del bueno.

import json
import os
import tempfile

def guardar_atomico(ruta, datos):
    directorio = os.path.dirname(os.path.abspath(ruta))
    fd, temporal = tempfile.mkstemp(dir=directorio, suffix='.tmp')
    try:
        with os.fdopen(fd, 'w', encoding='utf-8') as f:
            json.dump(datos, f, ensure_ascii=False, indent=2)
            f.flush()
            os.fsync(f.fileno())
        os.replace(temporal, ruta)
    except BaseException:
        if os.path.exists(temporal):
            os.remove(temporal)
        raise

Tres detalles que no son adorno. El temporal se crea en el mismo directorio porque el renombrado solo es atómico dentro del mismo sistema de ficheros. El fsync fuerza los bytes al disco de verdad, no a la memoria intermedia del sistema operativo, antes de que el renombrado los declare oficiales. Y os.replace es atómico tanto en Linux como en Windows: cualquier lector verá el fichero viejo entero o el nuevo entero, nunca una mezcla.

Mejor todavía: que lo haga una base de datos

Si vas a escribir varias cosas que deben cambiar juntas, un JSON se te queda corto enseguida. SQLite viene en la biblioteca estándar, no necesita servidor y te da transacciones y durabilidad hechas.

import sqlite3

con = sqlite3.connect('estado.db', timeout=30)
con.execute('PRAGMA journal_mode=WAL')
con.execute('PRAGMA synchronous=FULL')
con.execute("""
CREATE TABLE IF NOT EXISTS ordenes (
    client_order_id TEXT PRIMARY KEY,
    simbolo         TEXT NOT NULL,
    lado            TEXT NOT NULL,
    cantidad        REAL NOT NULL,
    estado          TEXT NOT NULL,
    barra           TEXT NOT NULL,
    broker_id       TEXT,
    creada_en       TEXT NOT NULL
)""")
con.commit()

Fíjate en la primera columna: es clave primaria. Esa restricción, que parece una nota al pie, es el pilar de todo el módulo siguiente.

Escenario trabajado: cuarenta segundos de ceguera

Escenario construido para el curso, no un caso documentado. Despliegas a las 15:44:10 y el proceso tarda cuarenta segundos en volver. En ese hueco, una orden de compra de 300 títulos que ya estaba en el libro se ejecuta parcialmente: 120 títulos. El proceso arranca, lee su fichero local, que se guardó antes del despliegue y dice "0 ejecutado, orden viva", y decide que sigue plano. La siguiente señal calcula el tamaño desde cero y manda otra compra de 300. Resultado: 420 títulos donde el modelo creía tener 300, un 40% de exposición extra que ninguna regla de riesgo autorizó, y todo por confiar en una caché de cuarenta segundos de antigüedad en lugar de preguntar. El fallo no fue del mercado ni del bróker: fue creer que un fichero sabe lo que pasa fuera.

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.