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
Instrumento
Identidad y condiciones relativamente estables.
Cotización
Observación de mercado con fuente e instante.
Orden
Instrucción con lado, cantidad, tipo y estado.
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.
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
| Tipo | Campos característicos | Validación adicional |
|---|---|---|
| Acción | Emisor, clase, derechos y unidad. | Clase compatible con el identificador. |
| Bono | Vencimiento, cupón, nominal y calendario. | Fechas y convención de interés. |
| Fondo | Administrador, clase, valor cuota y gastos. | Frecuencia y hora de valuación. |
| Divisa | Moneda base y cotizada. | Dirección del par y cantidad base. |
| Derivado | Subyacente, 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.
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.
34.9 Orden, ejecución y operación
| Objeto | Representa | Campos esenciales |
|---|---|---|
| Orden | Intención o instrucción. | ID, cuenta, instrumento, lado, cantidad, tipo y vigencia. |
| Ejecución | Parte efectivamente negociada. | ID, orden, cantidad, precio, mercado y hora. |
| Operación | Acuerdo 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.
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
nullen 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.
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ón | Representación posible | Dato a conservar |
|---|---|---|
| Entrada inválida | Error de validación. | Campo y regla incumplida. |
| Orden rechazada | Estado y evento de rechazo. | Código, fuente y hora. |
| Dato ausente | Resultado sin valor. | Motivo y alcance de la búsqueda. |
| Falla técnica | Excepció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.
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.