24. Funciones para calcular payoffs y resultados de estrategias

Una biblioteca de funciones para describir estrategias como listas de patas: payoffs, costo neto, resultado, tablas de escenarios y puntos de equilibrio, listos para los temas de implementación.

24.1 De ejemplos sueltos a una biblioteca

En el tema 18 apareció por primera vez una función genérica que recorre una lista de patas y calcula el resultado de una combinación de opciones. Funcionaba, pero estaba limitada: solo aceptaba calls y puts, no contemplaba cantidades, y cada estrategia nueva obligaba a escribir ejemplos sueltos alrededor. Este tema convierte aquella idea en una biblioteca de funciones: piezas chicas y puras que describen los instrumentos, calculan payoffs y resultados, arman tablas de escenarios, detectan puntos de equilibrio y resumen una estrategia completa.

DATOSuna pata = { tipo, posición, K, prima, cantidad }
LISTAla estrategia es un array de patas
CÁLCULOpayoff y costo neto, pata por pata
SALIDAresultado, tabla de escenarios y equilibrios

Idea central: una pata es un objeto de datos; una estrategia es un array de patas; los cálculos son funciones puras que reciben esos datos y devuelven números. Ese trío —datos, lista y funciones— es todo el diseño de la biblioteca que van a reutilizar los temas 25 a 27.

24.2 El modelo de datos de una pata

Todo empieza por definir qué es una pata (en inglés, leg): la unidad mínima de una estrategia. Cada pata describe un instrumento y la posición que se tiene en él, con cinco campos:

Campos de una pata de estrategia.
CampoValores posiblesEjemplo
tipo'call', 'put' o 'forward''call'
posicion'larga' (compra) o 'corta' (venta)'larga'
Kstrike de la opción o precio pactado del forward100
primaprima por unidad, siempre mayor o igual que 0 (en forwards, 0)10,4506
cantidadunidades o contratos de la pata (1 por defecto)2

Dos convenciones importantes. La primera: la prima es siempre positiva; quien paga o cobra lo decide la posición (la pata larga paga la prima, la corta la cobra). La segunda: en un forward la prima es 0 —el contrato se celebra a valor cero— y K es el precio pactado. La cantidad multiplica todo el resultado de la pata y es clave en estructuras como la mariposa, donde una pata central aparece duplicada.

24.3 El pago de cada instrumento

La base de todo son tres funciones de pago (payoff) bruto por unidad: no incluyen primas, posición ni cantidad. Las opciones usan Math.max(...) porque el dueño no ejerce si el resultado sería negativo; el forward, en cambio, paga siempre, porque es una obligación lineal.

function pagoCall(spot, strike) { return Math.max(spot - strike, 0); }
function pagoPut(spot, strike) { return Math.max(strike - spot, 0); }
function pagoForward(spot, strike) { return spot - strike; }

console.log(pagoCall(130, 100));    // 30
console.log(pagoPut(80, 100));      // 20
console.log(pagoForward(130, 100)); // 30
console.log(pagoForward(90, 100));  // -10

La call paga la diferencia cuando el spot supera el strike; la put, cuando queda por debajo; el forward puede pagar en cualquier dirección, incluso negativa, como muestra el último caso.

24.4 El payoff de una pata: posición y cantidad

El pago del instrumento todavía no dice quién cobra ni cuánto se pagó por el contrato. Eso lo agrega el payoff de la pata: el signo de la posición (larga +1, corta −1) y la cantidad. El resultado es el flujo bruto con signo, todavía sin descontar primas.

function pagoCall(spot, strike) { return Math.max(spot - strike, 0); }
function pagoPut(spot, strike) { return Math.max(strike - spot, 0); }
function pagoForward(spot, strike) { return spot - strike; }

function pagoInstrumento(pata, precioFinal) {
  if (pata.tipo === 'call') return pagoCall(precioFinal, pata.K);
  if (pata.tipo === 'put') return pagoPut(precioFinal, pata.K);
  return pagoForward(precioFinal, pata.K);
}

function payoffPata(pata, precioFinal) {
  const signo = pata.posicion === 'larga' ? 1 : -1;
  return signo * pagoInstrumento(pata, precioFinal) * pata.cantidad;
}

const callLarga = { tipo: 'call', posicion: 'larga', K: 100, prima: 10.4506, cantidad: 1 };
const callCorta = { tipo: 'call', posicion: 'corta', K: 110, prima: 6.0401, cantidad: 1 };
const putLarga = { tipo: 'put', posicion: 'larga', K: 100, prima: 5.5735, cantidad: 1 };
const forwardCorto = { tipo: 'forward', posicion: 'corta', K: 100, prima: 0, cantidad: 1 };

console.log(payoffPata(callLarga, 130));    // 30
console.log(payoffPata(callCorta, 130));    // -20
console.log(payoffPata(putLarga, 80));      // 20
console.log(payoffPata(forwardCorto, 90));  // 10

Lectura de los casos: la call larga cobra 30; la call corta de strike 110 paga 20 (por eso el signo negativo: es una obligación); la put larga cobra 20; y el forward corto pactado en 100 gana 10 cuando el precio final cae a 90.

24.5 Costo neto y resultado de la estrategia

El costo neto es la suma algebraica de las primas de todas las patas: las largas desembolsan (suman) y las cortas cobran (restan). El resultado al vencimiento es entonces:

resultado = payoff de la estrategia − costo neto
costo neto = Σ (signo × prima × cantidad) sobre todas las patas

Con estas dos funciones, cualquier combinación se valora con la misma receta. Ejemplo: un bull call spread que compra la call de strike 100 y vende la de strike 110, con las primas del modelo Black-Scholes del tema 23.

function pagoCall(spot, strike) { return Math.max(spot - strike, 0); }
function pagoPut(spot, strike) { return Math.max(strike - spot, 0); }
function pagoForward(spot, strike) { return spot - strike; }

function pagoInstrumento(pata, precioFinal) {
  if (pata.tipo === 'call') return pagoCall(precioFinal, pata.K);
  if (pata.tipo === 'put') return pagoPut(precioFinal, pata.K);
  return pagoForward(precioFinal, pata.K);
}

function payoffPata(pata, precioFinal) {
  const signo = pata.posicion === 'larga' ? 1 : -1;
  return signo * pagoInstrumento(pata, precioFinal) * pata.cantidad;
}

function payoffEstrategia(patas, precioFinal) {
  return patas.reduce((total, pata) => total + payoffPata(pata, precioFinal), 0);
}

function costoNeto(patas) {
  return patas.reduce((total, pata) => {
    const signo = pata.posicion === 'larga' ? 1 : -1;
    return total + signo * pata.prima * pata.cantidad;
  }, 0);
}

function resultadoEstrategia(patas, precioFinal) {
  return payoffEstrategia(patas, precioFinal) - costoNeto(patas);
}

const bullCall = [
  { tipo: 'call', posicion: 'larga', K: 100, prima: 10.4506, cantidad: 1 },
  { tipo: 'call', posicion: 'corta', K: 110, prima: 6.0401, cantidad: 1 }
];

console.log(costoNeto(bullCall).toFixed(4));                // "4.4105"
console.log(resultadoEstrategia(bullCall, 90).toFixed(4));  // "-4.4105"
console.log(resultadoEstrategia(bullCall, 105).toFixed(4)); // "0.5895"
console.log(resultadoEstrategia(bullCall, 110).toFixed(4)); // "5.5895"
console.log(resultadoEstrategia(bullCall, 120).toFixed(4)); // "5.5895"

El spread pagó 10,4506 por la call comprada y cobró 6,0401 por la vendida: costo neto 4,4105. Si el precio final queda en 100 o menos, ambas calls vencen sin valor y la pérdida es el costo (−4,4105). Desde 110 en adelante el payoff se estabiliza en 10 (la call comprada paga 10 más que la vendida) y la ganancia es 5,5895. La transición es lineal, y el equilibrio cae en 100 + 4,4105 = 104,4105.

24.6 La tabla de escenarios

Con resultadoEstrategia disponible, generar una tabla de escenarios es recorrer una lista de precios finales y guardar el resultado de cada uno. La salida queda como una lista de filas { precio, resultado }, lista para imprimir, graficar o analizar.

function pagoCall(spot, strike) { return Math.max(spot - strike, 0); }
function pagoPut(spot, strike) { return Math.max(strike - spot, 0); }
function pagoForward(spot, strike) { return spot - strike; }

function pagoInstrumento(pata, precioFinal) {
  if (pata.tipo === 'call') return pagoCall(precioFinal, pata.K);
  if (pata.tipo === 'put') return pagoPut(precioFinal, pata.K);
  return pagoForward(precioFinal, pata.K);
}

function payoffPata(pata, precioFinal) {
  const signo = pata.posicion === 'larga' ? 1 : -1;
  return signo * pagoInstrumento(pata, precioFinal) * pata.cantidad;
}

function payoffEstrategia(patas, precioFinal) {
  return patas.reduce((total, pata) => total + payoffPata(pata, precioFinal), 0);
}

function costoNeto(patas) {
  return patas.reduce((total, pata) => {
    const signo = pata.posicion === 'larga' ? 1 : -1;
    return total + signo * pata.prima * pata.cantidad;
  }, 0);
}

function resultadoEstrategia(patas, precioFinal) {
  return payoffEstrategia(patas, precioFinal) - costoNeto(patas);
}

function tablaEscenarios(patas, precios) {
  return precios.map(precio => ({ precio, resultado: resultadoEstrategia(patas, precio) }));
}

const straddle = [
  { tipo: 'call', posicion: 'larga', K: 100, prima: 10.4506, cantidad: 1 },
  { tipo: 'put', posicion: 'larga', K: 100, prima: 5.5735, cantidad: 1 }
];

tablaEscenarios(straddle, [80, 90, 100, 110, 120]).forEach(fila => {
  console.log(fila.precio, fila.resultado.toFixed(4));
});

Salida del bloque: 80 3.9759, 90 -6.0241, 100 -16.0241, 110 -6.0241 y 120 3.9759. El straddle compró la call y la put del mismo strike: costo 16,0241. La tabla completa, en formato de lectura:

Resultado de un straddle largo en 100 (call comprada a 10,4506 y put comprada a 5,5735) según el precio final.
Precio final (ST)Resultado del straddle
7013,9759
803,9759
90−6,0241
100−16,0241
110−6,0241
1203,9759
13013,9759

La estructura pierde en la zona central —si el precio final queda cerca de 100, ambas opciones vencen sin valor— y gana cuando el precio se aleja en cualquier dirección. La pérdida máxima es el costo total, 16,0241, justo cuando ST = 100.

24.7 Puntos de equilibrio sin resolver ecuaciones

El punto de equilibrio (break-even) es el precio final donde el resultado vale cero. Como las estrategias están armadas con instrumentos lineales a tramos, alcanza con recorrer el rango precio a precio, detectar los cambios de signo e interpolar linealmente dentro de cada tramo.

function pagoCall(spot, strike) { return Math.max(spot - strike, 0); }
function pagoPut(spot, strike) { return Math.max(strike - spot, 0); }
function pagoForward(spot, strike) { return spot - strike; }

function pagoInstrumento(pata, precioFinal) {
  if (pata.tipo === 'call') return pagoCall(precioFinal, pata.K);
  if (pata.tipo === 'put') return pagoPut(precioFinal, pata.K);
  return pagoForward(precioFinal, pata.K);
}

function payoffPata(pata, precioFinal) {
  const signo = pata.posicion === 'larga' ? 1 : -1;
  return signo * pagoInstrumento(pata, precioFinal) * pata.cantidad;
}

function payoffEstrategia(patas, precioFinal) {
  return patas.reduce((total, pata) => total + payoffPata(pata, precioFinal), 0);
}

function costoNeto(patas) {
  return patas.reduce((total, pata) => {
    const signo = pata.posicion === 'larga' ? 1 : -1;
    return total + signo * pata.prima * pata.cantidad;
  }, 0);
}

function resultadoEstrategia(patas, precioFinal) {
  return payoffEstrategia(patas, precioFinal) - costoNeto(patas);
}

function puntosEquilibrio(patas, desde, hasta, paso) {
  const equilibrios = [];
  let precioAnterior = desde;
  let resultadoAnterior = resultadoEstrategia(patas, desde);
  if (resultadoAnterior === 0) equilibrios.push(desde);
  const cantidadPasos = Math.round((hasta - desde) / paso);
  for (let i = 1; i <= cantidadPasos; i++) {
    const precio = desde + i * paso;
    const resultado = resultadoEstrategia(patas, precio);
    if (resultado === 0) {
      equilibrios.push(precio);
    } else if (resultadoAnterior * resultado < 0) {
      const cruce = precioAnterior + (precio - precioAnterior) * (-resultadoAnterior) / (resultado - resultadoAnterior);
      equilibrios.push(cruce);
    }
    precioAnterior = precio;
    resultadoAnterior = resultado;
  }
  return equilibrios;
}

const straddle = [
  { tipo: 'call', posicion: 'larga', K: 100, prima: 10.4506, cantidad: 1 },
  { tipo: 'put', posicion: 'larga', K: 100, prima: 5.5735, cantidad: 1 }
];

console.log(puntosEquilibrio(straddle, 60, 140, 5).map(e => e.toFixed(4)).join(', ')); // "83.9759, 116.0241"

Con un paso fino, los cruces quedan exactos: como el resultado es lineal entre strikes, la interpolación dentro del tramo es la solución exacta, no una aproximación. El straddle tiene dos equilibrios simétricos, 83,9759 y 116,0241 (100 ∓ 16,0241 de costo). Entre ambos pierde; fuera de ellos, gana.

24.8 Cantidades y proporciones: la mariposa

La cantidad permite armar estructuras con proporciones, donde una pata aparece repetida. El ejemplo clásico es la mariposa (butterfly) con calls: una call comprada en el strike inferior, dos calls vendidas en el strike central y una call comprada en el strike superior. La cantidad 2 de la pata central es lo que hace que el payoff se anule en los extremos y pague en el centro.

function pagoCall(spot, strike) { return Math.max(spot - strike, 0); }
function pagoPut(spot, strike) { return Math.max(strike - spot, 0); }
function pagoForward(spot, strike) { return spot - strike; }

function pagoInstrumento(pata, precioFinal) {
  if (pata.tipo === 'call') return pagoCall(precioFinal, pata.K);
  if (pata.tipo === 'put') return pagoPut(precioFinal, pata.K);
  return pagoForward(precioFinal, pata.K);
}

function payoffPata(pata, precioFinal) {
  const signo = pata.posicion === 'larga' ? 1 : -1;
  return signo * pagoInstrumento(pata, precioFinal) * pata.cantidad;
}

function payoffEstrategia(patas, precioFinal) {
  return patas.reduce((total, pata) => total + payoffPata(pata, precioFinal), 0);
}

function costoNeto(patas) {
  return patas.reduce((total, pata) => {
    const signo = pata.posicion === 'larga' ? 1 : -1;
    return total + signo * pata.prima * pata.cantidad;
  }, 0);
}

function resultadoEstrategia(patas, precioFinal) {
  return payoffEstrategia(patas, precioFinal) - costoNeto(patas);
}

function puntosEquilibrio(patas, desde, hasta, paso) {
  const equilibrios = [];
  let precioAnterior = desde;
  let resultadoAnterior = resultadoEstrategia(patas, desde);
  if (resultadoAnterior === 0) equilibrios.push(desde);
  const cantidadPasos = Math.round((hasta - desde) / paso);
  for (let i = 1; i <= cantidadPasos; i++) {
    const precio = desde + i * paso;
    const resultado = resultadoEstrategia(patas, precio);
    if (resultado === 0) {
      equilibrios.push(precio);
    } else if (resultadoAnterior * resultado < 0) {
      const cruce = precioAnterior + (precio - precioAnterior) * (-resultadoAnterior) / (resultado - resultadoAnterior);
      equilibrios.push(cruce);
    }
    precioAnterior = precio;
    resultadoAnterior = resultado;
  }
  return equilibrios;
}

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 }
];

console.log(costoNeto(mariposa).toFixed(4)); // "1.8383"
[90, 95, 100, 105, 110].forEach(precio => {
  console.log(precio, resultadoEstrategia(mariposa, precio).toFixed(4));
});
console.log(puntosEquilibrio(mariposa, 80, 120, 0.5).map(e => e.toFixed(4)).join(', ')); // "91.8383, 108.1617"

La mariposa cuesta 1,8383 (16,6994 − 2 × 10,4506 + 6,0401). Su payoff se anula fuera del intervalo [90, 110], alcanza 10 en el centro y el resultado máximo, 8,1617, se logra con ST = 100. Los equilibrios quedan en 91,8383 y 108,1617: pagar tan poco por una estructura limitada tiene como contrapartida que solo gana si el precio converge al centro.

24.9 El resumen de la estrategia

Para etiquetar una estrategia de un vistazo falta un último bloque: el resumen. La función recorre un rango de precios, calcula los resultados y devuelve un objeto con la ganancia máxima, la pérdida máxima y los equilibrios:

resumenEstrategia(patas, desde, hasta, paso) → { maxGanancia, maxPerdida, equilibrios }

Por ejemplo, para el straddle en el rango de 60 a 140 con paso 5: ganancia máxima 23,9759 (en 60 y en 140, los extremos del rango), pérdida máxima −16,0241 (en el centro) y equilibrios en 83,9759 y 116,0241. Es exactamente la información que resume el laboratorio y que en el tema 26 se usará para analizar sensibilidad.

Máximo en el rango, no absoluto: «máxima ganancia» y «máxima pérdida» se refieren a los precios explorados. En un bull call spread, por ejemplo, la ganancia tope se alcanza desde el strike superior en adelante: si el rango no lo incluye, el resumen no lo ve.

24.10 La biblioteca completa

Estas son, juntas, las once funciones de la biblioteca. Cada bloque del curso se ejecuta aislado, así que esta versión completa es la que se debe copiar cuando se quiera reutilizar todo el conjunto.

function pagoCall(spot, strike) { return Math.max(spot - strike, 0); }
function pagoPut(spot, strike) { return Math.max(strike - spot, 0); }
function pagoForward(spot, strike) { return spot - strike; }

function pagoInstrumento(pata, precioFinal) {
  if (pata.tipo === 'call') return pagoCall(precioFinal, pata.K);
  if (pata.tipo === 'put') return pagoPut(precioFinal, pata.K);
  return pagoForward(precioFinal, pata.K);
}

function payoffPata(pata, precioFinal) {
  const signo = pata.posicion === 'larga' ? 1 : -1;
  return signo * pagoInstrumento(pata, precioFinal) * pata.cantidad;
}

function payoffEstrategia(patas, precioFinal) {
  return patas.reduce((total, pata) => total + payoffPata(pata, precioFinal), 0);
}

function costoNeto(patas) {
  return patas.reduce((total, pata) => {
    const signo = pata.posicion === 'larga' ? 1 : -1;
    return total + signo * pata.prima * pata.cantidad;
  }, 0);
}

function resultadoEstrategia(patas, precioFinal) {
  return payoffEstrategia(patas, precioFinal) - costoNeto(patas);
}

function tablaEscenarios(patas, precios) {
  return precios.map(precio => ({ precio, resultado: resultadoEstrategia(patas, precio) }));
}

function puntosEquilibrio(patas, desde, hasta, paso) {
  const equilibrios = [];
  let precioAnterior = desde;
  let resultadoAnterior = resultadoEstrategia(patas, desde);
  if (resultadoAnterior === 0) equilibrios.push(desde);
  const cantidadPasos = Math.round((hasta - desde) / paso);
  for (let i = 1; i <= cantidadPasos; i++) {
    const precio = desde + i * paso;
    const resultado = resultadoEstrategia(patas, precio);
    if (resultado === 0) {
      equilibrios.push(precio);
    } else if (resultadoAnterior * resultado < 0) {
      const cruce = precioAnterior + (precio - precioAnterior) * (-resultadoAnterior) / (resultado - resultadoAnterior);
      equilibrios.push(cruce);
    }
    precioAnterior = precio;
    resultadoAnterior = resultado;
  }
  return equilibrios;
}

function resumenEstrategia(patas, desde, hasta, paso) {
  let maxGanancia = -Infinity;
  let maxPerdida = Infinity;
  const cantidadPasos = Math.round((hasta - desde) / paso);
  for (let i = 0; i <= cantidadPasos; i++) {
    const resultado = resultadoEstrategia(patas, desde + i * paso);
    maxGanancia = Math.max(maxGanancia, resultado);
    maxPerdida = Math.min(maxPerdida, resultado);
  }
  return { maxGanancia, maxPerdida, equilibrios: puntosEquilibrio(patas, desde, hasta, paso) };
}

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 resumen = resumenEstrategia(straddle, 60, 140, 5);
console.log(resumen.maxGanancia.toFixed(4)); // "23.9759"
console.log(resumen.maxPerdida.toFixed(4));  // "-16.0241"
console.log(resumen.equilibrios.map(e => e.toFixed(4)).join(', ')); // "83.9759, 116.0241"

Con esta biblioteca, describir una estrategia es declarar sus patas y elegir un rango de precios: todo lo demás —payoff, costo, resultado, tabla, equilibrios y resumen— sale de las funciones. Los temas siguientes la ponen a trabajar sobre modelos de valoración completos.

24.11 Laboratorio: constructor de estrategias

Elegí una estrategia y sus parámetros: el laboratorio arma la lista de patas, muestra el costo neto, el máximo y el mínimo en el rango explorado, una tabla de escenarios con payoff y resultado por unidad, y los puntos de equilibrio calculados con la función del apartado 24.7. El rango se construye alrededor del strike principal: de K1/2 a K1 × 1,5, en pasos de K1/20.

Convenciones de los campos: en el bull call spread, prima 1 ↔ K1 y prima 2 ↔ K2; en el bear put spread se compra la put de K2 (mayor) y se vende la de K1 (menor); en el straddle, prima 1 es la call y prima 2 la put, ambas de K1; en el strangle, prima 1 ↔ put de K1 y prima 2 ↔ call de K2; en la mariposa, prima 3 ↔ K3 y la pata central de K2 va con cantidad 2. El forward usa K1 y no usa primas. Los valores iniciales corresponden a las primas de Black-Scholes del tema 23 redondeadas a dos decimales.

Constructor de estrategias

Opciones europeas y forwards, un subyacente, un vencimiento, resultados por unidad.

Costo neto (por unidad)$4,41
Máximo en el rango$5,59
Mínimo en el rango-$4,41
Escenarios: payoff y resultado de la estrategia por unidad.
Precio final (ST)PayoffResultado

Costo neto: pagás $4,41. Equilibrios en el rango explorado: $104,41. Estrategia: call larga K=$100 (prima $10,45) + call corta K=$110 (prima $6,04).

24.12 Errores comunes y controles

Los errores que más aparecen al implementar estrategias:

  • Confundir payoff con resultado: el payoff no incluye las primas. La tabla del laboratorio muestra ambos justamente para que se vea que resultado = payoff − costo neto.
  • Equivocar el signo de la posición: una pata corta invierte el payoff. El error clásico es restar la prima de las cortas en lugar de sumarla (las cortas cobran).
  • Olvidar la cantidad: en la mariposa, la pata central es ×2. Sin ese 2, la estructura deja de anularse en los extremos.
  • Leer los máximos como absolutos: el resumen explora un rango finito; fuera de él, la ganancia o la pérdida pueden seguir creciendo.
  • Reutilizar nombres entre bloques: cada bloque del curso se ejecuta aislado; al copiar una función hay que redefinirla completa en el bloque nuevo (por eso la biblioteca se repite en los ejercicios).
  • No validar entradas: un NaN en cualquier campo contamina todos los resultados; el laboratorio avisa y muestra «No disponible» cuando detecta datos inválidos.

Lista de control de la biblioteca:

  1. ¿Cada pata tiene tipo, posicion, K, prima y cantidad?
  2. ¿Las primas están en positivo y la posición decide quién paga y quién cobra?
  3. ¿El resultado se calcula como payoff menos costo neto?
  4. ¿El rango de exploración contiene los strikes relevantes?
  5. ¿Los equilibrios detectados coinciden con los cambios de signo de la tabla?
  6. ¿Los valores son por unidad del subyacente? (Al multiplicar por contratos se escala toda la posición.)

Alcance educativo: los resultados ignoran comisiones, márgenes, garantías y el valor tiempo de las primas (no se descuentan al vencimiento); son comparaciones directas por unidad, útiles para comparar estructuras. Nada de esto constituye una recomendación de inversión.

24.13 Ejercicio propuesto: la mariposa paso a paso

Usando la biblioteca del apartado 24.10, armá una mariposa (butterfly) con calls de strikes 90, 100 y 110 —la pata central con cantidad 2— y las primas de Black-Scholes del tema 23: 16,6994, 10,4506 y 6,0401 respectivamente.

  1. Escribí el array de patas con sus cantidades y calculá el costo neto.
  2. Calculá el resultado en 90, 95, 100, 105 y 110.
  3. Encontrá los puntos de equilibrio con puntosEquilibrio(patas, 80, 120, 0.5).
  4. Verificá que la ganancia máxima es 10 − costo y que se alcanza en 100.
Ver orientación de resolución

Costo neto: 16,6994 − 2 × 10,4506 + 6,0401 = 1,8383. Resultados: 90 → −1,8383; 95 → 3,1617; 100 → 8,1617 (máximo = 10 − 1,8383); 105 → 3,1617; 110 → −1,8383. Equilibrios: 91,8383 y 108,1617, que también se leen como 90 + costo y 110 − costo.

function pagoCall(spot, strike) { return Math.max(spot - strike, 0); }
function pagoPut(spot, strike) { return Math.max(strike - spot, 0); }
function pagoForward(spot, strike) { return spot - strike; }

function pagoInstrumento(pata, precioFinal) {
  if (pata.tipo === 'call') return pagoCall(precioFinal, pata.K);
  if (pata.tipo === 'put') return pagoPut(precioFinal, pata.K);
  return pagoForward(precioFinal, pata.K);
}

function payoffPata(pata, precioFinal) {
  const signo = pata.posicion === 'larga' ? 1 : -1;
  return signo * pagoInstrumento(pata, precioFinal) * pata.cantidad;
}

function payoffEstrategia(patas, precioFinal) {
  return patas.reduce((total, pata) => total + payoffPata(pata, precioFinal), 0);
}

function costoNeto(patas) {
  return patas.reduce((total, pata) => {
    const signo = pata.posicion === 'larga' ? 1 : -1;
    return total + signo * pata.prima * pata.cantidad;
  }, 0);
}

function resultadoEstrategia(patas, precioFinal) {
  return payoffEstrategia(patas, precioFinal) - costoNeto(patas);
}

function puntosEquilibrio(patas, desde, hasta, paso) {
  const equilibrios = [];
  let precioAnterior = desde;
  let resultadoAnterior = resultadoEstrategia(patas, desde);
  if (resultadoAnterior === 0) equilibrios.push(desde);
  const cantidadPasos = Math.round((hasta - desde) / paso);
  for (let i = 1; i <= cantidadPasos; i++) {
    const precio = desde + i * paso;
    const resultado = resultadoEstrategia(patas, precio);
    if (resultado === 0) {
      equilibrios.push(precio);
    } else if (resultadoAnterior * resultado < 0) {
      const cruce = precioAnterior + (precio - precioAnterior) * (-resultadoAnterior) / (resultado - resultadoAnterior);
      equilibrios.push(cruce);
    }
    precioAnterior = precio;
    resultadoAnterior = resultado;
  }
  return equilibrios;
}

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 }
];

console.log(costoNeto(mariposa).toFixed(4));                    // "1.8383"
console.log(resultadoEstrategia(mariposa, 100).toFixed(4));     // "8.1617"
console.log(puntosEquilibrio(mariposa, 80, 120, 0.5).map(e => e.toFixed(4)).join(', ')); // "91.8383, 108.1617"

24.14 Ejercicio adicional: bear put spread

Armá un bear put spread —una estructura que gana cuando el precio baja— comprando la put de strike 110 (prima 10,6753) y vendiendo la put de strike 100 (prima 5,5735), ambas del tema 23.

  1. Calculá el costo neto de la estructura.
  2. Calculá el resultado en 90, 100, 105, 110 y 120.
  3. Encontrá el equilibrio con puntosEquilibrio(patas, 80, 120, 0.5) y comprobá que coincide con 110 − costo.
  4. ¿Cuál es la ganancia máxima de la estructura?
Ver orientación de resolución

Costo neto: 10,6753 − 5,5735 = 5,1018. Resultados: 90 → 4,8982; 100 → 4,8982 (máximo); 105 → −0,1018; 110 → −5,1018; 120 → −5,1018. Equilibrio: 104,8982 = 110 − 5,1018. Ganancia máxima: 10 − 5,1018 = 4,8982, cuando el precio final queda en 100 o por debajo: la put comprada paga 10 más que la vendida.

function pagoCall(spot, strike) { return Math.max(spot - strike, 0); }
function pagoPut(spot, strike) { return Math.max(strike - spot, 0); }
function pagoForward(spot, strike) { return spot - strike; }

function pagoInstrumento(pata, precioFinal) {
  if (pata.tipo === 'call') return pagoCall(precioFinal, pata.K);
  if (pata.tipo === 'put') return pagoPut(precioFinal, pata.K);
  return pagoForward(precioFinal, pata.K);
}

function payoffPata(pata, precioFinal) {
  const signo = pata.posicion === 'larga' ? 1 : -1;
  return signo * pagoInstrumento(pata, precioFinal) * pata.cantidad;
}

function payoffEstrategia(patas, precioFinal) {
  return patas.reduce((total, pata) => total + payoffPata(pata, precioFinal), 0);
}

function costoNeto(patas) {
  return patas.reduce((total, pata) => {
    const signo = pata.posicion === 'larga' ? 1 : -1;
    return total + signo * pata.prima * pata.cantidad;
  }, 0);
}

function resultadoEstrategia(patas, precioFinal) {
  return payoffEstrategia(patas, precioFinal) - costoNeto(patas);
}

function puntosEquilibrio(patas, desde, hasta, paso) {
  const equilibrios = [];
  let precioAnterior = desde;
  let resultadoAnterior = resultadoEstrategia(patas, desde);
  if (resultadoAnterior === 0) equilibrios.push(desde);
  const cantidadPasos = Math.round((hasta - desde) / paso);
  for (let i = 1; i <= cantidadPasos; i++) {
    const precio = desde + i * paso;
    const resultado = resultadoEstrategia(patas, precio);
    if (resultado === 0) {
      equilibrios.push(precio);
    } else if (resultadoAnterior * resultado < 0) {
      const cruce = precioAnterior + (precio - precioAnterior) * (-resultadoAnterior) / (resultado - resultadoAnterior);
      equilibrios.push(cruce);
    }
    precioAnterior = precio;
    resultadoAnterior = resultado;
  }
  return equilibrios;
}

const bearPut = [
  { tipo: 'put', posicion: 'larga', K: 110, prima: 10.6753, cantidad: 1 },
  { tipo: 'put', posicion: 'corta', K: 100, prima: 5.5735, cantidad: 1 }
];

console.log(costoNeto(bearPut).toFixed(4));                    // "5.1018"
console.log(resultadoEstrategia(bearPut, 100).toFixed(4));     // "4.8982"
console.log(puntosEquilibrio(bearPut, 80, 120, 0.5).map(e => e.toFixed(4)).join(', ')); // "104.8982"

24.15 Próximo paso: del payoff a la validación de modelos

La biblioteca queda lista para el resto del módulo de implementación. Sus usos inmediatos:

  • Tema 25: un árbol binomial de producción con validación sistemática. Los payoffs de las patas son el punto de partida del recorrido hacia atrás, y la biblioteca permitirá verificar paridad, ejercicio anticipado y convergencia contra Black-Scholes.
  • Tema 26: sensibilidad, escenarios y gestión de riesgos. Las tablas del apartado 24.6 y el resumen del 24.9 se usan como base para medir delta, gamma, vega y theta, y para comparar coberturas.
  • Tema 27: el proyecto integrador. El valuador de derivados con detección de arbitrajes combina el árbol, la fórmula de Black-Scholes y esta biblioteca de estrategias en un mismo programa.

Todas las funciones de este tema son puras: reciben datos y devuelven números, sin efectos colaterales. Esa propiedad es la que las hace tan reutilizables en lo que viene.