---
title: "3.6.4 Backtesting de opciones: datos, fills y eventos"
description: "Cadenas históricas y OPRA, fills al medio frente a bid/ask, cotizaciones congeladas y cruzadas, paridad, días sin cadena, dividendos y resultados."
url: "https://pythonparatrading.com/curso-python-trading/nivel-3/backtesting-de-opciones/"
lang: "es"
image: "https://pythonparatrading.com/og/curso-python-trading/nivel-3/backtesting-de-opciones.png"
---

# 3.6.4 · Backtesting de opciones: datos, fills y ajustes por eventos

> Un backtest de opciones trabaja con cientos de contratos por día, cada uno con su horquilla, y casi todos los errores vienen de los datos o de los precios de ejecución, no de la estrategia. Esta lección limpia una cadena diaria con defectos plantados y su lista de respuestas, mide cuánto cambia un resultado al ejecutar al medio o al bid/ask, y trata los dividendos, los resultados y el ejercicio anticipado de una acción con opciones americanas.

- Curso: [Nivel 3: Derivados: futuros, spreads, forex y opciones](https://pythonparatrading.com/curso-python-trading/nivel-3/)
- Módulo: 3.6 · Estrategias sistemáticas de volatilidad y opciones
- Duración: 75 min
- Requisitos: [3.4.1 · Payoffs, paridad put-call y límites de arbitraje](https://pythonparatrading.com/curso-python-trading/nivel-3/payoffs-paridad-put-call-y-limites-de-arbitraje/), [3.5.5 · Gestión: ajustes, roll, cierre anticipado, asignación temprana y pin risk](https://pythonparatrading.com/curso-python-trading/nivel-3/gestion-de-posiciones-de-opciones/), [3.6.1 · Venta sistemática de prima: put-write, strangles y su riesgo de cola](https://pythonparatrading.com/curso-python-trading/nivel-3/venta-sistematica-de-prima-y-riesgo-de-cola/)

## Al terminar esta lección

- Describir las fuentes de datos de opciones de EE. UU. (OPRA, resúmenes de fin de día, cadenas históricas) y qué trae cada una, con la simbología OSI.
- Detectar en una cadena cotizaciones cruzadas, congeladas y fuera de la paridad put-call, días sin cadena y bids nulos, y medir la precisión de cada regla contra una lista de respuestas.
- Construir un backtest de opciones con selección de strikes sin anticipación, valoración diaria al medio, liquidación al vencimiento y un modelo de fills configurable, y medir cuánto se lleva la horquilla.
- Tratar los eventos de una acción: precios sin ajustar para los strikes, caída de la implícita tras resultados y ejercicio anticipado de opciones americanas.

## Por qué los backtests de opciones fallan por los datos

Un backtest de acciones diario trabaja con un precio por activo y día. Uno de opciones, con una cadena: decenas de
strikes por vencimiento, varios vencimientos, call y put, cada contrato con su bid, su ask, su volumen y su interés
abierto. La cadena diaria del índice ORX de los datos del curso tiene unas 180 filas por sesión con solo tres
vencimientos; la de un índice real, con vencimientos semanales y diarios, tiene muchas más. Con tantos precios, los
errores de datos dejan de ser excepciones y pasan a ser una población: cotizaciones que no se actualizan, bids por
encima del ask, días sin datos. Y con horquillas que en las opciones fuera del dinero llegan a ser una fracción grande
del precio, el supuesto de ejecución pesa más que en cualquier otro activo.

```mermaid
flowchart TB
  D[Cadena cruda] --> L[Limpieza]
  L --> S[Selección en t]
  S --> E[Fill de entrada]
  E --> V[Valoración diaria]
  V --> X[Vence o se cierra]
  X --> M[Métricas y cola]
```

Cada caja tiene su trampa. La **selección** debe usar solo lo que se sabía al cierre de ese día (el strike de delta
0,16 se calcula con la implícita de ese cierre, no con la de mañana). El **fill** decide cuánto se cobra o se paga. La
**valoración** diaria al medio necesita una cotización válida cada día para cada pata. El **vencimiento** liquida por
el valor intrínseco o, en opciones americanas, puede llegar antes por ejercicio (lección 3.5.5). Esta lección recorre
las cajas con datos sintéticos que llevan defectos plantados y la lista de esos defectos
(`opciones3_incidencias.csv`): la respuesta conocida contra la que medir cada regla de limpieza.

## Fuentes de datos reales

En EE. UU., todas las bolsas de opciones publican sus cotizaciones y operaciones a través de un único procesador,
**OPRA** (Options Price Reporting Authority). Según su web, OPRA es un procesador de información de valores registrado
cuyos miembros son las bolsas de opciones autorizadas por la SEC, y lo que distribuye son, sobre todo, operaciones
(«last sale») y cotizaciones, más volúmenes, interés abierto y resúmenes de fin de día. Todo lo demás se deriva de ese
flujo:

| Producto | Qué trae | Granularidad | Uso típico |
|---|---|---|---|
| Flujo OPRA (p. ej. Databento `OPRA.PILLAR`) | Mejor bid/ask consolidado, operaciones, estadísticas, definiciones | Cada mensaje, o agregado por segundo o minuto | Microestructura, ejecución, intradía |
| Resumen de fin de día (p. ej. Cboe Option EOD Summary) | Bid/ask con tamaño a las 15:45 ET y al cierre; OHLC, volumen e interés abierto al cierre | Dos fotos diarias, desde enero de 2012 | Backtests diarios |
| Cadenas históricas con implícitas y griegas (proveedores académicos y comerciales) | Lo anterior más implícitas, griegas y superficies calculadas | Diaria | Research, superficies |

Dos detalles de la tabla importan en un backtest diario. El primero: Cboe da una foto a las 15:45 y otra al cierre
porque el cierre de las opciones y el del subyacente no son simultáneos, y una implícita calculada con precios de
horas distintas está mal. La foto de las 15:45 trae, según su página, las implícitas y griegas opcionales calculadas a
esa hora. El segundo: Databento indica que antes del 2023-03-28 su historia de OPRA procede de datos submuestreados,
con el BBO por minuto como máximo detalle; para fechas anteriores no hay cotizaciones mensaje a mensaje.

El código para pedir a Databento las cotizaciones de un día de las opciones del SPX es este (necesita clave y red; no
se ejecuta aquí). La simbología `parent` con el sufijo `.OPT` agrupa todas las opciones de una raíz, según su
documentación:

```python skip
import databento as db

client = db.Historical("CLAVE")
datos = client.timeseries.get_range(
    dataset="OPRA.PILLAR",
    schema="cbbo-1m",            # mejor bid/ask consolidado por minuto
    stype_in="parent",
    symbols=["SPX.OPT"],
    start="2024-03-15T19:00:00",
    end="2024-03-15T20:00:00",
)
df = datos.to_df()
```

Cada contrato llega con su símbolo OSI, el formato de la cámara (OCC): 21 caracteres con la raíz de seis caracteres
rellena con espacios, la fecha de vencimiento `AAMMDD`, `C` o `P` y el strike por 1.000 en ocho dígitos (el ejemplo de
Databento es `SPY   231117P00486000`). Construirlo y leerlo es lo primero que hace cualquier código que una datos de
dos fuentes:

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

cad = pd.read_parquet("opciones3_cadena.parquet")
ind = pd.read_csv("opciones3_indice.csv", parse_dates=["fecha"])
ind = ind.set_index("fecha")

def osi(raiz, venc, tipo, strike):
    return (f"{raiz:<6}{venc:%y%m%d}{tipo}"
            f"{int(round(strike * 1000)):08d}")

def lee_osi(s):
    return {"raiz": s[:6].strip(),
            "vencimiento": pd.Timestamp("20" + s[6:12]),
            "tipo": s[12], "strike": int(s[13:]) / 1000}

fila = cad.iloc[3]
s = osi("ORX", fila["vencimiento"], fila["tipo"], fila["strike"])
print(repr(s), len(s))
print(lee_osi(s))
print(lee_osi("SPY   231117P00486000"))
```
```output
'ORX   140117C01750000' 21
{'raiz': 'ORX', 'vencimiento': Timestamp('2014-01-17 00:00:00'), 'tipo': 'C', 'strike': 1750.0}
{'raiz': 'SPY', 'vencimiento': Timestamp('2023-11-17 00:00:00'), 'tipo': 'P', 'strike': 486.0}
```

## La cadena del curso y sus huecos

La cadena de ORX (`opciones3_cadena.parquet`) imita las opciones del SPX: europeas, liquidación en efectivo al cierre
del vencimiento (simplificación: en el S&P 500 real, las mensuales liquidan a la apertura), multiplicador de 100 USD,
tick de 0,05 por debajo de 3 puntos y de 0,10 por encima y los tres vencimientos mensuales más cercanos. Antes de
cualquier estrategia, cuatro preguntas: cuántos contratos hay, qué sesiones faltan, cuáles son anómalas y cómo son las
horquillas.

```python
por_dia = cad.groupby("fecha").size()
print("filas por sesión:", por_dia.describe()[["min", "50%", "max"]]
      .astype(int).to_dict())
faltan = ind.index.difference(cad["fecha"].unique())
print("sesiones del índice sin cadena:", [d.date() for d in faltan])
cortas = ind.index[ind["sesion_corta"]]
vol = cad.groupby("fecha")["volumen"].sum()
print(f"medias sesiones: {len(cortas)}; volumen medio frente a una "
      f"sesión normal: {vol.reindex(cortas).mean() / vol.mean():.0%}")
```
```output
filas por sesión: {'min': 86, '50%': 174, 'max': 396}
sesiones del índice sin cadena: [datetime.date(2019, 7, 23)]
medias sesiones: 26; volumen medio frente a una sesión normal: 35%
```

El 2019-07-23 el índice tiene cierre y la cadena no: el día sin cadena plantado. Un backtest que haga
`cadena[cadena.fecha == d]` ese día recibe una tabla vacía y, según cómo esté escrito, abre posiciones con precio
`NaN`, valora la cartera a cero o falla. La regla razonable es explícita: ese día no se abre nada y la valoración se
repite (o se interpola y se dice). Las 26 medias sesiones (cierre a las 13:00 en la NYSE) tienen un tercio del
volumen: no son un error, pero sus cotizaciones son menos representativas, y la theta de ese día es la de un día
natural completo.

Las horquillas, en % del medio, por distancia al dinero (opciones fuera del dinero, con bid positivo), y la fracción
de bids nulos:

```python
cad["medio"] = (cad["bid"] + cad["ask"]) / 2
cad["semi"] = (cad["ask"] - cad["bid"]) / 2
S = ind["cierre"]
cad["S"] = S.reindex(cad["fecha"]).to_numpy()
otm = np.where(cad["tipo"] == "P", cad["strike"] <= cad["S"],
               cad["strike"] >= cad["S"])
dist = np.abs(np.log(cad["strike"] / cad["S"]))
tramos = pd.cut(dist, [0, 0.02, 0.05, 0.10, 0.15, 1.0],
                labels=["<2 %", "2-5 %", "5-10 %", "10-15 %", ">15 %"])
ok = otm & (cad["bid"] > 0)
pct = 100 * 2 * cad["semi"] / cad["medio"]
print(f"bids nulos: {(cad['bid'] == 0).mean():.1%} de las filas")
pct[ok].groupby(tramos[ok], observed=True).median().round(1)
```
```output
bids nulos: 11.1% de las filas
<2 %       1.2
2-5 %      1.8
5-10 %     2.9
10-15 %    4.5
>15 %      6.7
dtype: float64
```

*Fig. — Horquilla mediana de las opciones fuera del dinero de ORX en % de su precio medio, por distancia al dinero (tramos de 1 %, bid positivo, 2014-2025; datos sintéticos).* Dos curvas que suben con la distancia al dinero desde un 1,1 % en el dinero, destacado. Las puts suben despacio: un 1,8 % a 5 puntos de distancia, un 2,7 % a 10 y en torno al 5 % a 20. Las calls, mucho más deprisa, porque fuera del dinero son muy baratas: un 3,6 % a 5 puntos, un 8,0 % a 10 y más del 15 % en las alas.

En el dinero, la horquilla mediana es del 1,2 % del precio; entre el 5 y el 10 % fuera, del 2,9 %; a más del 15 %,
del 6,7 %: las cifras de la respuesta conocida del README. La figura separa puts y calls: por el skew, las calls fuera
del dinero valen muy poco, y una horquilla parecida en puntos es un porcentaje mucho mayor de su precio. Y el 11,1 % de las filas tienen bid nulo: opciones tan
fuera del dinero que nadie paga nada por ellas. En esas filas el medio (la mitad del ask) es un precio que no existe:
nadie compra a ese precio y nadie puede vender. Vender una opción con bid nulo en un backtest al medio es fabricar
prima.

## Limpieza: cruzadas, congeladas y paridad

Tres reglas, cada una contrastada con la lista de incidencias plantadas. La primera es la más fácil: un bid por encima
del ask (cotización **cruzada**) no puede ser una cotización ejecutable.

```python
inc = pd.read_csv("opciones3_incidencias.csv",
                  parse_dates=["fecha", "vencimiento"])
clave = ["fecha", "vencimiento", "tipo", "strike"]
print(inc["incidencia"].value_counts().to_dict())
cruz = cad[cad["bid"] > cad["ask"]]
cruz.merge(inc, on=clave, how="left")["incidencia"].value_counts()
```
```output
{'cotizacion_congelada': 1500, 'cotizacion_cruzada': 12, 'paridad_violada': 8, 'dia_sin_cadena': 1}
incidencia
cotizacion_cruzada    12
Name: count, dtype: int64
```

Las 12 cruzadas que hay en la cadena son exactamente las 12 plantadas. Las **congeladas** son más difíciles: la
cotización de ayer repetida hoy. El generador las plantó en opciones lejanas (delta absoluta por debajo de 0,12) los
días en que el índice se movió más de un 1 %. La regla obvia, «igual que ayer en un día de movimiento», y una versión
que excluye los bids nulos:

```python
cad = cad.sort_values(clave[1:] + ["fecha"]).reset_index(drop=True)
g = cad.groupby(["vencimiento", "tipo", "strike"])
ses = pd.Series(sorted(cad["fecha"].unique()))
anterior = dict(zip(ses.iloc[1:], ses.iloc[:-1]))
cad["igual"] = ((cad["bid"] == g["bid"].shift())
                & (cad["ask"] == g["ask"].shift())
                & (g["fecha"].shift() == cad["fecha"].map(anterior)))
rend = np.log(S).diff()
cad["mueve"] = rend.abs().reindex(cad["fecha"]).to_numpy() > 0.01
verdad = inc[inc["incidencia"] == "cotizacion_congelada"]

def mide(marcadas):
    m = marcadas.merge(verdad, on=clave, how="left")
    acierto = m["incidencia"].notna().sum()
    return {"marcadas": len(marcadas), "aciertos": acierto,
            "precision": acierto / len(marcadas),
            "recall": acierto / len(verdad)}

reglas = {"igual y |r| > 1 %": cad["igual"] & cad["mueve"],
          "... y bid > 0": cad["igual"] & cad["mueve"]
          & (cad["bid"] > 0)}
pd.DataFrame({k: mide(cad[v]) for k, v in reglas.items()}).T.round(3)
```
```output
                   marcadas  aciertos  precision  recall
igual y |r| > 1 %   11336.0    1500.0      0.132   1.000
... y bid > 0        1940.0    1090.0      0.562   0.727
```

La primera regla encuentra todas (recall del 100 %), pero solo una de cada ocho marcadas es una congelada plantada: el
resto son opciones con bid 0 y ask en el tick mínimo, cuya cotización no cambia porque no vale casi nada. Exigir un
bid positivo sube la precisión al 56 % y deja escapar el 27 % de las congeladas: las de menos valor. ¿Importan las que
se escapan? Para saberlo hay que medir el error de precio de cada una frente al valor del modelo (con `iv_modelo`, la
implícita verdadera; en datos reales no existe y habría que compararlo con la sonrisa de los strikes vecinos):

```python
from scipy.special import ndtr

def bs_precio(tipo, S, K, T, iv, r, q):
    F = S * np.exp((r - q) * T)
    sd = iv * np.sqrt(T)
    d1 = (np.log(F / K) + 0.5 * sd * sd) / sd
    df = np.exp(-r * T)
    call = df * (F * ndtr(d1) - K * ndtr(d1 - sd))
    return np.where(tipo == "C", call, call - df * (F - K))

cg = cad.merge(verdad, on=clave, how="left")
cg = cg[cg["igual"] & cg["mueve"]].copy()
f_ = cg["fecha"]
cg["modelo"] = bs_precio(
    cg["tipo"].to_numpy(), cg["S"].to_numpy(),
    cg["strike"].to_numpy(),
    ((cg["vencimiento"] - f_).dt.days / 365).to_numpy(),
    cg["iv_modelo"].to_numpy(),
    ind["tipo_1m"].reindex(f_).to_numpy() / 100,
    ind["rentabilidad_dividendo"].reindex(f_).to_numpy() / 100)
cg["error_semis"] = (cg["medio"] - cg["modelo"]).abs() / cg["semi"]
cg["grupo"] = np.select(
    [cg["incidencia"].isna(), cg["bid"] > 0],
    ["no plantada", "plantada, bid > 0"], "plantada, bid 0")
cg.groupby("grupo")["error_semis"].describe(
    percentiles=[0.5, 0.9])[["count", "50%", "90%", "max"]].round(2)
```
```output
                    count    50%    90%     max
grupo
no plantada        9836.0   0.97   1.00    1.00
plantada, bid 0     410.0   0.99   1.00   33.52
plantada, bid > 0  1090.0  10.74  34.33  219.42
```

El error se mide en semihorquillas: 1 quiere decir que el medio está a una semihorquilla del valor, el límite a partir
del cual se podría comprar o vender con beneficio. Las congeladas plantadas con bid positivo tienen un error mediano de
10,7 semihorquillas, 34 en el percentil 90: un backtest que las use para entrar o para valorar «gana» o «pierde» un
precio que no existía. Las no plantadas no pasan de una semihorquilla (el medio de una cotización de 0 y 0,05 está a
media horquilla de un valor casi nulo), y las plantadas con bid 0 tampoco, salvo alguna: no hace falta limpiarlas para
no equivocarse. La regla útil no es «quitar todo lo que no cambia»,
sino «no ejecutar contra una cotización que no ha cambiado cuando el modelo dice que debería haberlo hecho».

La tercera regla es la **paridad put-call** (lección 3.4.1): para una call y una put europeas del mismo strike y
vencimiento, $C - P = e^{-rT}(F - K)$. En unos datos reales no se conoce el forward, pero la propia cadena lo da: cada
par implica $F_K = K + e^{rT}(C - P)$, y la mediana de los $F_K$ de los siete strikes más cercanos al dinero es una
estimación robusta. Con ella, se marcan los pares que se salen de la paridad por más de la suma de sus semihorquillas:

```python
pc = cad.pivot_table(index=["fecha", "vencimiento", "strike"],
                     columns="tipo", values=["bid", "ask", "medio"])
pc.columns = [f"{a}_{b}" for a, b in pc.columns]
pc = pc.dropna().reset_index()
pc["T"] = (pc["vencimiento"] - pc["fecha"]).dt.days / 365
curva = ind[["tipo_1m", "tipo_3m", "tipo_6m", "tipo_1a"]] / 100
plazos = [1 / 12, 0.25, 0.5, 1.0]
pc["r"] = [np.interp(t, plazos, fila) for t, fila in
           zip(pc["T"], curva.reindex(pc["fecha"]).to_numpy())]
pc["F_K"] = pc["strike"] + np.exp(pc["r"] * pc["T"]) * (
    pc["medio_C"] - pc["medio_P"])
pc["dist"] = (pc["strike"] - S.reindex(pc["fecha"]).to_numpy()).abs()
cerca = pc.groupby(["fecha", "vencimiento"])["dist"].rank(
    method="first") <= 7
F_est = (pc[cerca].groupby(["fecha", "vencimiento"])["F_K"].median()
         .rename("F_est"))
pc = pc.merge(F_est.reset_index(), on=["fecha", "vencimiento"])
err = (pc["medio_C"] - pc["medio_P"]
       - np.exp(-pc["r"] * pc["T"]) * (pc["F_est"] - pc["strike"]))
lim = (pc["ask_C"] - pc["bid_C"] + pc["ask_P"] - pc["bid_P"]) / 2
fuera = pc[err.abs() > lim]
ver = pd.read_parquet("opciones3_verdad.parquet")
cmp_ = F_est.reset_index().merge(ver, on=["fecha", "vencimiento"])
print(f"forward estimado frente al verdadero: error mediano "
      f"{(cmp_.F_est - cmp_.forward).abs().median():.3f} puntos")
print(f"pares fuera de la paridad: {len(fuera)} de {len(pc)}")
inc1 = inc.drop_duplicates(["fecha", "vencimiento", "strike"])
fuera.merge(inc1, on=["fecha", "vencimiento", "strike"],
            how="left")["incidencia"].value_counts(dropna=False)
```
```output
forward estimado frente al verdadero: error mediano 0.021 puntos
pares fuera de la paridad: 346 de 272274
incidencia
cotizacion_congelada    330
paridad_violada           8
cotizacion_cruzada        8
Name: count, dtype: int64
```

El forward estimado se equivoca en 0,02 puntos de mediana con un índice de miles: la paridad de los strikes cercanos lo
fija con mucha precisión. Quedan fuera 346 pares de 272.274, y **todos** tienen una incidencia plantada: 330 por una
pata congelada, las 8 paridades violadas y 8 de las cruzadas. Es la respuesta conocida del README (345 pares con el
forward verdadero, todos con incidencia). En la práctica es la mejor regla de las tres, porque no necesita modelo: una
violación de la paridad mayor que las horquillas en opciones europeas líquidas es, casi siempre, un dato malo y no un
arbitraje.

```mermaid
flowchart TB
  Q[Cotización] --> A{bid mayor que ask}
  A -- sí --> X1[Descartar]
  A -- no --> B{bid nulo}
  B -- sí --> X2[No vender]
  B -- no --> C{Paridad rota}
  C -- sí --> X3[Sospechosa]
  C -- no --> D{Igual que ayer}
  D -- sí --> X4[No ejecutar]
  D -- no --> OK[Usar]
```

## El motor: selección, fills y valoración

El motor de la celda siguiente vende (o compra) una estructura cada mes: el día de un vencimiento mensual abre las
patas en el vencimiento siguiente con al menos 20 días, elige cada strike con la información de ese cierre (delta de
Black-Scholes con la implícita de ese día o el strike ATM), valora cada día al medio, repite la valoración el día sin
cadena y liquida al vencimiento por el valor intrínseco. La caja rinde el tipo a 1 mes. El parámetro `frac` es el
modelo de fills: la fracción de la semihorquilla que se paga al entrar y al salir. `frac = 1` vende al bid y compra
al ask; `frac = 0`, todo al medio; `frac = 0,5`, a medio camino, un supuesto habitual para órdenes limitadas pacientes.
`cierre` permite deshacer la posición antes del vencimiento cuando su valor cae por debajo de una fracción del
crédito cobrado (lección 3.5.5), con un segundo cruce de horquillas.

```python
r1m = ind["tipo_1m"] / 100
q = ind["rentabilidad_dividendo"] / 100
por_fecha = {f: d.set_index(["vencimiento", "tipo", "strike"])
             for f, d in cad.groupby("fecha")}
vencs = sorted(cad["vencimiento"].unique())

def delta_bs(tipo, S0, K, T, iv, r0, q0):
    F = S0 * np.exp((r0 - q0) * T)
    sd = iv * np.sqrt(T)
    d1 = (np.log(F / K) + 0.5 * sd * sd) / sd
    return np.exp(-q0 * T) * (ndtr(d1) - (tipo == "P"))

def elige(sub, t, obj, f, T):
    st = sub.loc[t]
    if obj == "atm":       # put: el strike más alto bajo el índice
        ks = st.index[st.index <= S[f]]
        return float(ks.max())
    dl = delta_bs(t, S[f], st.index.to_numpy(), T,
                  st["iv_modelo"].to_numpy(), r1m[f], q[f])
    return float(st.index[np.argmin(np.abs(np.abs(dl) - obj))])

def backtest(patas, frac=1.0, garantia="indice", cierre=None):
    """patas: (tipo, delta objetivo o "atm", signo: -1 vende)."""
    caja, pos, venc, eq, ultimo = 1.0, [], None, {}, 1.0
    credito, coste = 0.0, 0.0
    for f in S.index:
        g = por_fecha.get(f)              # None el día sin cadena
        if venc is not None and f == venc:          # liquidación
            caja += sum(n * sg * max(k - S[f] if t == "P"
                                     else S[f] - k, 0.0)
                        for t, k, sg, n in pos)
            pos, venc = [], None
        if venc is None and g is not None:     # abre la siguiente
            v = next(x for x in vencs if (x - f).days >= 20)
            T = (v - f).days / 365
            sel = [(t, elige(g.loc[v], t, obj, f, T), sg)
                   for t, obj, sg in patas]
            n = caja / (sel[0][1] if garantia == "strike" else S[f])
            for t, k, sg in sel:
                fila = g.loc[(v, t, k)]
                caja -= sg * n * (fila["medio"] + sg * frac
                                  * fila["semi"])
                coste += n * frac * fila["semi"]
            credito = -sum(sg * g.loc[(v, t, k)]["medio"]
                           for t, k, sg in sel)
            pos, venc = [(t, k, sg, n) for t, k, sg in sel], v
        marca, valida = 0.0, g is not None
        if pos and valida:
            filas = [g.loc[(venc, t, k)] for t, k, sg, n in pos]
            marca = sum(sg * n * fl["medio"]
                        for (t, k, sg, n), fl in zip(pos, filas))
            valor = -sum(sg * fl["medio"]
                         for (t, k, sg, n), fl in zip(pos, filas))
            if cierre is not None and valor <= cierre * credito:
                for (t, k, sg, n), fl in zip(pos, filas):
                    caja += sg * n * (fl["medio"] - sg * frac
                                      * fl["semi"])
                    coste += n * frac * fl["semi"]
                pos, marca = [], 0.0
        eq[f] = ultimo if (pos and not valida) else caja + marca
        ultimo = eq[f]
        caja *= np.exp(r1m[f] / 252)
    return pd.Series(eq), coste

def metricas(eq):
    rr = eq.pct_change().dropna()
    ex = rr - r1m.reindex(rr.index) / 252
    mes = eq.resample("ME").last().pct_change().dropna()
    anos = (eq.index[-1] - eq.index[0]).days / 365.25
    return {"sharpe": ex.mean() / ex.std() * np.sqrt(252),
            "anual": (eq.iloc[-1] / eq.iloc[0]) ** (1 / anos) - 1,
            "caida_max": (eq / eq.cummax() - 1).min(),
            "peor_mes": mes.min()}
```

La primera prueba es la respuesta conocida del README: el put-write ATM con garantía en efectivo (nominal = strike,
como el índice PUT de Cboe) da un Sharpe de 0,52 y un 6,8 % anual al bid, y 0,53 y 6,9 % al medio.

```python
put = [("P", "atm", -1)]
res = {}
for frac in (1.0, 0.0):
    eq, cst = backtest(put, frac, garantia="strike")
    res[f"put-write, frac {frac:g}"] = metricas(eq) | {"horquilla": cst}
pd.DataFrame(res).T.round(3)
```
```output
                   sharpe  anual  caida_max  peor_mes  horquilla
put-write, frac 1   0.520  0.068     -0.395    -0.232       0.02
put-write, frac 0   0.533  0.069     -0.394    -0.232       0.00
```

Coincide con el README. En una put ATM a un mes, la horquilla es pequeña frente a la prima, y vender al bid en lugar
de al medio cuesta una décima de punto de rentabilidad al año. Aquí el supuesto de ejecución apenas importa. En una
estructura de cuatro patas fuera del dinero, sí: un iron condor vende la put y la call de delta 0,16 y compra la put y
la call de delta 0,05 como alas (lección 3.5.2), con nominal = índice; y su versión con cierre anticipado lo deshace
cuando ha ganado la mitad del crédito.

```python
condor = [("P", 0.16, -1), ("C", 0.16, -1),
          ("P", 0.05, 1), ("C", 0.05, 1)]
for frac in (1.0, 0.5, 0.0):
    eq, cst = backtest(condor, frac)
    res[f"condor, frac {frac:g}"] = metricas(eq) | {"horquilla": cst}
for frac in (1.0, 0.0):
    eq, cst = backtest(condor, frac, cierre=0.5)
    res[f"condor 50 %, frac {frac:g}"] = (metricas(eq)
                                          | {"horquilla": cst})
tabla = pd.DataFrame(res).T
tabla.round(3)
```
```output
                     sharpe  anual  caida_max  peor_mes  horquilla
put-write, frac 1     0.520  0.068     -0.395    -0.232      0.020
put-write, frac 0     0.533  0.069     -0.394    -0.232      0.000
condor, frac 1        0.053  0.020     -0.173    -0.055      0.026
condor, frac 0.5      0.079  0.021     -0.171    -0.055      0.013
condor, frac 0        0.104  0.022     -0.170    -0.055      0.000
condor 50 %, frac 1   0.266  0.026     -0.091    -0.055      0.039
condor 50 %, frac 0   0.364  0.029     -0.088    -0.055      0.000
```

La columna `horquilla` suma lo pagado en semihorquillas por unidad de nominal en toda la muestra. En el condor
mantenido al vencimiento, ejecutar al bid/ask en lugar de al medio baja el Sharpe de 0,10 a 0,05; con cierre
anticipado, que cruza las horquillas dos veces, de 0,36 a 0,27. La rentabilidad anual cambia poco en puntos (unas
décimas), pero la rentabilidad en exceso del condor es pequeña, y la horquilla se lleva una parte grande de ella. Un
backtest de una estructura así ejecutado al medio no dice casi nada; uno al bid/ask es pesimista para quien trabaja
con órdenes limitadas; la columna `frac 0,5` es el supuesto honesto si no hay datos de ejecuciones propias.

*Fig. — Capital del put-write ATM al bid y del iron condor con cierre al 50 % del crédito al medio y al bid/ask, fin de mes, 2014-2025 (datos sintéticos; incluye el tipo a 1 mes de la caja).* Tres líneas que empiezan en 1. El put-write termina en 2,19 con caídas fuertes en 2018, 2020 y 2022. Los dos condors suben despacio y casi en paralelo: al medio termina en 1,41 y al bid/ask, destacado, en 1,36; la distancia entre los dos crece con cada operación.

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

El condor con cierre anticipado tiene el mejor Sharpe de la tabla y una caída máxima pequeña en esta muestra. No es una
renta: vende la cola en las dos direcciones, su peor mes está en la tabla y sus alas de delta 0,05 limitan la pérdida
por operación, no la frecuencia de las pérdidas. Las cifras son de datos sintéticos con una prima de riesgo de
varianza plantada; no son evidencia de que la estructura gane en el S&P 500 real (lección 3.6.1).

</aside>

## Eventos: dividendos, resultados y ejercicio anticipado

Las opciones sobre un índice europeo tienen pocos eventos: los dividendos del índice están en el forward (por eso la
lección usa $F$ y no $S$ para la moneyness), y nada se ejerce antes de tiempo. Las opciones sobre acciones tienen
tres. La acción ficticia KRN de los datos (`accion3_diario.csv`, `accion3_eventos.csv`, `accion3_opciones.parquet`)
imita a una gran industrial de EE. UU. con dividendo trimestral, resultados cuatro veces al año y opciones americanas.

El primero es el **precio de referencia**. Los proveedores de series diarias suelen dar un cierre ajustado por
dividendos (y por splits): reescriben el pasado para que los rendimientos sean continuos. Los contratos de opciones,
en cambio, se listaron con los precios de entonces. Para la moneyness, la selección de strikes y el valor intrínseco,
el precio es siempre el **sin ajustar**:

```python
krn = pd.read_csv("accion3_diario.csv", parse_dates=["fecha"])
krn = krn.set_index("fecha")
div = krn["dividendo_ex"]
# ajuste hacia atrás, el de los proveedores: factor 1 - D / cierre
# del día anterior a la fecha ex, acumulado hacia el pasado
factor = (1 - div / krn["cierre"].shift()).fillna(1)
acum = factor[::-1].cumprod()[::-1].shift(-1).fillna(1)
ajustado = krn["cierre"] * acum
d0 = krn.index[0]
print(f"{d0.date()}: sin ajustar {krn['cierre'][d0]:.2f}, "
      f"ajustado {ajustado[d0]:.2f} "
      f"({ajustado[d0] / krn['cierre'][d0] - 1:.1%})")
print(f"escalones de 2,5 USD de diferencia: "
      f"{(krn['cierre'][d0] - ajustado[d0]) / 2.5:.1f}")
```
```output
2022-01-03: sin ajustar 117.52, ajustado 105.62 (-10.1%)
escalones de 2,5 USD de diferencia: 4.8
```

Con los 16 dividendos posteriores, el cierre ajustado del 3 de enero de 2022 queda un 10,1 % por debajo del real
(105,62 frente a 117,52 USD): casi cinco escalones de strike. Un backtest que elija el strike «ATM» con la serie ajustada compra opciones dentro o fuera del
dinero sin saberlo, y calcula mal su valor intrínseco al vencimiento. Los splits son peores (el ajuste cambia el precio
a la mitad o a un tercio), y en ellos la cámara ajusta también los contratos: cambian el número de acciones que
entrega cada contrato o el strike, y el contrato ajustado pasa a tener otro símbolo. Los dividendos ordinarios, en
cambio, no ajustan los contratos: los descuenta su precio[^1].

El segundo evento son los **resultados**. Antes de un anuncio, la implícita del vencimiento más cercano incluye la
varianza del salto esperado; al día siguiente, el salto ya ha pasado y la implícita cae. En KRN:

```python
ev = pd.read_csv("accion3_eventos.csv", parse_dates=["fecha"])
ops = pd.read_parquet("accion3_opciones.parquet")
res_ = ev[ev["evento"] == "resultados"]
dias = krn.index

def iv_atm(d):
    """Call ATM del primer vencimiento con el cierre del día."""
    sub = ops[(ops["fecha"] == d) & (ops["tipo"] == "C")]
    sub = sub[sub["vencimiento"] == sub["vencimiento"].min()]
    fila = sub.loc[(sub["strike"] - krn["cierre"][d]).abs().idxmin()]
    return 100 * fila["iv_modelo"]

filas = []
for _, e in res_.iterrows():
    j = dias.get_loc(e["fecha"])
    if j + 1 >= len(dias):
        continue                  # anuncio en la última sesión
    filas.append((e["fecha"].date(), iv_atm(dias[j]),
                  iv_atm(dias[j + 1]), 100 * e["sigma_evento"],
                  100 * e["movimiento_real"]))
crush = pd.DataFrame(filas, columns=["anuncio", "iv_antes",
                                     "iv_despues", "sigma_evento",
                                     "movimiento"])
print(crush[["iv_antes", "iv_despues", "sigma_evento"]].mean()
      .round(1).to_dict())
print(f"|movimiento| > sigma del evento: "
      f"{(crush.movimiento.abs() > crush.sigma_evento).mean():.0%}")
crush.round(1).head(4)
```
```output
{'iv_antes': 33.2, 'iv_despues': 24.6, 'sigma_evento': 5.7}
|movimiento| > sigma del evento: 31%
      anuncio  iv_antes  iv_despues  sigma_evento  movimiento
0  2022-01-25      21.5        16.9           3.6        -3.3
1  2022-04-26      28.8        14.8           6.4        -4.2
2  2022-07-26      34.4        20.2           7.1        10.0
3  2022-10-25      23.8        15.3           4.9         4.1
```

*Fig. — Implícita de la call ATM del primer vencimiento de KRN alrededor de sus 16 presentaciones de resultados, media por sesión, 2022-2025 (datos sintéticos).* Línea con once puntos, La implícita está en torno al 26,5 % cinco sesiones antes, sube a 31,8 % dos sesiones antes, cuando el primer vencimiento pasa a ser uno que incluye el anuncio, llega a 33,2 % el día del anuncio y cae a 24,6 % el día siguiente, destacado; después se queda en torno al 24,6-25,0 %,

La implícita ATM cae de media del 33,2 % al 24,6 % del cierre del anuncio al siguiente, con una desviación típica
plantada del movimiento del 5,7 %. Es la respuesta conocida del README. La lección para un backtest no es que venderla
gane: es que la implícita alrededor de un evento conocido no se puede comparar con la de otros días. Una regla del tipo
«vender cuando la implícita está en su percentil 90» venderá sistemáticamente antes de los resultados, y su resultado
dependerá de los días del salto, que aquí superan su desviación típica plantada en el 31 % de los anuncios. Los resultados
se evalúan evento a evento, con su peor caso.

El tercero es el **ejercicio anticipado**. Una call americana sobre una acción con dividendo puede valer más viva
que ejercida, salvo la víspera de la fecha ex, cuando ejercerla captura el dividendo; una put americana muy dentro del
dinero se ejerce antes con tipos altos, porque cobrar el strike hoy rinde intereses (lección 3.4.2). En los datos,
`precio_modelo` es la americana (árbol binomial con dividendos discretos) y `europea_modelo` la misma opción europea:

```python
ops["prima_ej"] = ops["precio_modelo"] - ops["europea_modelo"]
print(f"americana > europea + 0,01: "
      f"{(ops['prima_ej'] > 0.01).mean():.0%} de las filas; "
      f"máximo {ops['prima_ej'].max():.2f} USD")
ex_div = div[div > 0].index
ops["vispera_ex"] = ops["fecha"].isin(
    [dias[dias.get_loc(d) - 1] for d in ex_div])
s_ = krn["cierre"].reindex(ops["fecha"]).to_numpy()
ops["intrinseco"] = np.where(ops["tipo"] == "C",
                             np.maximum(s_ - ops["strike"], 0),
                             np.maximum(ops["strike"] - s_, 0))
# ejercer es óptimo si la americana no vale más que su intrínseco
ops["ejercer"] = ops["precio_modelo"] - ops["intrinseco"] < 0.005
itm = ops[ops["intrinseco"] > 0]
itm.groupby(["tipo", "vispera_ex"])["ejercer"].agg(
    ["count", "mean"]).round(3)
```
```output
americana > europea + 0,01: 50% de las filas; máximo 2.35 USD
                 count   mean
tipo vispera_ex
C    False       31423  0.005
     True          494  0.071
P    False       34301  0.215
     True          530  0.000
```

La prima por ejercicio anticipado existe en la mitad de las filas y llega a 2,35 USD por acción. La tabla mide otra
cosa: en qué fracción de las opciones dentro del dinero ejercer ya es lo óptimo (la americana vale su intrínseco). En
las calls, un 0,5 % de los días normales y un 7,1 % la víspera de una fecha ex, catorce veces más: es cuando su
titular la ejerce para cobrar el dividendo, y quien la vendió recibe la asignación y debe entregar las acciones (y
el dividendo). En las puts es al revés: ejercer es óptimo en el 21,5 % de los días normales, por los tipos del 4-5 %
(cobrar hoy el strike rinde intereses), y nunca la víspera de una fecha ex, porque la caída del precio por el
dividendo al día siguiente vale más que esperar un día. Un backtest que trate estas opciones como europeas y las lleve hasta el
vencimiento sobrevalora las ventas de calls cubiertas antes de los dividendos y pierde el riesgo de asignación, que
cambia la posición de un día para otro.

## Lista de comprobación de un backtest de opciones

- **Datos.** Foto a la misma hora para opciones y subyacente (15:45 o cierre, no mezcladas). Strikes por precio sin
  ajustar. Días sin cadena y medias sesiones identificados.
- **Limpieza.** Cruzadas fuera; bids nulos no se venden; paridad con el forward de la propia cadena; cotizaciones
  sin cambio en días de movimiento, sin ejecutar contra ellas.
- **Selección.** Strikes y deltas con información del cierre de la decisión; implícitas calculadas desde los precios
  de ese día, no una columna del futuro.
- **Ejecución.** Al menos dos supuestos de fill (medio y bid/ask) y, si se puede, uno intermedio; comisiones por
  contrato; el cierre anticipado vuelve a cruzar la horquilla.
- **Eventos.** Dividendos y resultados en el calendario; ejercicio anticipado y asignación en opciones americanas;
  ajustes de contratos en splits y operaciones societarias.
- **Resultados.** Peor mes, caída máxima y su fecha junto al Sharpe; una estrategia de venta de prima sin su peor
  episodio no está evaluada (lección 3.6.1).

## Ejercicios

1. Mejora la regla de las congeladas: además de «igual que ayer en un día de movimiento», exige que el modelo diga que
   la opción debería haberse movido más de una semihorquilla (usa `error_semis` como sustituto). ¿Qué precisión y qué
   recall obtienes?

**Solución.**

```python
marc = cg[cg["error_semis"] > 1]
acierto = marc["incidencia"].notna().sum()
print(f"marcadas {len(marc)}, aciertos {acierto}, "
      f"precisión {acierto / len(marc):.1%}, "
      f"recall {acierto / len(verdad):.1%}")
```
```output
marcadas 1049, aciertos 1037, precisión 98.9%, recall 69.1%
```

La precisión sube mucho: casi todo lo que la regla marca es una congelada plantada. El recall baja, pero lo que se
escapa son congeladas cuyo precio está a menos de una semihorquilla del valor: ejecutarse contra ellas no da ni quita
nada que la horquilla no cubra. En datos reales, el valor del modelo se sustituye por la implícita interpolada de los
strikes vecinos en la sonrisa de ese día (lección 3.4.5).

2. Calcula, para el iron condor al bid/ask, qué parte de la rentabilidad en exceso del colateral se lleva la horquilla
   frente al mismo condor al medio.

**Solución.**

```python
caja_media = np.exp(r1m.mean()) - 1   # la caja crece e^(r/252) al día
exc_bid = tabla.loc["condor, frac 1", "anual"] - caja_media
exc_med = tabla.loc["condor, frac 0", "anual"] - caja_media
print(f"tipo a 1 mes medio compuesto: {caja_media:.2%}")
print(f"exceso al medio {exc_med:.2%}, al bid/ask {exc_bid:.2%}: "
      f"la horquilla se lleva el {1 - exc_bid / exc_med:.0%}")
```
```output
tipo a 1 mes medio compuesto: 1.82%
exceso al medio 0.35%, al bid/ask 0.13%: la horquilla se lleva el 62%
```

Sobre la rentabilidad total, la diferencia entre fills parece pequeña; sobre la rentabilidad en exceso del colateral,
que es lo que gana la estrategia, la horquilla se lleva una parte grande. Es la forma correcta de presentar el coste de
ejecución de una estrategia de opciones: frente a lo que gana, no frente al capital.

3. Un backtest de venta de straddles semanales sobre un índice real, con datos de fin de día, da un Sharpe de 3. Enumera
   las comprobaciones de esta lección que harías antes de creerlo.

**Solución.**

Si los fills son al medio o al último precio (repetir con bid/ask y con frac 0,5; en opciones semanales la horquilla
pesa mucho más frente a la prima). Si la foto de las opciones y la del subyacente son de la misma hora (una implícita
calculada con un cierre de opciones a las 16:15 y uno del índice a las 16:00 está sesgada). Si hay cotizaciones
congeladas o cruzadas en las patas vendidas y si la estrategia vende opciones con bid nulo. Si la selección de strikes
usa datos del día siguiente (implícitas recalculadas a posteriori). Qué hace el día sin datos y el de media sesión.
Cómo liquida: a la apertura o al cierre, con qué precio de referencia. Y su peor semana: la venta de straddles
semanales tiene su cola en los saltos de una sesión, y un Sharpe de 3 sobre doce años sin una semana catastrófica es
más probablemente un error de datos que una ventaja.

[^1]: Lo que hace la cámara se resume en la notificación de la SEC sobre la propuesta de la OCC SR-OCC-2006-01
    (Federal Register 71 FR 67415, 21 de noviembre de 2006): «as a general rule, outstanding options will not be
    adjusted to compensate for ordinary cash dividends», mientras que los dividendos extraordinarios, los splits y las
    escisiones sí se ajustan:
    <https://www.govinfo.gov/content/pkg/FR-2006-11-21/html/E6-19619.htm>. Las reglas concretas vigentes están en los
    estatutos de la OCC (artículo VI, sección 11A).

## Autoevaluación

1. Una regla marca como congelada toda cotización igual a la del día anterior en un día en que el índice se movió más de un 1 %. Encuentra las 1.500 plantadas y otras 9.836. ¿De dónde salen las demás?

   - A. De errores del generador de datos
   - B. Sobre todo de opciones muy fuera del dinero con bid 0 y ask en el tick mínimo: su cotización no cambia porque no vale casi nada, no porque esté congelada
   - C. De la sesión del 2019-07-23
   - D. De las medias sesiones

   *Respuesta B: Sobre todo de opciones muy fuera del dinero con bid 0 y ask en el tick mínimo: su cotización no cambia porque no vale casi nada, no porque esté congelada.* Una opción con bid 0 y ask de 0,05 puede seguir así aunque el índice se mueva un 1 %. Exigir bid mayor que cero sube la precisión del 13 % al 56 %, a cambio de perder un 27 % de las congeladas reales. Las congeladas que se escapan son las de menos valor.

2. Con el forward estimado desde la propia cadena, 346 pares put-call quedan fuera de la paridad por más de su semihorquilla. ¿Qué son?

   - A. Oportunidades de arbitraje
   - B. Defectos de los datos: los 346 coinciden con una incidencia plantada (330 congeladas, 8 paridades violadas y 8 cruzadas)
   - C. Errores del cálculo del forward
   - D. El efecto del dividendo

   *Respuesta B: Defectos de los datos: los 346 coinciden con una incidencia plantada (330 congeladas, 8 paridades violadas y 8 cruzadas).* En unos datos limpios, la paridad se cumple dentro de las horquillas; lo que queda fuera es casi siempre una cotización mala. Un backtest que «compre la conversión» en esos pares estaría operando contra un dato roto.

3. Un put-write ATM mensual tiene un Sharpe de 0,53 al medio y de 0,52 al bid. Un iron condor de 16 y 5 delta, de 0,10 y 0,05. ¿Por qué tanta diferencia?

   - A. Porque el condor tiene más riesgo de cola
   - B. Porque el condor cruza cuatro horquillas, tres de ellas en opciones fuera del dinero, donde la horquilla es varias veces mayor en proporción a la prima, y cobra una prima neta pequeña
   - C. Porque el put-write usa garantía en efectivo
   - D. Por azar

   *Respuesta B: Porque el condor cruza cuatro horquillas, tres de ellas en opciones fuera del dinero, donde la horquilla es varias veces mayor en proporción a la prima, y cobra una prima neta pequeña.* El coste de ejecución se mide frente a la prima neta, no frente al nominal. La put ATM tiene una horquilla del 1,2 % de su precio; las patas del condor, de varias veces eso, y la prima neta es pequeña.

4. Para elegir el strike ATM de una opción sobre acciones de 2022 se usa el cierre ajustado por dividendos de un proveedor descargado en 2025. ¿Qué falla?

   - A. Nada, el ajuste no afecta a las opciones
   - B. El cierre ajustado de 2022 está por debajo del precio real de ese día (en KRN, un 10,1 % el 3 de enero de 2022); los strikes se refieren al precio sin ajustar, así que el strike «ATM» queda varios escalones dentro o fuera del dinero
   - C. Que los dividendos se pagan en otra fecha
   - D. Que las opciones americanas no tienen strike

   *Respuesta B: El cierre ajustado de 2022 está por debajo del precio real de ese día (en KRN, un 10,1 % el 3 de enero de 2022); los strikes se refieren al precio sin ajustar, así que el strike «ATM» queda varios escalones dentro o fuera del dinero.* El ajuste por dividendos reescribe el pasado para que los rendimientos sean continuos; los contratos de opciones se listaron con los precios de entonces. Para la moneyness y el valor intrínseco se usa siempre el precio sin ajustar.

5. En la acción KRN, la implícita ATM del primer vencimiento cae de media del 33,2 % al 24,6 % del día del anuncio de resultados al siguiente. ¿Qué implica para un backtest?

   - A. Que conviene comprar opciones antes de los resultados
   - B. Que la implícita alrededor de un evento conocido no es comparable con la de otros días: un backtest que venda volatilidad «cara» antes de los resultados tiene que contar con el movimiento del día siguiente, que en KRN tiene una desviación típica plantada media del 5,7 %
   - C. Que los datos están mal
   - D. Que la vega es cero tras el anuncio

   *Respuesta B: Que la implícita alrededor de un evento conocido no es comparable con la de otros días: un backtest que venda volatilidad «cara» antes de los resultados tiene que contar con el movimiento del día siguiente, que en KRN tiene una desviación típica plantada media del 5,7 %.* La implícita antes del anuncio incluye la varianza del salto esperado. Venderla puede ganar de media la caída de la implícita, pero pierde los días en que el movimiento supera lo descontado: hay que medirlo evento a evento, con la cola.

## Ideas clave

- OPRA es el procesador que consolida cotizaciones y operaciones de todas las bolsas de opciones de EE. UU.; los resúmenes de fin de día y las cadenas históricas se derivan de él. Los contratos se identifican con símbolos OSI de 21 caracteres.
- Limpiar una cadena es medir: en los datos del curso, las 12 cotizaciones cruzadas se detectan todas; las congeladas, con una precisión que depende de excluir las opciones sin valor; y la paridad con un forward estimado de la propia cadena señala 346 pares, todos defectuosos.
- El fill decide el resultado cuando la prima neta es pequeña: al bid/ask, el Sharpe del iron condor cae de 0,10 a 0,05 y el del condor con cierre anticipado, de 0,36 a 0,27, mientras que el put-write apenas cambia.
- Días sin cadena, medias sesiones, bids nulos y strikes que aparecen y desaparecen exigen reglas explícitas: repetir la valoración, no operar, no vender a cero.
- En acciones, los strikes se comparan con precios sin ajustar; los resultados inflan la implícita antes del anuncio y los dividendos adelantan el ejercicio de calls americanas.

## Recursos

### Documentación

- [OPRA: Options Price Reporting Authority (procesador de información de las bolsas de opciones de EE. UU.)](https://www.opraplan.com/) · opraplan.com
- [Databento: conjunto OPRA.PILLAR (esquemas y simbología OSI)](https://databento.com/docs/venues-and-datasets/opra-pillar) · databento.com
- [Databento: timeseries.get_range (API histórica)](https://databento.com/docs/api-reference-historical/timeseries/timeseries-get-range) · databento.com
- [Cboe DataShop: Option EOD Summary (fotos de las 15:45 ET y del cierre)](https://datashop.cboe.com/option-eod-summary) · datashop.cboe.com

### Datos

- [Datos del nivel 3: incidencias plantadas en la cadena de ORX (CSV)](https://pythonparatrading.com/curso/datos/nivel-3/opciones3_incidencias.csv) · Descarga · opciones3_incidencias.csv
- [Datos del nivel 3: opciones americanas de la acción KRN, 2022-2025 (Parquet)](https://pythonparatrading.com/curso/datos/nivel-3/accion3_opciones.parquet) · Descarga · accion3_opciones.parquet

Lección anterior: [3.6.3 · Estrategias con VIX y VSTOXX: futuros y ETP de volatilidad](https://pythonparatrading.com/curso-python-trading/nivel-3/estrategias-con-vix-y-vstoxx/)

Lección siguiente: [3.7.1 · Perpetuos: mecánica del funding, mark price y liquidaciones](https://pythonparatrading.com/curso-python-trading/nivel-3/perpetuos-funding-mark-price-y-liquidaciones/)
