34. Modelado de instrumentos, cotizaciones y operaciones con objetos

Un sistema financiero confiable comienza con objetos que expresan con claridad qué representan. Instrumentos, cotizaciones, órdenes y ejecuciones comparten relaciones, pero tienen identidades, tiempos y reglas diferentes.

34.1 Del concepto financiero al objeto

Modelar consiste en elegir qué datos y reglas del mundo real necesita una aplicación. El objetivo no es copiar toda la complejidad del mercado, sino representar sin ambigüedad las decisiones que el programa debe tomar.

Un objeto JavaScript agrupa propiedades relacionadas. Sin embargo, una colección de campos solo se convierte en un buen modelo cuando sus nombres, unidades, estados e invariantes están definidos.

Idea central: instrumento, cotización, orden y ejecución deben ser objetos separados. El instrumento describe qué es; la cotización, qué precio se observó; la orden, qué se intentó hacer; y la ejecución, qué ocurrió.

34.2 Separar entidades, observaciones e instrucciones

I

Instrumento

Identidad y condiciones relativamente estables.

Q

Cotización

Observación de mercado con fuente e instante.

O

Orden

Instrucción con lado, cantidad, tipo y estado.

E

Ejecución

Hecho confirmado con precio y cantidad.

Separarlos evita errores como cambiar el precio “del instrumento”, suponer que toda orden fue ejecutada o perder la fuente temporal de un valor.

34.3 Modelar un instrumento

Un modelo mínimo puede incluir identificador interno, símbolo, tipo, nombre, moneda de negociación, mercado y estado. Otros campos dependen del tipo: vencimiento y tasa para un bono; emisor y clase para una acción; subyacente y strike para una opción.

  • id: clave estable dentro del sistema.
  • symbol: código visible, no necesariamente único globalmente.
  • type: categoría que determina reglas adicionales.
  • currency: moneda de precio o liquidación según definición.
  • venue: mercado, fuente o ámbito relevante.
  • status: activo, suspendido, vencido u otro estado controlado.
function crearInstrumento({ id, symbol, name, type, currency, venue }) {
  const tipos = ["EQUITY", "BOND", "FUND", "FX", "DERIVATIVE"];
  if (![id, symbol, name, currency, venue].every(valor => typeof valor === "string" && valor.trim())) {
    throw new Error("Faltan datos obligatorios del instrumento");
  }
  if (!tipos.includes(type)) throw new RangeError("Tipo de instrumento no admitido");
  return Object.freeze({
    id, symbol: symbol.toUpperCase(), name, type,
    currency: currency.toUpperCase(), venue, status: "ACTIVE"
  });
}

console.log(crearInstrumento({
  id: "INS-001", symbol: "demo", name: "Acción de ejemplo",
  type: "EQUITY", currency: "ARS", venue: "MERCADO-DEMO"
}));

34.4 Identidad, símbolo y clave compuesta

Un símbolo puede repetirse entre mercados, monedas o clases. Por eso no siempre sirve como clave única. Una aplicación puede usar un identificador interno y conservar identificadores externos con su esquema y fuente.

clave de cotización = instrumento + mercado + moneda + fuenteAgregar el instante convierte esa clave en una observación histórica concreta.

Cambiar el símbolo comercial no debería romper referencias históricas. La identidad estable y la etiqueta visible cumplen propósitos diferentes.

34.5 Tipos comunes y propiedades específicas

TipoCampos característicosValidación adicional
AcciónEmisor, clase, derechos y unidad.Clase compatible con el identificador.
BonoVencimiento, cupón, nominal y calendario.Fechas y convención de interés.
FondoAdministrador, clase, valor cuota y gastos.Frecuencia y hora de valuación.
DivisaMoneda base y cotizada.Dirección del par y cantidad base.
DerivadoSubyacente, vencimiento, multiplicador y liquidación.Términos completos del contrato.

Un único objeto con decenas de campos opcionales facilita crear combinaciones inválidas. Puede usarse una propiedad discriminante y validadores específicos por tipo.

34.6 Dinero, cantidades y precisión

JavaScript representa los números ordinarios con punto flotante binario. Algunas fracciones decimales no se almacenan exactamente, por lo que sumar importes puede producir residuos inesperados.

Para importes discretos puede guardarse la unidad mínima como entero, siempre que la escala sea conocida. En cálculos financieros generales suelen emplearse bibliotecas o tipos decimales del entorno, junto con reglas explícitas de redondeo.

importe = precio × cantidad × multiplicadorMoneda, escala, unidad de cantidad y regla de redondeo forman parte del dato.

No uses toFixed() como solución contable: formatea una salida; no define por sí solo cómo calcular, acumular o conciliar importes.

34.7 Cotizaciones como observaciones temporales

Una cotización necesita instrumento, bid, ask, cantidades disponibles, moneda, fuente e instante. Un precio sin tiempo puede estar desactualizado; uno sin fuente puede corresponder a otro mercado.

Los valores derivados, como punto medio y spread, conviene calcularlos a partir de la observación original para evitar inconsistencias.

function analizarCotizacion({ instrumentId, bid, ask, timestamp, source }) {
  if (![bid, ask].every(Number.isFinite) || bid <= 0 || ask <= 0 || bid > ask) {
    throw new RangeError("La cotización requiere 0 < bid ≤ ask");
  }
  const time = new Date(timestamp);
  if (!Number.isFinite(time.getTime())) throw new RangeError("Marca temporal no válida");
  const mid = (bid + ask) / 2;
  return {
    instrumentId, bid, ask, timestamp: time.toISOString(), source,
    mid, spread: ask - bid, spreadPercent: (ask - bid) / mid * 100
  };
}

console.log(analizarCotizacion({
  instrumentId: "INS-001", bid: 99.80, ask: 100.20,
  timestamp: "2026-09-13T14:00:00Z", source: "FUENTE-DEMO"
}));

34.8 Vigencia, secuencia y calidad del dato

La marca temporal indica cuándo fue producida o recibida la observación. Ambas horas pueden conservarse para medir demora. La vigencia máxima depende del uso: ejecutar requiere otra frescura que elaborar un cierre diario.

Los mensajes pueden llegar fuera de orden. Antes de reemplazar una cotización debe compararse secuencia o instante y aplicar reglas para duplicados, correcciones y reinicios de fuente.

RECIBIRMensaje
VALIDAREsquema
ORDENARSecuencia
CALCULARDerivados
PUBLICAREstado
ARCHIVAROriginal

34.9 Orden, ejecución y operación

ObjetoRepresentaCampos esenciales
OrdenIntención o instrucción.ID, cuenta, instrumento, lado, cantidad, tipo y vigencia.
EjecuciónParte efectivamente negociada.ID, orden, cantidad, precio, mercado y hora.
OperaciónAcuerdo confirmado que genera obligaciones.Partes, fecha de operación, liquidación, moneda y estado.

Una orden puede producir cero, una o varias ejecuciones. Una ejecución no debe sobrescribir la cantidad original: ambas son necesarias para explicar el saldo pendiente.

34.10 Estados y transiciones de una orden

Los estados deben describir hechos verificables. Un flujo simplificado puede incluir nueva, aceptada, parcialmente ejecutada, ejecutada, cancelada y rechazada.

No toda transición es válida. Una orden ejecutada por completo no debería volver a “nueva”; una cancelación solicitada no equivale a cancelación confirmada.

const transicionesOrden = {
  NEW: ["ACCEPTED", "REJECTED"],
  ACCEPTED: ["PARTIALLY_FILLED", "FILLED", "CANCELED"],
  PARTIALLY_FILLED: ["PARTIALLY_FILLED", "FILLED", "CANCELED"],
  FILLED: [], CANCELED: [], REJECTED: []
};

function cambiarEstadoOrden(orden, nuevoEstado) {
  if (!transicionesOrden[orden.status]?.includes(nuevoEstado)) {
    throw new Error(`Transición inválida: ${orden.status} → ${nuevoEstado}`);
  }
  return { ...orden, status: nuevoEstado, version: orden.version + 1 };
}

const orden = { id: "ORD-1", status: "NEW", version: 1 };
console.log(cambiarEstadoOrden(orden, "ACCEPTED"));

34.11 Ejecuciones parciales y precio promedio

El precio promedio debe ponderarse por cantidad. Promediar precios sin considerar tamaño asigna la misma importancia a ejecuciones diferentes.

precio promedio = Σ(precio × cantidad) / Σ cantidadSolo deben combinarse ejecuciones del mismo instrumento, lado y unidad económica.
function resumirEjecuciones(orden, ejecuciones) {
  const propias = ejecuciones.filter(ejecucion => ejecucion.orderId === orden.id);
  const filledQuantity = propias.reduce((suma, ejecucion) => suma + ejecucion.quantity, 0);
  if (filledQuantity > orden.quantity) throw new Error("La ejecución supera la cantidad ordenada");
  const grossAmount = propias.reduce(
    (suma, ejecucion) => suma + ejecucion.quantity * ejecucion.price, 0
  );
  return {
    filledQuantity,
    remainingQuantity: orden.quantity - filledQuantity,
    averagePrice: filledQuantity ? grossAmount / filledQuantity : null
  };
}

console.log(resumirEjecuciones(
  { id: "ORD-1", quantity: 100 },
  [
    { orderId: "ORD-1", quantity: 40, price: 99.50 },
    { orderId: "ORD-1", quantity: 60, price: 100.25 }
  ]
));

34.12 Validación e invariantes del dominio

Una validación de esquema comprueba tipos y presencia. Una validación de dominio comprueba significado: cantidad positiva, bid no mayor que ask, moneda admitida, fecha coherente y transición autorizada.

  • Validá en el límite de entrada y nuevamente antes de una acción crítica.
  • Rechazá valores desconocidos en campos enumerados.
  • No conviertas silenciosamente cadenas vacías o null en cero.
  • Incluí campo, código y valor rechazado en un error controlado.
  • Separá datos inválidos de reglas de negocio no satisfechas.

Invariante útil: la suma de cantidades ejecutadas debe permanecer entre cero y la cantidad vigente de la orden.

34.13 Objetos inmutables y versiones

Modificar un objeto compartido dificulta saber qué valor utilizó cada cálculo. Crear una nueva versión conserva el estado anterior y permite comparar cambios.

Object.freeze() impide ciertas modificaciones directas al objeto, pero es superficial: los objetos anidados continúan siendo mutables si no se congelan o copian por separado.

function actualizarCotizacion(anterior, cambios) {
  if (cambios.sequence <= anterior.sequence) {
    throw new Error("La nueva secuencia debe ser mayor");
  }
  return Object.freeze({
    ...anterior,
    ...cambios,
    previousVersion: anterior.version,
    version: anterior.version + 1
  });
}

const inicial = Object.freeze({
  instrumentId: "INS-001", bid: 99.80, ask: 100.20, sequence: 10, version: 1
});
console.log(actualizarCotizacion(inicial, { bid: 99.90, ask: 100.10, sequence: 11 }));

34.14 Colecciones e índices con Map

Un arreglo conserva orden y permite recorrer todos los elementos. Un Map resulta conveniente para buscar por clave sin depender de la posición.

La clave debe tener una definición estable. Si se usa una clave compuesta, conviene construirla en una sola función para evitar variantes incompatibles.

function claveCotizacion({ instrumentId, venue, currency }) {
  return [instrumentId, venue, currency].join("|");
}

const cotizaciones = new Map();
const quote = {
  instrumentId: "INS-001", venue: "MERCADO-DEMO", currency: "ARS",
  bid: 99.80, ask: 100.20
};
cotizaciones.set(claveCotizacion(quote), quote);

console.log(cotizaciones.get("INS-001|MERCADO-DEMO|ARS"));

34.15 Serialización, fechas y contratos de datos

JSON.stringify() transforma valores compatibles a texto JSON, pero no preserva automáticamente clases, métodos, Map, precisión especial ni objetos Date como tales. Las fechas suelen viajar como cadenas con una convención acordada.

Un contrato de datos debe definir nombres, tipos, campos obligatorios, enumeraciones, unidad, zona horaria, versión y compatibilidad. Al recibir JSON, se debe parsear y validar antes de confiar.

function serializarMapa(mapa) {
  return JSON.stringify({
    schemaVersion: 1,
    entries: [...mapa.entries()]
  });
}

function restaurarMapa(texto) {
  const datos = JSON.parse(texto);
  if (datos.schemaVersion !== 1 || !Array.isArray(datos.entries)) {
    throw new Error("Formato no compatible");
  }
  return new Map(datos.entries);
}

const original = new Map([["INS-001", { currency: "ARS", status: "ACTIVE" }]]);
const restaurado = restaurarMapa(serializarMapa(original));
console.log(restaurado.get("INS-001"));

34.16 Eventos y trazabilidad

Guardar solo el último estado responde “cómo está”, pero no “cómo llegó”. Un registro de eventos conserva creación, aceptación, modificación, ejecución, cancelación y rechazo con hora y responsable.

Los eventos deberían ser inmutables e idempotentes: procesar dos veces el mismo identificador no debe duplicar una ejecución. El estado actual puede reconstruirse aplicando eventos en orden.

COMANDOSolicitud
VALIDACIÓNReglas
EVENTOHecho
ESTADOProyección
AUDITORÍAEvidencia
REPROCESOIdempotencia

34.17 Errores como resultados del modelo

Una excepción es útil para una condición inesperada. Un rechazo previsto —saldo insuficiente, mercado cerrado o cantidad inválida— puede representarse como resultado con código, mensaje y contexto.

SituaciónRepresentación posibleDato a conservar
Entrada inválidaError de validación.Campo y regla incumplida.
Orden rechazadaEstado y evento de rechazo.Código, fuente y hora.
Dato ausenteResultado sin valor.Motivo y alcance de la búsqueda.
Falla técnicaExcepción controlada.Identificador de seguimiento, sin exponer secretos.

34.18 Laboratorio: construir objetos relacionados

Modificá los datos para generar un instrumento, una cotización y una orden coherentes. El panel valida las relaciones y calcula valores derivados sin mezclarlos con el estado original.

Constructor de una operación

Ejemplo local y educativo: no envía órdenes ni consulta precios externos.

Punto medio$100,00
Spread$0,40 · 0,4000%
Nocional al límite$10.020,00
¿Alcanzaría la punta?Sí, alcanza el ask

            

Los tres objetos son válidos y comparten el identificador INS-DEMO.

34.19 Modelo integrador de una orden limitada

La función siguiente crea objetos relacionados sin mutar las entradas. Usa la punta correspondiente para determinar si el límite permitiría una ejecución inmediata, pero no garantiza que exista cantidad suficiente.

function crearCaso({ instrument, quote, side, quantity, limitPrice }) {
  if (quote.instrumentId !== instrument.id) throw new Error("La cotización pertenece a otro instrumento");
  if (![quantity, limitPrice].every(Number.isFinite) || quantity <= 0 || limitPrice <= 0) {
    throw new RangeError("Cantidad y límite deben ser positivos");
  }
  if (!["BUY", "SELL"].includes(side)) throw new RangeError("Lado no válido");
  const touch = side === "BUY" ? quote.ask : quote.bid;
  const marketable = side === "BUY" ? limitPrice >= touch : limitPrice <= touch;
  return Object.freeze({
    instrument: { ...instrument },
    quote: { ...quote },
    order: {
      id: "ORD-DEMO", instrumentId: instrument.id, side,
      quantity, type: "LIMIT", limitPrice, status: "NEW"
    },
    derived: { touch, notionalAtLimit: quantity * limitPrice, marketable }
  });
}

console.log(crearCaso({
  instrument: { id: "INS-DEMO", symbol: "DEMO", currency: "ARS" },
  quote: { instrumentId: "INS-DEMO", bid: 99.80, ask: 100.20 },
  side: "BUY", quantity: 100, limitPrice: 100.20
}));

34.20 Pruebas que protegen el modelo

Las pruebas deben cubrir el caso normal, límites y errores. Para una cotización: bid igual a ask, bid mayor que ask, cero, NaN, marca inválida y campos ausentes. Para una orden: cantidad parcial, exceso de ejecución y transición terminal.

  • Probá invariantes, no solo valores de ejemplo.
  • Verificá que las funciones no muten objetos recibidos.
  • Usá tolerancias explícitas cuando compares punto flotante.
  • Incluí mensajes duplicados y fuera de orden.
  • Comprobá serialización y restauración de cada versión del contrato.

Una prueba que solo repite la implementación puede aprobar el mismo error. Conviene expresar el resultado esperado desde la regla financiera.

34.21 Errores frecuentes

  • Usar el símbolo como identificador global único.
  • Guardar precio sin moneda, fuente ni instante.
  • Confundir orden enviada con operación ejecutada.
  • Promediar precios sin ponderar cantidades.
  • Usar números binarios sin política de precisión y redondeo.
  • Modificar objetos compartidos y perder el estado anterior.
  • Suponer que Object.freeze() congela toda la estructura anidada.
  • Confiar en JSON recibido sin validarlo.
  • Sobrescribir errores o eventos duplicados sin trazabilidad.

34.22 Ideas para recordar

  • Cada objeto debe representar un concepto y un momento definidos.
  • Identidad estable y etiqueta visible son datos distintos.
  • Precio, cantidad, moneda, escala y tiempo forman una unidad lógica.
  • Orden, ejecución y liquidación tienen ciclos de vida separados.
  • Las invariantes del dominio deben validarse en cada frontera.
  • Crear nuevas versiones mejora trazabilidad frente a la mutación.
  • Serializar exige un contrato y validación al restaurar.

En el próximo tema modelaremos posiciones, efectivo y resultado realizado y no realizado.