trading-platform/docs/02-definicion-modulos/OQI-003-trading-charts/especificaciones/ET-TRD-002-websocket.md
rckrdmrd a7cca885f0 feat: Major platform documentation and architecture updates
Changes include:
- Updated architecture documentation
- Enhanced module definitions (OQI-001 to OQI-008)
- ML integration documentation updates
- Trading strategies documentation
- Orchestration and inventory updates
- Docker configuration updates

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-07 05:33:35 -06:00

29 KiB

id title type status priority epic project version created_date updated_date
ET-TRD-002 Especificación Técnica - WebSocket Connections Technical Specification Done Alta OQI-003 trading-platform 1.0.0 2025-12-05 2026-01-04

ET-TRD-002: Especificación Técnica - WebSocket Connections

Version: 1.0.0 Fecha: 2025-12-05 Estado: Pendiente Épica: OQI-003 Requerimiento: RF-TRD-002


Resumen

Esta especificación detalla la implementación técnica del sistema de conexiones WebSocket bidireccionales para streaming de datos de mercado en tiempo real, incluyendo reconexión automática, gestión de suscripciones y sincronización de estado.


Arquitectura

┌─────────────────────────────────────────────────────────────────────────┐
│                         FRONTEND CLIENTS                                 │
│  ┌─────────────────┐  ┌─────────────────┐  ┌─────────────────┐         │
│  │   Client A      │  │   Client B      │  │   Client C      │         │
│  │   (Browser)     │  │   (Browser)     │  │   (Browser)     │         │
│  └────────┬────────┘  └────────┬────────┘  └────────┬────────┘         │
│           │ WSS                 │ WSS                │ WSS               │
└───────────┼─────────────────────┼────────────────────┼───────────────────┘
            │                     │                    │
            ▼                     ▼                    ▼
┌─────────────────────────────────────────────────────────────────────────┐
│                       BACKEND WS SERVER                                  │
│  ┌──────────────────────────────────────────────────────────────────┐   │
│  │                    WebSocket Server (ws)                         │   │
│  │  ┌─────────────────────────────────────────────────────────┐    │   │
│  │  │              Connection Manager                          │    │   │
│  │  │  - Client connections map                               │    │   │
│  │  │  - Subscription registry                                │    │   │
│  │  │  - Heartbeat monitor                                    │    │   │
│  │  └─────────────────────────────────────────────────────────┘    │   │
│  └──────────────────────────────────────────────────────────────────┘   │
│           │                     │                    │                  │
│           ▼                     ▼                    ▼                  │
│  ┌─────────────────┐  ┌─────────────────┐  ┌─────────────────┐        │
│  │  Subscription   │  │  Message        │  │  Reconnection   │        │
│  │  Manager        │  │  Broker         │  │  Handler        │        │
│  └─────────────────┘  └─────────────────┘  └─────────────────┘        │
└────────────────────────────────┬────────────────────────────────────────┘
                                 │
                                 │ WSS (Multiplexed)
                                 ▼
┌─────────────────────────────────────────────────────────────────────────┐
│                      BINANCE WEBSOCKET API                               │
│  ┌──────────────────────────────────────────────────────────────────┐   │
│  │  wss://stream.binance.com:9443/ws                                │   │
│  │  - <symbol>@kline_<interval>                                     │   │
│  │  - <symbol>@ticker                                               │   │
│  │  - <symbol>@depth                                                │   │
│  │  - <symbol>@trade                                                │   │
│  └──────────────────────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────────────────────┘

Componentes

1. WebSocket Server (websocket.server.ts)

Ubicación: apps/backend/src/modules/trading/websocket/websocket.server.ts

import { WebSocketServer, WebSocket } from 'ws';
import { Server as HTTPServer } from 'http';
import { IncomingMessage } from 'http';
import { parse } from 'url';
import jwt from 'jsonwebtoken';

export interface ClientConnection {
  id: string;
  ws: WebSocket;
  userId: string;
  subscriptions: Set<string>;
  lastHeartbeat: number;
  isAlive: boolean;
}

export interface WSMessage {
  type: 'subscribe' | 'unsubscribe' | 'ping' | 'pong';
  channel?: string;
  data?: any;
}

export class TradingWebSocketServer {
  private wss: WebSocketServer;
  private clients: Map<string, ClientConnection>;
  private subscriptions: Map<string, Set<string>>; // channel -> Set<clientId>
  private heartbeatInterval: NodeJS.Timeout;

  constructor(server: HTTPServer) {
    this.clients = new Map();
    this.subscriptions = new Map();

    this.wss = new WebSocketServer({
      server,
      path: '/ws/trading',
      verifyClient: this.verifyClient.bind(this),
    });

    this.initialize();
  }

  private async verifyClient(
    info: { origin: string; secure: boolean; req: IncomingMessage },
    callback: (verified: boolean, code?: number, message?: string) => void
  ): Promise<void> {
    try {
      const { query } = parse(info.req.url || '', true);
      const token = query.token as string;

      if (!token) {
        return callback(false, 401, 'Missing token');
      }

      // Verificar JWT
      const decoded = jwt.verify(token, process.env.JWT_SECRET) as any;

      if (!decoded.userId) {
        return callback(false, 401, 'Invalid token');
      }

      // Guardar userId en request para uso posterior
      (info.req as any).userId = decoded.userId;

      callback(true);
    } catch (error) {
      callback(false, 401, 'Authentication failed');
    }
  }

  private initialize(): void {
    this.wss.on('connection', this.handleConnection.bind(this));

    // Heartbeat cada 30 segundos
    this.heartbeatInterval = setInterval(() => {
      this.checkHeartbeats();
    }, 30000);

    console.log('Trading WebSocket Server initialized');
  }

  private handleConnection(ws: WebSocket, req: IncomingMessage): void {
    const clientId = this.generateClientId();
    const userId = (req as any).userId;

    const client: ClientConnection = {
      id: clientId,
      ws,
      userId,
      subscriptions: new Set(),
      lastHeartbeat: Date.now(),
      isAlive: true,
    };

    this.clients.set(clientId, client);

    console.log(`Client connected: ${clientId} (User: ${userId})`);

    // Configurar handlers
    ws.on('message', (data) => this.handleMessage(clientId, data));
    ws.on('close', () => this.handleDisconnection(clientId));
    ws.on('error', (error) => this.handleError(clientId, error));
    ws.on('pong', () => this.handlePong(clientId));

    // Enviar mensaje de bienvenida
    this.sendToClient(clientId, {
      type: 'connected',
      clientId,
      timestamp: Date.now(),
    });
  }

  private handleMessage(clientId: string, data: Buffer | string): void {
    try {
      const message: WSMessage = JSON.parse(data.toString());

      switch (message.type) {
        case 'subscribe':
          this.handleSubscribe(clientId, message.channel);
          break;

        case 'unsubscribe':
          this.handleUnsubscribe(clientId, message.channel);
          break;

        case 'ping':
          this.handlePing(clientId);
          break;

        default:
          console.warn(`Unknown message type: ${message.type}`);
      }
    } catch (error) {
      console.error('Error parsing message:', error);
      this.sendError(clientId, 'Invalid message format');
    }
  }

  private handleSubscribe(clientId: string, channel: string): void {
    const client = this.clients.get(clientId);
    if (!client) return;

    // Agregar a suscripciones del cliente
    client.subscriptions.add(channel);

    // Agregar a mapa global de suscripciones
    if (!this.subscriptions.has(channel)) {
      this.subscriptions.set(channel, new Set());
    }
    this.subscriptions.get(channel).add(clientId);

    console.log(`Client ${clientId} subscribed to ${channel}`);

    // Confirmar suscripción
    this.sendToClient(clientId, {
      type: 'subscribed',
      channel,
      timestamp: Date.now(),
    });
  }

  private handleUnsubscribe(clientId: string, channel: string): void {
    const client = this.clients.get(clientId);
    if (!client) return;

    // Remover de suscripciones del cliente
    client.subscriptions.delete(channel);

    // Remover de mapa global
    const subscribers = this.subscriptions.get(channel);
    if (subscribers) {
      subscribers.delete(clientId);
      if (subscribers.size === 0) {
        this.subscriptions.delete(channel);
      }
    }

    console.log(`Client ${clientId} unsubscribed from ${channel}`);

    this.sendToClient(clientId, {
      type: 'unsubscribed',
      channel,
      timestamp: Date.now(),
    });
  }

  private handlePing(clientId: string): void {
    const client = this.clients.get(clientId);
    if (!client) return;

    client.lastHeartbeat = Date.now();
    client.isAlive = true;

    this.sendToClient(clientId, {
      type: 'pong',
      timestamp: Date.now(),
    });
  }

  private handlePong(clientId: string): void {
    const client = this.clients.get(clientId);
    if (!client) return;

    client.lastHeartbeat = Date.now();
    client.isAlive = true;
  }

  private handleDisconnection(clientId: string): void {
    const client = this.clients.get(clientId);
    if (!client) return;

    // Limpiar todas las suscripciones
    client.subscriptions.forEach((channel) => {
      const subscribers = this.subscriptions.get(channel);
      if (subscribers) {
        subscribers.delete(clientId);
        if (subscribers.size === 0) {
          this.subscriptions.delete(channel);
        }
      }
    });

    this.clients.delete(clientId);
    console.log(`Client disconnected: ${clientId}`);
  }

  private handleError(clientId: string, error: Error): void {
    console.error(`WebSocket error for client ${clientId}:`, error);
  }

  private checkHeartbeats(): void {
    const now = Date.now();
    const timeout = 60000; // 60 segundos

    this.clients.forEach((client, clientId) => {
      if (now - client.lastHeartbeat > timeout) {
        console.log(`Client ${clientId} timeout, terminating connection`);
        client.ws.terminate();
        this.handleDisconnection(clientId);
      } else if (client.ws.readyState === WebSocket.OPEN) {
        client.isAlive = false;
        client.ws.ping();
      }
    });
  }

  // Broadcast a canal específico
  public broadcastToChannel(channel: string, data: any): void {
    const subscribers = this.subscriptions.get(channel);
    if (!subscribers || subscribers.size === 0) return;

    const message = JSON.stringify({
      type: 'data',
      channel,
      data,
      timestamp: Date.now(),
    });

    subscribers.forEach((clientId) => {
      const client = this.clients.get(clientId);
      if (client && client.ws.readyState === WebSocket.OPEN) {
        client.ws.send(message);
      }
    });
  }

  // Enviar a cliente específico
  private sendToClient(clientId: string, data: any): void {
    const client = this.clients.get(clientId);
    if (client && client.ws.readyState === WebSocket.OPEN) {
      client.ws.send(JSON.stringify(data));
    }
  }

  private sendError(clientId: string, message: string): void {
    this.sendToClient(clientId, {
      type: 'error',
      message,
      timestamp: Date.now(),
    });
  }

  private generateClientId(): string {
    return `client_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
  }

  // Obtener estadísticas
  public getStats() {
    return {
      totalClients: this.clients.size,
      totalChannels: this.subscriptions.size,
      channels: Array.from(this.subscriptions.entries()).map(([channel, subs]) => ({
        channel,
        subscribers: subs.size,
      })),
    };
  }

  public shutdown(): void {
    clearInterval(this.heartbeatInterval);

    this.clients.forEach((client) => {
      client.ws.close(1000, 'Server shutting down');
    });

    this.wss.close();
    console.log('WebSocket Server shutdown complete');
  }
}

2. Binance WebSocket Client (binance-ws.client.ts)

Ubicación: apps/backend/src/modules/trading/websocket/binance-ws.client.ts

import WebSocket from 'ws';
import { EventEmitter } from 'events';

export interface BinanceKlineStream {
  e: 'kline';
  E: number; // Event time
  s: string; // Symbol
  k: {
    t: number; // Kline start time
    T: number; // Kline close time
    s: string; // Symbol
    i: string; // Interval
    f: number; // First trade ID
    L: number; // Last trade ID
    o: string; // Open price
    c: string; // Close price
    h: string; // High price
    l: string; // Low price
    v: string; // Base asset volume
    n: number; // Number of trades
    x: boolean; // Is kline closed
    q: string; // Quote asset volume
    V: string; // Taker buy base asset volume
    Q: string; // Taker buy quote asset volume
  };
}

export interface BinanceTickerStream {
  e: '24hrTicker';
  E: number; // Event time
  s: string; // Symbol
  p: string; // Price change
  P: string; // Price change percent
  w: string; // Weighted average price
  c: string; // Last price
  Q: string; // Last quantity
  o: string; // Open price
  h: string; // High price
  l: string; // Low price
  v: string; // Total traded base asset volume
  q: string; // Total traded quote asset volume
}

export class BinanceWebSocketClient extends EventEmitter {
  private ws: WebSocket | null = null;
  private streams: Set<string>;
  private reconnectAttempts: number = 0;
  private maxReconnectAttempts: number = 10;
  private reconnectDelay: number = 1000;
  private pingInterval: NodeJS.Timeout | null = null;
  private baseUrl: string = 'wss://stream.binance.com:9443';

  constructor() {
    super();
    this.streams = new Set();
  }

  public connect(): void {
    if (this.ws && this.ws.readyState === WebSocket.OPEN) {
      console.log('WebSocket already connected');
      return;
    }

    const url = `${this.baseUrl}/ws`;
    this.ws = new WebSocket(url);

    this.ws.on('open', this.handleOpen.bind(this));
    this.ws.on('message', this.handleMessage.bind(this));
    this.ws.on('close', this.handleClose.bind(this));
    this.ws.on('error', this.handleError.bind(this));
    this.ws.on('pong', this.handlePong.bind(this));
  }

  private handleOpen(): void {
    console.log('Connected to Binance WebSocket');
    this.reconnectAttempts = 0;

    // Resubscribir a streams previos
    if (this.streams.size > 0) {
      this.subscribeMultiple(Array.from(this.streams));
    }

    // Iniciar ping cada 3 minutos (Binance timeout es 10min)
    this.pingInterval = setInterval(() => {
      if (this.ws && this.ws.readyState === WebSocket.OPEN) {
        this.ws.ping();
      }
    }, 180000);

    this.emit('connected');
  }

  private handleMessage(data: Buffer): void {
    try {
      const message = JSON.parse(data.toString());

      // Respuesta a subscribe/unsubscribe
      if (message.result === null && message.id) {
        console.log(`Stream operation successful: ${message.id}`);
        return;
      }

      // Datos del stream
      if (message.stream && message.data) {
        this.emit('stream', message.stream, message.data);

        // Emitir eventos específicos por tipo
        if (message.data.e === 'kline') {
          this.emit('kline', message.stream, message.data);
        } else if (message.data.e === '24hrTicker') {
          this.emit('ticker', message.stream, message.data);
        }
      }
    } catch (error) {
      console.error('Error parsing message:', error);
    }
  }

  private handleClose(code: number, reason: string): void {
    console.log(`WebSocket closed: ${code} - ${reason}`);

    if (this.pingInterval) {
      clearInterval(this.pingInterval);
      this.pingInterval = null;
    }

    this.emit('disconnected', code, reason);

    // Intentar reconexión
    if (this.reconnectAttempts < this.maxReconnectAttempts) {
      const delay = this.reconnectDelay * Math.pow(2, this.reconnectAttempts);
      console.log(`Reconnecting in ${delay}ms (attempt ${this.reconnectAttempts + 1})`);

      setTimeout(() => {
        this.reconnectAttempts++;
        this.connect();
      }, delay);
    } else {
      console.error('Max reconnection attempts reached');
      this.emit('reconnect-failed');
    }
  }

  private handleError(error: Error): void {
    console.error('WebSocket error:', error);
    this.emit('error', error);
  }

  private handlePong(): void {
    // console.log('Received pong from Binance');
  }

  // Suscribirse a stream
  public subscribe(stream: string): void {
    if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
      console.warn('WebSocket not connected, queuing subscription');
      this.streams.add(stream);
      return;
    }

    const id = Date.now();
    const message = {
      method: 'SUBSCRIBE',
      params: [stream],
      id,
    };

    this.ws.send(JSON.stringify(message));
    this.streams.add(stream);

    console.log(`Subscribed to ${stream}`);
  }

  // Suscribirse a múltiples streams
  public subscribeMultiple(streams: string[]): void {
    if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
      console.warn('WebSocket not connected, queuing subscriptions');
      streams.forEach((s) => this.streams.add(s));
      return;
    }

    const id = Date.now();
    const message = {
      method: 'SUBSCRIBE',
      params: streams,
      id,
    };

    this.ws.send(JSON.stringify(message));
    streams.forEach((s) => this.streams.add(s));

    console.log(`Subscribed to ${streams.length} streams`);
  }

  // Desuscribirse
  public unsubscribe(stream: string): void {
    if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
      this.streams.delete(stream);
      return;
    }

    const id = Date.now();
    const message = {
      method: 'UNSUBSCRIBE',
      params: [stream],
      id,
    };

    this.ws.send(JSON.stringify(message));
    this.streams.delete(stream);

    console.log(`Unsubscribed from ${stream}`);
  }

  public disconnect(): void {
    if (this.pingInterval) {
      clearInterval(this.pingInterval);
      this.pingInterval = null;
    }

    if (this.ws) {
      this.ws.close(1000, 'Client disconnect');
      this.ws = null;
    }

    this.streams.clear();
  }

  public isConnected(): boolean {
    return this.ws !== null && this.ws.readyState === WebSocket.OPEN;
  }

  public getActiveStreams(): string[] {
    return Array.from(this.streams);
  }
}

3. Stream Manager (stream-manager.ts)

Ubicación: apps/backend/src/modules/trading/websocket/stream-manager.ts

import { BinanceWebSocketClient } from './binance-ws.client';
import { TradingWebSocketServer } from './websocket.server';

export class StreamManager {
  private binanceClient: BinanceWebSocketClient;
  private wsServer: TradingWebSocketServer;
  private channelToStream: Map<string, string>; // internal channel -> binance stream

  constructor(wsServer: TradingWebSocketServer) {
    this.wsServer = wsServer;
    this.binanceClient = new BinanceWebSocketClient();
    this.channelToStream = new Map();

    this.initialize();
  }

  private initialize(): void {
    this.binanceClient.connect();

    // Forward kline data
    this.binanceClient.on('kline', (stream: string, data: any) => {
      const channel = this.streamToChannel(stream);
      this.wsServer.broadcastToChannel(channel, {
        type: 'kline',
        data: this.formatKline(data),
      });
    });

    // Forward ticker data
    this.binanceClient.on('ticker', (stream: string, data: any) => {
      const channel = this.streamToChannel(stream);
      this.wsServer.broadcastToChannel(channel, {
        type: 'ticker',
        data: this.formatTicker(data),
      });
    });

    // Handle reconnection
    this.binanceClient.on('connected', () => {
      console.log('Binance WebSocket reconnected, resubscribing...');
    });

    this.binanceClient.on('error', (error) => {
      console.error('Binance WebSocket error:', error);
    });
  }

  // Convertir canal interno a stream de Binance
  private channelToStream(channel: string): string {
    // channel format: "kline:BTCUSDT:1m" -> "btcusdt@kline_1m"
    // channel format: "ticker:BTCUSDT" -> "btcusdt@ticker"

    const parts = channel.split(':');
    const type = parts[0];
    const symbol = parts[1]?.toLowerCase();

    if (type === 'kline') {
      const interval = parts[2];
      return `${symbol}@kline_${interval}`;
    } else if (type === 'ticker') {
      return `${symbol}@ticker`;
    }

    return '';
  }

  // Convertir stream de Binance a canal interno
  private streamToChannel(stream: string): string {
    // "btcusdt@kline_1m" -> "kline:BTCUSDT:1m"
    // "btcusdt@ticker" -> "ticker:BTCUSDT"

    const cached = Array.from(this.channelToStream.entries())
      .find(([_, s]) => s === stream);

    if (cached) {
      return cached[0];
    }

    // Fallback parsing
    const [symbol, streamType] = stream.split('@');
    const upperSymbol = symbol.toUpperCase();

    if (streamType.startsWith('kline_')) {
      const interval = streamType.replace('kline_', '');
      return `kline:${upperSymbol}:${interval}`;
    } else if (streamType === 'ticker') {
      return `ticker:${upperSymbol}`;
    }

    return stream;
  }

  // Subscribe to channel
  public subscribeToChannel(channel: string): void {
    const binanceStream = this.channelToStream(channel);

    if (!binanceStream) {
      console.error(`Invalid channel format: ${channel}`);
      return;
    }

    this.channelToStream.set(channel, binanceStream);
    this.binanceClient.subscribe(binanceStream);
  }

  // Unsubscribe from channel
  public unsubscribeFromChannel(channel: string): void {
    const binanceStream = this.channelToStream.get(channel);

    if (!binanceStream) {
      return;
    }

    this.binanceClient.unsubscribe(binanceStream);
    this.channelToStream.delete(channel);
  }

  private formatKline(data: any) {
    return {
      symbol: data.s,
      interval: data.k.i,
      startTime: data.k.t,
      endTime: data.k.T,
      open: parseFloat(data.k.o),
      high: parseFloat(data.k.h),
      low: parseFloat(data.k.l),
      close: parseFloat(data.k.c),
      volume: parseFloat(data.k.v),
      closed: data.k.x,
      trades: data.k.n,
    };
  }

  private formatTicker(data: any) {
    return {
      symbol: data.s,
      price: parseFloat(data.c),
      priceChange: parseFloat(data.p),
      priceChangePercent: parseFloat(data.P),
      high: parseFloat(data.h),
      low: parseFloat(data.l),
      volume: parseFloat(data.v),
      quoteVolume: parseFloat(data.q),
    };
  }

  public shutdown(): void {
    this.binanceClient.disconnect();
  }
}

Componentes Frontend

WebSocket Client Hook

Ubicación: apps/frontend/src/modules/trading/hooks/useWebSocket.ts

import { useEffect, useRef, useCallback } from 'react';
import { useAuthStore } from '@/stores/auth.store';

export interface WSMessage {
  type: string;
  channel?: string;
  data?: any;
  timestamp?: number;
}

export function useWebSocket() {
  const wsRef = useRef<WebSocket | null>(null);
  const { token } = useAuthStore();
  const reconnectTimeoutRef = useRef<NodeJS.Timeout>();
  const reconnectAttempts = useRef(0);
  const maxReconnectAttempts = 5;

  const connect = useCallback(() => {
    if (!token) return;

    const wsUrl = `${process.env.REACT_APP_WS_URL}/ws/trading?token=${token}`;
    const ws = new WebSocket(wsUrl);

    ws.onopen = () => {
      console.log('WebSocket connected');
      reconnectAttempts.current = 0;
    };

    ws.onclose = (event) => {
      console.log('WebSocket disconnected:', event.code);

      // Intentar reconexión
      if (reconnectAttempts.current < maxReconnectAttempts) {
        const delay = Math.min(1000 * Math.pow(2, reconnectAttempts.current), 30000);
        console.log(`Reconnecting in ${delay}ms...`);

        reconnectTimeoutRef.current = setTimeout(() => {
          reconnectAttempts.current++;
          connect();
        }, delay);
      }
    };

    ws.onerror = (error) => {
      console.error('WebSocket error:', error);
    };

    wsRef.current = ws;
  }, [token]);

  const disconnect = useCallback(() => {
    if (reconnectTimeoutRef.current) {
      clearTimeout(reconnectTimeoutRef.current);
    }

    if (wsRef.current) {
      wsRef.current.close(1000, 'Client disconnect');
      wsRef.current = null;
    }
  }, []);

  const subscribe = useCallback((channel: string) => {
    if (wsRef.current?.readyState === WebSocket.OPEN) {
      wsRef.current.send(JSON.stringify({
        type: 'subscribe',
        channel,
      }));
    }
  }, []);

  const unsubscribe = useCallback((channel: string) => {
    if (wsRef.current?.readyState === WebSocket.OPEN) {
      wsRef.current.send(JSON.stringify({
        type: 'unsubscribe',
        channel,
      }));
    }
  }, []);

  const onMessage = useCallback((callback: (message: WSMessage) => void) => {
    if (wsRef.current) {
      wsRef.current.onmessage = (event) => {
        try {
          const message = JSON.parse(event.data);
          callback(message);
        } catch (error) {
          console.error('Error parsing WebSocket message:', error);
        }
      };
    }
  }, []);

  useEffect(() => {
    connect();
    return () => disconnect();
  }, [connect, disconnect]);

  return {
    subscribe,
    unsubscribe,
    onMessage,
    isConnected: wsRef.current?.readyState === WebSocket.OPEN,
  };
}

Configuración

// config/websocket.config.ts
export const websocketConfig = {
  server: {
    path: '/ws/trading',
    heartbeatInterval: 30000,
    clientTimeout: 60000,
  },

  binance: {
    baseUrl: 'wss://stream.binance.com:9443',
    pingInterval: 180000, // 3 minutes
    reconnect: {
      maxAttempts: 10,
      initialDelay: 1000,
      maxDelay: 30000,
    },
  },

  channels: {
    kline: 'kline:{symbol}:{interval}',
    ticker: 'ticker:{symbol}',
    depth: 'depth:{symbol}',
    trade: 'trade:{symbol}',
  },
};

Testing

describe('TradingWebSocketServer', () => {
  let server: TradingWebSocketServer;
  let httpServer: any;

  beforeEach(() => {
    httpServer = createServer();
    server = new TradingWebSocketServer(httpServer);
  });

  afterEach(() => {
    server.shutdown();
  });

  it('should accept authenticated connections', async () => {
    const token = generateTestToken();
    const ws = new WebSocket(`ws://localhost:3000/ws/trading?token=${token}`);

    await new Promise((resolve) => {
      ws.on('open', resolve);
    });

    expect(ws.readyState).toBe(WebSocket.OPEN);
  });

  it('should handle subscription', async () => {
    // Test implementation
  });
});

Referencias