Los paneles de trading se rompen de formas en las que una aplicación web normal no se rompe. Los datos llegan de forma continua, no bajo demanda; decenas de widgets dependen de recortes solapados del mismo estado, y un fotograma perdido durante un minuto volátil tiene un costo real para quien mira la pantalla. Este artículo recorre la arquitectura que usamos en TradeBot, un panel para operadores que ejecutan estrategias automatizadas en varios exchanges, y las decisiones que lo mantuvieron fluido.
Separe los tres tipos de estado
El primer error es meterlo todo en un único almacén. Una UI de trading tiene tres tipos de estado, con ciclos de vida distintos:
- Estado de servidor: lo que vive en una base de datos y cambia cuando se pide: ajustes de la cuenta, definiciones de estrategia, ejecuciones históricas, diseños guardados. Se obtiene, se guarda en caché y se invalida.
- Estado en streaming: lo que llega de forma continua por un socket: libros de órdenes, tickers, posiciones, órdenes abiertas, PnL. Se empuja, se normaliza y se fusiona.
- Estado de UI: lo que solo importa en el navegador: el mercado seleccionado, la pestaña activa, los modales abiertos, los anchos de columna.
Usamos TanStack Query para el estado de servidor y un almacén de Zustand para el estado en streaming y el de UI, y no dejamos que uno se filtre en el otro. El estado de servidor trae deduplicación de peticiones, refetch en segundo plano e invalidación de caché sin tener que construirlos. El estado en streaming tiene una forma plana y normalizada, y suscripciones selectivas. El estado de UI se queda local y barato.
La ventaja es que cada tipo de estado se puede optimizar por su cuenta. La configuración de la caché de Query no tiene nada que ver con el caudal del socket; el caudal del socket no tiene nada que ver con la frecuencia con la que se vuelve a renderizar un modal.
Normalice el flujo antes de que llegue a React
Los feeds de los exchanges llegan desordenados. Un venue envía snapshots completos del libro de órdenes cada segundo; otro envía deltas con números de secuencia; un tercero envía las posiciones con un esquema distinto al de sus ejecuciones. Si esa diversidad llega a los componentes, cada widget se convierte en una capa de traducción.
Nuestra regla: una sola capa de flujo, fuera de React, es dueña de las conexiones de socket y convierte cada mensaje en uno de un conjunto pequeño de eventos internos: book.snapshot, book.delta, ticker, position, order, fill. El almacén aplica esos eventos a una estructura normalizada, con clave por venue y por mercado:
type Store = {
books: Record<MarketId, OrderBook>;
tickers: Record<MarketId, Ticker>;
positions: Record<PositionId, Position>;
orders: Record<OrderId, Order>;
connection: Record<Venue, "live" | "reconnecting" | "stale">;
};
Los componentes no ven payloads propios de cada venue. Se suscriben a books[marketId] o a positions, y a nada más. Cuando se añade un venue, la capa de flujo gana un adaptador y el árbol de componentes no cambia.
Renderice solo lo que cambió
React aguanta con holgura miles de actualizaciones por segundo si cada una vuelve a renderizar un subárbol pequeño. No aguanta si cada actualización vuelve a renderizar el panel. Tres técnicas hacen la mayor parte del trabajo:
Suscripciones selectivas. Cada widget se suscribe, mediante un selector, al recorte más estrecho que necesita, y el almacén solo avisa a los suscriptores cuyo recorte cambió por referencia. Un widget de libro de órdenes de BTC/USDT no se vuelve a renderizar cuando hay un tick de ETH/USDT.
Agrupación y limitación en el límite. Los deltas del libro de órdenes pueden llegar más rápido de lo que refresca un monitor. La capa de flujo agrupa los deltas y los vuelca en el almacén como máximo una vez por fotograma de animación. No se pierde nada visible — el libro queda correcto tras cada volcado — y el trabajo de React baja un orden de magnitud en los picos.
Identidades estables para las filas. Las tablas de órdenes y de ejecuciones usan claves estables y componentes de fila memoizados, de modo que una ejecución nueva añade una fila, en lugar de reconciliar la lista entera. Las listas virtualizadas mantienen baratos los historiales largos.
Los gráficos siguen el mismo principio. A la biblioteca de gráficos le pasamos actualizaciones incrementales (update(lastBar)), en lugar de reemplazar la serie, y el estado del gráfico se queda por completo fuera de React, porque la biblioteca gestiona su propio canvas.
Diseñe para las desconexiones
Los sockets se caen. Los venues se reinician. Los portátiles entran en suspensión. Un panel que muestra en silencio la posición de ayer es peor que uno que no muestra nada. Tratamos el estado de la conexión como un dato de primera clase:
- Cada venue tiene un estado —
live,reconnecting,stale— derivado de los heartbeats y de la marca de tiempo del último mensaje, y cada widget que depende de ese venue muestra un indicador inequívoco cuando no está en vivo. - Las reconexiones usan backoff exponencial con jitter y vuelven a suscribirse solas. Tras reconectar, la capa de flujo pide un snapshot nuevo de cada libro suscrito y descarta los deltas cuyo número de secuencia es anterior al snapshot.
- Las consultas de estado de servidor que afectan a las decisiones (órdenes abiertas, posiciones) se vuelven a obtener tras una reconexión, porque el socket puede haber perdido eventos mientras estaba caído.
Es un trabajo poco vistoso, y marca la diferencia entre una demo y una herramienta a la que la gente confía su dinero.
Los controles del operador necesitan fricción
TradeBot no es una pantalla pasiva. Los operadores pausan y reanudan estrategias, ajustan parámetros y colocan órdenes manuales. Cualquiera de esas acciones puede perder dinero si es un clic erróneo, así que los controles son, a propósito, más lentos que los datos:
- Las acciones destructivas, o las que mueven dinero, exigen un paso de confirmación que vuelve a enunciar lo que va a pasar («¿Cancelar 12 órdenes abiertas en Binance BTC/USDT?»).
- Los cambios de parámetros se dejan preparados y se aplican de forma explícita, no con cada pulsación.
- Cada acción se escribe en un historial de acciones, con quién, qué, cuándo y la respuesta del servidor, visible en el propio panel.
Además, la actualización optimista se decide acción por acción. Cancelar una orden actualiza la UI de inmediato y se concilia con el servidor; colocar una orden espera el acuse, porque una orden que parece colocada y no lo está es un fallo peor que una breve espera.
Diseño y personalización
Los operadores observan cosas distintas. Entregamos diseños configurables — una cuadrícula de widgets por usuario, con tamaños y posiciones — y listas de seguimiento por usuario, persistidas como estado de servidor. Los diseños son documentos JSON validados con un esquema, de modo que un diseño antiguo sigue valiendo en una versión nueva del panel. El diseño por defecto es el que esperábamos que necesitara la mayoría; en la práctica, casi todo el mundo lo cambió en menos de un día, y de eso se trata.
Qué repetiríamos y qué cambiaríamos
Otra vez: la separación en tres estados, la capa de flujo normalizada y el estado de conexión por venue. Son la base que nos permitió añadir venues y widgets sin tocar el código existente.
Cambiar: al principio dejamos que los widgets se suscribieran al libro completo, en lugar de a los N primeros niveles. En un libro de 200 niveles eso era trabajo de más; la mayoría de los widgets solo renderiza 20 niveles. Exponga selectores limitados por profundidad desde el principio.
También cambiar: al principio invertimos de menos en una herramienta de replay. Poder grabar una sesión del flujo y reproducirla sobre la UI hace mucho más fáciles el trabajo de rendimiento y la reproducción de fallos. Constrúyala en el primer mes.
Resumen
Un panel de trading es un sistema de streaming al que se le acopla un frontend de React, no una aplicación de React que da la casualidad de usar WebSockets. Mantenga el estado en streaming fuera de los componentes, normalice a fondo, renderice de forma selectiva, trate el estado de la conexión como un dato y añada fricción a las acciones que mueven dinero. Haga eso y la elección de framework importará mucho menos de lo que se suele creer.