1. Los cinco sitios por donde falla un sistema de trading
Nivel 11: Cuando falla
Un sistema de trading nunca se rompe por donde estabas mirando. Pasas semanas afinando la señal y el día que salta la alarma resulta que el disco estaba lleno o que el proveedor mandó un precio de cero. Este módulo es el mapa de los cinco sitios por donde falla de verdad un sistema en producción, ordenados por frecuencia real: de cada uno, cómo se detecta y cuál es la respuesta segura por defecto.
El inventario, ordenado por frecuencia real
En un sistema con un año de operativa diaria, el reparto de sustos se parece a esto:
- Los datos. Varias veces al mes. Con diferencia, la capa que más falla.
- La red y el bróker. Cada pocas semanas: una espera agotada, un rechazo, una sesión caducada.
- Tu propio código. Concentrado alrededor de cada despliegue, casi nunca entre ellos.
- La infraestructura. Un par de veces al año, y casi siempre de madrugada.
- El humano. La menos frecuente de las cinco y, con diferencia, la más cara.
Atravesando las cinco hay una única regla: ante la duda, no operar. Un sistema parado se pierde una oportunidad; uno que opera con información falsa pierde dinero y crea posiciones que luego hay que deshacer. La asimetría manda.
Capa 1: los datos
Fallan de cuatro maneras, y no están ordenadas por gravedad como esperarías:
- No llegan. El caso benigno: hay un hueco donde debería haber un número, y se ve.
- Llegan tarde. El peligroso. Nada falla: recibes un precio bien formado que fue verdad hace cuarenta minutos y tu código lo trata como el de ahora.
- Llegan incompletos. Faltan seis tickers de quinientos. La media del universo se calcula igual, con 494, y nadie se entera.
- Llegan absurdos. Un cero, un negativo, un precio sin ajustar por un desdoblamiento ya aplicado, un decimal desplazado.
Las dos primeras se detectan con un reloj; las dos últimas, con un recuento y una banda de cordura.
FRESCURA_MAXIMA_S = 90
BANDA_MAXIMA = 0.20
def barra_utilizable(barra, ultimo_cierre_bueno, ahora):
if barra is None or barra.get('close') is None:
return False, 'sin dato'
edad = ahora - barra['ts']
if edad > FRESCURA_MAXIMA_S:
return False, 'dato viejo: %d s' % edad
precio = barra['close']
if precio <= 0:
return False, 'precio no positivo'
if ultimo_cierre_bueno:
salto = abs(precio / ultimo_cierre_bueno - 1)
if salto > BANDA_MAXIMA:
return False, 'salto del %.1f por ciento' % (salto * 100)
return True, ''La función no corrige nada: no interpola, no arrastra el último precio bueno, no rellena el hueco. Solo dice sí o no, y por qué. El umbral tampoco es universal: en una acción grande un 20% intradía es sospechoso, en una cripto no. El número es un parámetro; la comprobación, no.
Capa 2: la red y el bróker
También cuatro formas, de mejor a peor. Caído: rechaza la conexión y lo sabes en un segundo. Rechazando órdenes: contesta que no, con un motivo explícito. Con la sesión caducada: devuelve un 401 y basta renovar. Y lento: la petición no falla, tarda catorce segundos, y cuando llega la respuesta tu decisión ya no vale.
Toda llamada lleva un tiempo máximo de espera explícito: una petición sin límite deja el proceso colgado, la forma más silenciosa de estar caído. Y se vigila la latencia, no solo el porcentaje de errores: un bróker que pasa de 200 milisegundos a seis segundos está roto aunque sus respuestas sean correctas. Cuando cualquiera de las dos se sale de rango, se corta la emisión de órdenes hasta que un ciclo completo salga sano.
Capa 3: tu código, y capa 4: la máquina donde vive
El fallo de código que importa no es el que revienta al arrancar; ese es un regalo, porque no llegas a operar. El que importa es la excepción no capturada a mitad de ejecución: has enviado tres órdenes de las ocho de la rotación y el proceso muere. Eso no es un error, es media cartera.
La infraestructura falla en tres sabores: se llena el disco, a menudo por culpa de los registros que escribes; se agota la memoria y el sistema operativo mata el proceso; o la máquina se reinicia sola. Ninguno avisa con una excepción de Python.
import shutil
MARGEN_DISCO_MB = 500
def preparado_para_operar(ruta_datos):
libre_mb = shutil.disk_usage(ruta_datos).free / 1e6
if libre_mb < MARGEN_DISCO_MB:
return False, 'disco: solo %.0f MB libres' % libre_mb
return True, ''
def ciclo_protegido(motor, freno):
try:
motor.ejecutar_ciclo()
except Exception as e:
freno.activar('excepcion no prevista: %r' % e)
raiseLa comprobación de disco se ejecuta antes de cada ciclo, no una sola vez al arrancar. Y el envoltorio hace lo único razonable ante una excepción imprevista: activa el freno, deja constancia y vuelve a lanzarla. Lo que no hace es tragársela y seguir, porque un sistema que continúa tras un error que no entiende opera con un estado que ya no puede justificar.
Capa 5: el humano que toca algo mientras el sistema opera
Alguien cierra una posición desde el móvil porque la veía perder. Alguien edita la configuración a las once y media. Alguien reinicia el proceso para que coja el cambio. Ninguna de las tres es mala en sí; lo malo es que el sistema no se entera y sigue creyendo un estado que ya no existe. La detección es la reconciliación contra el bróker, y la respuesta es una bandera de mantenimiento explícita: antes de tocar nada a mano se para el sistema y se deja escrito que está parado. Así, ante una divergencia, el sistema distingue entre no cuadra por un fallo y no cuadra porque alguien avisó.
Caso real: Knight Capital, 1 de agosto de 2012
Knight Capital Group, uno de los mayores creadores de mercado de Estados Unidos, desplegó código nuevo en sus servidores de enrutamiento. Quedó activo un componente antiguo que llevaba años sin usarse y, en la apertura, empezó a mandar órdenes al mercado en bucle. En menos de una hora, unos 45 minutos, la pérdida rondó los 440 millones de dólares, más que el beneficio de la compañía del año anterior. Knight fue absorbida pocos meses después.
Es el ejemplo canónico porque falló en varias capas a la vez: código viejo que nadie limpió, un despliegue que no llegó a todas las máquinas y avisos automáticos que nadie tenía obligación de escalar. Y el sistema no tenía ningún límite propio capaz de decir esto ya es demasiado. De eso va el módulo siguiente.