37. Integración de datos de mercado mediante APIs

Un monitor deja de ser un prototipo cuando recibe información externa. El desafío no es solamente llamar a fetch(): hay que interpretar el contrato, validar respuestas, unificar formatos, controlar la vigencia y degradar el servicio sin presentar datos dudosos como actuales.

37.1 Del dato local a una integración confiable

En el tema anterior trabajamos con cotizaciones ficticias incorporadas al programa. Ahora diseñaremos la frontera que permite reemplazarlas por datos de un proveedor sin acoplar cálculos y componentes visuales a su formato particular.

La integración debe responder cinco preguntas: qué se pidió, qué contestó el servidor, si la respuesta cumple el contrato, cuándo representa al mercado y qué debe mostrar la aplicación cuando algo falla.

Objetivo: convertir una respuesta externa no confiable en un dato interno validado, trazable y con un estado explícito de calidad.

37.2 Contrato y responsabilidades

Proveedor

Define autenticación, símbolos, campos, frecuencia, límites y nivel de servicio.

Adaptador

Traduce el formato externo al modelo estable de la aplicación.

Dominio

Valida invariantes y calcula mid, spread, variación y vigencia.

Interfaz

Expone carga, éxito, ausencia, vencimiento y error sin ambigüedad.

El contrato técnico incluye URL, método, parámetros, encabezados, esquema y códigos de estado. El contrato de datos añade zona horaria, unidad, moneda, política de ajustes y significado de cada marca temporal.

37.3 El recorrido completo de una cotización

SOLICITUDSímbolo y alcance
TRANSPORTEHTTP y tiempo límite
DECODIFICARJSON sin asumir
VALIDARTipos e invariantes
NORMALIZARModelo interno
PUBLICAREstado y trazabilidad

Cada etapa puede fallar de manera distinta. Una desconexión no equivale a HTTP 429; un JSON malformado no equivale a un precio vencido. Conservar esa distinción mejora reintentos, alertas y diagnóstico.

37.4 Solicitudes asíncronas con Fetch

fetch() devuelve una promesa. Que la promesa se resuelva significa que existe una respuesta, no necesariamente que la operación HTTP haya sido exitosa; por eso se controla el estado antes de interpretar el cuerpo.

function leerRespuestaSimulada(response) {
  if (!response.ok) {
    throw new Error(`HTTP ${response.status}`);
  }
  return response.body;
}

const data = leerRespuestaSimulada({
  ok: true,
  status: 200,
  body: { symbol: "ALFA", bid: 99.8, ask: 100.2 }
});
console.log(data);

En producción, el cuerpo se obtiene normalmente con await response.json(). Conviene envolver detalles de transporte en una función para no repetirlos en cada pantalla.

37.5 Tiempo límite y cancelación

Esperar indefinidamente deja la interfaz en un estado engañoso. Un AbortController permite cancelar una solicitud por decisión del usuario, cambio de pantalla o vencimiento de un plazo propio.

function crearLimite(ms) {
  const controller = new AbortController();
  const timer = setTimeout(() => controller.abort("Tiempo agotado"), ms);
  return {
    signal: controller.signal,
    finalizar: () => clearTimeout(timer)
  };
}

const limite = crearLimite(1500);
console.log("Señal abortada al inicio:", limite.signal.aborted);
limite.finalizar();

Cancelar evita trabajo inútil, pero no garantiza que el servidor haya detenido un proceso que ya comenzó. La aplicación debe tratar la cancelación como un resultado propio.

37.6 Normalizar antes de calcular

Dos proveedores pueden llamar ticker o symbol al mismo concepto, expresar el tiempo en segundos o milisegundos y usar números como texto. El adaptador absorbe esas diferencias y entrega una forma canónica.

function normalizarCotizacion(raw, provider) {
  return Object.freeze({
    symbol: String(raw.ticker).trim().toUpperCase(),
    bid: Number(raw.bestBid),
    ask: Number(raw.bestAsk),
    currency: String(raw.currency).toUpperCase(),
    marketTime: new Date(raw.timestamp).toISOString(),
    receivedAt: new Date(raw.receivedAt).toISOString(),
    source: provider
  });
}

console.log(normalizarCotizacion({
  ticker: "alfa", bestBid: "99.80", bestAsk: "100.20",
  currency: "ars", timestamp: "2026-09-13T14:30:00Z",
  receivedAt: "2026-09-13T14:30:02Z"
}, "PROVEEDOR-DEMO"));

Nunca debe usarse Number() como validación suficiente: también produce NaN o conversiones inesperadas. La normalización y la validación son pasos relacionados, pero distintos.

37.7 Validación estructural y financiera

La estructura verifica presencia y tipo. Las reglas financieras verifican coherencia: símbolo no vacío, precios finitos y positivos, bid ≤ ask, moneda reconocida y marcas temporales válidas.

function validarCotizacion(q) {
  const errores = [];
  if (!q || typeof q.symbol !== "string" || !q.symbol.trim()) errores.push("symbol");
  if (!Number.isFinite(q.bid) || q.bid <= 0) errores.push("bid");
  if (!Number.isFinite(q.ask) || q.ask <= 0) errores.push("ask");
  if (Number.isFinite(q.bid) && Number.isFinite(q.ask) && q.bid > q.ask) errores.push("cruce");
  if (Number.isNaN(Date.parse(q.marketTime))) errores.push("marketTime");
  return { valid: errores.length === 0, errors: errores };
}

console.log(validarCotizacion({
  symbol: "ALFA", bid: 101, ask: 100,
  marketTime: "2026-09-13T14:30:00Z"
}));

Una respuesta inválida no debería reemplazar el último dato válido. Puede registrarse para diagnóstico, pero no promoverse al estado visible como si fuera una cotización aceptada.

37.8 Tres relojes diferentes

La marca de mercado indica cuándo ocurrió el dato; la recepción, cuándo llegó al sistema; y la visualización, cuándo se consultó. Confundirlas oculta retrasos.

edad de mercado = ahora − marketTime  ·  latencia observada = receivedAt − marketTimeAmbas magnitudes requieren instantes comparables y una política explícita para resultados negativos.

Un precio puede llegar recién recibido y aun así representar un mercado antiguo. La etiqueta “actualizado ahora” describe transporte, no necesariamente actualidad económica.

37.9 Reintentos, espera y límites

Un reintento tiene sentido ante fallas transitorias y operaciones seguras de repetir. Debe respetar indicaciones del servidor, limitar intentos e introducir espera creciente con una pequeña aleatoriedad para evitar que muchos clientes vuelvan simultáneamente.

function demoraReintento(intento, base = 500, maximo = 8000) {
  const exponencial = Math.min(maximo, base * 2 ** intento);
  const jitterDeterminista = intento * 37 % 101;
  return exponencial + jitterDeterminista;
}

console.log([0, 1, 2, 3, 4].map(i => demoraReintento(i)));

No todo error se reintenta: credenciales inválidas, parámetros incorrectos o un esquema incompatible requieren corrección, no más tráfico. Ante 429 o 503, se considera además Retry-After.

37.10 Caché con vigencia y última copia válida

Una caché reduce consumo, latencia y exposición a límites. Cada entrada necesita valor, momento de almacenamiento y duración admitida. Si vence, no desaparece: puede conservarse como último dato conocido con una advertencia visible.

function consultarCache(entry, now, ttlMs) {
  if (!entry) return { state: "MISS", value: null };
  const ageMs = Math.max(0, now - entry.savedAt);
  return {
    state: ageMs <= ttlMs ? "FRESH" : "STALE",
    value: entry.value,
    ageMs
  };
}

const cache = { value: { symbol: "ALFA", bid: 99.8 }, savedAt: 1000 };
console.log(consultarCache(cache, 4200, 5000));
console.log(consultarCache(cache, 7200, 5000));

La política stale-while-revalidate muestra temporalmente la última copia válida mientras intenta refrescarla. Debe distinguir visualmente “vigente” de “disponible pero vencida”.

37.11 Concurrencia y respuestas fuera de orden

Si el usuario cambia rápidamente de símbolo, una solicitud anterior puede terminar después de la nueva. Sin control, la respuesta tardía sobrescribe el dato correcto.

Dos defensas habituales son cancelar la solicitud previa o asociar un número incremental a cada petición y publicar únicamente la respuesta cuyo número coincide con el último solicitado.

let ultimaSolicitud = 0;

function iniciarSolicitud(symbol) {
  const requestId = ++ultimaSolicitud;
  return { requestId, symbol };
}

function puedePublicarse(requestId) {
  return requestId === ultimaSolicitud;
}

const primera = iniciarSolicitud("ALFA");
const segunda = iniciarSolicitud("BONO27");
console.log(puedePublicarse(primera.requestId), puedePublicarse(segunda.requestId));

37.12 Estados de interfaz

EstadoQué significaRespuesta visual
IdleAún no se solicitó información.Instrucción breve, sin números inventados.
LoadingHay una solicitud vigente.Conservar contexto y anunciar la carga.
SuccessDato válido y dentro de vigencia.Cotización y trazabilidad.
StaleExiste último dato válido, pero venció.Mostrarlo con edad y advertencia.
EmptyRespuesta válida sin registros.Explicar ausencia, no presentarla como error.
ErrorNo se obtuvo un dato publicable.Causa útil y acción posible.

El estado es parte del dato mostrado. Ocultar la carga o conservar silenciosamente cifras antiguas crea una precisión que el sistema no posee.

37.13 Laboratorio de ingesta

Este simulador no realiza conexiones externas. Ejecuta el mismo flujo conceptual con respuestas controladas para comparar resultados y observar cómo se preserva la última cotización válida.

Pipeline de cotizaciones · entorno simulado

Elegí un escenario y ejecutá la carga. La demora está acotada para facilitar la experimentación.

EstadoSin iniciar
Intentos0
Origen visibleSin datos
VigenciaNo evaluada
1 · Solicitar
2 · Decodificar
3 · Validar
4 · Normalizar
5 · Publicar

Último dato publicable

Símbolo—
Moneda—
Bid—
Ask—
Mid—
Edad—

Registro técnico

Esperando una solicitud…

Seleccioná un escenario para iniciar el flujo.

37.14 Seguridad, credenciales y CORS

Una clave incluida en HTML o JavaScript enviado al navegador deja de ser secreta. Para proveedores que requieren credenciales privadas, el navegador consulta un backend propio; ese backend autentica, limita, registra y llama al proveedor.

FronteraResponsabilidadNo debe hacer
NavegadorSolicitar datos permitidos y presentar estados.Contener secretos permanentes.
BackendGuardar credenciales, autorizar, cachear y limitar.Reenviar ciegamente cualquier parámetro.
ProveedorEntregar el servicio contratado.Definir por sí solo el modelo interno.

CORS no es autenticación: es una política del navegador sobre lectura entre orígenes. Aunque un origen esté permitido, la aplicación todavía necesita autorización, validación y control de abuso.

37.15 Polling, SSE y WebSocket

La frecuencia necesaria depende del caso de uso y de la licencia del dato. Actualizar un tablero educativo cada minuto no requiere la misma infraestructura que alimentar una pantalla de negociación.

MecanismoFlujoUso típicoCuidado principal
PollingCliente solicita periódicamente.Pocas series y baja frecuencia.Solapamiento, caché y límites.
SSEServidor envía eventos al cliente.Actualización unidireccional.Reconexión e identificador de evento.
WebSocketCanal bidireccional persistente.Suscripciones y mensajes frecuentes.Heartbeat, reanudación y presión de flujo.

Tiempo real no significa “sin latencia”. Deben medirse demora, pérdida, orden, duplicados y capacidad de recuperarse después de una desconexión.

37.16 Observabilidad y trazabilidad

Un registro útil evita datos sensibles y conserva contexto: identificador de solicitud, proveedor, ruta lógica, símbolo, código HTTP, duración, intento, resultado de validación y decisión de caché.

function eventoIntegracion({ requestId, symbol, status, durationMs, cache }) {
  return {
    event: "market_data_request",
    requestId,
    symbol,
    status,
    durationMs,
    cache,
    observedAt: "2026-09-13T14:30:05Z"
  };
}

console.log(eventoIntegracion({
  requestId: "req-1042", symbol: "ALFA",
  status: 200, durationMs: 184, cache: "MISS"
}));

Las métricas agregadas —latencia por percentiles, tasa de error, respuestas inválidas, uso de caché y edad del dato— permiten detectar degradaciones que una sola solicitud no revela.

37.17 Pruebas deterministas del adaptador

Las pruebas no deberían depender siempre de internet. Respuestas simuladas permiten cubrir éxito, vacío, campos faltantes, números inválidos, cruce de puntas, demora, 429, 500 y cancelación.

Los relojes también se inyectan: en lugar de invocar Date.now() dentro de toda función, se recibe now. Así, vigencia y latencia pueden probarse sin esperar tiempo real.

function esVigente(marketTimeMs, nowMs, maxAgeMs) {
  const age = Math.max(0, nowMs - marketTimeMs);
  return { fresh: age <= maxAgeMs, age };
}

const result = esVigente(10_000, 16_500, 7_000);
if (!result.fresh || result.age !== 6500) throw new Error("Prueba fallida");
console.log("Prueba de vigencia superada", result);

37.18 Criterio de finalización

Una integración está lista cuando el camino feliz funciona y los caminos adversos son explícitos. Antes de usar datos reales, verificá contrato y licencia, secretos fuera del cliente, timeout, validación, normalización, vigencia, caché, límite de reintentos, concurrencia, estados accesibles, registros y pruebas.

La arquitectura lograda mantiene una frontera estable: si cambia el proveedor, se reemplaza el adaptador; si cambia la interfaz, permanecen las reglas; si falla la red, el usuario conoce la calidad de lo que ve.

Principio final: una aplicación financiera confiable no promete que la fuente nunca fallará; garantiza que una falla no será confundida con un precio válido y actual.