27. Proyecto integrador: valuador de derivados con detección de arbitrajes

El cierre del curso: un valuador que reúne la fórmula de Black-Scholes, el árbol validado, la biblioteca de estrategias y un detector de arbitrajes que vigila la paridad, las cotas y el precio forward, con laboratorio interactivo.

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.

ARMARla posición como lista de patas del tema 24
VALORARcon Black-Scholes o con el árbol CRR
VERIFICARvalidación del modelo y detección de arbitrajes
INFORMARreporte, panel de riesgos y acción sugerida

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.

Los módulos del valuador y su tema de origen.
MóduloQué aportaOrigen
Biblioteca de patasEl modelo de datos { tipo, posicion, K, prima, cantidad } y los payoffs de opciones y forwardsTema 24
Motor Black-ScholesPrecio de calls y puts europeas; griegas en forma cerradaTemas 23 y 26
Motor de árbol CRREuropeas y americanas sin fórmula; valores de controlTemas 21, 22 y 25
Validador del modeloNo arbitraje del paso, paridad, cotas, ejercicio anticipado y convergenciaTema 25
Detector de arbitrajesParidad, cotas, topes y precio forward contra cotizaciones de mercadoTemas 12, 16 y 17
Panel de griegasDelta, gamma, vega y theta agregados por posiciónTema 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.

Cada instrumento, su motor de valuación y su dato de mercado comparable (valores con S = K = 100, r = 5 %, σ = 20 %, T = 1).
InstrumentoMotor de valuaciónValor del modeloDato de mercado comparable
Call europeaBlack-Scholes (tema 23)10,4506Prima cotizada C
Put europeaBlack-Scholes (tema 23)5,5735Prima cotizada P
Call o put americanaÁrbol CRR (temas 22 y 25)Put: 6,0824Prima cotizada (orientativa: sin paridad)
ForwardValor 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:

C − P = 11,50 − 5,57 = 5,93    S − K·e^(−rT) = 100 − 95,1229 = 4,8771    desvío = +1,0529 = 1,0494 + 0,0035

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).

Las relaciones de no arbitraje y la operación que las explota cuando se rompen.
RelaciónCondición de no arbitrajeSi la cotización la rompe
Paridad put-call europeaC − 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 callC ≥ max(S − PV(K), 0)Comprar la call, vender el subyacente en corto e invertir PV(K)
Cota inferior de la putP ≥ max(PV(K) − S, 0)Comprar la put, comprar el subyacente y financiar PV(K)
TopesC ≤ S y P ≤ PV(K)Vender la call y comprar el subyacente; o vender la put e invertir PV(K)
Precio forwardF = 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.

La batería de casos construidos del detector (datos de referencia S = K = 100, r = 5 %, T = 1; tolerancia 0,01).
CasoCotizacionesResultado esperadoEstrategia
Base en líneaC = 10,45; P = 5,57; F = 105,13Sin señal en paridad y forward—
Call sobrevaluadaC = 11,50; P = 5,57Paridad +1,0529 → conversionVender call, comprar put, comprar subyacente y financiar PV(K)
Call subvaluadaC = 9,50; P = 5,57Paridad −0,9471 → reverse conversionComprar call, vender put, vender subyacente en corto e invertir PV(K)
Call bajo la cotaC = 4,00Cota inferior −0,8771 → violaciónComprar call, vender subyacente en corto e invertir PV(K)
Put bajo la cota (S = 90)P = 4,00Cota inferior −1,1229 → violaciónComprar put, comprar subyacente y financiar PV(K)
Call sobre el topeC = 101,00Tope +1,0000 → violaciónVender call y comprar subyacente
Forward caroF = 107,00+1,8729 → cash-and-carryComprar subyacente financiado y vender forward
Forward baratoF = 103,00−2,1271 → reverse cash-and-carryVender 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 | Paridad put-call (C − P) | desvío 0.0029 | sin señal
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):

Bull call spread: delta 0.1872 | gamma -0.0010 | vega/pt -0.0205 | theta/día -0.0014
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
Griegas agregadas de las cuatro estructuras (por unidad; S = 100, r = 5 %, σ = 20 %, T = 1).
EstrategiaDeltaGammaVega (por punto)Theta (por día)
Bull call spread0,1872−0,0010−0,0205−0,0014
Bear put spread−0,18720,00100,02050,0027
Straddle0,27370,03750,7505−0,0221
Mariposa−0,0143−0,0042−0,08310,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:

  1. Una línea de parámetros: S, K, r, σ, T y N.
  2. 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.
  3. La validación: los siete chequeos del apartado 27.9.
  4. 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:

=== Valuador de derivados: informe ===
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.

Black-Scholes (europea)$10,45
Árbol CRR (tipo y estilo)$10,43
Delta (BS)0.6368
Forward teórico (S·e^(rT))$105,13
Validación del modelo con los parámetros actuales (árbol CRR).
ChequeoValor observadoEstado
Detección de arbitrajes contra las cotizaciones de mercado (tolerancia de un centavo).
VerificaciónObservadoReferenciaDesvíoVeredicto

Á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)n con i la 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 p no 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:

  1. ¿La calibración pasa los siete chequeos de validarModelo?
  2. ¿La tolerancia está definida, documentada y es coherente con el redondeo de las cotizaciones?
  3. ¿Las cotizaciones son del mismo subyacente, vencimiento y estilo que el modelo?
  4. ¿La capitalización y las unidades son consistentes en todos los módulos?
  5. ¿Cada señal informa desvío, dirección y estrategia?
  6. ¿El informe separa lo que dice el modelo de lo que dice el mercado?
  7. ¿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.

  1. Calculá el valor de referencia de la paridad S − K·e^(−rT) y el desvío de la relación observada C − P.
  2. Indicá la estrategia de conversion y cuánto asegura hoy y al vencimiento.
  3. Verificá que las cotas y los topes de ambas opciones se respetan con estos precios.
  4. Calculá el desvío del forward y la ganancia del cash-and-carry en T y su equivalente hoy.
  5. ¿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:

  1. 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.
  2. 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.
  3. ¿Por qué en ambos casos la ganancia queda fijada hoy y no depende de ST?
  4. ¿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.