6. Errores, reintentos y reconexión en producción
Nivel 13: Conectar con Brokers Reales
Un sistema de trading en vivo pasa la mayor parte de su vida gestionando cosas que no salen como esperaba. La calidad de un sistema no se mide por lo que hace cuando todo va bien, sino por cómo se comporta cuando falla la red.
Los códigos de respuesta y qué hacer con cada uno
No todos los errores se tratan igual, y esta es la clasificación que necesitas:
- 400 (petición mal formada) y 401/403 (autenticación o permisos): el problema está en tu código o en tus credenciales. Reintentar no sirve de nada: fallará exactamente igual. Hay que registrar el error, alertar y parar esa operación.
- 429 (demasiadas peticiones): has superado el límite de peticiones. Aquí sí se reintenta, pero esperando, con retroceso exponencial y jitter: cada intento espera el doble que el anterior más un componente aleatorio. El componente aleatorio evita que varios procesos que fallaron a la vez vuelvan a golpear el servidor sincronizados.
- 500, 502, 503 y timeouts de red: el problema es del otro lado o del camino. Son reintentables, con cuidado.
import random
def peticion_con_reintentos(func, intentos=5):
for i in range(intentos):
try:
r = func()
if r.status_code == 429 or r.status_code >= 500:
espera = (2 ** i) + random.random()
time.sleep(espera)
continue
return r
except requests.exceptions.RequestException:
espera = (2 ** i) + random.random()
time.sleep(espera)
raise RuntimeError('Peticion fallida tras agotar los reintentos')El caso peligroso: el timeout al enviar una orden
Aquí está la trampa más seria de todo el nivel. Si envías una orden y la petición expira por timeout, no sabes qué ha pasado. Puede que la orden nunca llegara al exchange. Pero también puede que llegara, se aceptara perfectamente, y lo que se perdiera fuera solo la respuesta de vuelta. Los dos escenarios son indistinguibles desde tu lado.
Reintentar a ciegas en esa situación puede duplicar tu posición. La respuesta correcta tiene dos partes:
1. Consultar antes de reintentar. Ante un timeout en un envío de orden, no reenvíes: pregunta. Consulta GET /orders y las ejecuciones recientes para averiguar si la orden existe. Solo si confirmas que no llegó, reenvía.
2. Usar un identificador de cliente. La mayoría de APIs permiten adjuntar un client_oid: un identificador único que tú generas para esa orden concreta. Si reenvías con el mismo identificador, el exchange reconoce que es la misma orden y no la duplica. Eso convierte el envío en una operación idempotente: repetirla no cambia el resultado.
La regla general: las operaciones de lectura son seguras de reintentar; las de escritura solo lo son si son idempotentes.
Reconexión del canal de datos
Una conexión WebSocket se cae. Siempre. Un cliente serio implementa:
- Reconexión automática con retroceso exponencial, no en bucle cerrado.
- Resuscripción a todos los canales tras reconectar: la nueva conexión no recuerda tus suscripciones anteriores.
- Resincronización del estado: mientras estabas desconectado ocurrieron cosas. Hay que pedir por REST una foto completa del libro y de tus posiciones, y descartar el estado que tenías en memoria.
- Heartbeat: si no llega ningún mensaje durante N segundos, la conexión se considera muerta aunque el socket siga técnicamente abierto. Este caso, el de la conexión zombi, es más frecuente y más traicionero que la desconexión limpia.
Y la pregunta que cierra el módulo: si pierdes la conexión con una posición abierta, ¿qué debe hacer el sistema? La respuesta razonable es dejar de generar señales nuevas, intentar reconectar, y al recuperar la conexión resincronizar el estado real antes de tomar ninguna decisión. Operar a ciegas con datos obsoletos es peor que no operar.