36. Proyecto integrador: monitor y comparador de instrumentos financieros

El proyecto reúne instrumentos, cotizaciones, costos, riesgo y posiciones en una superficie única. La meta no es elegir una inversión automáticamente, sino transformar datos heterogéneos en comparaciones explícitas, reproducibles y verificables.

36.1 Objetivo y alcance

Construiremos un monitor local que lista instrumentos, calcula punto medio, spread relativo, variación y antigüedad; permite buscar, filtrar y ordenar; compara dos alternativas; y resume una cartera en moneda base.

Los datos son ficticios y no se conectan a mercados. Esto permite concentrarse en arquitectura, validaciones y experiencia de uso sin confundir la demostración con precios o recomendaciones reales.

Resultado esperado: una pantalla explica qué dato observa, de dónde viene, cuándo fue actualizado y qué fórmula produjo cada indicador.

36.2 Requisitos funcionales

M

Monitorear

Mostrar puntas, variación, spread, edad y calidad.

F

Filtrar

Buscar por texto y limitar por tipo de instrumento.

C

Comparar

Usar porcentajes y convenciones comunes, sin ranking opaco.

P

Valuar

Integrar posiciones, efectivo y conversión de moneda.

También debe haber estados vacíos, advertencias por datos vencidos, validación de controles y diseño adaptable a pantallas pequeñas.

36.3 Arquitectura por capas

DATOSInstrumentos y precios
VALIDACIÓNEsquema y reglas
DOMINIOMétricas puras
ESTADOFiltros y selección
VISTATabla y tarjetas
CONTROLErrores y pruebas

Separar cálculo y presentación permite probar fórmulas sin navegador. La vista recibe valores ya validados y se limita a formatearlos y responder a eventos.

36.4 Modelo de datos integrado

Cada fila combina un instrumento relativamente estable con una cotización temporal. La cartera agrega cantidad, costo y tipo de cambio de valuación sin modificar el objeto original.

ObjetoCampos principalesRegla
InstrumentoID, símbolo, nombre, tipo y moneda.Identidad estable.
CotizaciónBid, ask, cierre previo, fuente y edad.Bid positivo y no mayor que ask.
SelecciónBúsqueda, tipo, orden y vigencia máxima.No altera los datos base.
PosiciónCantidad, costo promedio y conversión.Valuación en una fecha común.
const instrumento = Object.freeze({
  id: "INS-ALFA", symbol: "ALFA", name: "Acción Alfa",
  type: "EQUITY", currency: "ARS"
});

const cotizacion = Object.freeze({
  instrumentId: "INS-ALFA", bid: 99.80, ask: 100.20,
  previousClose: 98.50, source: "FUENTE-DEMO", ageSeconds: 4
});

console.log({ instrumento, cotizacion });

36.5 Métricas comparables

Los precios absolutos de instrumentos distintos no indican cuál es “más barato”. El monitor usa variación porcentual y spread relativo para poner cambios y costos de inmediatez sobre bases comparables.

mid = (bid + ask) / 2  ·  variación = (mid / cierre previo − 1) × 100  ·  spread % = (ask − bid) / mid × 100Cierre previo, puntas y moneda deben pertenecer a la misma serie comparable.
function metricas(quote) {
  const values = [quote.bid, quote.ask, quote.previousClose];
  if (!values.every(Number.isFinite) || quote.bid <= 0 || quote.ask < quote.bid || quote.previousClose <= 0) {
    throw new RangeError("Cotización no válida");
  }
  const mid = (quote.bid + quote.ask) / 2;
  return {
    mid,
    changePercent: (mid / quote.previousClose - 1) * 100,
    spreadPercent: (quote.ask - quote.bid) / mid * 100
  };
}

console.log(metricas({ bid: 99.80, ask: 100.20, previousClose: 98.50 }));

36.6 Monitor y comparador interactivo

Usá los controles para explorar la tabla. El botón “Comparar” asigna alternativamente el instrumento al panel A o B. También podés seleccionarlos desde las listas.

Monitor de instrumentos · datos ficticios

La edad simula segundos transcurridos desde cada cotización; no hay conexión externa.

Instrumentos visibles5
Datos vigentes3 de 5
Spread medio visible0,6274%
FuenteDatos locales de demostración
InstrumentoTipoBidAskMidVariaciónSpreadEdadAcción

Cartera de demostración en ARS

Efectivo$250.000,00
Valor de posiciones$631.330,00
Resultado no realizado$38.330,00
Patrimonio estimado$881.330,00

3 de 5 cotizaciones cumplen la vigencia máxima de 30 segundos. Las comparaciones no constituyen una recomendación.

36.7 Búsqueda, filtro y orden

La búsqueda se aplica sobre símbolo y nombre normalizados. El filtro por tipo reduce el conjunto; luego el comparador de orden decide la secuencia. Este orden de operaciones hace que el resumen describa exactamente las filas visibles.

sort() modifica el arreglo sobre el que opera. Para conservar el origen puede ordenarse una copia con [...datos].sort(...) o utilizar una alternativa no mutante disponible en el entorno objetivo.

function seleccionarInstrumentos(datos, { search = "", type = "ALL", compare }) {
  const term = search.trim().toLowerCase();
  return datos
    .filter(item => type === "ALL" || item.type === type)
    .filter(item => !term || `${item.symbol} ${item.name}`.toLowerCase().includes(term))
    .sort(compare);
}

const datos = [
  { symbol: "BETA", name: "Bono Beta", type: "BOND", spread: 0.60 },
  { symbol: "ALFA", name: "Acción Alfa", type: "EQUITY", spread: 0.40 }
];
console.log(seleccionarInstrumentos(datos, {
  search: "a", type: "ALL", compare: (a, b) => a.spread - b.spread
}));

36.8 Vigencia y calidad

La edad no dice si el precio es correcto, pero evita presentar una observación antigua como actual. El umbral se define por caso de uso e instrumento.

Una implementación real también distinguiría mercado cerrado, fuente desconectada, dato indicativo, corrección y secuencia fuera de orden.

vigente = edad de la observación ≤ umbral del usoUn dato vencido puede mostrarse para contexto si está claramente marcado y no se usa silenciosamente para ejecutar.

36.9 Comparar sin fabricar un ganador

El panel coloca los instrumentos lado a lado, pero no calcula un “mejor” global. Mayor variación, menor spread o dato más fresco responden preguntas diferentes.

Los nocionales en monedas distintas no son comparables hasta convertirlos con un tipo de cambio y una fecha. La interfaz conserva el código de moneda junto a cada importe.

function comparar(a, b, quantity) {
  if (!Number.isInteger(quantity) || quantity <= 0) throw new RangeError("Cantidad no válida");
  const resumen = item => {
    const mid = (item.bid + item.ask) / 2;
    return {
      symbol: item.symbol,
      currency: item.currency,
      spreadPercent: (item.ask - item.bid) / mid * 100,
      buyNotional: item.ask * quantity
    };
  };
  return { a: resumen(a), b: resumen(b), comparableDirectamente: a.currency === b.currency };
}

console.log(comparar(
  { symbol: "ALFA", currency: "ARS", bid: 99.80, ask: 100.20 },
  { symbol: "FONDO-G", currency: "USD", bid: 25.10, ask: 25.30 },
  100
));

36.10 Integrar posiciones y efectivo

La cartera del monitor usa cantidades y costos predeterminados. Valúa cada posición al punto medio, convierte su valor a ARS y suma efectivo.

Para una aplicación de producción deberían incorporarse precios autorizados, tipos de cambio con hora, resultados realizados, cargos, ingresos y estado de liquidación.

function valuarCartera(posiciones, cotizaciones, fxToBase, cash) {
  const values = posiciones.map(position => {
    const quote = cotizaciones.get(position.instrumentId);
    if (!quote) throw new Error(`Falta cotización: ${position.instrumentId}`);
    const fx = fxToBase[quote.currency];
    if (!Number.isFinite(fx) || fx <= 0) throw new Error(`Falta FX: ${quote.currency}`);
    const mid = (quote.bid + quote.ask) / 2;
    return position.quantity * mid * fx;
  });
  const positionsValue = values.reduce((sum, value) => sum + value, 0);
  return { positionsValue, equity: cash + positionsValue };
}

const quotes = new Map([
  ["ALFA", { bid: 99.80, ask: 100.20, currency: "ARS" }],
  ["FONDO-G", { bid: 25.10, ask: 25.30, currency: "USD" }]
]);
console.log(valuarCartera(
  [{ instrumentId: "ALFA", quantity: 100 }, { instrumentId: "FONDO-G", quantity: 20 }],
  quotes, { ARS: 1, USD: 1200 }, 250000
));

36.11 Estado derivado, no duplicado

El conjunto filtrado, las métricas y el resumen no se guardan como nuevas fuentes permanentes: se derivan de datos y controles actuales. Esto evita que una fila diga una cosa y la tarjeta resumen otra.

El estado mínimo contiene búsqueda, filtro, orden, umbral, selecciones y cantidad. Cada cambio dispara una única función que recalcula y renderiza.

EVENTOUsuario cambia
LEERControles
DERIVARDatos visibles
CALCULARMétricas
RENDERIZARVista completa
ANUNCIAREstado accesible

36.12 Renderizado seguro

Los textos provenientes de datos se asignan con textContent. Construir HTML concatenando cadenas no confiables puede convertir un símbolo o nombre en contenido ejecutable.

En el proyecto, las filas se generan con elementos DOM y las tarjetas de comparación usan nodos explícitos. Si una plantilla necesita HTML, debe escaparse o sanearse con una política adecuada.

Regla práctica: no uses innerHTML para insertar cotizaciones, nombres o mensajes recibidos de una fuente externa.

36.13 Formato y unidades

Intl.NumberFormat presenta moneda y porcentajes según una configuración regional. El valor interno continúa siendo numérico; no se debe volver a parsear el texto formateado.

Cada celda conserva unidad: ARS, USD, porcentaje o segundos. Colorear una variación ayuda, pero el signo y el texto siguen presentes para no depender únicamente del color.

36.14 Validación y degradación

Un dato inválido no debería derribar todo el monitor. Puede excluirse la fila, marcarse un error de fuente y mantener disponibles las demás observaciones.

ProblemaRespuesta de interfazRespuesta técnica
Sin coincidenciasEstado vacío claro.Conservar filtros para corregirlos.
Dato vencidoInsignia y resumen de vigencia.No ocultar la edad.
Cotización inválidaFila no disponible.Registrar fuente y regla fallida.
Falta conversiónValor no calculable.No asumir FX igual a uno.

36.15 Reconciliaciones

El número de filas visibles debe coincidir con el resumen; vigentes más vencidas debe igualar visibles; cada comparación debe referirse a un instrumento existente; y el patrimonio debe ser efectivo más posiciones.

function reconciliarMonitor({ visible, fresh, stale, cash, positionsValue, equity }) {
  const diferencias = [];
  if (fresh + stale !== visible) diferencias.push("CONTEO_DE_VIGENCIA");
  if (Math.abs(cash + positionsValue - equity) > 0.005) diferencias.push("PATRIMONIO");
  return { valido: diferencias.length === 0, diferencias };
}

console.log(reconciliarMonitor({
  visible: 5, fresh: 3, stale: 2,
  cash: 250000, positionsValue: 631330, equity: 881330
}));

36.16 Pruebas del proyecto

Las funciones puras se prueban con casos de borde; la integración verifica que los controles produzcan filas y resúmenes coherentes.

function assertAlmostEqual(actual, expected, tolerance = 1e-9) {
  if (Math.abs(actual - expected) > tolerance) {
    throw new Error(`Esperado ${expected}, recibido ${actual}`);
  }
}

const mid = (99.80 + 100.20) / 2;
const spreadPercent = (100.20 - 99.80) / mid * 100;
assertAlmostEqual(mid, 100);
assertAlmostEqual(spreadPercent, 0.4);
console.log("Pruebas de métricas superadas");

También conviene probar búsqueda sin resultados, igualdad de selecciones, cantidad cero, umbral inválido, monedas distintas y redondeos.

36.17 Accesibilidad y diseño adaptable

Los controles tienen etiquetas visibles, el estado usa aria-live y la información no depende solo del color. En pantallas pequeñas, los controles y tarjetas pasan a una columna y la tabla conserva desplazamiento horizontal.

Los botones mantienen foco visible y nombres concretos. Una versión más avanzada debería gestionar encabezados fijos, navegación de tabla y preferencias de movimiento.

36.18 Persistencia y datos reales

Para persistencia local se podría serializar únicamente preferencias no sensibles. Posiciones, órdenes y credenciales requieren almacenamiento, autenticación, cifrado, control de acceso y auditoría acordes al riesgo.

Una fuente real necesita conexión, reconexión, autenticación, límites de frecuencia, secuencias, instantáneas, actualizaciones y licencias de uso. El indicador “en línea” debe probar flujo reciente, no solo socket abierto.

Separación recomendada: adaptar mensajes externos a un modelo interno validado antes de permitir que lleguen a cálculos y componentes.

36.19 Extensiones posibles

  • Alertas por cambio, spread o vencimiento de datos.
  • Gráficos históricos con intervalo y fuente visibles.
  • Costos totales y precio efectivo por instrumento.
  • Comparación de rendimiento total con moneda común.
  • Profundidad del libro y nocional ejecutable.
  • Límites de concentración y escenarios de riesgo.
  • Importación de operaciones con vista previa y conciliación.
  • Registro de errores y panel de calidad de datos.

Cada extensión debería incorporarse con su modelo, validaciones, estados de error y pruebas, en lugar de acumular lógica directamente en el renderizado.

36.20 Cierre del curso

El monitor integra los principios centrales: comprender el instrumento antes de comparar, mantener precio y cantidad con sus unidades, separar orden de ejecución, incorporar costos, reconocer riesgos y conservar trazabilidad.

  • Un precio necesita instrumento, moneda, fuente e instante.
  • Una métrica necesita fórmula, horizonte y denominador.
  • Comparar exige normalizar sin ocultar diferencias económicas.
  • Una cartera combina posiciones, efectivo, conversiones y resultados.
  • La interfaz debe hacer visibles datos vencidos, faltantes o incomparables.
  • Las funciones puras, validaciones y reconciliaciones reducen errores.
  • El software organiza evidencia; no reemplaza criterio ni regulación.

Con este proyecto queda una base extensible para conectar datos, registrar operaciones y construir herramientas financieras más completas de manera responsable.