Intro
En este tutorial vamos a cubrir lo siguiente:
El modelo de Merton
Pricing de notas estructuradas
Elasticidad de precios de opciones
1 El modelo de Merton
En el tutorial pasado mencionamos que uno de los supuestos del modelo Black-Scholes es que la acción no reparte dividendos. Esto hace entonces que el modelo no sea certero para calcular el precio de una acción que sí distribuya dividendos. Para arreglar esto tenemos dos opciones:
Si hablamos de una acción individual, lo más fácil sería descontar el pago del dividendo al precio de la acción. Es decir, si la acción cuesta $100 dólares y sabemos que va a pagar un dividendo de $1 dólar en 1 mes, en vez de usar
spot = 100
en nuestra función usaríamosspot = spot_0 - div*exp(-rf*t_div)
dondespot_0 = 100
,div = 1
,t_div = 1/12
(1 mes que falta para que se pague el dividendo) yrf
es el risk-free rate. Este precio neto de dividendos es el que debemos usar en la fórmula de Black-Scholes en este caso.Si hablamos de un ETF, donde las acciones que lo componen van a estar pagando dividendos de manera continua a través del año, podemos usar el dividend yield del ETF y el modelo de Merton (que es un ajuste al modelo de Black-Scholes) para calcular el precio de la opción.
El caso de la acción individual queda muy claro, así que pasemos a explicar cómo funciona el modelo de Merton.
Según este modelo, el precio de un call y un put sobre un activo que paga dividendos de manera continua está dado por:
C = S*exp(-d*T)*N(d1) - K*exp(-r*T)*N(d2)
P = K*exp(-r*T)*N(-d2) - S*exp(-d*t)*N(-d1)
con d1 = [ln(S/K) + (r - d + 𝜎^2/2)*T]/(𝜎*T^1/2)
d2 = d1 - 𝜎*T^1/2
donde S : el precio de la acción
K : el strike de la opción
r : la tasa libre de riesgo
T : el tiempo a expiración de la opción
N(): el valor de la distribución normal estándar
𝜎 : la desviación estándar de los retornos logarítmicos
d : el dividend yield del activo
Podemos implementarlo de la siguiente manera en R (recuerden agregarlo a su archivo herramientas.R
):
merton.model <- function(strike, spot, rf = 0.02, t = 0,
sigma = 0.2, div = 0, tipo = "c") {
# Calcula el precio teórico de una opción usando el
# modelo de Merton (1973)
# Se necesita:
# - strike: strike de la opción
# - spot: precio spot del subyacente al momento de valuación
# - rf: tasa libre de riesgo (anualizada)
# - t: tiempo (en años) que falta para que expire la opción
# - sigma: volatilidad (anualizada) del subyacente
# - div: dividend yield del subyacente
# - tipo: "c" (call) o "p" (put)
# Se regresa:
# El precio de la opción según el modelo de Merton (1973)
tipo = tolower(tipo)
if (!(tipo %in% c("c", "p"))) {
stop("El tipo de la opción es incorrecto.
Elige uno de 'c' (call) o 'p' (put).")
}
if (t == 0) {
return(calcular.profit(strike, spot, precio = 0, tipo))
}
d1 <- (log(spot/strike) +
(rf - div + sigma^2/2)*t)/(sigma*sqrt(t))
d2 <- d1 - sigma*sqrt(t)
if (tipo == "c") {
return(spot*exp(-div*t)*pnorm(d1) -
strike*exp(-rf*t)*pnorm(d2))
} else {
return(strike*exp(-rf*t)*pnorm(-d2) -
spot*exp(-div*t)*pnorm(-d1))
}
}
Ahora podemos usar esta función para calcular precios de opciones sobre algún ETF de la siguiente manera (en el siguiente ejemplo usaré datos del SPY a 2022-01-16 para expiración dentro de un mes en 2022-02-16):
# 0. Limpiar la sesión
rm(list=ls())
if (names(dev.cur()) != "null device") {
dev.off()
}
cat("\014")
# 1. Cargar herramientas
source("scripts/herramientas.R")
# 2. Pricing de una opción con subyacente con div yield
spot <- 464.72
strike <- 465
t <- 1/12
rf <- 0.0005
div <- 0.013
sigma <- 0.13
call <- merton.model(strike, spot, rf, t, sigma, div, "c")
put <- merton.model(strike, spot, rf, t, sigma, div, "p")
Con estos datos, obtengo un precio para el call y put de:
Sin embargo, si usamos la volatilidad implícita que vimos el tutorial anterior para el corto plazo de ~16% obtenemos los siguientes precios:
Los cuales están muy cercanos a los valores reales para opciones que expiran dentro de un mes:
2 Pricing de notas estructuradas
“Nota estructurada” es el nombre que se le da a instrumentos de inversión compuestos de alguna combinación de bonos, acciones, derivados, monedas, etc. La idea es ofrecer perfiles de pago que no se encuentran en el mercado común para adaptarse a cualquier inversionista. Podemos calcular el precio “justo” de este tipo de productos con el modelo Black-Scholes antes de tomar una decisión de inversión. Veamos un ejemplo.
Imagina que un promotor de alguna casa de bolsa o el banco en el que guardes tu dinero te intenta vender un instrumento con las siguientes características:
Inversión inicial de $1,000 USD
A la vuelta de 5 años, te devuelvo tus $1,000 USD más el 50% del incremento (sólo participas en el upside, no hay pérdida para ti) del S&P 500
Suena a que es una muy buena oferta. Si el S&P 500 baja, en vez de perder dinero como sucedería si invierto directamente y compro un ETF ahora, no pierdo dinero sino que recibo mis $1,000 dólares de regreso. Si el S&P 500 sube, entonces recibiré 50% de ese rendimiento más mis $1,000 dólares originales. Vamos a ver si de verdad es tan bueno como suena.
La utilidad de este instrumento se puede escribir de la siguiente manera:
Utilidad = $1,000*[1 + 50%*max(S_T/S_0 - 1, 0)]
= $1,000 + $1,000*(50%/S_0)*max(S_T - S_0, 0)
donde S_0: el nivel del S&P 500 al momento de la compra
S_T: el nivel del S&P 500 5 años después de la compra
La primera parte del pago, los $1,000 USD de regreso, es simplemente el pago de un bono cupón cero. Puedo calcular su valor fácilmente con la tasa libre de riesgo correspondiente. La segunda parte contiene el pago de una opción call ATM = max(S_T - S_0, 0)
.
Veamos entonces con el siguiente script cuánto debería costar esta nota según el modelo Merton (usaremos este modelo para tomar en cuenta el dividend yield del S&P 500):
# 0. Limpiar la sesión
rm(list=ls())
if (names(dev.cur()) != "null device") {
dev.off()
}
cat("\014")
# 1. Cargar herramientas
source("scripts/herramientas.R")
# 2. Principal Protected, Upside Potential (PPUP)
# Parámetros de la nota
costo <- 1000
rf <- 0.0155
t <- 5
sigma <- .13
tasa_upside <- .5
div <- 0.013
spot <- 464.72
strike <- 464.72
# Valor del "bono" y la "opción" (bono + opción = nota)
bono <- exp(-rf*t)*costo
opcion <- costo*(tasa_upside/spot)*
merton.model(strike, spot, rf, t, sigma, div, "c")
valor <- bono + opcion
Según este modelo y usando datos del último año, el valor de la nota es de $982.19:
Y, por lo tanto, no deberíamos comprarla. Para que la nota valiera $1,000, la volatilidad del S&P 500 en los siguientes años debería ser de 17.35%:
# Vol. implícita para que la nota valga los $1,000
opcion_teorico <- (costo - bono)/costo/(tasa_upside/spot)
vol_imp <- vol.imp.merton(strike, spot, rf, t,
opcion_teorico, div, "c")
Donde vol.imp.merton()
es la siguiente función:
vol.imp.merton <- function(strike, spot, rf = 0.02, t = 0,
precio = 1, div = 0, tipo = "c") {
# Calcula la volatilidad implícita de una opción con el
# modelo de Merton (1973)
# Se necesita:
# - strike: strike de la opción
# - spot: precio spot del subyacente al momento de valuación
# - rf: tasa libre de riesgo (anualizada)
# - t: tiempo (en años) que falta para que expire la opción
# - precio: precio de la opción
# - tipo: "c" (call) o "p" (put)
# Se regresa:
# La volatilidad con la cual el modelo de Merton (1973)
# (merton.model) da el precio proporcionado
vol.alta <- 10
vol.baja <- 0
tolerancia <- 1e-6
iter <- 1
max.iter <- 1000
while((vol.alta - vol.baja) > tolerancia & iter < max.iter) {
if (merton.model(strike, spot, rf, t,
(vol.alta + vol.baja)/2, div,
tipo) > precio) {
vol.alta <- (vol.alta + vol.baja)/2
} else {
vol.baja <- (vol.alta + vol.baja)/2
}
iter <- iter + 1
}
return((vol.alta + vol.baja)/2)
}
Ya es cuestión de cada quién evaluar ahora la situación. Si esperas que la volatilidad del S&P 500 sea mayor a 17.35% en los siguientes 5 años, entonces la nota está subvaluada y sería una buena idea comprarla. De lo contrario, la nota está sobrevaluada y no deberías comprarla.
En la práctica, también habría que considerar comisiones, liquidez y el riesgo de la contraparte (¿estará todavía en operación el vendedor dentro de 5 años para hacer frente a su obligación contigo?). Pero por lo menos ahora tienen una manera fácil y rápida de evaluar estos instrumentos. Para resumir, el procedimiento es el siguiente:
Romper la utilidad en sus distintos componentes
Determinar el precio justo de cada flujo
Sumar y comparar contra el precio de venta
3 Elasticidad de precios de opciones
Las opciones son instrumentos que nos dan acceso a apalancamiento. Por eso, son muy útiles para situaciones en las que tenemos mucha convicción sobre un movimiento a la alza o a la baja. Sin embargo, una pregunta muy común es “¿qué strike y expiración debo de comprar para obtener el mayor rendimiento posible?” (o, cómo dirían los gringos, “¿cómo obtengo el mayor bang for my buck?”). Esta pregunta es fácil de responder con el modelo de Black-Scholes.
Calculemos a continuación el porcentaje de cambio del precio del call o put sobre el porcentaje de cambio en el precio de la acción:
Call bang = (dC/C)/(dS/S)
= (dC/dS)*(S/C)
= [d/dS S*N(d1) - K*exp(-r*T)*N(d2)]*(S/C)
= N(d1)*S/C
Put bang = (dP/P)/(dS/S)
= (dP/dS)*(S/P)
= [d/dS K*exp(-r*T)*N(-d2) - S*N(-d1)]*(S/P)
= -N(-d1)*S/P
= N(-d1)*S/P (por convención quitamos el negativo)
Podemos implementar esto en R de la siguiente manera:
# 0. Limpiar la sesión
rm(list=ls())
if (names(dev.cur()) != "null device") {
dev.off()
}
cat("\014")
# 1. Cargar herramientas
library(reshape2)
library(ggplot2)
source("scripts/herramientas.R")
# 2. Call y put bang
spot <- 25
strike <- 25
rf <- 0.06
t <- 0.5
sigma <- 0.3
call <- black.scholes.model(strike, spot, rf, t, sigma, "c")
put <- black.scholes.model(strike, spot, rf, t, sigma, "p")
d1 <- (log(spot/strike) + (rf + sigma^2/2)*t)/(sigma*sqrt(t))
call_bang <- pnorm(d1)*spot/call
put_bang <- pnorm(-d1)*spot/put
Y, con estas condiciones, tenemos que:
O sea que, por cada punto porcentual de cambio en el precio del activo, el precio del call subirá 6.05% y el del put 5.81% (siempre y cuando el movimiento sea a su favor, obviamente).
Veamos que sucede a lo largo de los diferentes strikes disponibles:
# 3. Cambio del "bang for the buck" con strike
strike <- 15:35
call <- black.scholes.model(strike, spot, rf, t, sigma, "c")
put <- black.scholes.model(strike, spot, rf, t, sigma, "p")
d1 <- (log(spot/strike) + (rf + sigma^2/2)*t)/(sigma*sqrt(t))
call_bang <- pnorm(d1)*spot/call
put_bang <- pnorm(-d1)*spot/put
par(mar = c(5.1, 4.1, 4.1, 8.1))
plot(x = strike, y = call_bang, type = "l", lwd = 2,
main = paste("'Bang for Buck' dependiendo del strike",
" (Spot = $", spot, ")", sep = ""),
xlab = "Strike de la opción", ylab = "Bang (elasticidad)",
ylim = c(0, 15))
lines(x = strike, y = put_bang, col = "red", lwd = 2)
legend("topright", inset = c(-0.15, 0), cex = 0.7,
legend = c("Call", "Put"),
lty = 1, col = c("black", "red"), lwd = 2,
title = "Opción", box.lty = 0, xpd = TRUE)
Podemos ver que, mientras más OTM esté la opción, vamos a tener más “bang for buck”.
Fijemos el strike en $25 y veamos ahora que pasa con la otra variable que podemos controlar, el tiempo a expiración:
# 4. Cambio del "bang for buck" con tiempo a expiración
strike <- 25
t <- seq(0.05, 1, by = 0.05)
call <- black.scholes.model(strike, spot, rf, t, sigma, "c")
put <- black.scholes.model(strike, spot, rf, t, sigma, "p")
d1 <- (log(spot/strike) + (rf + sigma^2/2)*t)/(sigma*sqrt(t))
call_bang <- pnorm(d1)*spot/call
put_bang <- pnorm(-d1)*spot/put
plot(x = t, y = call_bang, type = "l", lwd = 2,
main = "'Bang for Buck' dependiendo del tiempo",
xlab = "Tiempo a expiración", ylab = "Bang (elasticidad)")
lines(x = t, y = put_bang, col = "red", lwd = 2)
legend("topright", inset = c(-0.15, 0), cex = 0.7,
legend = c("Call", "Put"),
lty = 1, col = c("black", "red"), lwd = 2,
title = "Opción", box.lty = 0, xpd = TRUE)
Podemos ver que, mientras menos tiempo a expiración escojamos, vamos a tener más “bang for buck”.
Veamos a continuación la respuesta que nos da el modelo Black-Scholes a nuestra pregunta original en el caso de que esperemos un movimiento a la alza en el corto plazo y querramos usar calls:
# 5. Crear heatmap con estas dos variables para los calls
strike <- 15:35
t <- seq(0.05, 1, by = 0.05)
call_matrix <- matrix(nrow = length(t), ncol = length(strike))
dimnames(call_matrix) <- list(t, strike)
for (i in 1:length(t)) {
t_temp <- t[i]
call <- black.scholes.model(strike, spot, rf, t_temp,
sigma, "c")
d1 <- (log(spot/strike) +
(rf + sigma^2/2)*t_temp)/(sigma*sqrt(t_temp))
call_bang <- pnorm(d1)*spot/call
call_matrix[i, ] <- call_bang
}
p <- ggplot(melt(call_matrix), aes(Var1, Var2, fill = value)) +
labs(x = "Tiempo a expiración", y = "Strike",
title = "Call 'Bang for Buck' en función de T y K") +
geom_raster()
print(p)
Vemos entonces que la respuesta es clara: hay que comprar las opciones con menor tiempo a expiración y más OTM posibles para obtener el mayor rendimiento posible. Dicho de otra manera, “las opciones más riesgosas son las que están más OTM y tienen el menor tiempo a expiración”. ¿Por qué? Mientrás más OTM y menor tiempo a expiración, menor probabilidad de expirar ITM. Lo más seguro es que estas opciones expiren sin valor y entonces deberán considerar la liquidez de la opción que compren para no quedarse con la papa caliente cuando se termine la canción.
Eso fue todo por hoy. Espero que este tutorial les haya servido. Si tienen cualquier duda o comentario pueden dejarlo en la sección de abajo. Si quieren compartir este blog con sus amigos y compañeros o suscribirse, les dejo los botones aquí:
Hasta el próximo tutorial!