27.1 El proyecto integrador
Los veintiséis temas anteriores construyeron las piezas por separado: el contrato y su payoff (temas 3 y 24), la lógica de no arbitraje (tema 5), el descuento (tema 6), los forwards (temas 7 a 12), las relaciones de opciones (temas 16 y 17), el árbol binomial (temas 19 a 22), Black-Scholes (tema 23), la biblioteca de estrategias (tema 24), la validación (tema 25) y el mapa de riesgos (tema 26). Cada pieza quedó aislada, con su propio bloque de código ejecutable. Este tema las reúne en un único programa: el valuador de derivados con detección de arbitrajes.
Idea central: integrar no es pegar funciones: es fijar un contrato de datos (todo instrumento entra como pata) y un orden de ejecución (primero se valida el modelo, después se detectan arbitrajes). Cuando esas dos reglas se respetan, cada número del resultado puede rastrearse hasta el tema que lo fundamenta.
El valuador trabaja con los datos de referencia del curso —S = K = 100, tasa 5 % anual continua, volatilidad 20 %, T = 1 año y N = 100 pasos— y reproduce, sin cambios, los valores ya conocidos: la call europea 10,4506, la put 5,5735, el árbol 10,4306 y el precio forward teórico 105,1271. La novedad es el detector: contra cotizaciones de mercado, verifica la paridad put-call, las cotas y los topes de las opciones, y el precio del forward, y cuando alguna relación se rompe informa el desvío y la estrategia que lo capturaría.
27.2 La arquitectura del valuador
El programa se organiza en seis módulos. Ninguno se reescribe: se copian las versiones de producción construidas en el módulo de implementación, que ya pasaron por sus validaciones. La tabla fija el mapa completo, con el tema de origen de cada pieza.
| Módulo | Qué aporta | Origen |
|---|---|---|
| Biblioteca de patas | El modelo de datos { tipo, posicion, K, prima, cantidad } y los payoffs de opciones y forwards | Tema 24 |
| Motor Black-Scholes | Precio de calls y puts europeas; griegas en forma cerrada | Temas 23 y 26 |
| Motor de árbol CRR | Europeas y americanas sin fórmula; valores de control | Temas 21, 22 y 25 |
| Validador del modelo | No arbitraje del paso, paridad, cotas, ejercicio anticipado y convergencia | Tema 25 |
| Detector de arbitrajes | Paridad, cotas, topes y precio forward contra cotizaciones de mercado | Temas 12, 16 y 17 |
| Panel de griegas | Delta, gamma, vega y theta agregados por posición | Tema 26 |
El flujo interno es lineal y el orden importa: (1) se normalizan los datos; (2) se valoran los instrumentos con el modelo elegido; (3) se corre la validación del tema 25; (4) recién entonces se ejecuta el detector sobre las cotizaciones; (5) se emite el informe. Detectar antes de validar produce avisos falsos: un detector montado sobre un árbol mal calibrado marca arbitrajes que no existen.
Convenciones del programa: valores por unidad del subyacente; capitalización continua, como los temas 23 a 26, con factor de descuento e^(−rT); sin costos, impuestos, márgenes ni dividendos. Las relaciones que vigila el detector son las de opciones europeas. Nada de esto constituye asesoramiento de inversión.
27.3 El valorador de instrumentos
El primer bloque funcional son tres motores de precio. Para las opciones europeas, la fórmula del tema 23 escrita como una sola función que despacha por tipo. Para las opciones americanas (y como control de las europeas), el árbol CRR del tema 25, que no necesita fórmula. Y para el forward, el valor del contrato largo del tema 10: comprar el subyacente hoy financiado con un préstamo de K·e^(−rT), es decir S − K·e^(−rT).
function normalEstandar(x) {
const t = 1 / (1 + 0.2316419 * Math.abs(x));
const densidad = Math.exp(-x * x / 2) / Math.sqrt(2 * Math.PI);
const polinomio = t * (0.319381530 + t * (-0.356563782 + t * (1.781477937 + t * (-1.821255978 + t * 1.330274429))));
const acumulada = 1 - densidad * polinomio;
return x >= 0 ? acumulada : 1 - acumulada;
}
function bsPrecio(S, K, r, sigma, T, tipo) {
const d1 = (Math.log(S / K) + (r + sigma * sigma / 2) * T) / (sigma * Math.sqrt(T));
const d2 = d1 - sigma * Math.sqrt(T);
return tipo === 'call'
? S * normalEstandar(d1) - K * Math.exp(-r * T) * normalEstandar(d2)
: K * Math.exp(-r * T) * normalEstandar(-d2) - S * normalEstandar(-d1);
}
function pagoDeOpcion(tipo, strike) {
return tipo === 'call'
? (precio) => Math.max(precio - strike, 0)
: (precio) => Math.max(strike - precio, 0);
}
function valorarArbol(spot, u, d, p, descuento, pasos, pago, estilo) {
let valores = [];
for (let j = 0; j <= pasos; j++) {
valores.push(pago(spot * Math.pow(u, j) * Math.pow(d, pasos - j)));
}
const americana = estilo === 'americana';
for (let i = pasos - 1; i >= 0; i--) {
for (let j = 0; j <= i; j++) {
const continuacion = descuento * (p * valores[j + 1] + (1 - p) * valores[j]);
if (americana) {
const precio = spot * Math.pow(u, j) * Math.pow(d, i - j);
valores[j] = Math.max(continuacion, pago(precio));
} else {
valores[j] = continuacion;
}
}
}
return valores[0];
}
function arbolCRR(spot, strike, tasa, volatilidad, tiempo, pasos, tipo, estilo) {
const dt = tiempo / pasos;
const u = Math.exp(volatilidad * Math.sqrt(dt));
const d = 1 / u;
const g = Math.exp(tasa * dt);
const p = (g - d) / (u - d);
return valorarArbol(spot, u, d, p, 1 / g, pasos, pagoDeOpcion(tipo, strike), estilo);
}
function valorForward(S, K, r, T) {
return S - K * Math.exp(-r * T);
}
console.log(bsPrecio(100, 100, 0.05, 0.2, 1, 'call').toFixed(4)); // "10.4506"
console.log(bsPrecio(100, 100, 0.05, 0.2, 1, 'put').toFixed(4)); // "5.5735"
console.log(arbolCRR(100, 100, 0.05, 0.2, 1, 100, 'call', 'europea').toFixed(4)); // "10.4306"
console.log(arbolCRR(100, 100, 0.05, 0.2, 1, 100, 'put', 'americana').toFixed(4)); // "6.0824"
console.log(valorForward(100, 100, 0.05, 1).toFixed(4)); // "4.8771"Línea por línea: la call y la put europeas de Black-Scholes (10,4506 y 5,5735, los valores del tema 23); la call europea del árbol a 100 pasos (10,4306, la convergencia medida en el tema 25); la put americana (6,0824, que contiene la prima de ejercicio anticipado de 0,5288 del tema 22); y el valor del forward largo pactado en K = 100 (4,8771).
Ese último valor cierra un círculo: S − K·e^(−rT) es exactamente el miembro derecho de la paridad C − P = S − K·e^(−rT) del tema 17. Detectar una violación de la paridad es, con el mismo cálculo, detectar un forward mal cotizado. Esa equivalencia es una de las pruebas del detector del apartado 27.8.
27.4 La ficha de una posición: modelo y mercado
El valuador adopta el modelo de datos del tema 24 —una pata es { tipo, posicion, K, prima, cantidad }— y lo usa para dos tareas distintas: describir la posición que se quiere analizar y comparar cada instrumento contra su cotización. Cada instrumento queda así descripto dos veces: cuánto debería valer según el modelo y a cuánto se opera según el mercado.
| Instrumento | Motor de valuación | Valor del modelo | Dato de mercado comparable |
|---|---|---|---|
| Call europea | Black-Scholes (tema 23) | 10,4506 | Prima cotizada C |
| Put europea | Black-Scholes (tema 23) | 5,5735 | Prima cotizada P |
| Call o put americana | Árbol CRR (temas 22 y 25) | Put: 6,0824 | Prima cotizada (orientativa: sin paridad) |
| Forward | Valor S − K·e^(−rT) (tema 10) | 4,8771 (para K = 100) | Precio de entrega cotizado F |
El campo estilo decide el motor de las opciones: 'europea' va a Black-Scholes; 'americana', al árbol. En el forward no hay fórmula cerrada ni árbol: su valor es el de la cartera replicante —comprar el subyacente y pedir prestado K·e^(−rT)— y su precio de entrega teórico, el que se compara contra el mercado, es S·e^(rT) = 105,1271.
No confundir dos números del forward: el valor del contrato largo con precio pactado K = 100 es 4,8771; el precio de entrega de un forward nuevo, el que cotiza el mercado, es 105,1271. El detector del apartado 27.6 compara el segundo contra el mercado; el primero es el mismo cálculo de la paridad.
27.5 Qué es y qué no es un arbitraje
Con dos precios sobre la misma posición —el del modelo y el del mercado— la tentación es llamar arbitraje a cualquier diferencia. No lo es. El arbitraje ejecutable es una inconsistencia entre instrumentos que permite armar una cartera con ingreso inicial positivo y pagos finales no negativos en todos los escenarios, sin asumir riesgo. Que una prima cotizada difiera del modelo es otra cosa: es información sobre la volatilidad implícita, la liquidez y las expectativas del mercado.
Un caso concreto con los datos del curso. La call del modelo vale 10,4506 y cotiza 11,50: está 1,0494 por encima. La put del modelo vale 5,5735 y cotiza 5,57: está 0,0035 por debajo. Tomadas por separado, ninguna de esas cifras es un arbitraje: el modelo puede estar usando una volatilidad distinta de la que el mercado descuenta. Pero si se combinan, la relación del tema 17 las une:
Ese desvío ya no depende de ninguna expectativa: la cartera vender call, comprar put, comprar el subyacente y financiar PV(K) paga K en todos los escenarios y cuesta 1,0529 menos que sus propios flujos. Eso sí es una señal de arbitraje en el modelo sin costos del curso. La diferencia es de naturaleza, no de tamaño: el sobreprecio aislado especula; la violación de la paridad asegura.
La señal teórica no es una ganancia garantizada: para ejecutarla hay que poder operar las cuatro patas (call, put, subyacente y préstamo) al mismo tiempo, pagar comisiones y spread, y —si la señal es inversa— conseguir el título para venderlo en corto. El detector compara precios, no verifica que la operación sea posible; por eso informa el desvío y la estrategia, y deja la ejecución del lado humano.
27.6 Las relaciones que vigila el detector
El detector transforma las relaciones de no arbitraje del curso en comparaciones numéricas contra cotizaciones. Cinco familias, todas con el convenio de capitalización continua de los temas 23 a 26, y con PV(K) = K·e^(−rT).
| Relación | Condición de no arbitraje | Si la cotización la rompe |
|---|---|---|
| Paridad put-call europea | C − P = S − PV(K) | Por encima: conversion (vender call, comprar put, comprar subyacente, financiar PV(K)). Por debajo: reverse conversion (comprar call, vender put, vender subyacente en corto, invertir PV(K)) |
| Cota inferior de la call | C ≥ max(S − PV(K), 0) | Comprar la call, vender el subyacente en corto e invertir PV(K) |
| Cota inferior de la put | P ≥ max(PV(K) − S, 0) | Comprar la put, comprar el subyacente y financiar PV(K) |
| Topes | C ≤ S y P ≤ PV(K) | Vender la call y comprar el subyacente; o vender la put e invertir PV(K) |
| Precio forward | F = S·e^(rT) | Por encima: cash-and-carry (comprar el subyacente financiado y vender el forward). Por debajo: reverse cash-and-carry |
Cada fila tiene su versión construida en el curso: la paridad es el tema 17; las cotas y los topes, el tema 16; el precio forward, los temas 8 y 12. Y cada una tiene su demostración por flujos: la cartera que la explota se arma hoy y sus pagos al vencimiento se cancelan entre sí en todos los escenarios, como en las tablas del tema 12.
Paridad solo con europeas: con opciones americanas la igualdad se convierte en la banda S − K ≤ C − P ≤ S − K·e^(−rT) del apartado 17.7. El detector aplica la igualdad estricta únicamente cuando las cotizaciones son de opciones europeas; con americanas, el chequeo de paridad se apaga o se lee como orientativo, porque el valor del ejercicio anticipado empuja a C − P hacia abajo dentro de la banda.
Un último requisito de honestidad numérica: la tolerancia. Las cotizaciones llegan redondeadas al centavo, así que el detector considera «sin señal» todo desvío menor o igual a 0,01. Sin esa tolerancia, cualquier redondeo de cotización parecería un arbitraje de miles de pesos escalado por contratos.
27.7 Los detectores en código
La implementación son tres funciones puras que devuelven siempre la misma estructura: { nombre, observado, referencia, desvio, veredicto, accion }. El veredicto es 'Sin señal' cuando el desvío queda dentro de la tolerancia; la acción describe la cartera que explota la violación y queda vacía cuando no hay nada que hacer. Con esa estructura uniforme, el informe y el laboratorio se limitan a recorrer y mostrar.
function detectarParidad(S, K, r, T, precioCall, precioPut, tolerancia) {
const referencia = S - K * Math.exp(-r * T);
const observado = precioCall - precioPut;
const desvio = observado - referencia;
if (Math.abs(desvio) <= tolerancia) {
return { nombre: 'Paridad put-call (C − P)', observado, referencia, desvio, veredicto: 'Sin señal', accion: '' };
}
if (desvio > 0) {
return { nombre: 'Paridad put-call (C − P)', observado, referencia, desvio, veredicto: 'Conversion', accion: 'vender la call, comprar la put, comprar el subyacente y financiar PV(K)' };
}
return { nombre: 'Paridad put-call (C − P)', observado, referencia, desvio, veredicto: 'Reverse conversion', accion: 'comprar la call, vender la put, vender el subyacente en corto e invertir PV(K)' };
}
function detectarCotas(S, K, r, T, precioCall, precioPut, tolerancia) {
const pvK = K * Math.exp(-r * T);
const filas = [
{ nombre: 'Cota inferior call (C)', observado: precioCall, referencia: Math.max(S - pvK, 0), requisito: 'minimo', accion: 'comprar la call, vender el subyacente en corto e invertir PV(K)' },
{ nombre: 'Cota inferior put (P)', observado: precioPut, referencia: Math.max(pvK - S, 0), requisito: 'minimo', accion: 'comprar la put, comprar el subyacente y financiar PV(K)' },
{ nombre: 'Tope call (C ≤ S)', observado: precioCall, referencia: S, requisito: 'maximo', accion: 'vender la call y comprar el subyacente' },
{ nombre: 'Tope put (P ≤ PV(K))', observado: precioPut, referencia: pvK, requisito: 'maximo', accion: 'vender la put e invertir PV(K)' }
];
return filas.map(fila => {
const desvio = fila.observado - fila.referencia;
const violada = fila.requisito === 'minimo' ? desvio < -tolerancia : desvio > tolerancia;
return { nombre: fila.nombre, observado: fila.observado, referencia: fila.referencia, desvio, veredicto: violada ? 'Violación' : 'Sin señal', accion: violada ? fila.accion : '' };
});
}
function detectarForward(S, r, T, precioForward, tolerancia) {
const referencia = S * Math.exp(r * T);
const desvio = precioForward - referencia;
if (Math.abs(desvio) <= tolerancia) {
return { nombre: 'Forward (F vs S·e^(rT))', observado: precioForward, referencia, desvio, veredicto: 'Sin señal', accion: '' };
}
if (desvio > 0) {
return { nombre: 'Forward (F vs S·e^(rT))', observado: precioForward, referencia, desvio, veredicto: 'Cash-and-carry', accion: 'comprar el subyacente financiado y vender el forward' };
}
return { nombre: 'Forward (F vs S·e^(rT))', observado: precioForward, referencia, desvio, veredicto: 'Reverse cash-and-carry', accion: 'vender el subyacente en corto, invertir el efectivo y comprar el forward' };
}
const paridadCara = detectarParidad(100, 100, 0.05, 1, 11.50, 5.57, 0.01);
console.log(paridadCara.veredicto + ' | desvío ' + paridadCara.desvio.toFixed(4)); // "Conversion | desvío 1.0529"
console.log(paridadCara.accion); // "vender la call, comprar la put, comprar el subyacente y financiar PV(K)"
const cotaCall = detectarCotas(100, 100, 0.05, 1, 4.00, 5.57, 0.01)[0];
console.log(cotaCall.veredicto + ' | desvío ' + cotaCall.desvio.toFixed(4)); // "Violación | desvío -0.8771"
console.log(cotaCall.accion); // "comprar la call, vender el subyacente en corto e invertir PV(K)"
const forwardCaro = detectarForward(100, 0.05, 1, 107, 0.01);
console.log(forwardCaro.veredicto + ' | desvío ' + forwardCaro.desvio.toFixed(4)); // "Cash-and-carry | desvío 1.8729"
const forwardBarato = detectarForward(100, 0.05, 1, 103, 0.01);
console.log(forwardBarato.veredicto + ' | desvío ' + forwardBarato.desvio.toFixed(4)); // "Reverse cash-and-carry | desvío -2.1271"Lectura de los casos del bloque. Con la call cotizada a 11,50, la paridad da +1,0529: 'Conversion'. Con la call de strike 100 cotizada a 4,00, la cota inferior —que vale 4,8771— queda violada en −0,8771: comprar la call, vender el subyacente en corto e invertir PV(K) fija una ganancia inicial de 0,8771 y no pierde en ningún escenario. Con el forward a 107, el desvío contra 105,1271 es +1,8729 (cash-and-carry); a 103, es −2,1271 (reverse cash-and-carry). Los números provienen de las mismas relaciones del curso; lo nuevo es que ahora se calculan solos.
27.8 Pruebas del detector: la batería de casos construidos
Un detector sin batería de pruebas es tan confiable como un árbol sin validación. La tabla enumera los ocho casos construidos —dos sin señal y seis con señales de distinto tipo— y la batería los ejecuta y compara contra lo esperado. Es el equivalente para el detector de lo que la rutina validarModelo fue para el árbol en el tema 25.
| Caso | Cotizaciones | Resultado esperado | Estrategia |
|---|---|---|---|
| Base en línea | C = 10,45; P = 5,57; F = 105,13 | Sin señal en paridad y forward | — |
| Call sobrevaluada | C = 11,50; P = 5,57 | Paridad +1,0529 → conversion | Vender call, comprar put, comprar subyacente y financiar PV(K) |
| Call subvaluada | C = 9,50; P = 5,57 | Paridad −0,9471 → reverse conversion | Comprar call, vender put, vender subyacente en corto e invertir PV(K) |
| Call bajo la cota | C = 4,00 | Cota inferior −0,8771 → violación | Comprar call, vender subyacente en corto e invertir PV(K) |
| Put bajo la cota (S = 90) | P = 4,00 | Cota inferior −1,1229 → violación | Comprar put, comprar subyacente y financiar PV(K) |
| Call sobre el tope | C = 101,00 | Tope +1,0000 → violación | Vender call y comprar subyacente |
| Forward caro | F = 107,00 | +1,8729 → cash-and-carry | Comprar subyacente financiado y vender forward |
| Forward barato | F = 103,00 | −2,1271 → reverse cash-and-carry | Vender subyacente en corto, invertir y comprar forward |
function detectarParidad(S, K, r, T, precioCall, precioPut, tolerancia) {
const referencia = S - K * Math.exp(-r * T);
const observado = precioCall - precioPut;
const desvio = observado - referencia;
if (Math.abs(desvio) <= tolerancia) {
return { nombre: 'Paridad put-call (C − P)', observado, referencia, desvio, veredicto: 'Sin señal', accion: '' };
}
if (desvio > 0) {
return { nombre: 'Paridad put-call (C − P)', observado, referencia, desvio, veredicto: 'Conversion', accion: 'vender la call, comprar la put, comprar el subyacente y financiar PV(K)' };
}
return { nombre: 'Paridad put-call (C − P)', observado, referencia, desvio, veredicto: 'Reverse conversion', accion: 'comprar la call, vender la put, vender el subyacente en corto e invertir PV(K)' };
}
function detectarCotas(S, K, r, T, precioCall, precioPut, tolerancia) {
const pvK = K * Math.exp(-r * T);
const filas = [
{ nombre: 'Cota inferior call (C)', observado: precioCall, referencia: Math.max(S - pvK, 0), requisito: 'minimo', accion: 'comprar la call, vender el subyacente en corto e invertir PV(K)' },
{ nombre: 'Cota inferior put (P)', observado: precioPut, referencia: Math.max(pvK - S, 0), requisito: 'minimo', accion: 'comprar la put, comprar el subyacente y financiar PV(K)' },
{ nombre: 'Tope call (C ≤ S)', observado: precioCall, referencia: S, requisito: 'maximo', accion: 'vender la call y comprar el subyacente' },
{ nombre: 'Tope put (P ≤ PV(K))', observado: precioPut, referencia: pvK, requisito: 'maximo', accion: 'vender la put e invertir PV(K)' }
];
return filas.map(fila => {
const desvio = fila.observado - fila.referencia;
const violada = fila.requisito === 'minimo' ? desvio < -tolerancia : desvio > tolerancia;
return { nombre: fila.nombre, observado: fila.observado, referencia: fila.referencia, desvio, veredicto: violada ? 'Violación' : 'Sin señal', accion: violada ? fila.accion : '' };
});
}
function detectarForward(S, r, T, precioForward, tolerancia) {
const referencia = S * Math.exp(r * T);
const desvio = precioForward - referencia;
if (Math.abs(desvio) <= tolerancia) {
return { nombre: 'Forward (F vs S·e^(rT))', observado: precioForward, referencia, desvio, veredicto: 'Sin señal', accion: '' };
}
if (desvio > 0) {
return { nombre: 'Forward (F vs S·e^(rT))', observado: precioForward, referencia, desvio, veredicto: 'Cash-and-carry', accion: 'comprar el subyacente financiado y vender el forward' };
}
return { nombre: 'Forward (F vs S·e^(rT))', observado: precioForward, referencia, desvio, veredicto: 'Reverse cash-and-carry', accion: 'vender el subyacente en corto, invertir el efectivo y comprar el forward' };
}
function mostrarCasos(titulo, detecciones) {
detecciones.forEach(deteccion => {
const estado = deteccion.veredicto === 'Sin señal' ? 'sin señal' : 'SEÑAL: ' + deteccion.veredicto;
console.log(titulo + ' | ' + deteccion.nombre + ' | desvío ' + deteccion.desvio.toFixed(4) + ' | ' + estado);
});
}
const senales = detecciones => detecciones.filter(deteccion => deteccion.veredicto !== 'Sin señal');
mostrarCasos('Base', [
detectarParidad(100, 100, 0.05, 1, 10.45, 5.57, 0.01),
detectarForward(100, 0.05, 1, 105.13, 0.01)
]);
mostrarCasos('Call 11.50', senales([detectarParidad(100, 100, 0.05, 1, 11.50, 5.57, 0.01)]));
mostrarCasos('Call 9.50', senales([detectarParidad(100, 100, 0.05, 1, 9.50, 5.57, 0.01)]));
mostrarCasos('Call 4.00', senales(detectarCotas(100, 100, 0.05, 1, 4.00, 5.57, 0.01)));
mostrarCasos('S=90, put 4.00', senales(detectarCotas(90, 100, 0.05, 1, 5.0912, 4.00, 0.01)));
mostrarCasos('Call 101.00', senales(detectarCotas(100, 100, 0.05, 1, 101, 5.57, 0.01)));
mostrarCasos('Forward 107', senales([detectarForward(100, 0.05, 1, 107, 0.01)]));
mostrarCasos('Forward 103', senales([detectarForward(100, 0.05, 1, 103, 0.01)]));Salida esperada:
Base | Forward (F vs S·e^(rT)) | desvío 0.0029 | sin señal
Call 11.50 | Paridad put-call (C − P) | desvío 1.0529 | SEÑAL: Conversion
Call 9.50 | Paridad put-call (C − P) | desvío -0.9471 | SEÑAL: Reverse conversion
Call 4.00 | Cota inferior call (C) | desvío -0.8771 | SEÑAL: Violación
S=90, put 4.00 | Cota inferior put (P) | desvío -1.1229 | SEÑAL: Violación
Call 101.00 | Tope call (C ≤ S) | desvío 1.0000 | SEÑAL: Violación
Forward 107 | Forward (F vs S·e^(rT)) | desvío 1.8729 | SEÑAL: Cash-and-carry
Forward 103 | Forward (F vs S·e^(rT)) | desvío -2.1271 | SEÑAL: Reverse cash-and-carry
Dos observaciones sobre la batería. Primero: los dos casos base quedan en «sin señal» —el mercado que reproduce el modelo no dispara nada— y los seis restantes producen exactamente la señal buscada, incluida la dirección (a favor o en contra de la call, del forward caro o barato). Segundo: el caso de la put con S = 90 comprueba que la cota inferior de la put también vive del descuento: con S por debajo de PV(K) = 95,1229, la cota deja de ser cero y aparece la violación de −1,1229 en la put de 4,00.
27.9 Validación permanente del modelo
Antes de detectar nada, el valuador se valida a sí mismo. La rutina validarModelo del tema 25 —los siete chequeos de no arbitraje del paso, convergencia, paridad, cotas y ejercicio anticipado— se incorpora tal cual, como control de calidad permanente. Cada vez que se cambian los parámetros del modelo hay que volver a correrla: un detector sobre un modelo descalibrado inventa señales.
function normalEstandar(x) {
const t = 1 / (1 + 0.2316419 * Math.abs(x));
const densidad = Math.exp(-x * x / 2) / Math.sqrt(2 * Math.PI);
const polinomio = t * (0.319381530 + t * (-0.356563782 + t * (1.781477937 + t * (-1.821255978 + t * 1.330274429))));
const acumulada = 1 - densidad * polinomio;
return x >= 0 ? acumulada : 1 - acumulada;
}
function bsPrecio(S, K, r, sigma, T, tipo) {
const d1 = (Math.log(S / K) + (r + sigma * sigma / 2) * T) / (sigma * Math.sqrt(T));
const d2 = d1 - sigma * Math.sqrt(T);
return tipo === 'call'
? S * normalEstandar(d1) - K * Math.exp(-r * T) * normalEstandar(d2)
: K * Math.exp(-r * T) * normalEstandar(-d2) - S * normalEstandar(-d1);
}
function pagoDeOpcion(tipo, strike) {
return tipo === 'call'
? (precio) => Math.max(precio - strike, 0)
: (precio) => Math.max(strike - precio, 0);
}
function valorarArbol(spot, u, d, p, descuento, pasos, pago, estilo) {
let valores = [];
for (let j = 0; j <= pasos; j++) {
valores.push(pago(spot * Math.pow(u, j) * Math.pow(d, pasos - j)));
}
const americana = estilo === 'americana';
for (let i = pasos - 1; i >= 0; i--) {
for (let j = 0; j <= i; j++) {
const continuacion = descuento * (p * valores[j + 1] + (1 - p) * valores[j]);
if (americana) {
const precio = spot * Math.pow(u, j) * Math.pow(d, i - j);
valores[j] = Math.max(continuacion, pago(precio));
} else {
valores[j] = continuacion;
}
}
}
return valores[0];
}
function arbolCRR(spot, strike, tasa, volatilidad, tiempo, pasos, tipo, estilo) {
const dt = tiempo / pasos;
const u = Math.exp(volatilidad * Math.sqrt(dt));
const d = 1 / u;
const g = Math.exp(tasa * dt);
const p = (g - d) / (u - d);
return valorarArbol(spot, u, d, p, 1 / g, pasos, pagoDeOpcion(tipo, strike), estilo);
}
function validarModelo(S, K, tasa, volatilidad, tiempo, pasos) {
const dt = tiempo / pasos;
const u = Math.exp(volatilidad * Math.sqrt(dt));
const d = 1 / u;
const g = Math.exp(tasa * dt);
const p = (g - d) / (u - d);
const descuentoStrike = K * Math.exp(-tasa * tiempo);
const callEuropea = arbolCRR(S, K, tasa, volatilidad, tiempo, pasos, 'call', 'europea');
const putEuropea = arbolCRR(S, K, tasa, volatilidad, tiempo, pasos, 'put', 'europea');
const callAmericana = arbolCRR(S, K, tasa, volatilidad, tiempo, pasos, 'call', 'americana');
const putAmericana = arbolCRR(S, K, tasa, volatilidad, tiempo, pasos, 'put', 'americana');
const bs = bsPrecio(S, K, tasa, volatilidad, tiempo, 'call');
return [
['No arbitraje del paso (p en (0, 1))', p.toFixed(6), p > 0 && p < 1],
['Convergencia a Black-Scholes', Math.abs(callEuropea - bs).toFixed(4), Math.abs(callEuropea - bs) < 0.05],
['Paridad put-call europea (árbol)', Math.abs((callEuropea - putEuropea) - (S - descuentoStrike)).toFixed(8), Math.abs((callEuropea - putEuropea) - (S - descuentoStrike)) < 0.000001],
['Cota inferior de la call (árbol)', (callEuropea - Math.max(S - descuentoStrike, 0)).toFixed(4), callEuropea - Math.max(S - descuentoStrike, 0) > -0.000001],
['Cota inferior de la put (árbol)', (putEuropea - Math.max(descuentoStrike - S, 0)).toFixed(4), putEuropea - Math.max(descuentoStrike - S, 0) > -0.000001],
['Call americana = europea', Math.abs(callAmericana - callEuropea).toFixed(8), Math.abs(callAmericana - callEuropea) < 0.000001],
['Put americana mayor o igual que europea', (putAmericana - putEuropea).toFixed(4), putAmericana - putEuropea > -0.000001]
];
}
validarModelo(100, 100, 0.05, 0.2, 1, 100).forEach(fila => {
console.log((fila[2] ? 'OK' : 'REVISAR') + ' | ' + fila[0] + ': ' + fila[1]);
});Salida esperada: siete líneas OK —No arbitraje del paso: 0.507502, Convergencia a Black-Scholes: 0.0200, Paridad put-call europea (árbol): 0.00000000, Cota inferior de la call: 5.5536, Cota inferior de la put: 5.5536, Call americana = europea: 0.00000000 y Put americana ≥ europea: 0.5288—. Son los mismos números del tema 25: no se reescribió la validación, se reutiliza.
El gancho con el detector es directo: si esta rutina devuelve REVISAR en cualquier línea, el informe debe ignorarse. La validación responde «¿el modelo es confiable?»; el detector responde «¿el mercado es consistente con el modelo?». Mezclar el orden de las preguntas es el error conceptual más grave de un valuador automático.
27.10 El panel de riesgos de la posición
La mitad del informe valora y detecta; la otra mitad mide riesgo. Las griegas del tema 26 se agregan por pata con las mismas reglas del tema 24: signo de la posición (larga +1, corta −1) por cantidad, sumando delta, gamma, vega y theta de cada instrumento. Con eso, cualquier estructura de la biblioteca tiene su perfil de riesgo en una tabla.
function normalEstandar(x) {
const t = 1 / (1 + 0.2316419 * Math.abs(x));
const densidad = Math.exp(-x * x / 2) / Math.sqrt(2 * Math.PI);
const polinomio = t * (0.319381530 + t * (-0.356563782 + t * (1.781477937 + t * (-1.821255978 + t * 1.330274429))));
const acumulada = 1 - densidad * polinomio;
return x >= 0 ? acumulada : 1 - acumulada;
}
function densidadNormal(x) {
return Math.exp(-x * x / 2) / Math.sqrt(2 * Math.PI);
}
function griegasBS(S, K, r, sigma, T, tipo) {
const d1 = (Math.log(S / K) + (r + sigma * sigma / 2) * T) / (sigma * Math.sqrt(T));
const d2 = d1 - sigma * Math.sqrt(T);
const nd1 = normalEstandar(d1);
const nd2 = normalEstandar(d2);
const descuento = Math.exp(-r * T);
const raizT = Math.sqrt(T);
const terminoTiempo = -S * densidadNormal(d1) * sigma / (2 * raizT);
const vega = S * densidadNormal(d1) * raizT;
const gamma = densidadNormal(d1) / (S * sigma * raizT);
if (tipo === 'call') {
return { precio: S * nd1 - K * descuento * nd2, delta: nd1, gamma, vega, theta: terminoTiempo - r * K * descuento * nd2 };
}
return { precio: K * descuento * (1 - nd2) - S * (1 - nd1), delta: nd1 - 1, gamma, vega, theta: terminoTiempo + r * K * descuento * (1 - nd2) };
}
function griegasEstrategia(patas, S, r, sigma, T) {
return patas.reduce((acumulado, pata) => {
const g = griegasBS(S, pata.K, r, sigma, T, pata.tipo);
const factor = (pata.posicion === 'larga' ? 1 : -1) * pata.cantidad;
return {
delta: acumulado.delta + factor * g.delta,
gamma: acumulado.gamma + factor * g.gamma,
vega: acumulado.vega + factor * g.vega,
theta: acumulado.theta + factor * g.theta
};
}, { delta: 0, gamma: 0, vega: 0, theta: 0 });
}
const bull = [
{ tipo: 'call', posicion: 'larga', K: 100, prima: 10.4506, cantidad: 1 },
{ tipo: 'call', posicion: 'corta', K: 110, prima: 6.0401, cantidad: 1 }
];
const bear = [
{ tipo: 'put', posicion: 'larga', K: 110, prima: 10.6753, cantidad: 1 },
{ tipo: 'put', posicion: 'corta', K: 100, prima: 5.5735, cantidad: 1 }
];
const straddle = [
{ tipo: 'call', posicion: 'larga', K: 100, prima: 10.4506, cantidad: 1 },
{ tipo: 'put', posicion: 'larga', K: 100, prima: 5.5735, cantidad: 1 }
];
const mariposa = [
{ tipo: 'call', posicion: 'larga', K: 90, prima: 16.6994, cantidad: 1 },
{ tipo: 'call', posicion: 'corta', K: 100, prima: 10.4506, cantidad: 2 },
{ tipo: 'call', posicion: 'larga', K: 110, prima: 6.0401, cantidad: 1 }
];
[['Bull call spread', bull], ['Bear put spread', bear], ['Straddle', straddle], ['Mariposa', mariposa]].forEach(([nombre, patas]) => {
const g = griegasEstrategia(patas, 100, 0.05, 0.2, 1);
console.log(nombre + ': delta ' + g.delta.toFixed(4) + ' | gamma ' + g.gamma.toFixed(4) + ' | vega/pt ' + (g.vega / 100).toFixed(4) + ' | theta/día ' + (g.theta / 365).toFixed(4));
});Salida esperada (los valores del tema 26, ahora calculados pata por pata con la biblioteca del tema 24):
Bear put spread: delta -0.1872 | gamma 0.0010 | vega/pt 0.0205 | theta/día 0.0027
Straddle: delta 0.2737 | gamma 0.0375 | vega/pt 0.7505 | theta/día -0.0221
Mariposa: delta -0.0143 | gamma -0.0042 | vega/pt -0.0831 | theta/día 0.0027
| Estrategia | Delta | Gamma | Vega (por punto) | Theta (por día) |
|---|---|---|---|---|
| Bull call spread | 0,1872 | −0,0010 | −0,0205 | −0,0014 |
| Bear put spread | −0,1872 | 0,0010 | 0,0205 | 0,0027 |
| Straddle | 0,2737 | 0,0375 | 0,7505 | −0,0221 |
| Mariposa | −0,0143 | −0,0042 | −0,0831 | 0,0027 |
El panel se lee como en el tema 26: el bull spread reduce la exposición direccional a casi un tercio de la call sola y queda casi neutro en volatilidad; el bear spread es su espejo exacto (los signos se invierten, los módulos son los mismos); el straddle es la estructura de volatilidad pura —gamma y vega máximas, theta la más negativa—; y la mariposa queda prácticamente neutra en delta, vende dispersión (gamma y vega negativas) y cobra tiempo (theta positiva). Con el valuador integrado, cambiar un strike y ver moverse la tabla es cuestión de segundos.
27.11 El informe del valuador
Con todas las piezas, el informe se arma como una función que recorre el flujo completo. Su estructura es fija:
- Una línea de parámetros: S, K, r, σ, T y N.
- Los precios del modelo: Black-Scholes (call y put), las cuatro ramas del árbol (europeas y americanas) y el forward, con su valor y su precio de entrega.
- La validación: los siete chequeos del apartado 27.9.
- La detección de arbitrajes: seis verificaciones contra las cotizaciones, cada una con observado, referencia, desvío y veredicto; cuando hay señal, también la acción sugerida.
function normalEstandar(x) {
const t = 1 / (1 + 0.2316419 * Math.abs(x));
const densidad = Math.exp(-x * x / 2) / Math.sqrt(2 * Math.PI);
const polinomio = t * (0.319381530 + t * (-0.356563782 + t * (1.781477937 + t * (-1.821255978 + t * 1.330274429))));
const acumulada = 1 - densidad * polinomio;
return x >= 0 ? acumulada : 1 - acumulada;
}
function bsPrecio(S, K, r, sigma, T, tipo) {
const d1 = (Math.log(S / K) + (r + sigma * sigma / 2) * T) / (sigma * Math.sqrt(T));
const d2 = d1 - sigma * Math.sqrt(T);
return tipo === 'call'
? S * normalEstandar(d1) - K * Math.exp(-r * T) * normalEstandar(d2)
: K * Math.exp(-r * T) * normalEstandar(-d2) - S * normalEstandar(-d1);
}
function pagoDeOpcion(tipo, strike) {
return tipo === 'call'
? (precio) => Math.max(precio - strike, 0)
: (precio) => Math.max(strike - precio, 0);
}
function valorarArbol(spot, u, d, p, descuento, pasos, pago, estilo) {
let valores = [];
for (let j = 0; j <= pasos; j++) {
valores.push(pago(spot * Math.pow(u, j) * Math.pow(d, pasos - j)));
}
const americana = estilo === 'americana';
for (let i = pasos - 1; i >= 0; i--) {
for (let j = 0; j <= i; j++) {
const continuacion = descuento * (p * valores[j + 1] + (1 - p) * valores[j]);
if (americana) {
const precio = spot * Math.pow(u, j) * Math.pow(d, i - j);
valores[j] = Math.max(continuacion, pago(precio));
} else {
valores[j] = continuacion;
}
}
}
return valores[0];
}
function arbolCRR(spot, strike, tasa, volatilidad, tiempo, pasos, tipo, estilo) {
const dt = tiempo / pasos;
const u = Math.exp(volatilidad * Math.sqrt(dt));
const d = 1 / u;
const g = Math.exp(tasa * dt);
const p = (g - d) / (u - d);
return valorarArbol(spot, u, d, p, 1 / g, pasos, pagoDeOpcion(tipo, strike), estilo);
}
function valorForward(S, K, r, T) {
return S - K * Math.exp(-r * T);
}
function detectarParidad(S, K, r, T, precioCall, precioPut, tolerancia) {
const referencia = S - K * Math.exp(-r * T);
const observado = precioCall - precioPut;
const desvio = observado - referencia;
if (Math.abs(desvio) <= tolerancia) {
return { nombre: 'Paridad put-call (C − P)', observado, referencia, desvio, veredicto: 'Sin señal', accion: '' };
}
if (desvio > 0) {
return { nombre: 'Paridad put-call (C − P)', observado, referencia, desvio, veredicto: 'Conversion', accion: 'vender la call, comprar la put, comprar el subyacente y financiar PV(K)' };
}
return { nombre: 'Paridad put-call (C − P)', observado, referencia, desvio, veredicto: 'Reverse conversion', accion: 'comprar la call, vender la put, vender el subyacente en corto e invertir PV(K)' };
}
function detectarCotas(S, K, r, T, precioCall, precioPut, tolerancia) {
const pvK = K * Math.exp(-r * T);
const filas = [
{ nombre: 'Cota inferior call (C)', observado: precioCall, referencia: Math.max(S - pvK, 0), requisito: 'minimo', accion: 'comprar la call, vender el subyacente en corto e invertir PV(K)' },
{ nombre: 'Cota inferior put (P)', observado: precioPut, referencia: Math.max(pvK - S, 0), requisito: 'minimo', accion: 'comprar la put, comprar el subyacente y financiar PV(K)' },
{ nombre: 'Tope call (C ≤ S)', observado: precioCall, referencia: S, requisito: 'maximo', accion: 'vender la call y comprar el subyacente' },
{ nombre: 'Tope put (P ≤ PV(K))', observado: precioPut, referencia: pvK, requisito: 'maximo', accion: 'vender la put e invertir PV(K)' }
];
return filas.map(fila => {
const desvio = fila.observado - fila.referencia;
const violada = fila.requisito === 'minimo' ? desvio < -tolerancia : desvio > tolerancia;
return { nombre: fila.nombre, observado: fila.observado, referencia: fila.referencia, desvio, veredicto: violada ? 'Violación' : 'Sin señal', accion: violada ? fila.accion : '' };
});
}
function detectarForward(S, r, T, precioForward, tolerancia) {
const referencia = S * Math.exp(r * T);
const desvio = precioForward - referencia;
if (Math.abs(desvio) <= tolerancia) {
return { nombre: 'Forward (F vs S·e^(rT))', observado: precioForward, referencia, desvio, veredicto: 'Sin señal', accion: '' };
}
if (desvio > 0) {
return { nombre: 'Forward (F vs S·e^(rT))', observado: precioForward, referencia, desvio, veredicto: 'Cash-and-carry', accion: 'comprar el subyacente financiado y vender el forward' };
}
return { nombre: 'Forward (F vs S·e^(rT))', observado: precioForward, referencia, desvio, veredicto: 'Reverse cash-and-carry', accion: 'vender el subyacente en corto, invertir el efectivo y comprar el forward' };
}
function validarModelo(S, K, tasa, volatilidad, tiempo, pasos) {
const dt = tiempo / pasos;
const u = Math.exp(volatilidad * Math.sqrt(dt));
const d = 1 / u;
const g = Math.exp(tasa * dt);
const p = (g - d) / (u - d);
const descuentoStrike = K * Math.exp(-tasa * tiempo);
const callEuropea = arbolCRR(S, K, tasa, volatilidad, tiempo, pasos, 'call', 'europea');
const putEuropea = arbolCRR(S, K, tasa, volatilidad, tiempo, pasos, 'put', 'europea');
const callAmericana = arbolCRR(S, K, tasa, volatilidad, tiempo, pasos, 'call', 'americana');
const putAmericana = arbolCRR(S, K, tasa, volatilidad, tiempo, pasos, 'put', 'americana');
const bs = bsPrecio(S, K, tasa, volatilidad, tiempo, 'call');
return [
['No arbitraje del paso (p en (0, 1))', p.toFixed(6), p > 0 && p < 1],
['Convergencia a Black-Scholes', Math.abs(callEuropea - bs).toFixed(4), Math.abs(callEuropea - bs) < 0.05],
['Paridad put-call europea (árbol)', Math.abs((callEuropea - putEuropea) - (S - descuentoStrike)).toFixed(8), Math.abs((callEuropea - putEuropea) - (S - descuentoStrike)) < 0.000001],
['Cota inferior de la call (árbol)', (callEuropea - Math.max(S - descuentoStrike, 0)).toFixed(4), callEuropea - Math.max(S - descuentoStrike, 0) > -0.000001],
['Cota inferior de la put (árbol)', (putEuropea - Math.max(descuentoStrike - S, 0)).toFixed(4), putEuropea - Math.max(descuentoStrike - S, 0) > -0.000001],
['Call americana = europea', Math.abs(callAmericana - callEuropea).toFixed(8), Math.abs(callAmericana - callEuropea) < 0.000001],
['Put americana mayor o igual que europea', (putAmericana - putEuropea).toFixed(4), putAmericana - putEuropea > -0.000001]
];
}
function informeValuador(datos, mercado) {
const S = datos.S;
const K = datos.K;
const tasa = datos.tasa;
const volatilidad = datos.volatilidad;
const tiempo = datos.tiempo;
const pasos = datos.pasos;
const tolerancia = 0.01;
const callBS = bsPrecio(S, K, tasa, volatilidad, tiempo, 'call');
const putBS = bsPrecio(S, K, tasa, volatilidad, tiempo, 'put');
const callE = arbolCRR(S, K, tasa, volatilidad, tiempo, pasos, 'call', 'europea');
const putE = arbolCRR(S, K, tasa, volatilidad, tiempo, pasos, 'put', 'europea');
const callA = arbolCRR(S, K, tasa, volatilidad, tiempo, pasos, 'call', 'americana');
const putA = arbolCRR(S, K, tasa, volatilidad, tiempo, pasos, 'put', 'americana');
console.log('=== Valuador de derivados: informe ===');
console.log('Parámetros: S=' + S + ' | K=' + K + ' | r=' + (tasa * 100).toFixed(1) + '% | sigma=' + (volatilidad * 100).toFixed(1) + '% | T=' + tiempo + ' | N=' + pasos);
console.log('Black-Scholes (europeas): call ' + callBS.toFixed(4) + ' | put ' + putBS.toFixed(4));
console.log('Árbol CRR: call europea ' + callE.toFixed(4) + ' | put europea ' + putE.toFixed(4) + ' | call americana ' + callA.toFixed(4) + ' | put americana ' + putA.toFixed(4));
console.log('Forward: valor del contrato largo ' + valorForward(S, K, tasa, tiempo).toFixed(4) + ' | precio teórico de entrega ' + (S * Math.exp(tasa * tiempo)).toFixed(4));
console.log('--- Validación del modelo (árbol) ---');
validarModelo(S, K, tasa, volatilidad, tiempo, pasos).forEach(fila => {
console.log((fila[2] ? 'OK' : 'REVISAR') + ' | ' + fila[0] + ': ' + fila[1]);
});
console.log('--- Detección de arbitrajes (cotizaciones de mercado) ---');
const detecciones = [detectarParidad(S, K, tasa, tiempo, mercado.call, mercado.put, tolerancia)]
.concat(detectarCotas(S, K, tasa, tiempo, mercado.call, mercado.put, tolerancia))
.concat([detectarForward(S, tasa, tiempo, mercado.forward, tolerancia)]);
detecciones.forEach(deteccion => {
const estado = deteccion.veredicto === 'Sin señal' ? 'OK' : 'SEÑAL';
let linea = estado + ' | ' + deteccion.nombre + ': observado ' + deteccion.observado.toFixed(4) + ' | referencia ' + deteccion.referencia.toFixed(4) + ' | desvío ' + deteccion.desvio.toFixed(4);
if (deteccion.veredicto !== 'Sin señal') {
linea += ' → ' + deteccion.veredicto + ' (' + deteccion.accion + ')';
}
console.log(linea);
});
}
const datos = { S: 100, K: 100, tasa: 0.05, volatilidad: 0.2, tiempo: 1, pasos: 100 };
informeValuador(datos, { call: 11.50, put: 5.57, forward: 107 });Se ejecuta con un mercado deliberadamente distorsionado (call 11,50; put 5,57; forward 107) para ver las señales en acción. Salida esperada:
Parámetros: S=100 | K=100 | r=5.0% | sigma=20.0% | T=1 | N=100
Black-Scholes (europeas): call 10.4506 | put 5.5735
Árbol CRR: call europea 10.4306 | put europea 5.5536 | call americana 10.4306 | put americana 6.0824
Forward: valor del contrato largo 4.8771 | precio teórico de entrega 105.1271
--- Validación del modelo (árbol) ---
OK | No arbitraje del paso (p en (0, 1)): 0.507502
OK | Convergencia a Black-Scholes: 0.0200
OK | Paridad put-call europea (árbol): 0.00000000
OK | Cota inferior de la call (árbol): 5.5536
OK | Cota inferior de la put (árbol): 5.5536
OK | Call americana = europea: 0.00000000
OK | Put americana mayor o igual que europea: 0.5288
--- Detección de arbitrajes (cotizaciones de mercado) ---
SEÑAL | Paridad put-call (C − P): observado 5.9300 | referencia 4.8771 | desvío 1.0529 → Conversion (vender la call, comprar la put, comprar el subyacente y financiar PV(K))
OK | Cota inferior call (C): observado 11.5000 | referencia 4.8771 | desvío 6.6229
OK | Cota inferior put (P): observado 5.5700 | referencia 0.0000 | desvío 5.5700
OK | Tope call (C ≤ S): observado 11.5000 | referencia 100.0000 | desvío -88.5000
OK | Tope put (P ≤ PV(K)): observado 5.5700 | referencia 95.1229 | desvío -89.5529
SEÑAL | Forward (F vs S·e^(rT)): observado 107.0000 | referencia 105.1271 | desvío 1.8729 → Cash-and-carry (comprar el subyacente financiado y vender el forward)
Con cotizaciones en línea —call 10,45, put 5,57 y forward 105,13, los valores del laboratorio— las trece verificaciones (siete del modelo y seis de mercado) quedan en OK. Con el mercado distorsionado aparecen exactamente las dos señales construidas en los apartados anteriores: la conversion de la paridad (+1,0529) y el cash-and-carry del forward (+1,8729). El informe no decide por el usuario: mide, compara y propone; la ejecución —con costos, garantías y disponibilidad de títulos— queda del lado humano.
27.12 Laboratorio: el valuador con detección de arbitrajes
El laboratorio reúne todo lo anterior en una sola pantalla. Los seis primeros controles son del modelo (spot, strike, tasa, volatilidad, tiempo y pasos); siguen el tipo y el estilo de la opción, y las tres cotizaciones de mercado: prima de la call, prima de la put y precio del forward. Las cajas muestran el valor de Black-Scholes del tipo elegido, el valor del árbol con el estilo elegido, el delta y el precio forward teórico S·e^(rT). La primera tabla corre la validación completa del modelo; la segunda ejecuta el detector contra las cotizaciones, con la tolerancia fija de un centavo.
Valuador de derivados
Black-Scholes y árbol CRR con validación permanente y detección de arbitrajes sobre las cotizaciones ingresadas.
| Chequeo | Valor observado | Estado |
|---|
| Verificación | Observado | Referencia | Desvío | Veredicto |
|---|
Árbol N=100: call europea 10.4306; BS europea 10.4506; delta 0.6368. Validación del modelo: 7/7 OK. Mercado sin señales: paridad, cotas, topes y forward dentro de la tolerancia de 0.01.
Provocá las señales: subí la prima de la call a 11,50 y mirá la fila de la paridad (desvío +1,0529, conversion); bajala a 4,00 y mirá la cota inferior (violación de −0,8771); subí el forward a 107 (cash-and-carry de +1,8729) o bajalo a 103 (reverse de −2,1271). Después volvé a los valores en línea y comprobá que todo queda «sin señal»: ese ida y vuelta es la mejor prueba de que el detector distingue la inconsistencia del simple desacuerdo entre modelo y mercado.
27.13 Errores comunes y controles
Los tropiezos más frecuentes al integrar los módulos en un valuador:
- Llamar arbitraje a la diferencia entre modelo y mercado: son dos cosas distintas. El arbitraje vive entre instrumentos (paridad, cotas, carry); el desacuerdo modelo-mercado vive en los supuestos.
- Aplicar la paridad con opciones americanas: con americanas la igualdad es una banda; el detector debe recibir cotizaciones europeas para el chequeo exacto.
- Mezclar convenciones de capitalización: el detector usa
e^(−rT), como los temas 23 a 26. Con las tasas efectivas de los temas 8 a 12 hay que convertir primero:e^(−rT) = 1 / (1 + i)nconila tasa equivalente del período. - Tolerancia cero: las cotizaciones llegan redondeadas; sin tolerancia (0,01) el detector marca señales por centavos de redondeo.
- Ignorar costos y requisitos: la señal es teórica hasta que se descuentan comisiones, spread, préstamo de títulos y garantías.
- Olvidar el escalado: todas las cifras son por unidad; multiplicar por la cantidad de contratos antes de comparar contra un límite de riesgo.
- Correr el detector sin la validación: si
pno cae en (0, 1) o la paridad del árbol no cierra, el informe entero es humo. - Confundir los dos números del forward: valor del contrato (
S − K·e^(−rT)) contra precio de entrega teórico (S·e^(rT)). - Reutilizar nombres entre bloques: cada bloque del curso ejecuta aislado; la biblioteca completa se repite cuando hace falta.
Lista de control del valuador:
- ¿La calibración pasa los siete chequeos de
validarModelo? - ¿La tolerancia está definida, documentada y es coherente con el redondeo de las cotizaciones?
- ¿Las cotizaciones son del mismo subyacente, vencimiento y estilo que el modelo?
- ¿La capitalización y las unidades son consistentes en todos los módulos?
- ¿Cada señal informa desvío, dirección y estrategia?
- ¿El informe separa lo que dice el modelo de lo que dice el mercado?
- ¿Las ganancias están escaladas a la posición y netas de costos antes de decidir?
Alcance educativo: el valuador implementa los modelos del curso con supuestos simplificados (volatilidad constante, sin costos, sin dividendos, sin márgenes). Una señal del detector no es una recomendación: es una inconsistencia a auditar. La decisión y sus consecuencias siguen siendo del usuario.
27.14 Ejercicio propuesto: el mercado distorsionado
Usá los datos de referencia (S = K = 100, r = 5 % continua, σ = 20 %, T = 1) y supongá que el mercado cotiza así: call 11,50; put 5,57; forward 107. El modelo, ya validado, dice: call 10,4506; put 5,5735; forward teórico 105,1271.
- Calculá el valor de referencia de la paridad
S − K·e^(−rT)y el desvío de la relación observadaC − P. - Indicá la estrategia de conversion y cuánto asegura hoy y al vencimiento.
- Verificá que las cotas y los topes de ambas opciones se respetan con estos precios.
- Calculá el desvío del forward y la ganancia del cash-and-carry en T y su equivalente hoy.
- ¿Por qué las dos señales pueden convivir sin contradicción?
Ver orientación de resolución
La referencia de la paridad es 100 − 95,1229 = 4,8771 y C − P = 5,93: el desvío es +1,0529 (también se lee como el sobreprecio de la call, 1,0494, más el subprecio de la put, 0,0035). La estrategia de conversion —vender la call, comprar la put, comprar el subyacente y financiar PV(K)— cobra los 5,93 y paga 4,8771 por sus propios flujos: asegura 1,0529 hoy, equivalentes a 1,1069 al vencimiento. Las cotas se respetan con margen amplio: la call (11,50) supera su cota de 4,8771 y la put (5,57) la suya de 0; los topes también (11,50 ≤ 100 y 5,57 ≤ 95,1229). El forward queda +1,8729 sobre el teórico: financiar la compra del subyacente y vender el forward asegura 1,8729 en T (1,7815 hoy). Las dos señales conviven porque viven en instrumentos distintos: la primera se ejecuta con opciones, la segunda con spot y forward; que el mercado esté «raro» en dos segmentos a la vez es posible y el informe debe reportar ambas.
function detectarParidad(S, K, r, T, precioCall, precioPut, tolerancia) {
const referencia = S - K * Math.exp(-r * T);
const observado = precioCall - precioPut;
const desvio = observado - referencia;
if (Math.abs(desvio) <= tolerancia) {
return { nombre: 'Paridad put-call (C − P)', observado, referencia, desvio, veredicto: 'Sin señal', accion: '' };
}
if (desvio > 0) {
return { nombre: 'Paridad put-call (C − P)', observado, referencia, desvio, veredicto: 'Conversion', accion: 'vender la call, comprar la put, comprar el subyacente y financiar PV(K)' };
}
return { nombre: 'Paridad put-call (C − P)', observado, referencia, desvio, veredicto: 'Reverse conversion', accion: 'comprar la call, vender la put, vender el subyacente en corto e invertir PV(K)' };
}
function detectarForward(S, r, T, precioForward, tolerancia) {
const referencia = S * Math.exp(r * T);
const desvio = precioForward - referencia;
if (Math.abs(desvio) <= tolerancia) {
return { nombre: 'Forward (F vs S·e^(rT))', observado: precioForward, referencia, desvio, veredicto: 'Sin señal', accion: '' };
}
if (desvio > 0) {
return { nombre: 'Forward (F vs S·e^(rT))', observado: precioForward, referencia, desvio, veredicto: 'Cash-and-carry', accion: 'comprar el subyacente financiado y vender el forward' };
}
return { nombre: 'Forward (F vs S·e^(rT))', observado: precioForward, referencia, desvio, veredicto: 'Reverse cash-and-carry', accion: 'vender el subyacente en corto, invertir el efectivo y comprar el forward' };
}
const paridad = detectarParidad(100, 100, 0.05, 1, 11.50, 5.57, 0.01);
console.log(paridad.veredicto + ' | desvío ' + paridad.desvio.toFixed(4)); // "Conversion | desvío 1.0529"
console.log('ganancia asegurada hoy ' + paridad.desvio.toFixed(4) + ' | al vencimiento ' + (paridad.desvio * Math.exp(0.05)).toFixed(4)); // "ganancia asegurada hoy 1.0529 | al vencimiento 1.1069"
const forward = detectarForward(100, 0.05, 1, 107, 0.01);
console.log(forward.veredicto + ' | desvío ' + forward.desvio.toFixed(4)); // "Cash-and-carry | desvío 1.8729"
console.log('ganancia en T ' + forward.desvio.toFixed(4) + ' | equivalente hoy ' + (forward.desvio * Math.exp(-0.05)).toFixed(4)); // "ganancia en T 1.8729 | equivalente hoy 1.7815"27.15 Ejercicio adicional: las cotas rotas
Usá los datos de referencia (S = K = 100, r = 5 %, T = 1) salvo donde se indica, y supongá estas dos cotizaciones imposibles:
- Caso A: la call cotiza 4,00. Calculá su cota inferior
max(S − K·e^(−rT), 0), el desvío y describí la cartera que lo captura. - Caso B: con S = 90 y K = 100, la put cotiza 4,00. Calculá su cota
max(K·e^(−rT) − S, 0), el desvío y describí la cartera. - ¿Por qué en ambos casos la ganancia queda fijada hoy y no depende de
ST? - ¿Qué datos de ejecución revisarías antes de operar cualquiera de las dos?
Ver orientación de resolución
Caso A: la cota vale 4,8771 y la call cotiza 4,00: violación de −0,8771. La cartera es comprar la call, vender el subyacente en corto e invertir PV(K) = 95,1229: hoy entra 100 + 0 − 4,00 − 95,1229 = +0,8771; al vencimiento el payoff max(ST − 100, 0) más la inversión de 100 recompran el corto con resultado max(100 − ST, 0) ≥ 0. Caso B: la cota vale 95,1229 − 90 = 5,1229 y la put cotiza 4,00: violación de −1,1229. La cartera es comprar la put, comprar el subyacente y financiarse con un préstamo de PV(K): hoy entra 95,1229 − 4,00 − 90 = +1,1229; al vencimiento la put y la acción valen al menos K = 100 y repagan el préstamo: nunca sobra deuda. En ambos casos la ganancia es fijada hoy porque la cartera ya quedó armada completa: no queda ninguna exposición que dependa del precio final. Antes de operar hay que revisar que exista título para el corto (caso A), el costo del préstamo (caso B), las comisiones y el spread de las tres o cuatro patas: la ganancia de 0,8771 o 1,1229 por unidad es teórica hasta descontarlos.
function detectarCotas(S, K, r, T, precioCall, precioPut, tolerancia) {
const pvK = K * Math.exp(-r * T);
const filas = [
{ nombre: 'Cota inferior call (C)', observado: precioCall, referencia: Math.max(S - pvK, 0), requisito: 'minimo', accion: 'comprar la call, vender el subyacente en corto e invertir PV(K)' },
{ nombre: 'Cota inferior put (P)', observado: precioPut, referencia: Math.max(pvK - S, 0), requisito: 'minimo', accion: 'comprar la put, comprar el subyacente y financiar PV(K)' },
{ nombre: 'Tope call (C ≤ S)', observado: precioCall, referencia: S, requisito: 'maximo', accion: 'vender la call y comprar el subyacente' },
{ nombre: 'Tope put (P ≤ PV(K))', observado: precioPut, referencia: pvK, requisito: 'maximo', accion: 'vender la put e invertir PV(K)' }
];
return filas.map(fila => {
const desvio = fila.observado - fila.referencia;
const violada = fila.requisito === 'minimo' ? desvio < -tolerancia : desvio > tolerancia;
return { nombre: fila.nombre, observado: fila.observado, referencia: fila.referencia, desvio, veredicto: violada ? 'Violación' : 'Sin señal', accion: violada ? fila.accion : '' };
});
}
const callBarata = detectarCotas(100, 100, 0.05, 1, 4.00, 5.57, 0.01)[0];
console.log(callBarata.veredicto + ' | desvío ' + callBarata.desvio.toFixed(4)); // "Violación | desvío -0.8771"
console.log('ganancia asegurada hoy ' + (-callBarata.desvio).toFixed(4)); // "ganancia asegurada hoy 0.8771"
const putBarata = detectarCotas(90, 100, 0.05, 1, 5.0912, 4.00, 0.01)[1];
console.log(putBarata.veredicto + ' | desvío ' + putBarata.desvio.toFixed(4)); // "Violación | desvío -1.1229"
console.log('ganancia asegurada hoy ' + (-putBarata.desvio).toFixed(4)); // "ganancia asegurada hoy 1.1229"27.16 Próximo paso: del curso al sistema propio
El recorrido queda completo. Los cinco módulos del curso convergen en el valuador: los fundamentos y el no arbitraje (módulo 01) le dieron la lógica; los forwards y futuros (02), el carry y el cash-and-carry; las opciones y sus relaciones (03), la paridad, las cotas y las estrategias; los modelos (04), Black-Scholes y el árbol; y la implementación (05), las piezas de software validadas.
Valuación
Black-Scholes para europeas, árbol CRR para americanas y cualquier payoff, con validación permanente contra la paridad, las cotas y la convergencia.
Estrategias
El modelo de patas del tema 24: payoffs, costos netos, equilibrios y griegas agregadas sobre cualquier combinación de calls, puts y forwards.
Riesgo
Griegas y escenarios del tema 26 dentro del mismo panel: la posición se valora, se estresa y se cubre con una sola calibración.
Arbitraje
Paridad, cotas, topes y precio forward como chequeos automáticos: el programa deja de ser una calculadora y pasa a ser un auditor.
Trazabilidad: cada línea del informe se puede rastrear hasta un teorema del curso —el no arbitraje del paso al tema 25, la paridad al 17, el carry al 12, la prima de ejercicio anticipado al 22—. Esa trazabilidad, más que el código, es el producto del proyecto.
De acá en adelante, el orden de trabajo razonable es:
- Datos reales: curvas de tasas, dividendos y volatilidad implícita calibrada del propio mercado, en lugar de la σ constante de los ejemplos.
- Costos de ejecución: comisiones, spread, préstamo de títulos y garantías, para separar las señales teóricas de las operables.
- Más instrumentos: forwards con rentas (tema 9), futuros con ajuste diario (tema 11), barreras y otras estructuras sin fórmula, valoradas con el mismo árbol.
- Riesgo de modelo: escenarios sobre los propios supuestos (caída de liquidez, saltos, cambios de régimen) además de los escenarios de mercado del tema 26.
- Ingeniería: tests automáticos de la batería del apartado 27.8 en cada cambio, y separación estricta entre motor de valuación, datos de mercado y presentación.
El valuador no es el final del camino: es el punto de partida del próximo proyecto. Todo lo que sigue —feed de datos, ejecución, carteras completas— se construye sobre las mismas reglas de este curso: escribir los flujos, verificar que se replican, y no creerle a ningún número que no pase las pruebas.