---
title: "3.4.1 Payoffs, paridad put-call y límites de arbitraje"
description: "Replicación de payoffs, cotas de Merton y paridad put-call contrastadas sobre doce años de cadena: qué violaciones son datos malos y cuáles arbitraje."
url: "https://pythonparatrading.com/curso-python-trading/nivel-3/payoffs-paridad-put-call-y-limites-de-arbitraje/"
lang: "es"
image: "https://pythonparatrading.com/og/curso-python-trading/nivel-3/payoffs-paridad-put-call-y-limites-de-arbitraje.png"
---

# 3.4.1 · Payoffs, paridad put-call y límites de arbitraje

> Antes de cualquier modelo, el no arbitraje ya acota el precio de una opción: cotas, pendientes, convexidad y la paridad put-call. Esta lección convierte esas cotas en un escáner, lo pasa por doce años de cadena del índice ficticio ORX y separa las violaciones que son datos malos de las ocho que se plantaron a propósito.

- Curso: [Nivel 3: Derivados: futuros, spreads, forex y opciones](https://pythonparatrading.com/curso-python-trading/nivel-3/)
- Módulo: 3.4 · Teoría y valoración de opciones
- Duración: 65 min
- Requisitos: [1.2.5 · Opciones: call, put, valor intrínseco y valor temporal](https://pythonparatrading.com/curso-python-trading/nivel-1/opciones-call-put-y-valor-temporal/), [1.4.2 · Limpieza de datos: faltantes, ticks erróneos, huecos y zonas horarias](https://pythonparatrading.com/curso-python-trading/nivel-1/limpieza-de-datos-financieros/)

## Al terminar esta lección

- Construir payoffs con calls, puts, forwards y bonos, y deducir de la réplica la paridad put-call y las cotas de Merton.
- Estimar el forward implícito de cada vencimiento con la mediana de la paridad y comprobar la paridad con precios ejecutables en 272.274 pares.
- Separar violaciones aparentes debidas a cotizaciones congeladas o cruzadas de las violaciones reales, y confirmar éstas con una caja (box spread).
- Comprobar las cotas de pendiente y convexidad de una cadena y las desigualdades de la paridad para opciones americanas con dividendos discretos.

## Payoffs como piezas de construcción

La lección 1.2.5 dibujó el resultado al vencimiento de las cuatro posiciones básicas y comprobó la paridad put-call en
una cadena de un solo día. Aquí se da un paso más: los payoffs se tratan como **piezas** que se suman, y de esas sumas
salen todas las relaciones de no arbitraje que deben cumplir los precios, sin suponer ningún modelo de cómo se mueve el
subyacente.

La idea es la de la réplica estática: si dos carteras pagan exactamente lo mismo al vencimiento en todos los
escenarios, hoy deben costar lo mismo; si una paga siempre al menos lo que la otra, no puede costar menos. Con cuatro
piezas (call, put, forward y bono cupón cero) se construye casi todo:

| Cartera | Paga al vencimiento | Nombre |
|---|---|---|
| Call comprada en K + put vendida en K | $S_T - K$ | forward sintético comprado |
| Call comprada en K1 − call vendida en K2 | entre 0 y K2 − K1 | call spread (vertical) |
| Sintético comprado en K1 + sintético vendido en K2 | $K_2 - K_1$ siempre | caja (box spread) |
| Call en K1 − 2 calls en K2 + call en K3 | triángulo, nunca negativo | mariposa |
| Call spread de anchura ΔK dividido por ΔK | ≈ 1 si $S_T > K$ | digital (aproximada) |

```mermaid
flowchart TB
  A[Call comprada] --> X[Cartera A]
  B[Put vendida] --> X
  X --> P["Paga S − K"]
  F[Forward a K] --> P
  P --> H[Mismo precio hoy]
```

La cartera A (call comprada y put vendida) y un forward comprado a K pagan $S_T - K$ en todos los escenarios. Un
forward a K cuesta hoy el valor presente de la diferencia entre el forward de mercado $F$ y $K$, así que:

$$
C - P = e^{-rT}\,(F - K).
$$

Con dos strikes, la diferencia de dos forwards sintéticos es la **caja**: paga $K_2 - K_1$ pase lo que pase y vale
$e^{-rT}(K_2 - K_1)$. Es un préstamo a tipo fijo construido con opciones, y es la forma más limpia de confirmar una
violación de la paridad sin tocar el subyacente. La celda construye los payoffs con NumPy sobre una rejilla de precios
finales para ver que la caja es plana y que la mariposa nunca es negativa:

```python
import numpy as np
import pandas as pd

ST = np.linspace(4000, 6000, 2001)       # índice al vencimiento
call = lambda K: np.maximum(ST - K, 0)
put = lambda K: np.maximum(K - ST, 0)

caja = (call(4900) - put(4900)) - (call(5000) - put(5000))
mariposa = call(4900) - 2 * call(5000) + call(5100)
digital = (call(4995) - call(5005)) / 10
print("caja: mín", caja.min(), "máx", caja.max())
print("mariposa: mín", mariposa.min(), "máx", mariposa.max())
print("digital en 4.990, 5.000 y 5.010:",
      digital[np.searchsorted(ST, [4990, 5000, 5010])])
```
```output
caja: mín 100.0 máx 100.0
mariposa: mín 0.0 máx 100.0
digital en 4.990, 5.000 y 5.010: [0.  0.5 1. ]
```

La caja paga 100 puntos en toda la rejilla; la mariposa vale entre 0 y 100 (máximo en el strike central); el call
spread estrecho, dividido por su anchura, salta de 0 a 1 alrededor del strike como una opción digital. De esas tres
observaciones salen las cotas que siguen.

## Las cotas de Merton

Merton (1973) reunió las condiciones que debe cumplir cualquier precio de opción sin arbitraje. Para opciones europeas
sobre un índice con forward $F$, factor de descuento $DF = e^{-rT}$ y strikes $K_1 < K_2 < K_3$:

| Condición | Fórmula | Por qué |
|---|---|---|
| Cota inferior de la call | $C \ge DF\,(F - K)^+$ | la call paga al menos lo que el forward sintético, y nunca menos de 0 |
| Cota superior de la call | $C \le DF\,F = S\,e^{-qT}$ | nunca paga más que el índice |
| Cotas de la put | $DF\,(K - F)^+ \le P \le DF\,K$ | ídem; nunca paga más que el strike |
| Monotonía | $C(K_1) \ge C(K_2)$, $P(K_1) \le P(K_2)$ | la call de strike menor paga siempre al menos lo mismo |
| Pendiente | $C(K_1) - C(K_2) \le DF\,(K_2 - K_1)$ | el call spread paga como mucho $K_2 - K_1$ |
| Convexidad | $\lambda C(K_1) + (1-\lambda) C(K_3) \ge C(K_2)$ | la mariposa paga algo no negativo; $\lambda = \frac{K_3 - K_2}{K_3 - K_1}$ |

Ninguna depende de la volatilidad ni de la distribución del subyacente: valen para Black-Scholes (lección 3.4.2),
para Heston (3.4.7) y para cualquier precio que publique un mercado sin arbitraje. Por eso son el primer filtro de
cualquier conjunto de datos de opciones y, en la lección 3.4.5, las condiciones que debe respetar una superficie de
volatilidad ajustada. La convexidad tiene además un significado económico: la segunda derivada del precio de la call
respecto al strike es, descontada, la densidad de probabilidad neutral al riesgo del índice al vencimiento (Breeden y
Litzenberger), y una densidad no puede ser negativa.

Hay una cota más entre vencimientos (a igualdad de moneyness en forward, más plazo no puede valer menos), que con
dividendos y tipos se expresa mejor en varianza total; se trata con la superficie en la lección 3.4.5.

## Doce años de cadena: el forward implícito

Los datos son la cadena diaria de opciones europeas sobre **ORX**, un índice ficticio de 500 valores que imita al
S&P 500, de 2014 a 2025: los tres vencimientos mensuales más cercanos, con su bid, su ask, el volumen y el interés
abierto. Liquidan en efectivo al cierre del día de vencimiento, con un multiplicador de 100 USD por punto, como las
opciones SPX de Cboe (las mensuales reales liquidan a la apertura; aquí, al cierre, por simplicidad). Son **datos
sintéticos**: los precios salen de una superficie sin arbitraje conocida (`opciones3_verdad.parquet`), con horquillas,
ruido y una lista de defectos plantados (`opciones3_incidencias.csv`). La lista no se usa para buscar: se usa al final,
para comprobar si el escáner los encuentra todos y solo ellos.

```python
cad = pd.read_parquet("opciones3_cadena.parquet")
ind = pd.read_csv("opciones3_indice.csv", parse_dates=["fecha"],
                  index_col="fecha")
ancho = cad.pivot_table(index=["fecha", "vencimiento", "strike"],
                        columns="tipo", values=["bid", "ask"])
ancho.columns = [f"{c}_{t.lower()}" for c, t in ancho.columns]
ancho = ancho.dropna().reset_index()
print(len(cad), "opciones;", len(ancho), "pares call-put;",
      ancho["fecha"].nunique(), "sesiones")
ancho.head(3)
```
```output
544548 opciones; 272274 pares call-put; 3017 sesiones
       fecha vencimiento  strike  ask_c  ask_p  bid_c  bid_p
0 2014-01-02  2014-01-17  1600.0  227.3    0.2  225.7   0.15
1 2014-01-02  2014-01-17  1650.0  177.4    0.5  176.1   0.45
2 2014-01-02  2014-01-17  1700.0  127.9    1.3  127.1   1.20
```

Para comprobar la paridad hacen falta $r$ y $F$ de cada fecha y vencimiento. El tipo sale de la curva publicada en
`opciones3_indice.csv` (tipos continuos a 1, 3, 6, 12 y 24 meses), interpolada linealmente en el plazo y plana por
debajo de un mes, como hace Cboe con el VIX. El plazo se mide en **días naturales / 365**, porque las opciones vencen
al cierre de un día concreto y el interés corre también en fin de semana.

El forward no se observa: ORX no tiene futuro en los datos. Se deduce de la propia cadena. Despejando la paridad,
cada strike da una estimación $F_K = K + (C - P)/DF$ con los precios medios; la **mediana** de las estimaciones de los
strikes a menos de un 3 % del índice es robusta a que una de ellas esté mal, que es justo lo que se busca detectar:

```python
plazos = np.array([1 / 12, 0.25, 0.5, 1.0, 2.0])
cols = ["tipo_1m", "tipo_3m", "tipo_6m", "tipo_1a", "tipo_2a"]
ancho["T"] = (ancho["vencimiento"] - ancho["fecha"]).dt.days / 365
curva = ind.loc[ancho["fecha"], cols].to_numpy() / 100
ancho["r"] = [np.interp(t, plazos, c)
              for t, c in zip(ancho["T"], curva)]
ancho["DF"] = np.exp(-ancho["r"] * ancho["T"])
ancho["S"] = ind.loc[ancho["fecha"], "cierre"].to_numpy()
medio_c = (ancho["bid_c"] + ancho["ask_c"]) / 2
medio_p = (ancho["bid_p"] + ancho["ask_p"]) / 2
ancho["F_k"] = ancho["strike"] + (medio_c - medio_p) / ancho["DF"]
cerca = (ancho["strike"] / ancho["S"] - 1).abs() < 0.03
F = (ancho[cerca].groupby(["fecha", "vencimiento"])["F_k"]
     .median().rename("F"))
ancho = ancho.join(F, on=["fecha", "vencimiento"])

verdad = pd.read_parquet("opciones3_verdad.parquet")
cortes = (ancho.drop_duplicates(["fecha", "vencimiento"])
          .merge(verdad, on=["fecha", "vencimiento"]))
err = cortes["F"] - cortes["forward"]
print(f"{len(cortes)} cortes; error del forward: mediana "
      f"{err.median():.3f}, p99 {err.abs().quantile(0.99):.3f},"
      f" máximo {err.abs().max():.3f} puntos")
dr = (cortes["r"] - cortes["tipo_libre"]).abs().max()
print(f"Error máximo del tipo: {dr:.1e}")
```
```output
9051 cortes; error del forward: mediana 0.000, p99 0.181, máximo 0.457 puntos
Error máximo del tipo: 4.9e-07
```

En los 9.051 cortes (fecha y vencimiento) el forward estimado coincide con el verdadero con una mediana de error nula,
un 99 % de los cortes a menos de 0,18 puntos y un máximo de 0,46 puntos sobre un índice de miles. El tipo interpolado
es el de la verdad salvo redondeo. Con datos reales no hay verdad con la que comparar; este paso es el que permite
fiarse del método antes de usarlo.

## La paridad como escáner

Con $F$ y $DF$, cada par call-put admite dos operaciones de arbitraje ejecutables con los precios de pantalla:

- **Conversión** (la call está cara): vender la call al bid, comprar la put al ask y comprar el forward. Gana
  $C_{bid} - P_{ask} - DF\,(F - K)$ si es positivo.
- **Reversal** o conversión inversa (la put está cara): comprar la call al ask, vender la put al bid y vender el
  forward. Gana $DF\,(F - K) - (C_{ask} - P_{bid})$.

Una propiedad útil: las dos condiciones juntas equivalen a que la desviación de la paridad calculada con los precios
**medios** supere, en valor absoluto, la **semisuma de las dos horquillas**. Basta restar: $C_{bid} - P_{ask}$ es el
medio de $C - P$ menos esa semisuma. La celda lo comprueba sobre los 272.274 pares:

```python
teo = ancho["DF"] * (ancho["F"] - ancho["strike"])
ancho["conversion"] = ancho["bid_c"] - ancho["ask_p"] - teo
ancho["reversal"] = teo - (ancho["ask_c"] - ancho["bid_p"])
viol = ancho[(ancho["conversion"] > 0) | (ancho["reversal"] > 0)]
print(len(viol), "pares con arbitraje aparente de", len(ancho))

desv = medio_c - medio_p - teo
semi = ((ancho["ask_c"] - ancho["bid_c"])
        + (ancho["ask_p"] - ancho["bid_p"])) / 2
print("La misma cuenta con medios:", (desv.abs() > semi).sum())
```
```output
346 pares con arbitraje aparente de 272274
La misma cuenta con medios: 346
```

346 pares de 272.274, un 0,13 %. La figura de la cabecera muestra cómo se ve uno en un día concreto: el 4 de octubre
de 2024, vencimiento de diciembre (77 días), la desviación de cada strike está dentro de su banda ejecutable (las
barras grises, de menos a más la semisuma de horquillas) salvo en la put de 4.950, la más cercana al índice, que está
4,09 puntos por debajo de la línea con una banda de 1,05.

Antes de llamar arbitraje a nada hay que preguntarse por qué existiría. Un 0,13 % de los pares es poco para un
mercado eficiente y mucho para que todos sean dinero gratis. Los sospechosos habituales están en los datos:

- **Cotizaciones congeladas.** Una opción que repite exactamente el bid y el ask del día anterior en una sesión en que
  el índice se movió más de un 1 % no se ha actualizado: el proveedor arrastra el último precio de un contrato que no
  ha cotizado.
- **Cotizaciones cruzadas.** Un bid por encima del ask es imposible en un libro que funciona: es un error de captura o
  de sincronización.

Las dos reglas se aplican a cada pata, sin mirar la lista de incidencias:

```python
cad = cad.sort_values(["vencimiento", "tipo", "strike", "fecha"])
g = cad.groupby(["vencimiento", "tipo", "strike"])
igual = ((cad["bid"] == g["bid"].shift())
         & (cad["ask"] == g["ask"].shift()))
mov = np.log(ind["cierre"]).diff().abs()
cad["congelada"] = igual & (
    mov.reindex(cad["fecha"]).to_numpy() > 0.01)
cad["cruzada"] = cad["bid"] > cad["ask"]
print(cad[["congelada", "cruzada"]].sum().to_dict())

marcas = cad.groupby(["fecha", "vencimiento", "strike"])[
    ["congelada", "cruzada"]].any()
viol = viol.join(marcas, on=["fecha", "vencimiento", "strike"])
viol["causa"] = np.select(
    [viol["cruzada"], viol["congelada"]],
    ["cotización cruzada", "cotización congelada"], "ninguna")
viol["causa"].value_counts()
```
```output
{'congelada': 11336, 'cruzada': 12}
causa
cotización congelada    330
ninguna                   8
cotización cruzada        8
Name: count, dtype: int64
```

La regla de las congeladas es tosca: marca 11.336 cotizaciones de toda la cadena, la mayoría opciones muy fuera del
dinero que siguen cotizando a 0 y 0,05 porque no valen nada y su precio no cambia aunque el índice se mueva. Para el escáner da
igual, porque esas no generan violaciones. De las 346 violaciones, 330 tienen una pata congelada y 8 una cruzada. Solo
quedan 8 sin explicación en los datos.

*Fig. — Las 346 violaciones ejecutables de la paridad en la cadena de ORX, por causa.* Barras horizontales: 330 violaciones tienen una pata con la cotización congelada del día anterior, 8 una cotización cruzada (bid por encima del ask) y 8, destacadas, ningún defecto en los datos: son las ocho violaciones plantadas.

```mermaid
flowchart TB
  V[Violación] --> Q{¿Cruzada?}
  Q -- sí --> D1[Dato erróneo]
  Q -- no --> C{¿Congelada?}
  C -- sí --> D1
  C -- no --> B{¿Caja confirma?}
  B -- no --> D2[Revisar F y r]
  B -- sí --> K{¿Cubre costes?}
  K -- sí --> OP[Candidata]
  K -- no --> NO[Descartar]
```

Las 8 restantes se comparan ahora, por primera vez, con la lista de incidencias plantadas:

```python
inc = pd.read_csv("opciones3_incidencias.csv",
                  parse_dates=["fecha", "vencimiento"])
quedan = viol[viol["causa"] == "ninguna"].merge(
    inc, on=["fecha", "vencimiento", "strike"], how="left")
quedan["semi"] = ((quedan["ask_c"] - quedan["bid_c"])
                  + (quedan["ask_p"] - quedan["bid_p"])) / 2
quedan["usd"] = 100 * quedan["reversal"]
quedan[["fecha", "vencimiento", "strike", "tipo", "incidencia",
        "reversal", "semi", "usd"]].round(
    {"reversal": 2, "semi": 2, "usd": 2})
```
```output
       fecha vencimiento  strike tipo       incidencia  reversal  semi      usd
0 2015-01-20  2015-04-17  2025.0    P  paridad_violada      1.51  0.50   150.90
1 2017-09-25  2017-12-15  2575.0    P  paridad_violada      1.79  0.55   178.68
2 2018-01-09  2018-03-16  2675.0    P  paridad_violada      1.50  0.50   150.00
3 2019-08-28  2019-11-15  2600.0    P  paridad_violada      1.77  0.60   176.54
4 2020-03-26  2020-06-19  2325.0    P  paridad_violada     11.85  3.90  1185.36
5 2022-09-26  2022-12-16  3100.0    P  paridad_violada      5.65  1.75   564.58
6 2024-10-04  2024-12-20  4950.0    P  paridad_violada      3.04  1.05   304.03
7 2024-10-07  2024-12-20  4950.0    P  paridad_violada      2.59  1.00   259.07
```

Las ocho son las `paridad_violada` de la lista, y no hay ninguna más: el escáner las encuentra todas y no confunde
ninguna otra cosa con ellas. Todas son una put cercana al dinero del tercer vencimiento desplazada al alza, así que
todas se explotan con una reversal. El beneficio en papel va de 1,50 a 11,85 puntos por par, de 150 a 1.185 USD por
contrato antes de costes; el mayor, el 26 de marzo de 2020, en plena crisis, cuando las horquillas eran también las
más anchas (semisuma de 3,90 puntos).

## Confirmar con una caja

La reversal necesita vender el forward de ORX, que no existe. En la práctica se usaría un futuro sobre el índice, con
otro vencimiento y otro horario, y la operación tendría riesgo de base. La **caja** evita el subyacente: con el strike
sospechoso como $K_1$ y el siguiente como $K_2$, comprar la caja (call comprada y put vendida en $K_1$, call vendida y
put comprada en $K_2$) cuesta $C_{ask}(K_1) - C_{bid}(K_2) + P_{ask}(K_2) - P_{bid}(K_1)$ y paga $K_2 - K_1$ seguro:

```python
ancho = ancho.sort_values(["fecha", "vencimiento", "strike"])
sig = ancho.groupby(["fecha", "vencimiento"]).shift(-1)
caja = ancho[["fecha", "vencimiento", "strike"]].assign(
    K2=sig["strike"],
    coste=(ancho["ask_c"] - sig["bid_c"]
           + sig["ask_p"] - ancho["bid_p"]),
    valor=ancho["DF"] * (sig["strike"] - ancho["strike"]))
caja["beneficio"] = caja["valor"] - caja["coste"]
claves = quedan[["fecha", "vencimiento", "strike"]]
claves.merge(caja, on=["fecha", "vencimiento", "strike"]).round(
    {"coste": 2, "valor": 2, "beneficio": 2})
```
```output
       fecha vencimiento  strike      K2  coste  valor  beneficio
0 2015-01-20  2015-04-17  2025.0  2050.0   23.9  25.00       1.10
1 2017-09-25  2017-12-15  2575.0  2600.0   23.6  24.94       1.34
2 2018-01-09  2018-03-16  2675.0  2700.0   23.9  24.94       1.04
3 2019-08-28  2019-11-15  2600.0  2625.0   23.7  24.89       1.19
4 2020-03-26  2020-06-19  2325.0  2350.0   16.9  25.00       8.10
5 2022-09-26  2022-12-16  3100.0  3150.0   45.9  49.66       3.76
6 2024-10-04  2024-12-20  4950.0  5000.0   47.5  49.47       1.97
7 2024-10-07  2024-12-20  4950.0  5000.0   47.8  49.49       1.69
```

Las ocho cajas confirman la violación con precios ejecutables: el beneficio, con bid y ask en las cuatro patas, va de
1,04 a 8,10 puntos. Es menor que el de la reversal porque la caja paga cuatro horquillas en lugar de dos más el
forward. Una caja es, además, un préstamo a tipo fijo: su precio implica un tipo de interés que se puede comparar con el de
la curva (ejercicio 2).

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

Que una caja o una reversal salgan positivas con los precios de cierre no significa que se pudieran ejecutar. Hacen
falta cuatro patas a la vez (o una orden combinada), los precios de cierre de la cadena y del índice deben ser
simultáneos y quedan las comisiones, el margen que bloquea la caja hasta el vencimiento y, en opciones americanas, el
riesgo de asignación anticipada de la pata vendida. Con beneficios de 1 a 2 puntos, cualquiera de esos detalles se
los come.

</aside>

## Pendiente y convexidad en toda la cadena

Las cotas de Merton se comprueban igual, con la condición de ejecución en cada pata: para la monotonía, comprar la
call de strike menor al ask y vender la de strike mayor al bid; para la mariposa, comprar las alas al ask y vender el
centro al bid. La función recibe la tabla ancha y devuelve cuántas filas incumplen cada cota; se aplica a la cadena
entera y a la cadena sin las filas sospechosas:

```python
def cotas(a):
    a = a.sort_values(["fecha", "vencimiento", "strike"])
    g = a.groupby(["fecha", "vencimiento"])
    s1, s2 = g.shift(-1), g.shift(-2)
    dk = s1["strike"] - a["strike"]
    lam = (s2["strike"] - s1["strike"]) / (s2["strike"]
                                           - a["strike"])
    DF, F, K = a["DF"], a["F"], a["strike"]
    return pd.Series({
        "C >= DF(F-K)+": (a["ask_c"] < DF * (F - K).clip(0)).sum(),
        "P >= DF(K-F)+": (a["ask_p"] < DF * (K - F).clip(0)).sum(),
        "C <= DF·F": (a["bid_c"] > DF * F).sum(),
        "P <= DF·K": (a["bid_p"] > DF * K).sum(),
        "C decrece": (a["ask_c"] < s1["bid_c"]).sum(),
        "P crece": (s1["ask_p"] < a["bid_p"]).sum(),
        "pendiente C": (a["bid_c"] - s1["ask_c"] > DF * dk).sum(),
        "pendiente P": (s1["bid_p"] - a["ask_p"] > DF * dk).sum(),
        "mariposa C": (lam * a["ask_c"] + (1 - lam) * s2["ask_c"]
                       < s1["bid_c"]).sum(),
        "mariposa P": (lam * a["ask_p"] + (1 - lam) * s2["ask_p"]
                       < s1["bid_p"]).sum(),
    })

ancho = ancho.join(marcas, on=["fecha", "vencimiento", "strike"])
sosp = ancho["congelada"] | ancho["cruzada"]
pd.DataFrame({"todas": cotas(ancho),
              "sin sospechosas": cotas(ancho[~sosp])})
```
```output
               todas  sin sospechosas
C >= DF(F-K)+      0                0
P >= DF(K-F)+      0                0
C <= DF·F          0                0
P <= DF·K          0                0
C decrece        138                0
P crece          204                1
pendiente C        4                0
pendiente P        0                0
mariposa C       278                0
mariposa P       434                8
```

Con la cadena entera, las cotas de nivel (inferior y superior) se cumplen siempre, pero hay cientos de violaciones de
monotonía y de convexidad: una cotización congelada de ayer, tras un día de caída, deja una put más barata que la de
strike inferior o una mariposa de precio negativo. Sin las filas sospechosas quedan solo 9 incumplimientos, todos en
las puts: 8 mariposas centradas en la put plantada (que está cara respecto a sus vecinas) y una monotonía, la de marzo
de 2020, donde el desplazamiento de 11,85 puntos deja la put de 2.325 por encima de la de 2.350. Las cotas de Merton
y la paridad señalan los mismos ocho defectos, lo que confirma que la superficie verdadera no tiene arbitraje y que
todo lo demás es ruido de datos.

## Opciones americanas: la paridad se convierte en desigualdad

Con ejercicio anticipado la réplica de la paridad deja de ser exacta: la put americana puede ejercerse antes y la call
puede ejercerse justo antes de un dividendo (lección 1.2.5). Lo que queda son dos desigualdades. Para una acción con
dividendos discretos de valor presente $D$ hasta el vencimiento:

$$
S - D - K \;\le\; C - P \;\le\; S - K\,e^{-rT}.
$$

La de la derecha sale de que la put americana vale al menos la europea; la de la izquierda, de comparar la call con
una cartera de put, efectivo $K$ y la acción. Los datos de **KRN**, una acción ficticia que paga un dividendo
trimestral, traen opciones americanas con su precio de modelo (árbol binomial con dividendos discretos, lección 3.4.2)
y el de la opción europea equivalente. Con ellos se comprueban la igualdad europea y las desigualdades americanas:

```python
krn = pd.read_parquet("accion3_opciones.parquet")
acc = pd.read_csv("accion3_diario.csv", parse_dates=["fecha"],
                  index_col="fecha")
ev = pd.read_csv("accion3_eventos.csv", parse_dates=["fecha"])
divs = ev[ev["evento"] == "dividendo"].set_index("fecha")[
    "dividendo"]
k = krn.pivot_table(index=["fecha", "vencimiento", "strike"],
                    columns="tipo",
                    values=["precio_modelo", "europea_modelo",
                            "bid", "ask"]).dropna()
k.columns = [f"{c}_{t.lower()}" for c, t in k.columns]
k = k.reset_index()
k["T"] = (k["vencimiento"] - k["fecha"]).dt.days / 365
curva = ind.loc[k["fecha"], cols].to_numpy() / 100
k["r"] = [np.interp(t, plazos, c) for t, c in zip(k["T"], curva)]
k["S"] = acc.loc[k["fecha"], "cierre"].to_numpy()

def vp_dividendos(f, v, r):
    d = divs[(divs.index > f) & (divs.index <= v)]
    t = (d.index - f).days / 365
    return float((d * np.exp(-r * t)).sum())

u = k.drop_duplicates(["fecha", "vencimiento"])
vp = {(f, v): vp_dividendos(f, v, r)
      for f, v, r in zip(u["fecha"], u["vencimiento"], u["r"])}
k["D"] = [vp[x] for x in zip(k["fecha"], k["vencimiento"])]
k["KDF"] = k["strike"] * np.exp(-k["r"] * k["T"])

eu = (k["europea_modelo_c"] - k["europea_modelo_p"]
      - (k["S"] - k["D"] - k["KDF"]))
cp = k["precio_modelo_c"] - k["precio_modelo_p"]
tol = 0.0051                  # el cierre está redondeado al céntimo
dentro = cp.between(k["S"] - k["D"] - k["strike"] - tol,
                    k["S"] - k["KDF"] + tol)
print(f"{len(k)} pares; paridad europea: error máx "
      f"{eu.abs().max():.4f}")
print(f"Americanas dentro de sus cotas: {dentro.mean():.2%}")
```
```output
66763 pares; paridad europea: error máx 0.0051
Americanas dentro de sus cotas: 100.00%
```

Las europeas cumplen la igualdad con un error máximo de medio céntimo, el redondeo del cierre de la acción. Las
americanas, todas, caen entre sus dos cotas. La figura muestra por qué no cumplen la igualdad: en un día con un
dividendo por delante, $C - P$ de las americanas se separa de la línea europea hacia arriba en los strikes bajos (calls
dentro del dinero, que se ejercerían la víspera de la fecha ex) y hacia abajo en los altos (puts dentro del dinero, que
se ejercerían para cobrar antes el strike).

*Fig. — Opciones americanas de KRN entre las cotas de la paridad, 3 de noviembre de 2025, vencimiento del 21 de noviembre con un dividendo de 1,04 USD el día 13.* Por strike, de 145 a 225 USD, C − P de las opciones americanas menos la paridad europea S − D − K·e^(−rT) (línea destacada), entre la cota superior (+1,04, el valor presente del dividendo) y la inferior (−K(1 − e^(−rT)), de −0,28 a −0,43). En strikes bajos queda hasta 0,88 USD por encima de cero (calls dentro del dinero que se ejercerían antes del dividendo); en los altos, hasta 0,19 por debajo (puts dentro del dinero).

Si se aplica la igualdad europea a las cotizaciones de las americanas, el escáner de la sección anterior encuentra
miles de arbitrajes falsos:

```python
teo_eu = k["S"] - k["D"] - k["KDF"]
conv = k["bid_c"] - k["ask_p"] - teo_eu
rev = teo_eu - (k["ask_c"] - k["bid_p"])
print("conversiones aparentes:", (conv > 0).sum())
print("reversals aparentes:", (rev > 0).sum())
```
```output
conversiones aparentes: 1779
reversals aparentes: 1006
```

Son 2.785 pares de 66.763, un 4 %, y ninguno es real: la «conversión» vende una call americana que el comprador
ejercerá antes del dividendo, y la «reversal» vende una put americana que le asignarán en cuanto convenga ejercerla.

## Lo que cambia en el trading real

- **Primero los datos.** En opciones, un escáner de arbitraje encuentra sobre todo errores: precios arrastrados,
  cotizaciones cruzadas, call y put tomadas en momentos distintos, un vencimiento mal asignado. Las reglas de la
  sección del escáner (congelada, cruzada) son también la primera limpieza de un backtest de opciones (lección 3.6.4).
- **El forward, de la cadena.** Usar el contado con un dividendo supuesto en lugar del forward implícito produce
  violaciones sistemáticas en todos los strikes; la mediana de la paridad cerca del dinero es robusta y no necesita
  conocer el dividendo.
- **Ejecutable o nada.** Una desviación con medios dentro de la semisuma de horquillas no es explotable, por grande que
  parezca en un gráfico.
- **Estilo de ejercicio.** Con opciones americanas (acciones, ETF, muchas opciones sobre futuros) la paridad es una
  desigualdad y las cajas tienen riesgo de asignación: hay que conocer el estilo de cada contrato antes de valorarlo
  (lección 3.4.2).
- **Las cotas sirven para limpiar superficies.** Antes de ajustar una sonrisa (lección 3.4.5) o calcular un índice de
  volatilidad (3.4.6), los puntos que incumplen monotonía o convexidad se quitan o se corrigen; si no, el ajuste
  hereda el arbitraje.

## Ejercicios

1. Repite el escáner de paridad con el forward y el tipo verdaderos de `opciones3_verdad.parquet` en lugar del forward
   estimado. ¿Cambia el número de violaciones? ¿Por qué no cambia apenas?

**Solución.**

```python
con_verdad = ancho.merge(
    verdad[["fecha", "vencimiento", "forward", "tipo_libre"]],
    on=["fecha", "vencimiento"])
DFv = np.exp(-con_verdad["tipo_libre"] * con_verdad["T"])
teo_v = DFv * (con_verdad["forward"] - con_verdad["strike"])
conv_v = con_verdad["bid_c"] - con_verdad["ask_p"] - teo_v
rev_v = teo_v - (con_verdad["ask_c"] - con_verdad["bid_p"])
v2 = con_verdad[(conv_v > 0) | (rev_v > 0)]
print(len(v2), "violaciones con la verdad;",
      len(viol), "con el forward estimado")
```
```output
345 violaciones con la verdad; 346 con el forward estimado
```

Con la verdad salen 345 violaciones, una menos que con el forward estimado. El error del forward estimado es casi
siempre de centésimas de punto, muy por debajo de la semisuma de horquillas (de 0,5 a varios puntos), así que solo
cambia el veredicto de algún par que estaba justo en el borde de su banda. La mediana cerca del dinero es una buena
estimación del forward; lo importante es que no se deje arrastrar por el strike defectuoso.

2. Calcula el tipo implícito de las cajas. Con precios medios, $C - P = DF\,F - DF\,K$ es una recta en el strike
   cuya pendiente es $-DF$: ajusta esa recta cada miércoles en el segundo vencimiento, con los strikes a menos de un
   10 % del forward, y deduce $r_{caja} = -\ln(DF)/T$. Compáralo con el tipo de la curva. ¿Qué esperarías en datos
   reales?

**Solución.**

```python
c2 = ancho.assign(
    medio_cp=(ancho["bid_c"] + ancho["ask_c"]
              - ancho["bid_p"] - ancho["ask_p"]) / 2)
c2 = c2[c2["fecha"].dt.weekday == 2]
orden = c2.groupby("fecha")["vencimiento"].rank(method="dense")
c2 = c2[(orden == 2) & ((c2["strike"] / c2["F"] - 1).abs() < 0.10)]
filas = []
for f, d in c2.groupby("fecha"):
    pendiente = np.polyfit(d["strike"], d["medio_cp"], 1)[0]
    T = d["T"].iloc[0]
    filas.append((f, -np.log(-pendiente) / T, d["r"].iloc[0],
                  len(d)))
cajas = pd.DataFrame(filas, columns=["fecha", "r_caja", "r", "n"])
dif = 1e4 * (cajas["r_caja"] - cajas["r"])
print(len(cajas), "miércoles; strikes por ajuste (mediana):",
      int(cajas["n"].median()))
print("diferencia (pb): mediana", round(dif.median(), 1),
      "| p5", round(dif.quantile(0.05), 1),
      "| p95", round(dif.quantile(0.95), 1))
```
```output
619 miércoles; strikes por ajuste (mediana): 15
diferencia (pb): mediana -0.6 | p5 -27.6 | p95 33.0
```

La pendiente de $C - P$ frente al strike es una caja con todos los strikes a la vez: equivale a promediar muchas cajas
y reduce el ruido de cada precio medio. Aun así, el tipo implícito se mueve decenas de puntos básicos alrededor del
de la curva: con 30 a 60 días de plazo, un error de pocas centésimas en la pendiente ya son muchos puntos básicos de
tipo. Una caja entre dos strikes vecinos sería mucho peor, porque su error se divide por $K_2 - K_1$ (25 o 50 puntos)
en lugar de por todo el rango de strikes. En los datos sintéticos no hay ninguna prima: los precios salen de la curva.
Con datos reales, van Binsbergen, Diamond y Grotteria (2022) obtienen tipos libres de riesgo con la paridad put-call
de las opciones SPX (las «cajas») y encuentran que las letras del Tesoro rinden unos 40 puntos básicos menos: la
prima que se paga por la liquidez y la seguridad de la deuda pública (*convenience yield*).

3. Un compañero te enseña una «conversión» de 6 puntos en una call de delta 0,05 el día después de una caída del 3 %
   del índice, y propone automatizar la búsqueda. Explica qué comprobarías antes de enviar una orden.

**Solución.**

Primero, si la call repite el bid y el ask del día anterior: tras una caída del 3 %, una call lejana con el precio de
ayer está cara solo en el fichero, y es el perfil exacto de las 330 congeladas de la lección. Segundo, si el bid no
supera al ask y si la call y la put llevan la misma marca de tiempo. Tercero, si la violación sobrevive con el forward
estimado de la mediana y con una caja con el strike vecino. Cuarto, si el beneficio cubre las comisiones de cuatro
patas, el margen y el riesgo de ejecución de una orden combinada. Y, si la opción es americana, si la igualdad europea
siquiera se aplica. Automatizar la búsqueda tiene sentido como control de calidad de datos; como estrategia, solo con
precios en tiempo real y órdenes combinadas.

## Autoevaluación

1. En un par call-put, la desviación de la paridad calculada con los precios medios es de −4,09 puntos y la semisuma de las dos horquillas es 1,05. ¿Qué se puede hacer?

   - A. Nada: con precios medios no se sabe si hay arbitraje
   - B. Comprar la call al ask, vender la put al bid y vender el forward: se ingresan unos 3 puntos más de lo que vale la posición
   - C. Vender la call y comprar la put, porque la call está cara
   - D. Esperar a que la desviación supere el doble de la horquilla

   *Respuesta B: Comprar la call al ask, vender la put al bid y vender el forward: se ingresan unos 3 puntos más de lo que vale la posición.* Una desviación medida con medios mayor (en valor absoluto) que la semisuma de las horquillas equivale exactamente a una operación ejecutable con bid y ask. Aquí C − P está por debajo de su valor teórico: la put está cara y la reversal (call comprada, put vendida, forward vendido) gana unos 3,04 puntos antes de costes, como la put de 4.950 del 2024-10-04.

2. El escáner de paridad encuentra 346 violaciones ejecutables en doce años de cadena. ¿Qué explica la mayoría?

   - A. Errores en el forward estimado
   - B. Cotizaciones que repiten el bid y el ask del día anterior en sesiones en que el índice se movió más de un 1 %
   - C. Oportunidades reales que nadie aprovechó
   - D. El tipo de interés de la curva

   *Respuesta B: Cotizaciones que repiten el bid y el ask del día anterior en sesiones en que el índice se movió más de un 1 %.* 330 de las 346 tienen una pata congelada: el precio es el de ayer y el índice se ha movido. Otras 8 tienen una cotización cruzada (bid por encima del ask). Solo quedan 8, que son exactamente las violaciones plantadas en los datos. Un escáner de arbitraje es, sobre todo, un detector de datos malos.

3. ¿Por qué una caja (box spread) sirve para confirmar una violación de la paridad sin necesidad de operar el subyacente?

   - A. Porque la caja no tiene horquilla
   - B. Porque combina dos sintéticos del mismo vencimiento con strikes distintos y paga K2 − K1 en cualquier escenario: su precio solo depende del tipo de interés
   - C. Porque la caja es una opción americana
   - D. Porque no necesita margen

   *Respuesta B: Porque combina dos sintéticos del mismo vencimiento con strikes distintos y paga K2 − K1 en cualquier escenario: su precio solo depende del tipo de interés.* Call comprada y put vendida en K1 más call vendida y put comprada en K2 pagan siempre K2 − K1 al vencimiento. Vale el descuento de esa cantidad. Si la put de un strike está cara, la caja que la vende cuesta menos que ese valor: en las ocho violaciones, el beneficio de la caja con bid y ask va de 1,04 a 8,10 puntos.

4. En una cadena limpia, ¿cuál de estas relaciones entre calls del mismo vencimiento puede incumplirse sin que haya arbitraje?

   - A. C(K1) ≥ C(K2) si K1 < K2
   - B. C(K1) − C(K2) ≤ e^(−rT)·(K2 − K1)
   - C. La mariposa C(K1) − 2C(K2) + C(K3) ≥ 0 con strikes equidistantes
   - D. Ninguna: las tres son cotas de no arbitraje

   *Respuesta D: Ninguna: las tres son cotas de no arbitraje.* Las tres salen de comparar carteras: la call de strike menor domina a la de strike mayor; la diferencia entre las dos no puede pagar más que K2 − K1; y la mariposa paga siempre algo no negativo. En la cadena de ORX, tras quitar las cotizaciones sospechosas, solo las incumplen las puts plantadas.

5. KRN tiene opciones americanas y paga un dividendo antes del vencimiento. ¿Qué dice la paridad?

   - A. C − P = S − D − K·e^(−rT), igual que para las europeas
   - B. S − D − K ≤ C − P ≤ S − K·e^(−rT), con D el valor presente del dividendo
   - C. C − P = S − K
   - D. No hay ninguna relación entre la call y la put americanas

   *Respuesta B: S − D − K ≤ C − P ≤ S − K·e^(−rT), con D el valor presente del dividendo.* Con ejercicio anticipado la paridad se convierte en dos desigualdades. En KRN, los 66.763 pares de precios del modelo caen dentro (con medio céntimo de redondeo), mientras que aplicar la igualdad europea a sus cotizaciones americanas daría 2.785 arbitrajes aparentes.

## Ideas clave

- Una call comprada y una put vendida del mismo strike pagan lo mismo que un forward comprado a ese strike: de ahí la paridad C − P = e^(−rT)(F − K) y, con dos strikes, el valor fijo de una caja.
- Las cotas de Merton, la monotonía, la pendiente y la convexidad en el strike son condiciones de no arbitraje que no dependen de ningún modelo.
- Una desviación de la paridad medida con medios es ejecutable si supera la semisuma de las horquillas: es la misma condición que comprar y vender con bid y ask.
- En doce años de cadena hay 346 violaciones ejecutables: 330 por cotizaciones congeladas, 8 por cotizaciones cruzadas y 8 reales, las plantadas, que una caja confirma con 1,04 a 8,10 puntos de beneficio antes de costes.
- Con opciones americanas y dividendos discretos la paridad es una desigualdad: aplicar la igualdad europea produce miles de arbitrajes falsos.

## Recursos

### Research

- [Usos de la Volatilidad y Volatilidad Actual - VIX1D](https://pythonparatrading.com/usos-de-la-volatilidad/) · Research-0003 · 6 min

### Documentación

- [Cboe: especificaciones de las opciones SPX (europeas, liquidación en efectivo)](https://www.cboe.com/tradable_products/sp_500/spx_options/specifications/) · cboe.com

### Libros

- [Merton, R. C. (1973): Theory of Rational Option Pricing. The Bell Journal of Economics and Management Science 4(1), 141-183](https://doi.org/10.2307/3003143) · doi.org
- [Stoll, H. R. (1969): The Relationship Between Put and Call Option Prices. The Journal of Finance 24(5), 801-824](https://doi.org/10.1111/j.1540-6261.1969.tb01694.x) · doi.org
- [John C. Hull, Options, Futures, and Other Derivatives (Pearson, 11.ª ed., 2021)](https://www.pearson.com/en-us/subject-catalog/p/options-futures-and-other-derivatives/P200000005938) · pearson.com
- [van Binsbergen, J. H., Diamond, W. F. y Grotteria, M. (2022): Risk-free interest rates. Journal of Financial Economics 143(1), 1-29](https://doi.org/10.1016/j.jfineco.2021.06.012) · doi.org

### Datos

- [Datos del nivel 3: cadena diaria de opciones sobre ORX, 2014-2025 (Parquet)](https://pythonparatrading.com/curso/datos/nivel-3/opciones3_cadena.parquet) · Descarga · opciones3_cadena.parquet
- [Datos del nivel 3: incidencias plantadas en la cadena (CSV)](https://pythonparatrading.com/curso/datos/nivel-3/opciones3_incidencias.csv) · Descarga · opciones3_incidencias.csv

Lección anterior: [3.3.3 · Riesgo de divisa en carteras multi-moneda; ejecución vía OANDA, IBKR o MT5](https://pythonparatrading.com/curso-python-trading/nivel-3/riesgo-de-divisa-y-ejecucion-fx/)

Lección siguiente: [3.4.2 · Árboles binomiales, Black-Scholes-Merton, Black-76 y Monte Carlo](https://pythonparatrading.com/curso-python-trading/nivel-3/binomial-black-scholes-black-76-y-monte-carlo/)
