Lesson 3.5.2 · 75 min

Volatility and range structures: straddles, strangles, condors and butterflies

A straddle does not bet on direction but on the size of the move, and its winged relatives cap the risk of selling it. The lesson builds the seven volatility and range structures with the chain of the ORX index, compares what they price in with what the index moved afterward and measures the worst month of each over 143 expirations.

Fig. — Short February 2026 5,800/5,900/6,300/6,400 iron condor, opened on 31 December 2025 for a 31.8-point credit (at the bid and ask): P&L today, in 25 days and at expiry
Level 3 lessons
  1. 3.1 · Futures in depth

    1. 3.1.1 Main futures contracts
    2. 3.1.2 Futures curve: basis, cost of carry, contango and roll yield
    3. 3.1.3 Roll management, margins, SPAN and implied leverage
    4. 3.1.4 Diversified futures systems: Carver's approach
  2. 3.2 · Futures spreads

    1. 3.2.1 Calendar spreads, butterflies and condors
    2. 3.2.2 Intercommodity spreads: crack, spark and crush
    3. 3.2.3 Rate and index spreads: 2s10s, NOB, NQ/ES and Europe versus the U.S.
    4. 3.2.4 Hedge ratios: by notional, by DV01 and by volatility
    5. 3.2.5 Spread data and backtesting: listed, synthetic and legged
    6. 3.2.6 Seasonality of spreads: multi-year analysis, patterns and traps
  3. 3.3 · Advanced forex

    1. 3.3.1 FX microstructure: OTC, ECN, last look and sessions
    2. 3.3.2 Carry and crash risk, momentum and value in currencies; baskets
    3. 3.3.3 Currency risk in multi-currency portfolios; execution via OANDA, IBKR or MT5
  4. 3.4 · Option theory and pricing

    1. 3.4.1 Payoffs, put-call parity and arbitrage bounds
    2. 3.4.2 Binomial trees, Black-Scholes-Merton, Black-76 and Monte Carlo
    3. 3.4.3 Greeks: delta, gamma, vega, theta, rho, vanna, volga and charm
    4. 3.4.4 Implied volatility: Newton-Raphson, Brent, py_vollib and QuantLib
    5. 3.4.5 Smile, skew and term structure; volatility surface with SVI
    6. 3.4.6 Realized vs implied volatility; volatility risk premium; VIX and VSTOXX
    7. 3.4.7 Advanced models: Heston, SABR and local volatility
  5. 3.5 · Option structures in Python

    1. 3.5.1 Directional and vertical structures: call, put, covered call, collar and spreads
    2. 3.5.2 Volatility and range structures: straddles, strangles, condors and butterflies
    3. 3.5.3 Calendars, diagonals, ratios, backspreads, risk reversals and jade lizard
    4. 3.5.4 A custom tool: payoff, aggregate Greeks and scenarios
    5. 3.5.5 Management: adjustments, rolls, early closes, early assignment and pin risk
  6. 3.6 · Systematic volatility and options strategies

    1. 3.6.1 Systematic premium selling: put-write, strangles and their tail risk
    2. 3.6.2 Delta hedging, gamma scalping and dispersion
    3. 3.6.3 VIX and VSTOXX strategies: volatility futures and ETPs
    4. 3.6.4 Options backtesting: data, fills and event adjustments
  7. 3.7 · Crypto derivatives and specific strategies

    1. 3.7.1 Perpetuals: funding mechanics, mark price and liquidations
    2. 3.7.2 Funding arbitrage, basis with futures and cross-exchange arbitrage
    3. 3.7.3 Crypto options, on-chain data and DeFi
    4. 3.7.4 ccxt for trading on multiple exchanges
    5. 3.7.5 MiCA: the EU framework for crypto-assets and their service providers (CASPs)

Buying or selling movement

The structures in lesson 3.5.1 bet on a direction. Those in this one bet on the size of the move. A long straddle (a call and a put with the same strike, usually at the money) gains if the index moves away from the strike by more than it cost, up or down; a strangle does the same with an out-of-the-money put and call, cheaper and with the breakeven points farther away. Sold, they gain if the index stays still.

Buy movementGamma and vega +Pays thetaSell movementGamma and vega −Collects theta
Mermaid source
flowchart LR
  C[Buy movement] --> CG[Gamma and vega +]
  CG --> CT[Pays theta]
  V[Sell movement] --> VG[Gamma and vega −]
  VG --> VT[Collects theta]

Both positions are a bet on the volatility that will be realized versus the one the market prices into the premium. The seller collects the volatility risk premium (lesson 3.4.6): on average, implied volatility trades above the volatility realized afterward, because buying protection against large moves has value. But the seller collects it in exchange for keeping those large moves, and the history at the end of the lesson shows what that means.

Wings change the risk. Selling a straddle has unlimited loss; selling a straddle and buying a farther put and call is an iron butterfly, with capped loss. The same turns a short strangle into an iron condor. By parity, each one has a twin made only with calls (or only with puts): the butterfly and the condor. And if one of the wings is wider than the other, the butterfly is a broken-wing butterfly.

plus wingsplus wingsparityparityone wing widerShort straddleIron butterflyShort strangleIron condorCall butterflyCall condorBroken wing
Mermaid source
flowchart TB
  SD[Short straddle] -- plus wings --> IB[Iron butterfly]
  SG[Short strangle] -- plus wings --> IC[Iron condor]
  IB -- parity --- MC[Call butterfly]
  IC -- parity --- CC[Call condor]
  MC -- one wing wider --> BW[Broken wing]

The February 2026 chain

The data and conventions are those of lesson 3.5.1: the synthetic chain of the ORX index on December 31, 2025, expiration on February 20, 2026 (51 calendar days), prices in points per unit (× 100 USD per contract), T in calendar days / 365, continuous r and q, theta per calendar day and vega per volatility point. Each lesson of the course runs in its own process, so the first cell repeats the functions from 3.5.1 (bsm, pata, apertura, valor, resumen, griegas, con_delta and strike_delta); each one is explained there. Remember that the data is synthetic, with a planted volatility premium: it serves to see the mechanics, not as proof of what happens in the real market.

IN [01]python
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):
    """Continuous rate r and dividend q (decimal fraction) of an
    expiration: r from the curve; q implied by the forward."""
    v = verdad[(verdad.fecha == fecha)
               & (verdad.vencimiento == vto)].iloc[0]
    T = v.dias / 365                   # calendar days / 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):
    """Price (points), delta, gamma (per point), vega (per
    volatility point) and theta (per calendar day).
    S can be an array; with T = 0 it returns the 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):
    """A leg: n > 0 long, n < 0 short. Type "S" is the index
    itself (a portfolio that replicates it)."""
    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):
    """Cost in points at mid and crossing the bid-ask spread:
    long legs at the ask, short legs at the bid (> 0 = debit)."""
    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"):
    """Value (or Greek) of the structure if the index is at S
    on `fecha`, with each leg's IV frozen."""
    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)   # from 1 point to 3 times S0


def resumen(patas):
    """Cost, maximum, minimum and breakeven points at expiration
    (the structure's first one), in points per unit."""
    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]          # slope toward large S
    return {"medio": round(float(medio), 2),
            "real": round(float(real), 2),
            "horquilla": round(float(real - medio), 2),
            "maximo": "unlimited" if final > 1e-9
            else round(float(pnl.max()), 2),
            "minimo": "unlimited" 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):
    """Aggregate Greeks per unit (× 100 USD per contract)."""
    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):
    """The chain of one expiration with the BSM delta of each row."""
    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()])
IN [02]python
r, q = mercado(VTO)
dias = (VTO - HOY).days
hoy = con_delta()
print(f"ORX {S0}  days {dias}  r {r:.4f}  q {q:.4f}")
x = hoy.pivot(index="strike", columns="tipo",
              values=["bid", "ask", "delta"])
x.loc[5600:6600].round(3)
OUT [02]The February 2026 chain
ORX 6089.45  days 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.986

The February ladder around the money: strikes every 100 points, the 6,300 and 6,400 calls with deltas of 0.214 and 0.096, the 5,900 and 5,600 puts with −0.261 and −0.105. These are the legs of the examples.

Long straddle and strangle

IN [03]python
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"implied move {100 * coste / S0:.2f} %"
      f"  rule 0.8·σ·√T·S {regla:.1f}")
OUT [03]Long straddle and strangle
straddle 6100 {'medio': 228.1, 'real': 229.1, 'horquilla': 1.0, 'maximo': 'unlimited', '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': 'unlimited', 'minimo': -85.0, 'muertos': [5815.0, 6385.0]}
    {'delta': -0.047, 'gamma': 0.0022, 'vega': 14.02, 'theta': -1.74}
implied move 3.76 %  rule 0.8·σ·√T·S 229.1

The 6,100 straddle costs 229.1 points at the ask (one point more than at mid) and its breakeven points are 5,870.9 and 6,329.1: the index has to move 3.76% in 51 days, in either direction, for it not to lose. That percentage is the implied move, and the rule of thumb 0.8 σT S0.8\,\sigma\sqrt{T}\,S (which comes from the expected value of ∣Z∣|Z| in a normal distribution being 2/π≈0.8\sqrt{2/\pi} \approx 0.8) gives 229.1 with the volatility of the ATM leg: the rule is approximate, here it matches to the decimal. The 5,900/6,300 strangle costs 85.0 and needs the index to leave 5,815.0-6,385.0, 4.5% down or 4.9% up.

The Greeks summarize the position: delta almost zero (0.049; the strike is not exactly at the forward), gamma of 0.00278, vega of 18.1 points per volatility point (1,810 USD per contract) and theta of −2.23 points a day. Gamma and theta are not independent. The Black-Scholes equation, for a delta-neutral portfolio, says that what is gained through gamma offsets what is lost through the passage of time:

Θ≈−12 Γ σ2S2(per year; per day, divided by 365).\Theta \approx -\tfrac{1}{2}\,\Gamma\,\sigma^2 S^2 \quad \text{(per year; per day, divided by 365)}.
IN [04]python
S = np.array([5700.0, 5900, 6000, 6100, 6200, 6300, 6500])
antes = pd.DataFrame({
    "today": valor(straddle, S, HOY) - coste,
    "in 25 days": valor(straddle, S, HOY + pd.Timedelta(days=25))
    - coste,
    "expiration": 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)
OUT [04]The straddle before expiration
theta -2.23  −½·Γ·σ²·S²/365 -2.23
        today  in 25 days  expiration
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.9

The relationship holds to two decimal places (−2.23 and −2.23). It is the most useful way to think about a volatility structure: whoever buys gamma pays theta and whoever sells gamma collects theta, and the balance is decided by whether the index moves more or less than volatility implies. The table shows it in prices: with the index at 6,100 the straddle is roughly flat today (−0.4) and loses 65.7 if the index is still there in 25 days; at 5,700 it gains 170.7 today and 163.4 in 25 days. Moving fast is worth more than moving late. At 5,700 today's P&L and the one at expiration almost coincide (170.7 and 170.9), but not at 6,500 (212.0 and 170.9): besides time value, with r > q an in-the-money call is worth more than its intrinsic value (the strike is paid later) and an in-the-money put, somewhat less.

The price of the straddle versus what the index moved

How much did the index move, in the data, compared with what the straddle priced in? The next cell prepares the monthly history used by the rest of the lesson (the same rule as in 3.5.1: open on the session after each expiration with the next expiration, buys at the ask and sells at the bid, settlement at the expiration close, P&L in % of the index on the entry day) and compares the price of the ATM straddle with the realized move:

IN [05]python
vtos = np.sort(cadena.vencimiento.unique())
sesiones = indice.index
por_dia = dict(tuple(cadena.groupby(["fecha", "vencimiento"])))
entradas = []                  # (session, expiration, chain)
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):
    """Opens the structure on the session after each monthly
    expiration with the next expiration, at bid/ask, and
    settles it at the expiration close. P&L in % of the index."""
    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):
    """The listed strike closest to 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)} entries; mean implied move "
      f"{mov.coste.mean():.2f} %, mean realized "
      f"{mov.realizado.mean():.2f} %")
print(f"months with realized > implied: "
      f"{100 * (mov.realizado > mov.coste).mean():.1f} %")
mov.nlargest(4, "realizado")[["coste", "realizado", "pnl"]].round(2)
OUT [05]Twelve years of monthly entries
143 entries; mean implied move 3.73 %, mean realized 3.87 %
months with realized > implied: 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.02
Fig. — Implied move (ATM straddle price at the ask, in % of the index) and realised move to expiry in the 143 monthly trades from 2014 to 2025

The mean implied move is 3.73% and the mean realized move, 3.87%: on average, the index moved somewhat more than the straddle cost. And yet the realized move only exceeded the implied one in 39% of the months. Both things at once are the signature of a fat tail: in most months the index moves less than priced in (the seller gains a little) and in a few months it moves far more. The four largest are from 2020: at the February 24 entry the straddle cost 3.08% and the index moved 28.24%; the long straddle gained 24.83% of the index. That month's volatility was not in the premium: the implied volatility on February 24 was that of a calm market.

Selling the straddle and the strangle

IN [06]python
corto_straddle = [pata("C", 6100, -1), pata("P", 6100, -1)]
corto_strangle = [pata("C", 6300, -1), pata("P", 5900, -1)]
for nombre, e in [("short straddle", corto_straddle),
                  ("short strangle", corto_strangle)]:
    print(nombre, resumen(e))
    print("   ", griegas(e))
OUT [06]Selling the straddle and the strangle
short straddle {'medio': -228.1, 'real': -227.1, 'horquilla': 1.0, 'maximo': 227.1, 'minimo': 'unlimited', 'muertos': [5872.9, 6327.1]}
    {'delta': -0.049, 'gamma': -0.00278, 'vega': -18.1, 'theta': 2.23}
short strangle {'medio': -84.3, 'real': -83.6, 'horquilla': 0.7, 'maximo': 83.6, 'minimo': 'unlimited', 'muertos': [5816.4, 6383.6]}
    {'delta': 0.047, 'gamma': -0.0022, 'vega': -14.02, 'theta': 1.74}

Sold, the straddle collects 227.1 (at the bid; two points less than buying it would cost) and the strangle 83.6. They are mirror images of the purchases: negative gamma and vega, positive theta (+2.23 and +1.74 points a day) and unlimited loss on both sides. That is why almost nobody sells them naked except with a lot of capital and active management; the winged version is the usual one.

Iron condor and iron butterfly

IN [07]python
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))
OUT [07]Iron condor and iron butterfly
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}

The header figure is the 5,800/5,900/6,300/6,400 iron condor: it sells the 5,900/6,300 strangle and buys the 5,800 and 6,400 wings. It collects 31.8 points (1.2 less than at mid), gains that if the index ends between the short strikes and loses at most 68.2 outside the wings: the width of 100 points minus the credit. Its breakeven points are at 5,868.2 and 6,331.8. Wings cost money: the short strangle collected 83.6 and the iron condor collects 31.8, but the risk goes from unlimited to 68.2 points (6,820 USD per contract). Vega is divided by 3.6 (from −14.02 to −3.91).

The 5,900/6,100/6,300 iron butterfly sells the at-the-money straddle and buys wings 200 points away. It collects 142.1 and loses at most 57.9: its ratio between maximum gain and maximum loss is the inverse of the iron condor's, in exchange for a narrow profit range (5,957.9-6,242.1) in which the maximum is almost never collected, because that requires ending exactly at 6,100.

Butterflies, condors and broken wings

The call butterfly buys a low-strike call, sells two of the middle strike and buys one of the high strike. By parity, each call is a put plus a linear piece, and the linear pieces of the four legs cancel out: the call butterfly and the long iron butterfly have the same payoff except for a constant (the width of the wing).

IN [08]python
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 [("butterfly 5900/6100/6300", mariposa),
                  ("call condor 5900/6000/6200/6300", condor_c)]:
    print(nombre, resumen(e))
    print("   ", griegas(e))
ancho = 200 * np.exp(-r * dias / 365)
print(f"parity: butterfly {apertura(mariposa)[0]:.2f}  discounted"
      f" width − credit {ancho + apertura(iron_bfly)[0]:.2f}")
OUT [08]Call butterfly and call condor
butterfly 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}
call condor 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}
parity: butterfly 55.45  discounted width − credit 55.18

The 5,900/6,100/6,300 butterfly costs 57.6 and gains at most 142.4; the short iron butterfly collected 142.1 and lost at most 57.9. They are the same position, with the same Greeks (delta −0.096, vega −4.08) and breakeven points three tenths apart. Parity is checked at mid: 55.45 for the butterfly versus 55.18 for the discounted width minus the credit of the iron butterfly. What changes is the bid-ask spread: the butterfly, which buys an in-the-money call (5,900, spread of 1.7 points), loses 2.15 on opening; the iron butterfly, with all its legs out of the money or at the money, 1.7. Between two equivalent structures, you open the one that costs less spread. The 5,900/6,000/6,200/6,300 call condor separates the middle strikes: it costs 43.7, gains up to 56.3 between 6,000 and 6,200 and has its breakeven points somewhat wider apart.

A broken-wing butterfly uses wings of different widths. The put version in the example buys the 6,000, sells two 5,900 and buys the 5,700: the upper wing measures 100 points and the lower one 200.

IN [09]python
bwb = [pata("P", 6000, 1), pata("P", 5900, -2), pata("P", 5700, 1)]
print("put BWB 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 at expiration").round(1)
OUT [09]Broken-wing butterfly
put BWB 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 at expiration, dtype: float64
Fig. — February 2026 5,900/6,100/6,300 call butterfly, 5,900/6,000/6,200/6,300 call condor and 6,000/5,900/5,700 broken-wing put butterfly: P&L at expiry with the opening at the bid and ask

It is opened for a credit of 3.2 points. If the index rises or stays above 6,000, it gains those 3.2; if it ends at 5,900, it gains 103.2; below 5,700 it loses 96.8. It is a butterfly that pays for itself in exchange for moving all the risk to one side: the sharp fall. Its delta is almost zero today (0.026) and its vega negative (−1.21): it sells some volatility and, above all, it sells the left tail. With such a small credit, the bid-ask spread eats a third of it (at mid it would be 4.75).

The iron condor before expiration

The payoff at expiration is half the story. While the position is open, its Greeks depend on where the index is and on how much time is left, and they can change sign:

IN [10]python
credito = apertura(condor)[1]
filas = {}
for nombre, f in [("today", HOY),
                  ("in 25 days", HOY + pd.Timedelta(days=25)),
                  ("in 45 days", 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})
OUT [10]The iron condor before expiration
                    pnl  delta    gamma  vega  theta
today      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
in 25 days 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
in 45 days 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.15

Today the iron condor loses a few points in any direction (−1.9 at 6,100, −24.0 at 6,300) and its whole profile is smooth: negative gamma in the center, negative vega (−3.91 at 6,100). As the days go by, the center gains (+12.1 at 25 days, +29.9 at 45) and the edges harden. Three things that do not show in the payoff:

  • Gamma changes sign outside the short strikes. At 45 days (six before expiration), with the index at 6,400, gamma is +0.00300 and vega +1.79: the long wing dominates and the structure behaves like a long call. At 6,300, on the short strike, gamma is −0.00256, the largest in absolute value in the whole table: there each index point moves delta the most, and delta is already −0.413.
  • Theta does not always work in your favor. At 5,800 and 6,400, near the wings, theta is negative at 45 days (−1.58 and −1.15): the passage of time can also cost money in a premium sale.
  • Delta grows toward the sides. Neutral today (−0.057 at 6,100), delta at 5,800 and at 6,400 goes from 0.10 and −0.14 today to 0.31 and −0.38 six days before expiration. Management (adjusting, closing or rolling a leg) is the topic of lesson 3.5.5.

Four legs, four bid-ask spreads

IN [11]python
estructuras = {"long straddle": straddle,
               "long strangle": strangle,
               "short straddle": corto_straddle,
               "iron condor": condor, "iron butterfly": iron_bfly,
               "call butterfly": mariposa, "put BWB": bwb}
filas = []
for nombre, e in estructuras.items():
    medio, real = apertura(e)
    res = resumen(e)
    tope = res["maximo"] if res["maximo"] != "unlimited" 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)
OUT [11]Four legs, four bid-ask spreads
                patas  prima_medio  horquilla  pct_prima  pct_max_ganancia
estructura
long straddle       2       228.10       1.00       0.44               NaN
long strangle       2        84.30       0.70       0.83               NaN
short straddle      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
call butterfly      3        55.45       2.15       3.88              1.51
put BWB             3        -4.75       1.55      32.63              1.50

The long straddle loses 0.44% of its premium crossing the bid-ask spread; the iron condor, 3.64% of what it collects; the call butterfly, 3.88%; the broken-wing one, 32.6%. The more legs and the smaller the net premium, the more the spread weighs. And the iron condor that is closed before expiration pays it twice: somewhat more than 7% of the credit on a quiet day, before counting commissions (four per opening at a broker that charges per leg).

Twelve years of each structure

With the same history machinery, seven structures opened every month. Strikes are chosen by delta (16D is the option with a delta of 0.16 in absolute value; 16D for the strangle and the inner wings of the iron condor, 5D for the outer ones) or by distance to the index (the call butterfly at ±3% and the broken-wing one with strikes at −1.5%, −3% and −6%, the closest listed):

IN [12]python
d16 = lambda d, t, x: strike_delta(d, t, x)
recetas = {
    "long straddle": lambda d, S: [
        ("C", atm(d), 1), ("P", atm(d), 1)],
    "short straddle": lambda d, S: [
        ("C", atm(d), -1), ("P", atm(d), -1)],
    "short strangle 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, 16D wings": 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)],
    "call butterfly ±3%": lambda d, S: [
        ("C", cerca(d, 0.97 * S), 1), ("C", cerca(d, S), -2),
        ("C", cerca(d, 1.03 * S), 1)],
    "put BWB": 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)
OUT [12]Seven structures, 143 expirations
                           media  mediana  pct_pos  desv   peor  fecha_peor  mejor
long straddle               0.11    -0.54    38.46  3.85  -8.79  2020-05-18  24.83
short straddle             -0.15     0.50    60.14  3.85 -24.86  2020-02-24   8.60
short strangle 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, 16D wings  -0.07    -0.13    46.85  1.72  -7.21  2020-04-20   6.15
call butterfly ±3%         -0.16    -0.46    37.76  0.96  -2.12  2017-06-19   2.02
put BWB                     0.11     0.07    65.73  0.65  -2.58  2025-02-24   2.05
Fig. — 143 monthly trades from 2014 to 2025 (opened at the bid and ask, settled at expiry): mean and worst trade of each volatility structure, in % of the index

No structure clearly wins over 143 months: the averages range from −0.16% to +0.11% per entry. What sets them apart is the shape of the distribution:

  • Long straddle: it wins in 38% of the months, with a median of −0.54%, and its best month is February 24, 2020 (+24.8%). Its worst month loses 8.8%: the premium paid in May 2020, with implied volatility through the roof.
  • Short straddle and strangle: they win in 60% and 78% of the months and lose 24.9% and 22.2% in the worst, the February 24, 2020 entry. This is the left tail of the volatility premium: the sale collects in normal months what it gives back, multiplied, in one.
  • Iron condor and iron butterfly: the wings cut the worst entry to −7.4% and −7.2%. The dates change: the worst iron condor is the one of the March 23, 2020 entry, when with high volatility the 16D and 5D wings were far apart and the 30% rebound went through them on the upside (the same lesson as the credit vertical in 3.5.1).
  • Butterflies: small, capped risk (worst entries of −2.1% and −2.6%), but the call one loses on average: it pays bid-ask spread and premium for a bet that only collects the maximum if the index ends right at the center. The broken-wing one, opened for a credit, wins in 66% of the months and its worst is the one of February 2025, when the index fell 6.6% to expiration and went through its wide wing.

Common mistakes

  • Reading the hit rate as an edge. 78% positive months coexists with a negative average: what counts is how much is lost when it loses.
  • Evaluating without the worst year. Removing 2020 turns the short straddle from loser into winner (exercise 2).
  • Looking at the Greeks only today. The gamma and theta of an iron condor change sign with price and with time.
  • Choosing the expensive structure out of two equivalent ones. Call butterfly or iron butterfly: same position, different bid-ask spread.
  • Forgetting that the unhedged straddle bets on the drift. In a rising index, the short calls lose.
  • Confusing implied move with forecast. The price of the straddle includes the volatility risk premium and gives no warning of crises: the February 2020 entry cost 3.1%.

Exercises

  1. Sell a 10D strangle (5,600 put and 6,400 call) on the February chain. Compare its credit, its breakeven points and its Greeks with those of the iron condor in the header.
Solution
IN [13]python
strangle_10 = [pata("P", 5600, -1), pata("C", 6400, -1)]
print(resumen(strangle_10))
griegas(strangle_10)
OUT [13]Short 10D strangle
{'medio': -32.2, 'real': -31.8, 'horquilla': 0.4, 'maximo': 31.8, 'minimo': 'unlimited', 'muertos': [5568.2, 6431.8]}
{'delta': 0.009, 'gamma': -0.00122, 'vega': -8.03, 'theta': 1.13}

The 10D strangle collects exactly the same as the iron condor in the header, 31.8 points, with the breakeven points much farther away (5,568.2 and 6,431.8, 8.6% down and 5.6% up, versus 5,868.2 and 6,331.8). In exchange, its loss is unlimited and its Greeks are more than double: vega −8.03 versus −3.91, theta +1.13 versus +0.41, gamma −0.00122 versus −0.00068. With the same credit, the strangle wins more often and loses much more when it loses: the iron condor pays for the 68.2-point cap on its loss with a narrow range.

  1. Repeat the history table without the 2020 entries. Which structures change sign? What is the worst entry of each one outside 2020?
Solution
IN [14]python
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)
OUT [14]The history without 2020
                           media  pct_pos   peor  fecha_peor  media_2020
long straddle              -0.25    38.17  -5.25  2018-03-19        4.11
short straddle              0.22    60.31 -10.37  2021-04-19       -4.20
short strangle 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, 16D wings  -0.02    47.33  -3.99  2018-11-19       -0.67
call butterfly ±3%         -0.15    38.17  -2.12  2017-06-19       -0.29
put BWB                     0.10    64.89  -2.58  2025-02-24        0.19

Without 2020 four change sign: the long straddle (from +0.11% to −0.25%), the short straddle (from −0.15% to +0.22%), the short strangle (from −0.10% to +0.21%) and the iron condor (from 0.00% to +0.13%). The twelve months of 2020 are worth +4.11% on average for the long straddle and −4.20% for the short one. Outside 2020, the worst entries of the naked sales are those of April 19, 2021 (−10.4% and −9.2%): a rally that went through the short call, the right tail. Those of the winged structures, from 2018 (−4.2% and −4.0%). The butterflies do not change: their worst month was not in 2020. The lesson is not that 2020 is an anomaly to be removed, but that the average of a volatility sale depends on whether the sample contains a crisis.

  1. A 16D/5D iron condor and a put broken-wing butterfly have similar averages in the history (0.00% and 0.11%) and worst entries of −7.4% and −2.6%. Does that mean the broken-wing butterfly is better?
Solution

Not necessarily. These are figures from a single history of 143 entries: with monthly standard deviations of 0.65% and 1.33%, the standard error of each average is 0.05 and 0.11 points, of the same order as the average itself. The two structures, moreover, do not risk the same amount nor in the same way. The maximum loss of the delta-based iron condor grows with volatility (its wings spread apart); that of the broken-wing butterfly is set by its widths in % of the index rounded to the listed strikes, and that rounding changes the structure. On February 24, 2020 the recipe gave 2,900/2,850/2,800: equal wings, a regular butterfly that lost 0.13% with the index 28% lower. On February 24, 2025 it gave 5,450/5,400/5,200: a lower wing four times wider, which lost 2.58% with a 6.6% fall. To compare, you have to equalize the maximum risk per entry and check which structure was actually opened each month, not rely on two figures.

Each answer is folded under its question

Self-assessment

  1. The February 6,100 straddle costs 229.1 points with the index at 6,089.45. What is that price saying?

    • AThat the market expects the index to rise 3.76%
    • BThat implied volatility is 3.76%
    • CThat the straddle gains if the index does not move
    • DThat, to avoid a loss, the index has to end outside 5,870.9-6,329.1: a move of 3.76% in either direction
    Show answer

    Answer D. That, to avoid a loss, the index has to end outside 5,870.9-6,329.1: a move of 3.76% in either direction

    The long straddle loses the whole premium if the index ends at the strike and gains whatever it moves away from it minus what was paid. Its breakeven points are 6,100 ± 229.1, and 229.1 / 6,089.45 = 3.76% is the implied move. The rule of thumb (0.8 times volatility, times the square root of T and times the index, with volatility 0.1258 and T = 51/365) gives the same.

  2. A short 5,900/6,100/6,300 iron butterfly collects 142.1 and a long 5,900/6,100/6,300 call butterfly costs 57.6. What is the relationship between them?

    • AThey are the same position by parity: the payoff of the butterfly is that of the iron butterfly plus a constant, and its mid price satisfies that the butterfly is worth the discounted width minus the credit of the iron butterfly
    • BNone: one is built with puts and calls and the other only with calls
    • CThe butterfly has positive vega and the iron butterfly negative
    • DThe iron butterfly has unlimited risk
    Show answer

    Answer A. They are the same position by parity: the payoff of the butterfly is that of the iron butterfly plus a constant, and its mid price satisfies that the butterfly is worth the discounted width minus the credit of the iron butterfly

    Both gain the most at 6,100 and lose the same outside 5,900-6,300. At mid, 55.45 versus 200·e^(−rT) − 143.8 = 55.18: parity holds within the bid-ask spreads. They have the same Greeks; you choose the one that costs less spread to open.

  3. The short iron condor has negative gamma today at 6,100. Six days before expiration, with the index at 6,400 (the long call wing), its gamma is positive. Why?

    • ABecause of a rounding error
    • BBecause near expiration gamma concentrates at the strike closest to the index, and at 6,400 that strike is the long call's
    • CBecause implied volatility has risen
    • DBecause the iron condor always has positive gamma
    Show answer

    Answer B. Because near expiration gamma concentrates at the strike closest to the index, and at 6,400 that strike is the long call's

    The gamma of an at-the-money option grows sharply as expiration approaches and that of the others vanishes. With the index at the long wing, the gamma of the structure is that of that long call: +0.00300 at 45 days after opening. That is why the Greeks of a structure have to be looked at across several prices and dates, not only today.

  4. In the 2014-2025 history, the short straddle wins in 60% of the months but its average is −0.15% per entry. Without 2020, its average is +0.22%. What does it teach?

    • AThat 2020 is wrong data and should be removed from the backtest
    • BThat the short straddle is a riskless strategy outside crises
    • CThat the average of a volatility sale is decided by very few months: those of 2020 lose 4.2% on average each and wipe out eleven years of gains
    • DThat the volatility premium does not exist in the data
    Show answer

    Answer C. That the average of a volatility sale is decided by very few months: those of 2020 lose 4.2% on average each and wipe out eleven years of gains

    Removing the bad year turns a losing strategy into a winning one: it is the most dangerous bias when evaluating premium selling. The worst entry (February 24, 2020) loses 24.9% of the index; even without 2020, the worst one (April 2021, the call leg in a rally) loses 10.4%.

  5. The 6,000/5,900/5,700 put broken-wing butterfly is opened for a credit of 3.2 points. What risk does it carry?

    • AIt gains 3.2 if the index rises or stays put; it gains up to 103.2 if it ends at 5,900; it loses up to 96.8 if it ends below 5,700
    • BNone, because it collects at opening
    • CIt loses if the index rises
    • DIt loses at most the credit
    Show answer

    Answer A. It gains 3.2 if the index rises or stays put; it gains up to 103.2 if it ends at 5,900; it loses up to 96.8 if it ends below 5,700

    The lower wing is twice as wide as the upper one (200 versus 100 points): that finances the butterfly and removes the upside risk, but leaves a loss of 200 − 100 − 3.2 = 96.8 points below 5,700. The bid-ask spread, moreover, eats a third of the credit: at mid it would be 4.75 points.

Key points

  • A straddle or a strangle buys or sells movement, not direction: the price of the ATM straddle is the implied move to expiration, about 0.8 times volatility times the square root of the time.
  • Gamma and theta go together: theta is approximately −½ × gamma × (volatility × index)², per year; whoever collects theta sells gamma and whoever pays theta buys it.
  • Wings turn a short straddle or strangle with unlimited risk into an iron butterfly or iron condor with a known maximum loss; parity makes them equivalent to call butterflies and condors.
  • With four legs the bid-ask spread weighs: the iron condor in the example loses 3.6% of its credit on opening and the broken-wing butterfly, a third.
  • The history of movement sales is decided by the tail: the short straddle wins in 60% of the months, loses 24.9% in the worst and its average goes from −0.15% to +0.22% if 2020 is removed. Wings cut the worst entry to −7.4%.

Resources