Módulo 4: Casos Prácticos Resueltos
Valoración de Opciones — Laboratorio en Python
📋 Introducción
Estos ejercicios convierten la teoría de Black-Scholes en código funcional. Implementarás la fórmula desde cero, calcularás las griegas, extraerás volatilidad implícita y visualizarás la sonrisa de volatilidad. Ejecuta cada bloque y experimenta cambiando los parámetros.
Requisitos Previos
pip install numpy scipy matplotlib yfinance
🧪 Caso Práctico 1: Implementar Black-Scholes desde Cero
Objetivo
Programar la fórmula de Black-Scholes para Calls y Puts europeas.
Código
import numpy as np
from scipy.stats import norm
def black_scholes(S, K, T, r, sigma, tipo="call"):
"""
Precio de una opción europea según Black-Scholes.
S: precio del activo
K: strike
T: tiempo al vencimiento (en años)
r: tasa libre de riesgo
sigma: volatilidad
tipo: 'call' o 'put'
"""
d1 = (np.log(S/K) + (r + 0.5*sigma**2)*T) / (sigma*np.sqrt(T))
d2 = d1 - sigma*np.sqrt(T)
if tipo == "call":
precio = S*norm.cdf(d1) - K*np.exp(-r*T)*norm.cdf(d2)
elif tipo == "put":
precio = K*np.exp(-r*T)*norm.cdf(-d2) - S*norm.cdf(-d1)
else:
raise ValueError("tipo debe ser 'call' o 'put'")
return precio
# Ejemplo: opción sobre activo a 100€
S, K, T, r, sigma = 100, 100, 1.0, 0.05, 0.20
call = black_scholes(S, K, T, r, sigma, "call")
put = black_scholes(S, K, T, r, sigma, "put")
print(f"Parámetros: S={S}, K={K}, T={T}año, r={r:.0%}, σ={sigma:.0%}")
print(f"Precio Call: {call:.4f}€")
print(f"Precio Put: {put:.4f}€")
# Verificar la paridad Put-Call: C - P = S - K·e^(-rT)
paridad_izq = call - put
paridad_der = S - K*np.exp(-r*T)
print(f"\nParidad Put-Call: {paridad_izq:.4f} = {paridad_der:.4f} ✓")
Interpretación
- Para una opción “at the money” (S=K), la Call vale más que la Put porque el activo tiende a crecer a la tasa libre de riesgo
- La paridad Put-Call (C − P = S − K·e^(−rT)) es una relación de no-arbitraje que SIEMPRE se cumple. Si tu código la respeta, está bien implementado
- Experimenta: sube la volatilidad a 0.40 y observa cómo ambas opciones se encarecen (efecto vega)
🧪 Caso Práctico 2: Calcular las Cinco Griegas
Objetivo
Implementar las griegas y entender numéricamente qué mide cada una.
Código
import numpy as np
from scipy.stats import norm
def griegas(S, K, T, r, sigma, tipo="call"):
d1 = (np.log(S/K) + (r + 0.5*sigma**2)*T) / (sigma*np.sqrt(T))
d2 = d1 - sigma*np.sqrt(T)
# Delta
if tipo == "call":
delta = norm.cdf(d1)
else:
delta = norm.cdf(d1) - 1
# Gamma (igual para call y put)
gamma = norm.pdf(d1) / (S*sigma*np.sqrt(T))
# Vega (por cada 1% de cambio en volatilidad → /100)
vega = S*norm.pdf(d1)*np.sqrt(T) / 100
# Theta (por día → /365)
if tipo == "call":
theta = (-S*norm.pdf(d1)*sigma/(2*np.sqrt(T))
- r*K*np.exp(-r*T)*norm.cdf(d2)) / 365
else:
theta = (-S*norm.pdf(d1)*sigma/(2*np.sqrt(T))
+ r*K*np.exp(-r*T)*norm.cdf(-d2)) / 365
# Rho (por cada 1% → /100)
if tipo == "call":
rho = K*T*np.exp(-r*T)*norm.cdf(d2) / 100
else:
rho = -K*T*np.exp(-r*T)*norm.cdf(-d2) / 100
return {"delta": delta, "gamma": gamma, "vega": vega,
"theta": theta, "rho": rho}
# Calcular para una Call at-the-money
S, K, T, r, sigma = 100, 100, 1.0, 0.05, 0.20
g = griegas(S, K, T, r, sigma, "call")
print("=== GRIEGAS DE UNA CALL (S=K=100) ===")
print(f"Delta: {g['delta']:.4f} (cambio por 1€ del activo)")
print(f"Gamma: {g['gamma']:.4f} (cambio del delta por 1€)")
print(f"Vega: {g['vega']:.4f} (cambio por 1% de volatilidad)")
print(f"Theta: {g['theta']:.4f} (pérdida por día)")
print(f"Rho: {g['rho']:.4f} (cambio por 1% de tipos)")
Interpretación
- Delta ~0.6: una Call at-the-money con esta configuración tiene delta algo superior a 0.5
- Theta negativo: confirma el decaimiento temporal (la opción pierde valor cada día)
- Vega positivo: más volatilidad encarece la opción
- Comprobación: verifica que el delta de una Put = delta de la Call − 1
🧪 Caso Práctico 3: Visualizar Cómo Cambian Precio y Delta
Objetivo
Graficar el precio de una opción y su delta en función del precio del activo.
Código
import numpy as np
import matplotlib.pyplot as plt
K, T, r, sigma = 100, 0.5, 0.05, 0.25
precios_activo = np.linspace(60, 140, 200)
precios_call = [black_scholes(S, K, T, r, sigma, "call") for S in precios_activo]
deltas = [griegas(S, K, T, r, sigma, "call")["delta"] for S in precios_activo]
payoff = [max(S - K, 0) for S in precios_activo] # valor al vencimiento
fig, axes = plt.subplots(1, 2, figsize=(14, 5))
# Precio de la opción vs payoff al vencimiento
axes[0].plot(precios_activo, precios_call, label="Precio Call (hoy)", linewidth=2)
axes[0].plot(precios_activo, payoff, "--", label="Payoff (vencimiento)", color="gray")
axes[0].axvline(K, color="red", linestyle=":", alpha=0.5, label="Strike")
axes[0].set_title("Precio de la Call vs. Payoff al vencimiento")
axes[0].set_xlabel("Precio del activo")
axes[0].set_ylabel("Valor de la opción")
axes[0].legend()
axes[0].grid(True, alpha=0.3)
# Delta vs precio del activo
axes[1].plot(precios_activo, deltas, color="darkgreen", linewidth=2)
axes[1].axvline(K, color="red", linestyle=":", alpha=0.5, label="Strike")
axes[1].set_title("Delta de la Call vs. precio del activo")
axes[1].set_xlabel("Precio del activo")
axes[1].set_ylabel("Delta")
axes[1].legend()
axes[1].grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
Interpretación
Gráfico izquierdo:
- La curva del precio (hoy) está siempre por encima del payoff (vencimiento): la diferencia es el “valor temporal”
- Cerca del vencimiento, el precio convergería hacia la línea de payoff
Gráfico derecho:
- El delta va de ~0 (muy out of the money) a ~1 (muy in the money)
- La transición es suave alrededor del strike: ahí el gamma es máximo (el delta cambia rápido)
- Esta forma de “S” es por qué el delta se interpreta como probabilidad de ejercicio
🧪 Caso Práctico 4: El Efecto del Tiempo y la Volatilidad
Objetivo
Visualizar cómo el valor temporal se erosiona (theta) y cómo la volatilidad encarece las opciones (vega).
Código
import numpy as np
import matplotlib.pyplot as plt
S, K, r = 100, 100, 0.05
fig, axes = plt.subplots(1, 2, figsize=(14, 5))
# Efecto del TIEMPO: precio según días al vencimiento
dias = np.linspace(0.01, 1.0, 100)
sigma = 0.20
precios_tiempo = [black_scholes(S, K, T, r, sigma, "call") for T in dias]
axes[0].plot(dias*365, precios_tiempo, color="purple", linewidth=2)
axes[0].set_title("Efecto del tiempo (Theta): valor de la Call")
axes[0].set_xlabel("Días al vencimiento")
axes[0].set_ylabel("Precio de la Call")
axes[0].grid(True, alpha=0.3)
axes[0].annotate("El valor cae más rápido\ncerca del vencimiento",
xy=(20, precios_tiempo[5]), xytext=(120, 3),
arrowprops=dict(arrowstyle="->", color="red"))
# Efecto de la VOLATILIDAD: precio según sigma
T = 0.5
vols = np.linspace(0.05, 0.60, 100)
precios_vol = [black_scholes(S, K, T, r, s, "call") for s in vols]
axes[1].plot(vols*100, precios_vol, color="darkorange", linewidth=2)
axes[1].set_title("Efecto de la volatilidad (Vega): valor de la Call")
axes[1].set_xlabel("Volatilidad (%)")
axes[1].set_ylabel("Precio de la Call")
axes[1].grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
Interpretación
Gráfico izquierdo (Theta):
- El valor de la opción cae a medida que se acerca el vencimiento
- La caída se acelera cerca de cero días: por eso theta no es lineal y los traders vigilan las últimas semanas
Gráfico derecho (Vega):
- El precio sube de forma casi lineal con la volatilidad
- A mayor volatilidad, mayor probabilidad de movimientos grandes → más valor en el derecho asimétrico
- Esto es por qué “valorar opciones es pronosticar volatilidad”
🧪 Caso Práctico 5: Calcular la Volatilidad Implícita
Objetivo
Dado el precio de mercado de una opción, “invertir” Black-Scholes para hallar la volatilidad implícita.
Código
import numpy as np
from scipy.optimize import brentq
def volatilidad_implicita(precio_mercado, S, K, T, r, tipo="call"):
"""
Encuentra la sigma que iguala el precio Black-Scholes al precio de mercado.
Usa búsqueda de raíces (Brent).
"""
def objetivo(sigma):
return black_scholes(S, K, T, r, sigma, tipo) - precio_mercado
try:
# Buscar sigma entre 1% y 500%
iv = brentq(objetivo, 0.01, 5.0)
return iv
except ValueError:
return np.nan
# Ejemplo: una Call cotiza a 12€ en el mercado
S, K, T, r = 100, 100, 1.0, 0.05
precio_mercado = 12.0
iv = volatilidad_implicita(precio_mercado, S, K, T, r, "call")
print(f"Precio de mercado de la Call: {precio_mercado}€")
print(f"Volatilidad implícita: {iv:.2%}")
# Verificar: meter esa IV en Black-Scholes debe dar el precio de mercado
verificacion = black_scholes(S, K, T, r, iv, "call")
print(f"Verificación (BS con esa IV): {verificacion:.4f}€ ✓")
Interpretación
- La volatilidad implícita es la σ que hace que Black-Scholes “coincida” con el precio de mercado
- Como no se puede despejar analíticamente, se usa un método numérico (búsqueda de raíces)
- Es la opinión del mercado sobre la volatilidad futura. Si la IV es alta, las opciones están caras (el mercado espera turbulencia)
- Experimenta: sube el precio de mercado a 18€ y verás cómo la IV aumenta
🧪 Caso Práctico 6: Construir la Sonrisa de Volatilidad
Objetivo
Extraer la volatilidad implícita de opciones reales con distintos strikes y visualizar la sonrisa.
Código
import yfinance as yf
import numpy as np
import matplotlib.pyplot as plt
from datetime import datetime
# Descargar la cadena de opciones de un activo real
ticker = yf.Ticker("SPY")
fechas = ticker.options # fechas de vencimiento disponibles
fecha_venc = fechas[3] # elige un vencimiento intermedio
cadena = ticker.option_chain(fecha_venc)
calls = cadena.calls
# Precio actual del subyacente
S = ticker.history(period="1d")["Close"].iloc[-1]
r = 0.05
# Tiempo al vencimiento en años
hoy = datetime.now()
venc = datetime.strptime(fecha_venc, "%Y-%m-%d")
T = (venc - hoy).days / 365
print(f"Subyacente SPY: {S:.2f}, vencimiento: {fecha_venc} (T={T:.3f} años)")
# yfinance ya trae 'impliedVolatility', pero la recalculamos para aprender
strikes, ivs = [], []
for _, fila in calls.iterrows():
K = fila["strike"]
precio_op = (fila["bid"] + fila["ask"]) / 2 # precio medio
# Filtrar opciones con precios razonables y cercanas al dinero
if precio_op > 0.5 and 0.8*S < K < 1.2*S:
iv = volatilidad_implicita(precio_op, S, K, T, r, "call")
if not np.isnan(iv) and 0.01 < iv < 2:
strikes.append(K)
ivs.append(iv)
# Visualizar la sonrisa
plt.figure(figsize=(12, 6))
plt.plot(strikes, np.array(ivs)*100, "o-", color="crimson")
plt.axvline(S, color="blue", linestyle="--", alpha=0.6, label=f"Precio actual ({S:.0f})")
plt.title(f"Sonrisa de volatilidad - SPY Calls ({fecha_venc})")
plt.xlabel("Strike")
plt.ylabel("Volatilidad implícita (%)")
plt.legend()
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
Interpretación
Qué observas:
- La volatilidad implícita NO es plana entre strikes → forma una curva (sonrisa o skew)
- En índices como el SPY, suele verse un “skew”: las opciones de strike bajo (Puts de protección) tienen IV más alta
- Esto demuestra empíricamente que el mercado no cree en la hipótesis de volatilidad constante de Black-Scholes
Conexión con el Módulo 2: la sonrisa es la huella de las colas pesadas y la skewness negativa de los retornos. El mercado cobra más por la protección contra cracks porque sabe que ocurren más de lo que predice la normal.
Nota: los datos de opciones de yfinance pueden ser limitados o tener retrasos. Si la cadena viene vacía, prueba con otro ticker líquido como AAPL o con otra fecha de vencimiento.
🎓 Proyecto del Módulo
Enunciado
Construye un análisis de valoración de opciones completo:
- Implementa Black-Scholes y las cinco griegas (puedes reutilizar el código)
- Elige un activo y un strike; valora la Call y la Put
- Verifica la paridad Put-Call
- Calcula y muestra las cinco griegas
- Extrae la volatilidad implícita de una opción real de ese activo
- Escribe un párrafo: ¿la volatilidad implícita es mayor o menor que la histórica? ¿Qué implica?
Solución de Referencia (Plantilla)
import yfinance as yf
import numpy as np
# (Reutiliza black_scholes, griegas y volatilidad_implicita de arriba)
mi_ticker = "AAPL" # cámbialo
tk = yf.Ticker(mi_ticker)
S = tk.history(period="1d")["Close"].iloc[-1]
# Volatilidad histórica (1 año)
hist = tk.history(period="1y")
ret = np.log(hist["Close"]/hist["Close"].shift(1)).dropna()
vol_hist = ret.std()*np.sqrt(252)
# Parámetros
K = round(S) # strike at-the-money
T = 0.25 # 3 meses
r = 0.05
print(f"{mi_ticker}: precio={S:.2f}, vol histórica={vol_hist:.2%}")
# Valorar con la volatilidad histórica como estimación
call = black_scholes(S, K, T, r, vol_hist, "call")
put = black_scholes(S, K, T, r, vol_hist, "put")
print(f"Call (vol hist): {call:.2f}€, Put: {put:.2f}€")
# Griegas
g = griegas(S, K, T, r, vol_hist, "call")
print(f"Griegas Call: {dict((k, round(v,4)) for k,v in g.items())}")
# Tu análisis aquí: compara con la IV real de yfinance
Criterios de Evaluación
- Black-Scholes correcto: la paridad Put-Call se cumple (25%)
- Griegas correctas: valores y signos coherentes (25%)
- Volatilidad implícita: bien extraída (25%)
- Análisis: comparación razonada implícita vs. histórica (25%)
📝 Resumen del Módulo Práctico
Has aprendido a:
✓ Implementar Black-Scholes para Calls y Puts desde cero ✓ Verificar la paridad Put-Call como control de calidad ✓ Calcular las cinco griegas y entender qué mide cada una ✓ Visualizar el efecto del precio, el tiempo y la volatilidad ✓ Extraer la volatilidad implícita invirtiendo la fórmula ✓ Construir la sonrisa de volatilidad con datos reales
“Has programado el modelo que ganó un Nobel y cambió las finanzas. Pero recuerda: el modelo no captura los cracks, los saltos ni el pánico. La sonrisa de volatilidad es el mercado recordándote, cada día, que ningún modelo es perfecto.”
En el Módulo 5 nos centraremos en medir el riesgo y el rendimiento de carteras y estrategias: Sharpe, Sortino, VaR, CVaR y drawdown — las métricas con las que se juzga todo en finanzas cuantitativas.
Fin de los Casos Prácticos del Módulo 4