Lección 3.5.2 · 75 min
Estructuras de volatilidad y rango: straddles, strangles, cóndores y mariposas
Un straddle no apuesta por la dirección sino por el tamaño del movimiento, y sus parientes con alas acotan el riesgo de venderlo. La lección construye las siete estructuras de volatilidad y rango con la cadena del índice ORX, compara lo que descuentan con lo que el índice se movió después y mide el peor mes de cada una en 143 vencimientos.
- Módulo
- 3.5 · Estructuras de opciones en Python
- Duración
- 75 min
Lecciones del nivel 3
3.1 · Futuros en profundidad
3.2 · Spreads de futuros
- 3.2.1 Spreads de calendario, butterflies y condors
- 3.2.2 Spreads intercommodity: crack, spark y crush
- 3.2.3 Spreads de tipos y de índices: 2s10s, NOB, NQ/ES y Europa frente a EE. UU.
- 3.2.4 Ratios de cobertura: por nominal, por DV01 y por volatilidad
- 3.2.5 Datos y backtesting de spreads: cotizados, sintéticos y por patas
- 3.2.6 Estacionalidad de spreads: análisis multi-año, patrones y trampas
3.3 · Forex avanzado
3.4 · Teoría y valoración de opciones
- 3.4.1 Payoffs, paridad put-call y límites de arbitraje
- 3.4.2 Árboles binomiales, Black-Scholes-Merton, Black-76 y Monte Carlo
- 3.4.3 Griegas: delta, gamma, vega, theta, rho, vanna, volga y charm
- 3.4.4 Volatilidad implícita: Newton-Raphson, Brent, py_vollib y QuantLib
- 3.4.5 Sonrisa, skew y estructura temporal; superficie de volatilidad con SVI
- 3.4.6 Volatilidad realizada frente a implícita; prima de riesgo de volatilidad; VIX y VSTOXX
- 3.4.7 Modelos avanzados: Heston, SABR y volatilidad local
3.5 · Estructuras de opciones en Python
- 3.5.1 Estructuras direccionales y verticales: call, put, covered call, collar y spreads
- 3.5.2 Estructuras de volatilidad y rango: straddles, strangles, cóndores y mariposas
- 3.5.3 Calendarios, diagonales, ratios, backspreads, risk reversals y jade lizard
- 3.5.4 Herramienta propia: payoff, griegas agregadas y escenarios
- 3.5.5 Gestión: ajustes, roll, cierre anticipado, asignación temprana y pin risk
3.6 · Estrategias sistemáticas de volatilidad y opciones
3.7 · Cripto derivados y estrategias específicas
Comprar o vender movimiento
Las estructuras de la lección 3.5.1 apostaban por una dirección. Las de esta apuestan por el tamaño del movimiento. Un straddle comprado (una call y una put del mismo strike, normalmente en el dinero) gana si el índice se aleja del strike más de lo que costó, hacia arriba o hacia abajo; un strangle hace lo mismo con una put y una call fuera del dinero, más barato y con los puntos muertos más lejos. Vendidos, ganan si el índice se queda quieto.
Fuente Mermaid
flowchart LR C[Comprar movimiento] --> CG[Gamma y vega +] CG --> CT[Paga theta] V[Vender movimiento] --> VG[Gamma y vega −] VG --> VT[Cobra theta]
Las dos posiciones son una apuesta sobre la volatilidad que se va a realizar frente a la que el mercado descuenta en la prima. Quien vende cobra la prima de riesgo de volatilidad (lección 3.4.6): de media, la implícita cotiza por encima de la realizada después, porque comprar protección contra los movimientos grandes tiene valor. Pero la cobra a cambio de quedarse con esos movimientos grandes, y la historia del final de la lección enseña lo que eso significa.
Las alas cambian el riesgo. Vender un straddle tiene la pérdida ilimitada; vender un straddle y comprar una put y una call más lejanas es un iron butterfly, con la pérdida acotada. Lo mismo convierte un strangle vendido en un iron condor. Por paridad, cada uno tiene un gemelo hecho solo con calls (o solo con puts): la mariposa y el cóndor. Y si una de las alas es más ancha que la otra, la mariposa es de alas rotas (broken-wing butterfly).
Fuente Mermaid
flowchart TB SD[Straddle vendido] -- más alas --> IB[Iron butterfly] SG[Strangle vendido] -- más alas --> IC[Iron condor] IB -- paridad --- MC[Mariposa de calls] IC -- paridad --- CC[Cóndor de calls] MC -- un ala más ancha --> BW[Alas rotas]
La cadena de febrero de 2026
Los datos y las convenciones son los de la lección 3.5.1: la cadena sintética del índice ORX del 31 de diciembre
de 2025, vencimiento del 20 de febrero de 2026 (51 días naturales), precios en puntos por unidad (× 100 USD por
contrato), T en días naturales / 365, r y q continuos, theta por día natural y vega por punto de volatilidad. Cada
lección del curso se ejecuta en su propio proceso, así que la primera celda repite las funciones de la 3.5.1 (bsm,
pata, apertura, valor, resumen, griegas, con_delta y strike_delta); la explicación de cada una está allí.
Recuerda que los datos son sintéticos, con una prima de volatilidad plantada: sirven para ver la mecánica, no como
prueba de lo que pasa en el mercado real.
import numpy as np
import pandas as pd
from scipy.stats import norm
cadena = pd.read_parquet("opciones3_cadena.parquet")
indice = pd.read_csv("opciones3_indice.csv", parse_dates=["fecha"],
index_col="fecha")
verdad = pd.read_parquet("opciones3_verdad.parquet")
HOY = pd.Timestamp("2025-12-31")
VTO = pd.Timestamp("2026-02-20")
S0 = indice.loc[HOY, "cierre"]
def mercado(vto, fecha=HOY):
"""Tipo r y dividendo q continuos (tanto por uno) de un
vencimiento: r de la curva; q implícito en el forward."""
v = verdad[(verdad.fecha == fecha)
& (verdad.vencimiento == vto)].iloc[0]
T = v.dias / 365 # días naturales / 365
S = indice.loc[fecha, "cierre"]
return v.tipo_libre, v.tipo_libre - np.log(v.forward / S) / T
def bsm(S, K, T, sig, tipo, r, q):
"""Precio (puntos), delta, gamma (por punto), vega (por
punto de volatilidad) y theta (por día natural).
S puede ser un array; con T = 0 devuelve el payoff."""
S = np.asarray(S, dtype=float)
if T <= 0:
pago = np.maximum(S - K, 0) if tipo == "C" else \
np.maximum(K - S, 0)
signo = 1.0 if tipo == "C" else -1.0
delta = np.where(signo * (S - K) > 0, signo, 0.0)
cero = np.zeros_like(S)
return dict(precio=pago, delta=delta, gamma=cero,
vega=cero, theta=cero)
raiz = sig * np.sqrt(T)
d1 = (np.log(S / K) + (r - q + sig**2 / 2) * T) / raiz
d2 = d1 - raiz
dq, dr = np.exp(-q * T), np.exp(-r * T)
gamma = dq * norm.pdf(d1) / (S * raiz)
vega = S * dq * norm.pdf(d1) * np.sqrt(T)
decay = -S * dq * norm.pdf(d1) * sig / (2 * np.sqrt(T))
if tipo == "C":
precio = S * dq * norm.cdf(d1) - K * dr * norm.cdf(d2)
delta = dq * norm.cdf(d1)
theta = (decay - r * K * dr * norm.cdf(d2)
+ q * S * dq * norm.cdf(d1))
else:
precio = K * dr * norm.cdf(-d2) - S * dq * norm.cdf(-d1)
delta = -dq * norm.cdf(-d1)
theta = (decay + r * K * dr * norm.cdf(-d2)
- q * S * dq * norm.cdf(-d1))
return dict(precio=precio, delta=delta, gamma=gamma,
vega=vega / 100, theta=theta / 365)
def pata(tipo, K, n, vto=VTO, fecha=HOY):
"""Una pata: n > 0 comprada, n < 0 vendida. tipo "S" es
el propio índice (una cartera que lo replica)."""
if tipo == "S":
return dict(tipo="S", K=0.0, n=n, bid=S0, ask=S0,
iv=0.0, vto=vto, r=0.0, q=0.0)
o = cadena[(cadena.fecha == fecha)
& (cadena.vencimiento == vto)
& (cadena.tipo == tipo)
& (cadena.strike == K)].iloc[0]
rr, qq = mercado(vto, fecha)
return dict(tipo=tipo, K=K, n=n, bid=o.bid, ask=o.ask,
iv=o.iv_modelo, vto=vto, r=rr, q=qq)
def apertura(patas):
"""Coste en puntos al medio y al cruzar la horquilla:
las compradas al ask, las vendidas al bid (> 0 = débito)."""
medio = sum(p["n"] * (p["bid"] + p["ask"]) / 2 for p in patas)
real = sum(p["n"] * (p["ask"] if p["n"] > 0 else p["bid"])
for p in patas)
return medio, real
def valor(patas, S, fecha, griega="precio"):
"""Valor (o griega) de la estructura si el índice vale S
en `fecha`, con la IV de cada pata congelada."""
S = np.asarray(S, dtype=float)
total = np.zeros_like(S)
for p in patas:
if p["tipo"] == "S":
x = {"precio": S, "delta": np.ones_like(S)}
total += p["n"] * x.get(griega, np.zeros_like(S))
continue
T = (p["vto"] - fecha).days / 365
g = bsm(S, p["K"], T, p["iv"], p["tipo"], p["r"], p["q"])
total += p["n"] * g[griega]
return total
REJILLA = np.arange(1.0, 3 * S0, 1.0) # de 1 punto a 3 veces S0
def resumen(patas):
"""Coste, máximo, mínimo y puntos muertos al vencimiento
(el primero de la estructura), en puntos por unidad."""
medio, real = apertura(patas)
fin = min(p["vto"] for p in patas)
pnl = valor(patas, REJILLA, fin) - real
i = np.nonzero(np.diff(np.sign(pnl)))[0]
muertos = REJILLA[i] - pnl[i] / (pnl[i + 1] - pnl[i])
muertos = np.unique(np.round(muertos, 1))
final = pnl[-1] - pnl[-2] # pendiente hacia S grande
return {"medio": round(float(medio), 2),
"real": round(float(real), 2),
"horquilla": round(float(real - medio), 2),
"maximo": "ilimitado" if final > 1e-9
else round(float(pnl.max()), 2),
"minimo": "ilimitado" if final < -1e-9
else round(float(pnl.min()), 2),
"muertos": [round(float(x), 1) for x in muertos]}
def griegas(patas, S=S0, fecha=HOY):
"""Griegas agregadas por unidad (× 100 USD por contrato)."""
g = {k: float(valor(patas, S, fecha, k))
for k in ["delta", "gamma", "vega", "theta"]}
return {"delta": round(g["delta"], 3),
"gamma": round(g["gamma"], 5),
"vega": round(g["vega"], 2),
"theta": round(g["theta"], 2)}
def con_delta(fecha=HOY, vto=VTO):
"""La cadena de un vencimiento con la delta BSM de cada fila."""
d = cadena[(cadena.fecha == fecha)
& (cadena.vencimiento == vto)].copy()
rr, qq = mercado(vto, fecha)
S, T = indice.loc[fecha, "cierre"], (vto - fecha).days / 365
for t in ("C", "P"):
m = d.tipo == t
d.loc[m, "delta"] = bsm(S, d.strike[m].to_numpy(), T,
d.iv_modelo[m].to_numpy(), t,
rr, qq)["delta"]
return d
def strike_delta(d, tipo, objetivo):
x = d[d.tipo == tipo]
return float(x.strike.iloc[(x.delta - objetivo).abs().argmin()])r, q = mercado(VTO)
dias = (VTO - HOY).days
hoy = con_delta()
print(f"ORX {S0} días {dias} r {r:.4f} q {q:.4f}")
x = hoy.pivot(index="strike", columns="tipo",
values=["bid", "ask", "delta"])
x.loc[5600:6600].round(3)ORX 6089.45 días 51 r 0.0368 q 0.0117
bid ask delta
tipo C P C P C P
strike
5600.0 529.50 22.5 532.60 22.9 0.893 -0.105
5700.0 438.20 30.5 440.80 31.1 0.856 -0.143
5800.0 349.80 41.5 351.90 42.1 0.806 -0.193
5900.0 265.80 56.5 267.50 57.3 0.737 -0.261
6000.0 187.90 77.8 189.20 78.7 0.645 -0.353
6100.0 118.80 108.3 119.80 109.3 0.523 -0.475
6200.0 63.60 152.4 64.50 153.6 0.372 -0.626
6300.0 27.10 215.0 27.70 216.4 0.214 -0.784
6400.0 9.30 296.4 9.70 298.1 0.096 -0.902
6600.0 0.85 486.6 0.95 489.2 0.013 -0.986La escalera de febrero alrededor del dinero: strikes cada 100 puntos, las calls de 6.300 y 6.400 con deltas de 0,214 y 0,096, las puts de 5.900 y 5.600 con −0,261 y −0,105. Son las patas de los ejemplos.
Straddle y strangle comprados
straddle = [pata("C", 6100, 1), pata("P", 6100, 1)]
strangle = [pata("C", 6300, 1), pata("P", 5900, 1)]
for nombre, e in [("straddle 6100", straddle),
("strangle 5900/6300", strangle)]:
print(nombre, resumen(e))
print(" ", griegas(e))
iv = straddle[0]["iv"]
coste = apertura(straddle)[1]
regla = 0.8 * iv * np.sqrt(dias / 365) * S0
print(f"movimiento implícito {100 * coste / S0:.2f} %"
f" regla 0,8·σ·√T·S {regla:.1f}")straddle 6100 {'medio': 228.1, 'real': 229.1, 'horquilla': 1.0, 'maximo': 'ilimitado', 'minimo': -229.1, 'muertos': [5870.9, 6329.1]}
{'delta': 0.049, 'gamma': 0.00278, 'vega': 18.1, 'theta': -2.23}
strangle 5900/6300 {'medio': 84.3, 'real': 85.0, 'horquilla': 0.7, 'maximo': 'ilimitado', 'minimo': -85.0, 'muertos': [5815.0, 6385.0]}
{'delta': -0.047, 'gamma': 0.0022, 'vega': 14.02, 'theta': -1.74}
movimiento implícito 3.76 % regla 0,8·σ·√T·S 229.1El straddle 6.100 cuesta 229,1 puntos al ask (un punto más que al medio) y sus puntos muertos son 5.870,9 y 6.329,1: el índice tiene que moverse un 3,76 % en 51 días, en cualquier dirección, para que no pierda. Ese porcentaje es el movimiento implícito, y la regla de mano (que sale de que el valor esperado de en una normal es ) da 229,1 con la volatilidad de la pata ATM: la regla es aproximada, aquí coincide en el decimal. El strangle 5.900/6.300 cuesta 85,0 y necesita que el índice salga de 5.815,0-6.385,0, un 4,5 % abajo o un 4,9 % arriba.
Las griegas resumen la posición: delta casi nula (0,049; el strike no está exactamente en el forward), gamma de 0,00278, vega de 18,1 puntos por punto de volatilidad (1.810 USD por contrato) y theta de −2,23 puntos al día. Gamma y theta no son independientes. La ecuación de Black-Scholes, para una cartera sin delta, dice que lo que se gana por gamma compensa lo que se pierde por el paso del tiempo:
S = np.array([5700.0, 5900, 6000, 6100, 6200, 6300, 6500])
antes = pd.DataFrame({
"hoy": valor(straddle, S, HOY) - coste,
"en 25 días": valor(straddle, S, HOY + pd.Timedelta(days=25))
- coste,
"vencimiento": valor(straddle, S, VTO) - coste}, index=S)
antes.index.name = "ORX"
g = griegas(straddle)
theta_gamma = -0.5 * g["gamma"] * (iv * S0) ** 2 / 365
print(f"theta {g['theta']} −½·Γ·σ²·S²/365 {theta_gamma:.2f}")
antes.round(1)theta -2.23 −½·Γ·σ²·S²/365 -2.23
hoy en 25 días vencimiento
ORX
5700.0 170.7 163.4 170.9
5900.0 38.9 -2.6 -29.1
6000.0 5.7 -52.0 -129.1
6100.0 -0.4 -65.7 -229.1
6200.0 20.8 -41.4 -129.1
6300.0 66.7 15.3 -29.1
6500.0 212.0 185.6 170.9La relación se cumple en las dos cifras decimales (−2,23 y −2,23). Es la forma más útil de pensar una estructura de volatilidad: quien compra gamma paga theta y quien vende gamma cobra theta, y el saldo lo decide si el índice se mueve más o menos de lo que implica la volatilidad. La tabla lo muestra en precios: con el índice en 6.100 el straddle está en tablas hoy (−0,4) y pierde 65,7 si dentro de 25 días el índice sigue ahí; en 5.700 gana 170,7 hoy y 163,4 en 25 días. Moverse deprisa vale más que moverse tarde. En 5.700 el P&L de hoy y el del vencimiento casi coinciden (170,7 y 170,9), pero en 6.500 no (212,0 y 170,9): además del valor temporal, con r > q una call dentro del dinero vale más que su valor intrínseco (se paga el strike más tarde) y una put dentro del dinero, algo menos.
El precio del straddle frente a lo que se movió el índice
¿Cuánto se movió el índice, en los datos, frente a lo que descontaba el straddle? La celda siguiente prepara la historia mensual que usa el resto de la lección (la misma regla que en la 3.5.1: se abre la sesión siguiente a cada vencimiento con el vencimiento siguiente, compras al ask y ventas al bid, liquidación al cierre del vencimiento, P&L en % del índice el día de la entrada) y compara el precio del straddle ATM con el movimiento realizado:
vtos = np.sort(cadena.vencimiento.unique())
sesiones = indice.index
por_dia = dict(tuple(cadena.groupby(["fecha", "vencimiento"])))
entradas = [] # (sesión, vencimiento, cadena)
for ant, vto in zip(vtos[:-1], vtos[1:]):
if vto > sesiones[-1]:
break
f = sesiones[sesiones.searchsorted(ant, side="right")]
if (f, vto) in por_dia:
entradas.append((f, vto, con_delta(f, vto)))
def historia(receta):
"""Abre la estructura la sesión siguiente a cada vencimiento
mensual con el vencimiento siguiente, al bid/ask, y la
liquida al cierre del vencimiento. P&L en % del índice."""
filas = []
for f, vto, d in entradas:
S, ST = indice.loc[f, "cierre"], indice.loc[vto, "cierre"]
coste = pago = 0.0
for tipo, K, n in receta(d, S):
o = d[(d.tipo == tipo) & (d.strike == K)].iloc[0]
coste += n * (o.ask if n > 0 else o.bid)
pago += n * (max(ST - K, 0) if tipo == "C"
else max(K - ST, 0))
filas.append({"entrada": f, "vto": vto, "S": S, "ST": ST,
"coste": 100 * coste / S,
"pnl": 100 * (pago - coste) / S})
return pd.DataFrame(filas).set_index("entrada")
def atm(d):
return strike_delta(d, "C", 0.50)
def cerca(d, K):
"""El strike listado más cercano a K."""
ks = d.strike.unique()
return float(ks[np.abs(ks - K).argmin()])
mov = historia(lambda d, S: [("C", atm(d), 1), ("P", atm(d), 1)])
mov["realizado"] = 100 * (mov.ST / mov.S - 1).abs()
print(f"{len(mov)} entradas; movimiento implícito medio "
f"{mov.coste.mean():.2f} %, realizado medio "
f"{mov.realizado.mean():.2f} %")
print(f"meses con realizado > implícito: "
f"{100 * (mov.realizado > mov.coste).mean():.1f} %")
mov.nlargest(4, "realizado")[["coste", "realizado", "pnl"]].round(2)143 entradas; movimiento implícito medio 3.73 %, realizado medio 3.87 %
meses con realizado > implícito: 39.2 %
coste realizado pnl
entrada
2020-03-23 13.40 29.87 14.83
2020-02-24 3.08 28.24 24.83
2020-07-20 7.40 23.53 16.00
2020-04-20 10.31 19.04 9.02El movimiento implícito medio es un 3,73 % y el realizado medio, un 3,87 %: de media, el índice se movió algo más de lo que costaba el straddle. Y, sin embargo, el realizado solo superó al implícito en el 39 % de los meses. Las dos cosas a la vez son la firma de una cola gruesa: la mayoría de los meses el índice se mueve menos de lo descontado (el vendedor gana un poco) y unos pocos meses se mueve muchísimo más. Los cuatro mayores son de 2020: en la entrada del 24 de febrero el straddle costaba un 3,08 % y el índice se movió un 28,24 %; el straddle comprado ganó un 24,83 % del índice. La volatilidad de ese mes no estaba en la prima: la implícita del 24 de febrero era la de un mercado tranquilo.
Vender el straddle y el strangle
corto_straddle = [pata("C", 6100, -1), pata("P", 6100, -1)]
corto_strangle = [pata("C", 6300, -1), pata("P", 5900, -1)]
for nombre, e in [("straddle vendido", corto_straddle),
("strangle vendido", corto_strangle)]:
print(nombre, resumen(e))
print(" ", griegas(e))straddle vendido {'medio': -228.1, 'real': -227.1, 'horquilla': 1.0, 'maximo': 227.1, 'minimo': 'ilimitado', 'muertos': [5872.9, 6327.1]}
{'delta': -0.049, 'gamma': -0.00278, 'vega': -18.1, 'theta': 2.23}
strangle vendido {'medio': -84.3, 'real': -83.6, 'horquilla': 0.7, 'maximo': 83.6, 'minimo': 'ilimitado', 'muertos': [5816.4, 6383.6]}
{'delta': 0.047, 'gamma': -0.0022, 'vega': -14.02, 'theta': 1.74}Vendidos, el straddle cobra 227,1 (al bid; dos puntos menos que lo que costaría comprarlo) y el strangle 83,6. Son imágenes especulares de las compras: gamma y vega negativas, theta positiva (+2,23 y +1,74 puntos al día) y pérdida ilimitada por los dos lados. Por eso casi nadie los vende desnudos salvo con mucho capital y gestión activa; la versión con alas es la habitual.
Iron condor e iron butterfly
condor = [pata("P", 5800, 1), pata("P", 5900, -1),
pata("C", 6300, -1), pata("C", 6400, 1)]
iron_bfly = [pata("P", 5900, 1), pata("P", 6100, -1),
pata("C", 6100, -1), pata("C", 6300, 1)]
for nombre, e in [("iron condor 5800/5900/6300/6400", condor),
("iron butterfly 5900/6100/6300", iron_bfly)]:
print(nombre, resumen(e))
print(" ", griegas(e))iron condor 5800/5900/6300/6400 {'medio': -33.0, 'real': -31.8, 'horquilla': 1.2, 'maximo': 31.8, 'minimo': -68.2, 'muertos': [5868.2, 6331.8]}
{'delta': -0.05, 'gamma': -0.00068, 'vega': -3.91, 'theta': 0.41}
iron butterfly 5900/6100/6300 {'medio': -143.8, 'real': -142.1, 'horquilla': 1.7, 'maximo': 142.1, 'minimo': -57.9, 'muertos': [5957.9, 6242.1]}
{'delta': -0.096, 'gamma': -0.00058, 'vega': -4.08, 'theta': 0.49}La figura de la cabecera es el iron condor 5.800/5.900/6.300/6.400: vende el strangle 5.900/6.300 y compra las alas 5.800 y 6.400. Cobra 31,8 puntos (1,2 menos que al medio), gana eso si el índice acaba entre los strikes vendidos y pierde como mucho 68,2 fuera de las alas: el ancho de 100 puntos menos el crédito. Sus puntos muertos están en 5.868,2 y 6.331,8. Las alas cuestan dinero: el strangle vendido solo cobraba 83,6 y el iron condor cobra 31,8, pero el riesgo pasa de ilimitado a 68,2 puntos (6.820 USD por contrato). La vega se divide entre 3,6 (de −14,02 a −3,91).
El iron butterfly 5.900/6.100/6.300 vende el straddle en el dinero y compra alas a 200 puntos. Cobra 142,1 y pierde como mucho 57,9: su relación entre ganancia y pérdida máximas es la inversa de la del iron condor, a cambio de un rango de beneficio estrecho (5.957,9-6.242,1) en el que casi nunca se cobra el máximo, porque eso exige acabar exactamente en 6.100.
Mariposas, cóndores y alas rotas
La mariposa de calls compra una call de strike bajo, vende dos del central y compra una del alto. Por paridad, cada call es una put más un tramo lineal, y los tramos lineales de las cuatro patas se cancelan: la mariposa de calls y el iron butterfly comprado tienen el mismo payoff salvo una constante (el ancho del ala).
mariposa = [pata("C", 5900, 1), pata("C", 6100, -2),
pata("C", 6300, 1)]
condor_c = [pata("C", 5900, 1), pata("C", 6000, -1),
pata("C", 6200, -1), pata("C", 6300, 1)]
for nombre, e in [("mariposa 5900/6100/6300", mariposa),
("cóndor de calls 5900/6000/6200/6300", condor_c)]:
print(nombre, resumen(e))
print(" ", griegas(e))
ancho = 200 * np.exp(-r * dias / 365)
print(f"paridad: mariposa {apertura(mariposa)[0]:.2f} ancho"
f" descontado − crédito {ancho + apertura(iron_bfly)[0]:.2f}")mariposa 5900/6100/6300 {'medio': 55.45, 'real': 57.6, 'horquilla': 2.15, 'maximo': 142.4, 'minimo': -57.6, 'muertos': [5957.6, 6242.4]}
{'delta': -0.096, 'gamma': -0.00058, 'vega': -4.08, 'theta': 0.51}
cóndor de calls 5900/6000/6200/6300 {'medio': 41.45, 'real': 43.7, 'horquilla': 2.25, 'maximo': 56.3, 'minimo': -43.7, 'muertos': [5943.7, 6256.3]}
{'delta': -0.066, 'gamma': -0.00045, 'vega': -3.03, 'theta': 0.37}
paridad: mariposa 55.45 ancho descontado − crédito 55.18La mariposa 5.900/6.100/6.300 cuesta 57,6 y gana como mucho 142,4; el iron butterfly vendido cobraba 142,1 y perdía como mucho 57,9. Son la misma posición, con las mismas griegas (delta −0,096, vega −4,08) y puntos muertos a tres décimas. La paridad se comprueba al medio: 55,45 de la mariposa frente a 55,18 del ancho descontado menos el crédito del iron butterfly. Lo que cambia es la horquilla: la mariposa, que compra una call dentro del dinero (5.900, horquilla de 1,7 puntos), pierde 2,15 al abrirse; el iron butterfly, con todas sus patas fuera del dinero o en él, 1,7. Entre dos estructuras equivalentes se abre la que menos horquilla cuesta. El cóndor de calls 5.900/6.000/6.200/6.300 separa los strikes centrales: cuesta 43,7, gana hasta 56,3 entre 6.000 y 6.200 y tiene los puntos muertos algo más abiertos.
Una mariposa de alas rotas usa alas de anchos distintos. La de puts del ejemplo compra la 6.000, vende dos 5.900 y compra la 5.700: el ala superior mide 100 puntos y la inferior 200.
bwb = [pata("P", 6000, 1), pata("P", 5900, -2), pata("P", 5700, 1)]
print("BWB de puts 6000/5900/5700", resumen(bwb))
print(" ", griegas(bwb))
S = np.array([5500.0, 5700, 5800, 5900, 6000, 6100, 6300])
pd.Series(valor(bwb, S, VTO) - apertura(bwb)[1], index=S,
name="P&L al vencimiento").round(1)BWB de puts 6000/5900/5700 {'medio': -4.75, 'real': -3.2, 'horquilla': 1.55, 'maximo': 103.2, 'minimo': -96.8, 'muertos': [5796.8]}
{'delta': 0.026, 'gamma': -0.00014, 'vega': -1.21, 'theta': 0.15}
5500.0 -96.8
5700.0 -96.8
5800.0 3.2
5900.0 103.2
6000.0 3.2
6100.0 3.2
6300.0 3.2
Name: P&L al vencimiento, dtype: float64Se abre con un crédito de 3,2 puntos. Si el índice sube o se queda por encima de 6.000, gana esos 3,2; si acaba en 5.900, gana 103,2; por debajo de 5.700 pierde 96,8. Es una mariposa que se paga sola a cambio de mover todo el riesgo a un lado: la caída fuerte. Su delta es casi nula hoy (0,026) y su vega negativa (−1,21): vende algo de volatilidad y, sobre todo, vende la cola izquierda. Con un crédito tan pequeño, la horquilla se come un tercio de él (al medio serían 4,75).
El iron condor antes del vencimiento
El payoff al vencimiento es la mitad de la historia. Mientras la posición está abierta, sus griegas dependen de dónde esté el índice y de cuánto falte, y pueden cambiar de signo:
credito = apertura(condor)[1]
filas = {}
for nombre, f in [("hoy", HOY),
("en 25 días", HOY + pd.Timedelta(days=25)),
("en 45 días", HOY + pd.Timedelta(days=45))]:
for S in (5800.0, 5900, 6100, 6300, 6400):
g = griegas(condor, S, f)
filas[(nombre, S)] = {
"pnl": float(valor(condor, S, f)) - credito, **g}
pd.DataFrame(filas).T.round({"pnl": 1, "delta": 3, "gamma": 5,
"vega": 2, "theta": 2}) pnl delta gamma vega theta
hoy 5800.0 -12.4 0.102 -0.00023 -0.60 -0.10
5900.0 -3.7 0.067 -0.00046 -2.07 0.10
6100.0 -1.9 -0.057 -0.00067 -3.91 0.42
6300.0 -24.0 -0.144 -0.00010 -1.38 0.26
6400.0 -38.3 -0.137 0.00023 0.50 0.07
en 25 días 5800.0 -17.2 0.161 -0.00006 0.29 -0.32
5900.0 -2.0 0.138 -0.00041 -0.92 0.03
6100.0 12.1 -0.024 -0.00118 -3.50 0.75
6300.0 -15.0 -0.218 -0.00033 -1.52 0.50
6400.0 -36.9 -0.205 0.00054 1.01 -0.00
en 45 días 5800.0 -30.1 0.310 0.00075 0.87 -1.58
5900.0 1.2 0.284 -0.00118 -0.87 0.76
6100.0 29.9 0.027 -0.00082 -0.68 0.77
6300.0 1.7 -0.413 -0.00256 -1.79 1.77
6400.0 -42.9 -0.380 0.00300 1.79 -1.15Hoy el iron condor pierde unos puntos en cualquier dirección (−1,9 en 6.100, −24,0 en 6.300) y todo su perfil es suave: gamma negativa en el centro, vega negativa (−3,91 en 6.100). A medida que pasan los días, el centro gana (+12,1 a los 25 días, +29,9 a los 45) y los bordes se endurecen. Tres cosas que no se ven en el payoff:
- La gamma cambia de signo fuera de los strikes vendidos. A los 45 días (seis antes del vencimiento), con el índice en 6.400, la gamma es +0,00300 y la vega +1,79: el ala comprada domina y la estructura se comporta como una call comprada. En 6.300, sobre el strike vendido, la gamma es −0,00256, la mayor en valor absoluto de toda la tabla: ahí cada punto del índice mueve más la delta, que ya es −0,413.
- La theta no siempre juega a favor. En 5.800 y 6.400, cerca de las alas, la theta es negativa a los 45 días (−1,58 y −1,15): el paso del tiempo también puede costar en una venta de prima.
- La delta crece hacia los lados. Neutral hoy (−0,057 en 6.100), la delta en 5.800 y en 6.400 pasa de 0,10 y −0,14 hoy a 0,31 y −0,38 a seis días del vencimiento. La gestión (ajustar, cerrar o rodar una pata) es el tema de la lección 3.5.5.
Cuatro patas, cuatro horquillas
estructuras = {"straddle comprado": straddle,
"strangle comprado": strangle,
"straddle vendido": corto_straddle,
"iron condor": condor, "iron butterfly": iron_bfly,
"mariposa de calls": mariposa, "BWB de puts": bwb}
filas = []
for nombre, e in estructuras.items():
medio, real = apertura(e)
res = resumen(e)
tope = res["maximo"] if res["maximo"] != "ilimitado" else np.nan
filas.append({"estructura": nombre, "patas": len(e),
"prima_medio": medio, "horquilla": real - medio,
"pct_prima": 100 * (real - medio) / abs(medio),
"pct_max_ganancia": 100 * (real - medio) / tope})
pd.DataFrame(filas).set_index("estructura").round(2) patas prima_medio horquilla pct_prima pct_max_ganancia
estructura
straddle comprado 2 228.10 1.00 0.44 NaN
strangle comprado 2 84.30 0.70 0.83 NaN
straddle vendido 2 -228.10 1.00 0.44 0.44
iron condor 4 -33.00 1.20 3.64 3.77
iron butterfly 4 -143.80 1.70 1.18 1.20
mariposa de calls 3 55.45 2.15 3.88 1.51
BWB de puts 3 -4.75 1.55 32.63 1.50El straddle comprado pierde un 0,44 % de su prima al cruzar la horquilla; el iron condor, un 3,64 % de lo que cobra; la mariposa de calls, un 3,88 %; la de alas rotas, un 32,6 %. Cuantas más patas y menos prima neta, más pesa la horquilla. Y el iron condor que se cierra antes del vencimiento la paga dos veces: algo más de un 7 % del crédito en un día tranquilo, antes de contar comisiones (cuatro por apertura en un bróker que cobre por pata).
Doce años de cada estructura
Con la misma maquinaria de la historia, siete estructuras abiertas cada mes. Los strikes se eligen por delta (16D es la opción de delta 0,16 en valor absoluto; 16D para el strangle y las alas internas del iron condor, 5D para las externas) o por distancia al índice (la mariposa de calls a ±3 % y la de alas rotas con strikes a −1,5 %, −3 % y −6 %, los más cercanos listados):
d16 = lambda d, t, x: strike_delta(d, t, x)
recetas = {
"straddle comprado": lambda d, S: [
("C", atm(d), 1), ("P", atm(d), 1)],
"straddle vendido": lambda d, S: [
("C", atm(d), -1), ("P", atm(d), -1)],
"strangle vendido 16D": lambda d, S: [
("C", d16(d, "C", 0.16), -1), ("P", d16(d, "P", -0.16), -1)],
"iron condor 16D/5D": lambda d, S: [
("P", d16(d, "P", -0.05), 1), ("P", d16(d, "P", -0.16), -1),
("C", d16(d, "C", 0.16), -1), ("C", d16(d, "C", 0.05), 1)],
"iron butterfly, alas 16D": lambda d, S: [
("P", d16(d, "P", -0.16), 1), ("P", atm(d), -1),
("C", atm(d), -1), ("C", d16(d, "C", 0.16), 1)],
"mariposa de calls ±3 %": lambda d, S: [
("C", cerca(d, 0.97 * S), 1), ("C", cerca(d, S), -2),
("C", cerca(d, 1.03 * S), 1)],
"BWB de puts": lambda d, S: [
("P", cerca(d, 0.985 * S), 1), ("P", cerca(d, 0.97 * S), -2),
("P", cerca(d, 0.94 * S), 1)],
}
hist = pd.DataFrame({k: historia(v)["pnl"]
for k, v in recetas.items()})
pd.DataFrame({
"media": hist.mean(), "mediana": hist.median(),
"pct_pos": 100 * (hist > 0).mean(), "desv": hist.std(),
"peor": hist.min(),
"fecha_peor": hist.idxmin().dt.strftime("%Y-%m-%d"),
"mejor": hist.max()}).round(2) media mediana pct_pos desv peor fecha_peor mejor
straddle comprado 0.11 -0.54 38.46 3.85 -8.79 2020-05-18 24.83
straddle vendido -0.15 0.50 60.14 3.85 -24.86 2020-02-24 8.60
strangle vendido 16D -0.10 0.64 78.32 2.79 -22.20 2020-02-24 2.39
iron condor 16D/5D 0.00 0.44 76.22 1.33 -7.36 2020-03-23 1.66
iron butterfly, alas 16D -0.07 -0.13 46.85 1.72 -7.21 2020-04-20 6.15
mariposa de calls ±3 % -0.16 -0.46 37.76 0.96 -2.12 2017-06-19 2.02
BWB de puts 0.11 0.07 65.73 0.65 -2.58 2025-02-24 2.05Ninguna estructura gana de forma clara en 143 meses: las medias van de −0,16 % a +0,11 % por entrada. Lo que las separa es la forma de la distribución:
- Straddle comprado: gana en el 38 % de los meses, con una mediana de −0,54 %, y su mejor mes es el 24 de febrero de 2020 (+24,8 %). Su peor mes pierde un 8,8 %: la prima pagada en mayo de 2020, con la implícita por las nubes.
- Straddle y strangle vendidos: ganan en el 60 % y el 78 % de los meses y pierden un 24,9 % y un 22,2 % en el peor, la entrada del 24 de febrero de 2020. Es la cola izquierda de la prima de volatilidad: la venta cobra en los meses normales lo que devuelve, multiplicado, en uno.
- Iron condor e iron butterfly: las alas recortan la peor entrada a −7,4 % y −7,2 %. Las fechas cambian: el peor iron condor es el de la entrada del 23 de marzo de 2020, cuando con la volatilidad alta las alas de 16D y 5D quedaban muy separadas y el rebote del 30 % las atravesó por arriba (la misma lección del vertical de crédito de la 3.5.1).
- Mariposas: riesgo pequeño y acotado (peores de −2,1 % y −2,6 %), pero la de calls pierde de media: paga horquilla y prima por una apuesta que solo cobra el máximo si el índice acaba justo en el centro. La de alas rotas, con crédito, gana en el 66 % de los meses y su peor es el de febrero de 2025, cuando el índice cayó un 6,6 % hasta el vencimiento y atravesó su ala ancha.
Errores típicos
- Leer el porcentaje de aciertos como ventaja. Un 78 % de meses positivos convive con una media negativa: lo que cuenta es cuánto se pierde cuando se pierde.
- Evaluar sin el peor año. Quitar 2020 convierte el straddle vendido de perdedor en ganador (ejercicio 2).
- Mirar las griegas solo hoy. La gamma y la theta de un iron condor cambian de signo con el precio y con el tiempo.
- Elegir la estructura cara entre dos equivalentes. Mariposa de calls o iron butterfly: misma posición, distinta horquilla.
- Olvidar que el straddle sin cobertura apuesta por la deriva. En un índice alcista, las calls vendidas pierden.
- Confundir movimiento implícito con previsión. El precio del straddle incluye la prima de riesgo de volatilidad y no avisa de las crisis: la entrada de febrero de 2020 costaba un 3,1 %.
Ejercicios
- Vende un strangle de 10D (put 5.600 y call 6.400) en la cadena de febrero. Compara su crédito, sus puntos muertos y sus griegas con los del iron condor de la cabecera.
Solución
strangle_10 = [pata("P", 5600, -1), pata("C", 6400, -1)]
print(resumen(strangle_10))
griegas(strangle_10){'medio': -32.2, 'real': -31.8, 'horquilla': 0.4, 'maximo': 31.8, 'minimo': 'ilimitado', 'muertos': [5568.2, 6431.8]}
{'delta': 0.009, 'gamma': -0.00122, 'vega': -8.03, 'theta': 1.13}El strangle de 10D cobra exactamente lo mismo que el iron condor de la cabecera, 31,8 puntos, con los puntos muertos mucho más lejos (5.568,2 y 6.431,8, un 8,6 % abajo y un 5,6 % arriba, frente a 5.868,2 y 6.331,8). A cambio, su pérdida es ilimitada y sus griegas son más del doble: vega −8,03 frente a −3,91, theta +1,13 frente a +0,41, gamma −0,00122 frente a −0,00068. Con el mismo crédito, el strangle gana más a menudo y pierde mucho más cuando pierde: el iron condor paga con un rango estrecho el tope de 68,2 puntos a su pérdida.
- Repite la tabla de la historia sin las entradas de 2020. ¿Qué estructuras cambian de signo? ¿Cuál es la peor entrada de cada una fuera de 2020?
Solución
sin_2020 = hist[hist.index.year != 2020]
pd.DataFrame({"media": sin_2020.mean(),
"pct_pos": 100 * (sin_2020 > 0).mean(),
"peor": sin_2020.min(),
"fecha_peor": sin_2020.idxmin().dt.strftime("%Y-%m-%d"),
"media_2020": hist[hist.index.year == 2020].mean()
}).round(2) media pct_pos peor fecha_peor media_2020
straddle comprado -0.25 38.17 -5.25 2018-03-19 4.11
straddle vendido 0.22 60.31 -10.37 2021-04-19 -4.20
strangle vendido 16D 0.21 80.15 -9.23 2021-04-19 -3.58
iron condor 16D/5D 0.13 77.86 -4.24 2018-01-22 -1.41
iron butterfly, alas 16D -0.02 47.33 -3.99 2018-11-19 -0.67
mariposa de calls ±3 % -0.15 38.17 -2.12 2017-06-19 -0.29
BWB de puts 0.10 64.89 -2.58 2025-02-24 0.19Sin 2020 cambian de signo cuatro: el straddle comprado (de +0,11 % a −0,25 %), el straddle vendido (de −0,15 % a +0,22 %), el strangle vendido (de −0,10 % a +0,21 %) y el iron condor (de 0,00 % a +0,13 %). Los doce meses de 2020 valen un +4,11 % de media para el straddle comprado y un −4,20 % para el vendido. Fuera de 2020, las peores entradas de las ventas desnudas son las del 19 de abril de 2021 (−10,4 % y −9,2 %): un rally que atravesó la call vendida, la cola derecha. Las de las estructuras con alas, de 2018 (−4,2 % y −4,0 %). Las mariposas no cambian: su peor mes no era de 2020. La lección no es que 2020 sea una anomalía que haya que quitar, sino que una media de venta de volatilidad depende de si la muestra contiene una crisis.
- Un iron condor de 16D/5D y una mariposa de alas rotas de puts tienen medias parecidas en la historia (0,00 % y 0,11 %) y peores entradas de −7,4 % y −2,6 %. ¿Significa eso que la mariposa de alas rotas es mejor?
Solución
No necesariamente. Son cifras de una sola historia de 143 entradas: con desviaciones mensuales de 0,65 % y 1,33 %, el error típico de cada media es de 0,05 y 0,11 puntos, del orden de la propia media. Las dos estructuras, además, no arriesgan lo mismo ni del mismo modo. La pérdida máxima del iron condor por delta crece con la volatilidad (sus alas se separan); la de la mariposa de alas rotas la fijan sus anchos en % del índice redondeados a los strikes listados, y ese redondeo cambia la estructura. El 24 de febrero de 2020 la receta dio 2.900/2.850/2.800: alas iguales, una mariposa normal que perdió un 0,13 % con el índice un 28 % más abajo. El 24 de febrero de 2025 dio 5.450/5.400/5.200: un ala inferior cuatro veces más ancha, que perdió un 2,58 % con una caída del 6,6 %. Para comparar hay que igualar el riesgo máximo por entrada y comprobar qué estructura se abrió de verdad cada mes, no fiarse de dos cifras.
Respuesta plegada en cada pregunta
Autoevaluación
El straddle 6.100 de febrero cuesta 229,1 puntos con el índice en 6.089,45. ¿Qué está diciendo ese precio?
- AQue el mercado espera que el índice suba un 3,76 %
- BQue, para no perder, el índice tiene que acabar fuera de 5.870,9-6.329,1: un movimiento de un 3,76 % en cualquier dirección
- CQue la volatilidad implícita es del 3,76 %
- DQue el straddle gana si el índice no se mueve
Ver respuesta
Respuesta B. Que, para no perder, el índice tiene que acabar fuera de 5.870,9-6.329,1: un movimiento de un 3,76 % en cualquier dirección
El straddle comprado pierde la prima entera si el índice acaba en el strike y gana lo que se aleje de él menos lo pagado. Sus puntos muertos son 6.100 ± 229,1, y 229,1 / 6.089,45 = 3,76 % es el movimiento implícito. Con la regla de mano (0,8 por la volatilidad, por la raíz de T y por el índice, con volatilidad 0,1258 y T = 51/365) sale lo mismo.
Un iron butterfly 5.900/6.100/6.300 vendido cobra 142,1 y una mariposa de calls 5.900/6.100/6.300 comprada cuesta 57,6. ¿Qué relación hay entre ellas?
- ANinguna: una se monta con puts y calls y la otra solo con calls
- BSon la misma posición por paridad: el payoff de la mariposa es el del iron butterfly más una constante, y su precio al medio cumple que la mariposa vale el ancho descontado menos el crédito del iron butterfly
- CLa mariposa tiene vega positiva y el iron butterfly negativa
- DEl iron butterfly tiene riesgo ilimitado
Ver respuesta
Respuesta B. Son la misma posición por paridad: el payoff de la mariposa es el del iron butterfly más una constante, y su precio al medio cumple que la mariposa vale el ancho descontado menos el crédito del iron butterfly
Las dos ganan como mucho en 6.100 y pierden lo mismo fuera de 5.900-6.300. Al medio, 55,45 frente a 200·e^(−rT) − 143,8 = 55,18: la paridad se cumple dentro de las horquillas. Tienen las mismas griegas; se elige la que menos horquilla cuesta abrir.
El iron condor vendido tiene gamma negativa hoy en 6.100. A seis días del vencimiento, con el índice en 6.400 (el ala comprada de calls), su gamma es positiva. ¿Por qué?
- APor un error de redondeo
- BPorque cerca del vencimiento la gamma se concentra en el strike más cercano al índice, y en 6.400 ese strike es el de la call comprada
- CPorque la volatilidad implícita ha subido
- DPorque el iron condor siempre tiene gamma positiva
Ver respuesta
Respuesta B. Porque cerca del vencimiento la gamma se concentra en el strike más cercano al índice, y en 6.400 ese strike es el de la call comprada
La gamma de una opción en el dinero crece mucho al acercarse el vencimiento y la de las demás desaparece. Con el índice en el ala comprada, la gamma de la estructura es la de esa call comprada: +0,00300 a 45 días de la apertura. Por eso las griegas de una estructura hay que mirarlas en varios precios y fechas, no solo hoy.
En la historia de 2014-2025, el straddle vendido gana en el 60 % de los meses pero su media es −0,15 % por entrada. Sin 2020, su media es +0,22 %. ¿Qué enseña?
- AQue 2020 es un dato erróneo y debe quitarse del backtest
- BQue la media de una venta de volatilidad la deciden muy pocos meses: los de 2020 pierden un 4,2 % de media cada uno y borran once años de ganancias
- CQue el straddle vendido es una estrategia sin riesgo fuera de las crisis
- DQue la prima de volatilidad no existe en los datos
Ver respuesta
Respuesta B. Que la media de una venta de volatilidad la deciden muy pocos meses: los de 2020 pierden un 4,2 % de media cada uno y borran once años de ganancias
Quitar el año malo convierte una estrategia perdedora en ganadora: es el sesgo más peligroso al evaluar ventas de prima. La peor entrada (24 de febrero de 2020) pierde un 24,9 % del índice; incluso sin 2020, la peor (abril de 2021, la pata de calls en un rally) pierde un 10,4 %.
La mariposa de alas rotas 6.000/5.900/5.700 de puts se abre con un crédito de 3,2 puntos. ¿Qué riesgo tiene?
- ANinguno, porque se cobra al abrir
- BGana 3,2 si el índice sube o se queda; gana hasta 103,2 si acaba en 5.900; pierde hasta 96,8 si acaba por debajo de 5.700
- CPierde si el índice sube
- DPierde como mucho el crédito
Ver respuesta
Respuesta B. Gana 3,2 si el índice sube o se queda; gana hasta 103,2 si acaba en 5.900; pierde hasta 96,8 si acaba por debajo de 5.700
El ala inferior es el doble de ancha que la superior (200 frente a 100 puntos): eso financia la mariposa y elimina el riesgo al alza, pero deja una pérdida de 200 − 100 − 3,2 = 96,8 puntos por debajo de 5.700. La horquilla, además, se come un tercio del crédito: al medio serían 4,75 puntos.
Ideas clave
- Un straddle o un strangle compran o venden movimiento, no dirección: el precio del straddle ATM es el movimiento implícito hasta el vencimiento, unas 0,8 veces la volatilidad por la raíz del plazo.
- Gamma y theta van juntas: la theta es aproximadamente −½ × gamma × (volatilidad × índice)², por año; quien cobra theta vende gamma y quien paga theta la compra.
- Las alas convierten una venta de straddle o strangle con riesgo ilimitado en iron butterfly o iron condor con pérdida máxima conocida; la paridad los hace equivalentes a mariposas y cóndores de calls.
- Con cuatro patas la horquilla pesa: el iron condor del ejemplo pierde un 3,6 % de su crédito al abrirse y la mariposa de alas rotas, un tercio.
- La historia de las ventas de movimiento la decide la cola: el straddle vendido gana en el 60 % de los meses, pierde un 24,9 % en el peor y su media pasa de −0,15 % a +0,22 % si se quita 2020. Las alas recortan la peor entrada a −7,4 %.