---
title: "3.5.1 Directional options and vertical spreads"
description: "Long calls and puts, covered call, protective put, collar and debit and credit verticals, at bid/ask on the chain and over twelve years of history."
url: "https://pythonparatrading.com/en/python-trading-course/level-3/directional-and-vertical-option-spreads/"
lang: "en"
image: "https://pythonparatrading.com/og/en/python-trading-course/level-3/directional-and-vertical-option-spreads.png"
---

# 3.5.1 · Directional and vertical structures: call, put, covered call, collar and spreads

> An options structure is a sum of legs, and everything that matters about it comes from that sum: what it costs to open, what it can gain or lose, where its breakeven points are and how it responds to price, time and volatility. This lesson builds the directional structures and the verticals with the synthetic chain of the ORX index and follows them over 143 expirations.

- Course: [Level 3: Derivatives: futures, spreads, forex and options](https://pythonparatrading.com/en/python-trading-course/level-3/)
- Module: 3.5 · Option structures in Python
- Duration: 75 min
- Prerequisites: [3.4.1 · Payoffs, put-call parity and arbitrage bounds](https://pythonparatrading.com/en/python-trading-course/level-3/option-payoffs-put-call-parity-arbitrage/), [3.4.2 · Binomial trees, Black-Scholes-Merton, Black-76 and Monte Carlo](https://pythonparatrading.com/en/python-trading-course/level-3/binomial-black-scholes-black-76-monte-carlo/), [3.4.3 · Greeks: delta, gamma, vega, theta, rho, vanna, volga and charm](https://pythonparatrading.com/en/python-trading-course/level-3/first-and-second-order-option-greeks/)

## By the end of this lesson

- Represent a structure as a list of legs and compute its opening cost at mid and crossing the bid-ask spread (buys at the ask, sells at the bid).
- Obtain the maximum, the minimum and the breakeven points at expiration, and the P&L before expiration with BSM and the implied volatility of each leg.
- Aggregate the Greeks of a structure with their units (theta per calendar day, vega per volatility point, per unit and per contract) and check them against QuantLib.
- Tell covered call, protective put and collar, and debit and credit verticals apart by their real risk and not by the sign of the premium.
- Measure the historical behavior of each structure with monthly entries and its worst episode.

## A structure is a sum of legs

A long call is a directional bet with the loss limited to the premium. Almost everything else done with options is a
combination: a long call and a short call with a higher strike, a stock with a short call on top, a long put to set a
floor. Module 3.4 valued each option separately; this module puts them together. The idea that makes any structure
manageable is linearity: the payoff, the value before expiration and each Greek of a structure are the sum of those of
its legs, each with its sign (long or short) and its quantity.

$$
V_{\text{structure}}(S, t) = \sum_i n_i \, V_i(S, t), \qquad
\Delta_{\text{structure}} = \sum_i n_i \, \Delta_i, \quad \ldots
$$

The structures in this lesson are grouped by what they need to happen:

```mermaid
flowchart LR
  V[View] --> A[Bullish]
  V --> B[Bearish]
  V --> C[I already hold the index]
  A --> A1[Long call]
  A --> A2[Bull call or put]
  B --> B1[Long put]
  B --> B2[Bear put or call]
  C --> C1[Covered call]
  C --> C2[Protective put]
  C --> C3[Collar]
```

- **Simple directional**: a long call or a long put. Risk limited to the premium, uncapped gain (for the put, until
  the index reaches zero).
- **On the underlying**: the index (in practice, an ETF or a future that replicates it) with an option on top. The
  **covered call** sells a call and trades the upside above the strike for a premium; the **protective put** buys a
  put and pays for a floor; the **collar** does both and uses the premium of the call to pay for the put.
- **Verticals**: two options of the same type and expiration with different strikes, one long and one short. They cap
  both gain and loss. Depending on which leg is more expensive, they are opened for a **debit** (you pay) or a
  **credit** (you collect).

The rest of the module follows the same template with volatility structures (3.5.2) and multi-expiration structures
(3.5.3); lesson 3.5.4 gathers these functions into a tool and 3.5.5 covers the management of open positions.

## The chain of December 31, 2025

The data is the daily chain of the fictitious **ORX** index (`opciones3_cadena.parquet`), which imitates SPX options:
European, cash-settled, a multiplier of 100 USD per point (the same as SPX according to the Cboe specifications), three
monthly expirations listed every day. It is synthetic data with a known volatility surface and a planted volatility
risk premium (implied volatility trades on average above the volatility realized afterward); nothing measured here is
evidence that a structure works in the real market.

Before valuing anything, it is worth fixing the conventions, because every library and every broker uses its own:

| Quantity | Lesson convention |
|---|---|
| Option price | In index points per unit; per contract, × 100 USD |
| Time T | Calendar days to expiration / 365 (options expire at the close) |
| Rate r | Continuous, as a decimal fraction, from the curve interpolated at T (`tipo_libre` in `opciones3_verdad`) |
| Dividend q | Continuous yield, implied by the forward: q = r − ln(F/S)/T |
| Volatility $\sigma$ | Annual, as a decimal fraction (0.1258 = 12.58%) |
| Delta | Option points per index point; per contract, × 100 = USD per point |
| Gamma | Change in delta per index point |
| Vega | Option points per volatility point ($\sigma$ from 0.1258 to 0.1358) |
| Theta | Option points per calendar day |

```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


r, q = mercado(VTO)
dias = (VTO - HOY).days
print(f"ORX {S0}  days {dias}  r {r:.4f}  q {q:.4f}")
print("published q:", indice.loc[HOY, "rentabilidad_dividendo"] / 100)
```
```output
ORX 6089.45  days 51  r 0.0368  q 0.0117
published q: 0.011732
```

The index closes at 6,089.45 and the February expiration is 51 calendar days away. The dividend implied by the forward
(0.0117) matches the dividend yield published in `opciones3_indice.csv` (1.1732% a year): the true forward was built
with it, so both routes give the same result. In a real chain that truth column does not exist and q (or the forward
directly) is obtained from the put-call parity of the mids (lesson 3.4.1).

Valuation uses the Black-Scholes-Merton model with continuous dividend from lesson 3.4.2, with the analytical Greeks
from 3.4.3, written so that they accept a vector of index prices (what is needed to draw a payoff) and so that, with
zero time, they return the value at expiration:

```python
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)


fila = cadena[(cadena.fecha == HOY) & (cadena.vencimiento == VTO)
              & (cadena.tipo == "C") & (cadena.strike == 6100)]
fila = fila.iloc[0]
g = bsm(S0, 6100, dias / 365, fila.iv_modelo, "C", r, q)
print(f"bid {fila.bid}  ask {fila.ask}  iv {fila.iv_modelo}")
{k: round(float(x), 4) for k, x in g.items()}
```
```output
bid 118.8  ask 119.8  iv 0.1258
{'precio': 119.3813, 'delta': 0.5235, 'gamma': 0.0014, 'vega': 9.0491, 'theta': -1.3226}
```

The 6,100 call is quoted at 118.8-119.8 and BSM with its implied volatility gives 119.38, inside the bid-ask spread.
The `iv_modelo` column in the data is the true implied volatility, without noise; in a real chain it is computed by
inverting the mid (lesson 3.4.4) and the difference is small compared with the spread. The Greeks, with the units of
the table: delta 0.5235 (52 USD per point and contract), vega 9.05 points per volatility point (905 USD per contract)
and theta −1.32 points per calendar day (132 USD a day per contract). The check with QuantLib, with flat curves at the
same r and q and the same Actual/365 day count, matches to four decimal places; QuantLib's `vega()` is per unit of
volatility (it has to be divided by 100) and `thetaPerDay()` divides the annual theta by 365:

```python
import QuantLib as ql

hoy = ql.Date(31, 12, 2025)
ql.Settings.instance().evaluationDate = hoy
dc = ql.Actual365Fixed()
proceso = ql.BlackScholesMertonProcess(
    ql.QuoteHandle(ql.SimpleQuote(S0)),
    ql.YieldTermStructureHandle(ql.FlatForward(hoy, q, dc)),
    ql.YieldTermStructureHandle(ql.FlatForward(hoy, r, dc)),
    ql.BlackVolTermStructureHandle(
        ql.BlackConstantVol(hoy, ql.TARGET(), fila.iv_modelo, dc)))
opcion = ql.EuropeanOption(
    ql.PlainVanillaPayoff(ql.Option.Call, 6100),
    ql.EuropeanExercise(ql.Date(20, 2, 2026)))
opcion.setPricingEngine(ql.AnalyticEuropeanEngine(proceso))
print(f"price {opcion.NPV():.4f}  delta {opcion.delta():.4f}"
      f"  gamma {opcion.gamma():.4f}")
print(f"vega/100 {opcion.vega() / 100:.4f}"
      f"  theta/day {opcion.thetaPerDay():.4f}")
```
```output
price 119.3813  delta 0.5235  gamma 0.0014
vega/100 9.0491  theta/day -1.3226
```

## Legs, opening and valuation

A leg is a dictionary with the type, the strike, the quantity (positive long, negative short), the day's bid and ask,
the implied volatility, the expiration and its r and q. The index is represented as one more leg, of type `"S"`, with
delta 1 and no optionality. Three functions are enough:

- `apertura`: what it costs to open the structure, at mid and **crossing the bid-ask spread** (every buy at the ask,
  every sell at the bid). This second figure is the one that counts: it is what an order that executes right away
  would pay. A positive price is a debit; a negative one, a credit.
- `valor`: the value of the structure (or any of its Greeks) if the index were worth `S` on a date. For dates before
  expiration it assumes that the implied volatility of each leg does not change (*sticky strike*); what happens when
  it does change is covered in 3.5.3 and, with price × volatility × time scenarios, in 3.5.4.
- `resumen`: the P&L at expiration on a grid from 1 to 3 times the index, with the maximum, the minimum and the
  breakeven points (the zero crossings, interpolated). If the slope at the end of the grid is not zero, the maximum or
  the minimum is unlimited.

```python
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
```

```python
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)}


call = [pata("C", 6100, 1)]
put = [pata("P", 6100, 1)]
for nombre, e in [("call 6100", call), ("put 6100", put)]:
    print(nombre, resumen(e))
    print("   ", griegas(e))
```
```output
call 6100 {'medio': 119.3, 'real': 119.8, 'horquilla': 0.5, 'maximo': 'unlimited', 'minimo': -119.8, 'muertos': [6219.8]}
    {'delta': 0.523, 'gamma': 0.00139, 'vega': 9.05, 'theta': -1.32}
put 6100 {'medio': 108.8, 'real': 109.3, 'horquilla': 0.5, 'maximo': 5989.7, 'minimo': -109.3, 'muertos': [5990.7]}
    {'delta': -0.475, 'gamma': 0.00139, 'vega': 9.05, 'theta': -0.91}
```

The long call costs 119.8 at the ask (half a point more than at mid) and that is its maximum loss; its breakeven point
is the strike plus what was paid, 6,219.8, 2.1% above the index. The put costs 109.3 and its breakeven point is at
5,990.7; its maximum gain is not unlimited but the strike minus the premium if the index went to zero (the grid starts
at 1 point, hence the 5,989.7). Both have the same gamma and the same vega (that is parity: call − put is linear in S)
and deltas that differ by exactly $e^{-qT}$: 0.523 − (−0.475) = 0.998. The theta of the put (−0.91) is smaller in
absolute value than that of the call (−1.32) because with r > q the financing term works in its favor.

## The P&L before expiration

The payoff at expiration is a kinked line; before it, the structure is worth the payoff plus the time value, and the
curve is smooth. The header figure shows the three curves of the bull call spread from the next section; for the call
alone it shows up in a table:

```python
coste = apertura(call)[1]
S = np.array([5700.0, 5900, 6100, 6300, 6500])
antes = pd.DataFrame({
    "today": valor(call, S, HOY) - coste,
    "in 25 days": valor(call, S, HOY + pd.Timedelta(days=25))
    - coste,
    "expiration": valor(call, S, VTO) - coste}, index=S)
antes.index.name = "ORX"
antes.round(1)
```
```output
        today  in 25 days  expiration
ORX
5700.0 -108.9      -118.0      -119.8
5900.0  -75.0      -101.0      -119.8
6100.0    5.2       -32.7      -119.8
6300.0  138.5       107.7        80.2
6500.0  311.0       292.8       280.2
```

With the index at 6,100 the call would gain 5.2 points today (it is 10 points from S0 and its delta is 0.52), would
lose 32.7 if mid-January arrives at the same level and would lose the whole premium at expiration. That difference is
the accumulated theta: in 25 days 38 points are lost at a constant price, somewhat more than 25 × 1.32 suggests because
the theta of an at-the-money option grows as expiration approaches (lesson 1.2.5). At 6,300 the call gains 138.5 today
and 80.2 at expiration: rising fast is worth more than rising late.

## Covered call, protective put and collar

With the index in the portfolio there are three classic structures. Strikes are chosen by delta (the traders'
convention: a “25 delta” call, 25D in the tables), with a function that adds the BSM delta of each row to the chain.
With 100 points between strikes, the delta obtained is the closest to the target, not the exact one:

```python
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()])


hoy = con_delta()
for t, obj in [("C", 0.25), ("P", -0.25), ("P", -0.10)]:
    k = strike_delta(hoy, t, obj)
    fila = hoy[(hoy.tipo == t) & (hoy.strike == k)].iloc[0]
    print(t, obj, "->", k, f"delta {fila.delta:.3f}",
          f"bid {fila.bid} ask {fila.ask}")
```
```output
C 0.25 -> 6300.0 delta 0.214 bid 27.1 ask 27.7
P -0.25 -> 5900.0 delta -0.261 bid 56.5 ask 57.3
P -0.1 -> 5600.0 delta -0.105 bid 22.5 ask 22.9
```

The 25D call is the 6,300 (delta 0.214, 3.5% from the index) and the 25D put is the 5,900 (−0.261, 3.1% away). The
two wings are not at the same distance even though they have the same delta: the index smile has skew (the
volatility of the 5,900 put is higher than that of the 6,300 call), and the r − q drift moves the forward upward.

```python
cubierta = [pata("S", 0, 1), pata("C", 6300, -1)]
protegida = [pata("S", 0, 1), pata("P", 5900, 1)]
collar = [pata("S", 0, 1), pata("P", 5900, 1), pata("C", 6300, -1)]
for nombre, e in [("covered call", cubierta),
                  ("protective put", protegida), ("collar", collar)]:
    res = resumen(e)
    res["real"] = round(float(res["real"] - S0), 2)  # without S0
    res["medio"] = round(float(res["medio"] - S0), 2)
    print(nombre, res)
    print("   ", griegas(e))
```
```output
covered call {'medio': -27.4, 'real': -27.1, 'horquilla': 0.3, 'maximo': 237.65, 'minimo': -6061.35, 'muertos': [6062.3]}
    {'delta': 0.786, 'gamma': -0.00127, 'vega': -6.63, 'theta': 0.74}
protective put {'medio': 56.9, 'real': 57.3, 'horquilla': 0.4, 'maximo': 'unlimited', 'minimo': -246.75, 'muertos': [6146.8]}
    {'delta': 0.739, 'gamma': 0.00093, 'vega': 7.39, 'theta': -1.0}
collar {'medio': 29.5, 'real': 30.2, 'horquilla': 0.7, 'maximo': 180.35, 'minimo': -219.65, 'muertos': [6119.6]}
    {'delta': 0.525, 'gamma': -0.00034, 'vega': 0.76, 'theta': -0.26}
```

*Fig. — Index alone, 6,300 covered call, 5,900 protective put and 5,900/6,300 collar for February 2026: P&L at expiry with the opening at the bid and ask.* Four P&L lines against ORX at expiry, from 5,400 to 6,800. The index alone is a straight line. The covered call follows the index on the downside and flattens above 6,300. The protective put flattens below 5,900 at a floor of -247 points and follows the index on the upside, slightly below. The collar, highlighted, is capped on both sides.

- **6,300 covered call.** It collects 27.1 points (at the bid). The gain is capped at 237.65 (the 210.55 points up to
  the strike plus the premium); below that, it loses the same as the index minus 27.1. The breakeven point drops from
  6,089.45 to 6,062.3, 0.4%. Its Greeks say it all: delta 0.786 (less than the index alone), negative gamma and vega
  and positive theta (+0.74 points a day). It is a short volatility position.
- **5,900 protective put.** It pays 57.3. Below 5,900 the loss stays at a floor of 246.75 points (4.1% of the index:
  the 189.45 down to the strike plus the premium); above, it gains like the index minus 57.3. It is a long volatility
  position: positive gamma and vega, theta of −1.0.
- **5,900/6,300 collar.** Both at once. It costs 30.2 points (57.3 − 27.1), loses at most 219.65 and gains at most
  180.35. Vega almost disappears (0.76): the long put and the short call offset each other.

Put-call parity ties the three together. Since $C - P = e^{-qT}S - e^{-rT}K$, the index plus a short call is, with the
same strike, a short put plus a bond: the covered call is a put sale under another name. Likewise, the index plus a
long put is a long call plus a bond, and the collar is a bull spread plus a bond:

```mermaid
flowchart LR
  CC[Covered call] --- PV[Short put + bond]
  PP[Protective put] --- CCo[Long call + bond]
  CO[Collar] --- BS[Vertical + bond]
```

<aside class="callout" data-kind="nota">

The index leg in the diagrams does not collect dividends: with q = 1.17% a year, in 51 days that is about 10 points
per unit that improve every structure with the index in the portfolio. The history further down includes them (the
total return index, `cierre_rt`). In practice the “index” leg is an ETF (with its own bid-ask spread and commissions)
or a future (which does not pay dividends but discounts them in its price, lesson 3.1.2).

</aside>

A **zero-cost collar** is the one that picks the call so that its bid pays for the ask of the put:

```python
puts = hoy[(hoy.tipo == "P") & (hoy.strike == 5900)].iloc[0]
calls = hoy[hoy.tipo == "C"].set_index("strike")
print("ask of the 5900 put:", puts.ask)
calls.loc[6200:6600, ["bid", "ask", "delta"]].round(3)
```
```output
ask of the 5900 put: 57.3
          bid    ask  delta
strike
6200.0  63.60  64.50  0.372
6300.0  27.10  27.70  0.214
6400.0   9.30   9.70  0.096
6600.0   0.85   0.95  0.013
```

The 5,900 put costs 57.3. The 6,300 call only pays 27.1; the 6,200 pays 63.6. A 5,900/6,200 collar is opened for a
credit of 6.3 points: it protects below a 3.1% fall in exchange for giving up everything beyond a 1.8% rise. Skew is
what makes protection expensive: at the same distance, the put costs more than the call, and to pay for it you have to
sell a closer call.

## Verticals: debit and credit

A bullish vertical can be built with calls (buy the low strike, sell the high strike: you pay) or with puts (buy the
low strike, sell the high strike: you collect). With the same strikes, both pay the same at expiration except for a
constant: the bull put is the bull call minus the width, $K_2 - K_1$.

```python
bull_call = [pata("C", 6100, 1), pata("C", 6300, -1)]
bull_put = [pata("P", 6100, 1), pata("P", 6300, -1)]
for nombre, e in [("bull call 6100/6300", bull_call),
                  ("bull put 6100/6300", bull_put)]:
    print(nombre, resumen(e))
    print("   ", griegas(e))
ancho = 200 * np.exp(-r * dias / 365)
m_call, m_put = apertura(bull_call)[0], apertura(bull_put)[0]
print(f"parity: debit {m_call:.2f}"
      f"  discounted width − credit {ancho + m_put:.2f}")
```
```output
bull call 6100/6300 {'medio': 91.9, 'real': 92.7, 'horquilla': 0.8, 'maximo': 107.3, 'minimo': -92.7, 'muertos': [6192.7]}
    {'delta': 0.309, 'gamma': 0.00012, 'vega': 2.42, 'theta': -0.58}
bull put 6100/6300 {'medio': -106.9, 'real': -105.7, 'horquilla': 1.2, 'maximo': 105.7, 'minimo': -94.3, 'muertos': [6194.3]}
    {'delta': 0.309, 'gamma': 0.00012, 'vega': 2.42, 'theta': -0.6}
parity: debit 91.90  discounted width − credit 92.08
```

The two 6,100/6,300 verticals have the same Greeks (delta 0.309, vega 2.42; theta differs by 0.02 because of the
financing of the in-the-money put) and almost the same profile: the call version costs 92.7 and gains at most 107.3;
the put version collects 105.7, gains at most that and loses at most 94.3. Parity holds at mid: the debit of the calls
(91.90) is the discounted width minus the credit of the puts (92.08), with a difference of 0.18 points, within the
bid-ask spreads. That one “collects” and the other “pays” is accounting: whoever sells the bull put does not receive a
gift, they receive a premium they have to give back with high probability. The real difference is in the bid-ask
spread: the bull put loses 1.2 points crossing it and the bull call 0.8, because the 6,300 put is in the money and its
spread is wider in points.

The header figure shows the bull call spread: at expiration, the kinked line between −92.7 and +107.3 with the
breakeven point at 6,192.7 (the low strike plus the debit); today, a smooth, flattened curve, because the two legs
offset each other. The Greeks of a vertical are small compared with those of each leg: the vega of the structure is
2.42 versus 9.05 for the long call. It is a position on direction, with little volatility in it.

Bearish verticals are the mirror image, and the usual one in practice is the **out-of-the-money credit vertical**:
sell a 25D put and buy a 10D put to cap the loss.

```python
bear_put = [pata("P", 6100, 1), pata("P", 5900, -1)]
bear_call = [pata("C", 5900, -1), pata("C", 6100, 1)]
put_credito = [pata("P", 5900, -1), pata("P", 5600, 1)]
for nombre, e in [("bear put 6100/5900", bear_put),
                  ("bear call 5900/6100", bear_call),
                  ("bull put 5900/5600", put_credito)]:
    print(nombre, resumen(e))
    print("   ", griegas(e))
```
```output
bear put 6100/5900 {'medio': 51.9, 'real': 52.8, 'horquilla': 0.9, 'maximo': 147.2, 'minimo': -52.8, 'muertos': [6047.2]}
    {'delta': -0.214, 'gamma': 0.00046, 'vega': 1.66, 'theta': 0.09}
bear call 5900/6100 {'medio': -147.35, 'real': -146.0, 'horquilla': 1.35, 'maximo': 146.0, 'minimo': -54.0, 'muertos': [6046.0]}
    {'delta': -0.214, 'gamma': 0.00046, 'vega': 1.66, 'theta': 0.07}
bull put 5900/5600 {'medio': -34.2, 'real': -33.6, 'horquilla': 0.6, 'maximo': 33.6, 'minimo': -266.4, 'muertos': [5866.4]}
    {'delta': 0.156, 'gamma': -0.00051, 'vega': -3.25, 'theta': 0.27}
```

The 6,100/5,900 bear put costs 52.8 and gains up to 147.2; the 5,900/6,100 bear call is its credit twin (it collects
146.0 and loses at most 54.0). The 5,900/5,600 bull put is a different animal: it collects 33.6 and can lose 266.4,
almost eight times more. It wins if the index does not fall more than 3.7% in 51 days (breakeven point 5,866.4). Its
Greeks are those of a capped volatility sale: negative gamma and vega (−3.25), positive theta (+0.27).

## What crossing the bid-ask spread costs

Each leg is opened by crossing its bid-ask spread and, if it is closed before expiration, the spread is crossed again.
On the index, with spreads of 1-3% of the mid near the money, a leg costs little; in a multi-leg structure with a small
net premium, the relative cost multiplies:

```python
estructuras = {"call 6100": call, "put 6100": put,
               "covered call": cubierta, "collar": collar,
               "bull call 6100/6300": bull_call,
               "bull put 6100/6300": bull_put,
               "bull put 5900/5600": put_credito}
filas = []
for nombre, e in estructuras.items():
    opciones = [p for p in e if p["tipo"] != "S"]
    medio, real = apertura(opciones)
    tope = resumen(e)["maximo"]
    tope = np.nan if tope == "unlimited" or tope > 1000 else tope
    filas.append({"estructura": nombre, "patas": len(opciones),
                  "prima_medio": medio, "horquilla": real - medio,
                  "usd_contrato": 100 * (real - medio),
                  "pct_prima": 100 * (real - medio) / abs(medio),
                  "pct_max_ganancia": 100 * (real - medio) / tope})
pd.DataFrame(filas).set_index("estructura").round(2)
```
```output
                     patas  prima_medio  horquilla  usd_contrato  pct_prima  pct_max_ganancia
estructura
call 6100                1        119.3        0.5          50.0       0.42               NaN
put 6100                 1        108.8        0.5          50.0       0.46               NaN
covered call             1        -27.4        0.3          30.0       1.09              0.13
collar                   2         29.5        0.7          70.0       2.37              0.39
bull call 6100/6300      2         91.9        0.8          80.0       0.87              0.75
bull put 6100/6300       2       -106.9        1.2         120.0       1.12              1.14
bull put 5900/5600       2        -34.2        0.6          60.0       1.75              1.79
```

The at-the-money call loses 0.42% of its premium on opening; the collar, which pays the difference between two
premiums, 2.4%; the out-of-the-money credit vertical, 1.75% of what it collects and 1.79% of what it can gain. These
are figures from December 31, 2025, a quiet day. Under stress the bid-ask spreads in the data widen (by a factor of
1 + 2.5 × stress in the generator's formula) and in the four-leg structures of lesson 3.5.2 the relative cost is
higher. Opening and closing before expiration doubles it; a spread order at mid (combinations trade as a single
instrument in real markets) recovers part of it, with no guarantee of execution.

## Twelve years of each structure

How do these structures behave if they are opened every month? The history is as simple as possible: on the session
after each monthly expiration the structure is opened with the next expiration (about 28-35 days), with strikes chosen
by delta, buys at the ask and sells at the bid; it is held to expiration and settled at the index close. P&L is
measured in % of the index on the entry day, without interest on the premium. The index legs use the total return
index, with dividends.

```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"]
        crece = (indice.loc[vto, "cierre_rt"]
                 / indice.loc[f, "cierre_rt"])
        coste = pago = 0.0
        for tipo, K, n in receta(d):
            if tipo == "S":                # index with dividends
                coste, pago = coste + n * S, pago + n * S * crece
                continue
            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,
                      "pnl": 100 * (pago - coste) / S})
    return pd.DataFrame(filas).set_index("entrada")


def atm(d):
    return strike_delta(d, "C", 0.50)


recetas = {
    "index": lambda d: [("S", 0, 1)],
    "call ATM": lambda d: [("C", atm(d), 1)],
    "put ATM": lambda d: [("P", atm(d), 1)],
    "covered call 25D": lambda d: [
        ("S", 0, 1), ("C", strike_delta(d, "C", 0.25), -1)],
    "protective put 25D": lambda d: [
        ("S", 0, 1), ("P", strike_delta(d, "P", -0.25), 1)],
    "collar 25D": lambda d: [
        ("S", 0, 1), ("P", strike_delta(d, "P", -0.25), 1),
        ("C", strike_delta(d, "C", 0.25), -1)],
    "bull call ATM/25D": lambda d: [
        ("C", atm(d), 1), ("C", strike_delta(d, "C", 0.25), -1)],
    "bull put, same strikes": lambda d: [
        ("P", atm(d), 1), ("P", strike_delta(d, "C", 0.25), -1)],
    "bull put 25D/10D": lambda d: [
        ("P", strike_delta(d, "P", -0.25), -1),
        ("P", strike_delta(d, "P", -0.10), 1)],
}
hist = pd.DataFrame({k: historia(v)["pnl"]
                     for k, v in recetas.items()})
print(len(hist), "entries", hist.index[0].date(), "to",
      hist.index[-1].date())
```
```output
143 entries 2014-01-21 to 2025-11-24
```

```python
tabla = 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()})
tabla.round(2)
```
```output
                        media  mediana  pct_pos  desv   peor  fecha_peor  mejor
index                    1.05     1.33    63.64  5.91 -28.13  2020-02-24  30.06
call ATM                 0.51    -0.44    46.15  3.58  -5.05  2020-05-18  22.43
put ATM                 -0.40    -1.38    27.27  3.47  -7.60  2020-03-23  26.49
covered call 25D         0.65     1.72    68.53  4.38 -27.41  2020-02-24  13.57
protective put 25D       0.67     0.56    56.64  4.69 -11.46  2020-04-20  26.86
collar 25D               0.27     1.07    62.24  2.84 -10.02  2020-04-20  10.37
bull call ATM/25D        0.11     0.06    50.35  1.42  -3.52  2020-05-18   5.94
bull put, same strikes   0.09     0.05    50.35  1.41  -3.58  2020-04-20   5.85
bull put 25D/10D         0.23     0.42    88.11  1.08  -8.50  2020-04-20   1.97
```

*Fig. — 143 monthly trades from 2014 to 2025 (opened at the bid and ask, settled at expiry): mean and worst trade of each structure, in % of the index.* Two panels of horizontal bars, one row per structure. Left, the mean per trade: the index earns 1.05% a month, the covered call 0.65%, the verticals around 0.1% and the ATM put loses 0.40%. Right, the worst trade: the index loses 28.1% and the covered call, highlighted, 27.4%, almost the same; the protective put and the collar stop at 11.5% and 10.0%; the verticals, below 9%.

With 143 entries, and remembering that this is synthetic data with a planted volatility premium:

- **Buying options loses on average what is paid for volatility.** The ATM put loses 0.40% of the index per entry and
  only wins in 27% of the months. The ATM call gains 0.51% on average because the index rises (1.05% a month with
  dividends), but its median is negative (−0.44%): it gains a little often and a lot now and then.
- **The covered call trades the right tail for frequency.** It wins in 69% of the months (the index, in 64%), with an
  average somewhat lower than the index (0.65% versus 1.05%) and a best entry of +13.6% versus +30.1%. Its worst
  entry, that of February 24, 2020, loses 27.4%: almost the same as the index (−28.1%). This is parity in action: a
  short put protects against nothing.
- **The protective put and the collar do cut the tail**, to −11.5% and −10.0%, but their worst entry is not the one of
  the March crash but the one of April 20, 2020: after the rebound, with volatility still very high, the 25D put was
  far from the index and the 19% fall to the May expiration landed almost entirely in the unhedged zone.
- **The bull call and the bull put with the same strikes are the same position**: averages of 0.11% and 0.09%, the same
  percentage of positive months (50.35%) and worst entries of −3.5% and −3.6%. The small difference in the average is
  the bid-ask spread of the in-the-money put.
- **The 25D/10D credit vertical is premium selling with a net.** It wins in 88% of the entries, 0.23% on average, and
  its worst entry loses 8.5%: almost forty months of average gain in one. Its best entry does not exceed 1.97%.

The worst entry of the credit vertical teaches something more than the tail. With strikes chosen by delta, the width
between the two puts depends on the day's volatility:

```python
for dia in ["2020-02-24", "2020-04-20"]:
    f, vto, d = next(e for e in entradas
                     if e[0] == pd.Timestamp(dia))
    k1 = strike_delta(d, "P", -0.25)
    k2 = strike_delta(d, "P", -0.10)
    x = d[d.tipo == "P"].set_index("strike")
    S = indice.loc[f, "cierre"]
    iv_atm = d.iv_modelo[d.strike == atm(d)].iloc[0]
    credito = x.loc[k1, "bid"] - x.loc[k2, "ask"]
    print(f.date(), f"ORX {S:.0f}  ATM iv {iv_atm:.3f}",
          f"strikes {k1:.0f}/{k2:.0f}",
          f"width {100 * (k1 - k2) / S:.1f} %",
          f"credit {credito:.2f}",
          f"at exp {indice.loc[vto, 'cierre']:.0f}")
```
```output
2020-02-24 ORX 2960  ATM iv 0.147 strikes 2850/2750 width 3.4 % credit 9.60 at exp 2124
2020-04-20 ORX 2667  ATM iv 0.489 strikes 2425/2125 width 11.2 % credit 39.00 at exp 2159
```

On February 24, 2020, with ATM volatility at 0.147, the 25D and 10D puts were 100 points apart (3.4% of the index) and
the vertical collected 9.60. The index fell to 2,124 and the maximum was lost: 90.4 points, 3.1%. On April 20, with
volatility at 0.489, the same deltas were 300 points apart (11.2%) and the vertical collected 39.00; the index ended at
2,159, below both, and the loss was 227 points, 8.5%. A “defined” maximum loss is as large as the width you choose,
and a width set by delta widens precisely when the market is most dangerous. Fixing the width in points or in % of the
index, or sizing by the maximum loss and not by the number of contracts, are the usual answers; lesson 3.6.1 covers the
sizing of systematic premium selling by margin and by stress.

<aside class="callout" data-kind="riesgo">

A credit vertical is not an income stream. In the data it wins in 126 of 143 months and gives back what it earned all
at once in the worst ones. The positive average comes from the volatility premium planted in the generator; in the
real market a similar premium exists in index options, but its size varies with the period and the tail is that of
real crises, which give no warning.

</aside>

## Common mistakes

- **Valuing at mid.** The backtest that opens at mid earns the bid-ask spread it would never collect. Opening at the
  ask and at the bid is the minimum; closing before expiration costs another spread.
- **Confusing credit with low risk.** With the same strikes, debit and credit are the same position. What matters is
  the maximum loss and the probability of reaching it.
- **Believing that the covered call protects.** It cushions with the premium, but the fall of the index stays whole.
- **Forgetting the multiplier and the units.** A theta of −1.32 is per unit and calendar day: 132 USD per contract and
  day, Saturday and Sunday included. A vega of 9.05 is per volatility point.
- **Choosing strikes by delta without looking at the width.** The same delta is a different distance when volatility
  changes; the risk of a vertical is measured in points, not in deltas.
- **Ignoring dividends and the rate.** The February forward is 0.35% above the index; with r and q set wrong, the
  deltas and parity come out wrong (lesson 3.4.1).
- **Stock options are American.** A short in-the-money call before a dividend, or a short deep in-the-money put, can be
  assigned before expiration (lesson 3.5.5); ORX options are European.

## Exercises

1. Build an out-of-the-money bear call credit spread with the 6,300 (short) and 6,400 (long) calls of the February
   expiration. How much does it collect, how much can it lose, where is its breakeven point and what Greeks does it
   have?

**Solution.**

```python
bear_call_otm = [pata("C", 6300, -1), pata("C", 6400, 1)]
print(resumen(bear_call_otm))
griegas(bear_call_otm)
```
```output
{'medio': -17.9, 'real': -17.4, 'horquilla': 0.5, 'maximo': 17.4, 'minimo': -82.6, 'muertos': [6317.4]}
{'delta': -0.118, 'gamma': -0.00047, 'vega': -2.75, 'theta': 0.35}
```

It collects 17.4 points (27.1 for the 6,300 at the bid minus 9.7 for the 6,400 at the ask; at mid it would be 17.9)
and can lose 82.6: the width of 100 minus the credit, 4.7 times what it gains. It wins as long as the index does not
rise more than 3.7% up to the breakeven point, 6,317.4. It is a bearish, short volatility position: delta −0.118,
negative gamma and vega (−2.75) and positive theta, +0.35 points a day (35 USD per contract). The ratio between what
it collects and what it risks is that of any out-of-the-money credit vertical: likely and small against unlikely and
large.

2. Repeat the covered call history with 10D, 25D and 40D calls. How do the average, the percentage of positive months
   and the worst entry change?

**Solution.**

```python
cc = {f"cc {int(100 * x)}D": historia(
          lambda d, x=x: [("S", 0, 1),
                          ("C", strike_delta(d, "C", x), -1)])["pnl"]
      for x in (0.10, 0.25, 0.40)}
cc = pd.DataFrame(cc)
pd.DataFrame({"media": cc.mean(), "pct_pos": 100 * (cc > 0).mean(),
              "peor": cc.min(), "mejor": cc.max()}).round(2)
```
```output
        media  pct_pos   peor  mejor
cc 10D   0.78    65.03 -27.91  20.83
cc 25D   0.65    68.53 -27.41  13.57
cc 40D   0.51    70.63 -27.41   9.45
```

The closer to the money the call is sold (40D), the more positive months (70.6%) and the lower the average (0.51%):
more premium is collected, but the upside is given up sooner, and in a rising index that weighs more. The 10D one
resembles the index alone (average 0.78%, best entry +20.8%). The worst entry barely changes with delta: −27.9%,
−27.4% and −27.4%. No call premium covers a 28% fall: the strike of the short call decides how much upside is given
away, not how much downside is avoided.

3. A colleague says their covered call “cannot lose much because it already collected the premium” and that the
   protective put “is money thrown away because it is almost never used”. With the results of the lesson, what would
   you answer?

**Solution.**

That the covered call is, by parity, a short put: its loss when the index falls is that of the index minus the premium,
and the premium of a 25D call is 0.4% of the index. Its worst month in the data loses 27.4%, seven tenths less than the
index. The protective put, on the other hand, costs some return on average (it is a volatility purchase, which carries
a premium against it) but cuts the worst entry to −11.5%. Neither is “better”: one sells the right tail and gets paid
for it, the other buys protection of the left tail and pays for it. What cannot be said is that the covered call
protects. And the protection of a 25D put depends on the day's volatility: after the 2020 crash it was so far away that
the worst entry came with the hedge in place.

## Self-assessment

1. A 6,100/6,300 bull call spread is opened paying 119.8 for the long call and collecting 27.1 for the short one. What is its maximum loss per contract with a multiplier of 100?

   - A. 9,270 USD, the net debit
   - B. 11,980 USD, the premium of the long call
   - C. 20,000 USD, the width between strikes
   - D. Unlimited, because there is a short call

   *Answer A: 9,270 USD, the net debit.* The maximum loss of a debit vertical is what was paid: 119.8 − 27.1 = 92.7 points, 9,270 USD per contract. The short call is covered by the long call with the lower strike, so there is no unlimited risk; the maximum gain is the width minus the debit, 107.3 points.

2. With the same 6,100 and 6,300 strikes, the bull call spread is a debit spread and the bull put spread a credit spread. What really sets them apart?

   - A. The credit one is safer because it collects at opening
   - B. Almost nothing: by put-call parity they have the same payoff except for the discounted width; what changes is the bid-ask spread paid and the financing of the premium
   - C. The debit one cannot lose more than what was paid and the credit one can
   - D. The credit one has positive vega

   *Answer B: Almost nothing: by put-call parity they have the same payoff except for the discounted width; what changes is the bid-ask spread paid and the financing of the premium.* Bull put = bull call − (K2 − K1), and the discounted width (200·e^(−rT)) equates the premiums: at mid, 91.90 versus 92.08. Greeks and risk are the same. In the data, the put version costs more bid-ask spread (1.2 versus 0.8 points) because its short put is in the money.

3. In the 2014-2025 history, the worst entry of the 25D covered call loses 27.4% and that of the index only 28.1%. Why are they so similar?

   - A. Because the short call was exercised
   - B. Because the bid-ask spread of the call is very wide in crises
   - C. Because a covered call is, by parity, a short put: it keeps the full fall of the index and only cushions it with the premium collected
   - D. Because of an error in the March 2020 data

   *Answer C: Because a covered call is, by parity, a short put: it keeps the full fall of the index and only cushions it with the premium collected.* Selling a call caps the upside, not the downside. The premium of a 25D call is small (27 points in the example, 0.4% of the index), so in a 28% fall it barely shows. Capping the downside requires a long put: the protective put and the collar stay at −11.5% and −10.0%.

4. The 25D/10D bull put spread opened on February 24, 2020 loses 3.1% of the index and the one from April 20, 2020, 8.5%. What explains the difference?

   - A. That the index fell more in April
   - B. That the bid-ask spread was wider in April
   - C. That in February the long put could not be exercised
   - D. That with strikes chosen by delta the width between them grows with volatility: 3.4% of the index with an ATM IV of 0.147 and 11.2% with 0.489

   *Answer D: That with strikes chosen by delta the width between them grows with volatility: 3.4% of the index with an ATM IV of 0.147 and 11.2% with 0.489.* Delta depends on the distance to the strike measured in standard deviations over the period (volatility times the square root of T). With volatility three times higher, the 25D and 10D puts end up three times farther apart, and the maximum loss (width − credit) grows with them. “Defined risk” does not mean “small risk”.

5. The 6,100 call has a theta of −1.32 with the units of the lesson. What does it mean?

   - A. That, all else equal, it loses 1.32 points per calendar day: 132 USD per contract
   - B. That it loses 1.32 USD a year
   - C. That it loses 1.32% a day
   - D. That it loses 1.32 points per trading session

   *Answer A: That, all else equal, it loses 1.32 points per calendar day: 132 USD per contract.* Theta is expressed here per calendar day (the annual one divided by 365, like QuantLib's `thetaPerDay` with Actual/365) and per index unit. Per contract it is multiplied by 100. A weekend counts as two calendar days even though there is no session.

## Key points

- A structure is a list of legs; its cost, payoff, P&L and Greeks are the sum of those of each leg with its sign and quantity.
- The real cost is that of crossing the bid-ask spread: buys at the ask and sells at the bid. In the example chain it ranges from 0.4% of the premium in an at-the-money call to 1.8% in an out-of-the-money credit vertical.
- By put-call parity, a covered call is equivalent to a short put and a protective put to a long call: selling a call does not protect against the fall, as its worst month in the history showed, −27.4% versus −28.1% for the index.
- Debit or credit is accounting: with the same strikes, the bull call and the bull put have the same payoff except for the discounted width, and the same Greeks.
- Credit verticals win often and lose all at once: the 25D/10D is positive in 88% of entries and its worst entry loses 37 times its average gain; with strikes set by delta, the maximum loss grows with volatility.

## Resources

### Research

- [Uses of Volatility and Current Volatility - VIX1D](https://pythonparatrading.com/en/uses-of-volatility/) · Research-0003 · 5 min

### Documentation

- [Cboe: S&P 500 Index Options (SPX), contract specifications](https://www.cboe.com/tradable_products/sp_500/spx_options/specifications/) · cboe.com
- [Cboe: options strategy benchmark indices (BXM, PPUT, CLL…)](https://www.cboe.com/us/indices/benchmark_indices/) · cboe.com
- [QuantLib: documentation](https://www.quantlib.org/docs.shtml) · quantlib.org

### Books

- [Hull, J. C.: Options, Futures, and Other Derivatives (Pearson)](https://www.pearson.com/en-us/subject-catalog/p/options-futures-and-other-derivatives/P200000005938) · pearson.com

### Data

- [Level 3 data: daily options chain of the ORX index, 2014-2025 (Parquet)](https://pythonparatrading.com/curso/datos/nivel-3/opciones3_cadena.parquet) · Download · opciones3_cadena.parquet
- [Level 3 data: ORX index, dividends and yield curve (CSV)](https://pythonparatrading.com/curso/datos/nivel-3/opciones3_indice.csv) · Download · opciones3_indice.csv

Previous lesson: [3.4.7 · Advanced models: Heston, SABR and local volatility](https://pythonparatrading.com/en/python-trading-course/level-3/heston-sabr-local-volatility/)

Next lesson: [3.5.2 · Volatility and range structures: straddles, strangles, condors and butterflies](https://pythonparatrading.com/en/python-trading-course/level-3/straddles-strangles-condors-butterflies/)
