---
title: "7.6.3 · Capstone C2: libro de opciones y griegas"
description: "Venta sistemática de prima con alas de cola sobre la cadena de ORX: superficie SVI, tamaño por el peor escenario, griegas diarias, kill switch y DSR."
url: "https://pythonparatrading.com/curso-python-trading/nivel-7/capstone-c2-libro-de-opciones-y-griegas/"
lang: "es"
image: "https://pythonparatrading.com/og/curso-python-trading/nivel-7/capstone-c2-libro-de-opciones-y-griegas.png"
---

# 7.6.3 · Capstone C2: libro de opciones sistemático, superficie de volatilidad y griegas en tiempo real

> Un libro que vende prima cada mes y compra su propia cobertura de cola, con la superficie de volatilidad ajustada a las cotizaciones, el tamaño fijado por el peor escenario y las griegas vigiladas cada día. Esta guía es el enunciado del proyecto C2, su arquitectura, un esqueleto que funciona de punta a punta con los datos del curso y la forma de validarlo, operarlo en papel y defenderlo.

- Curso: [Nivel 7: Producción, MLOps, regulación, carrera y capstone](https://pythonparatrading.com/curso-python-trading/nivel-7/)
- Módulo: 7.6 · Capstone final
- Duración: 70 min
- Requisitos: [7.6.1 · Capstone: requisitos comunes, rúbrica del tribunal y preparación de la defensa](https://pythonparatrading.com/curso-python-trading/nivel-7/capstone-requisitos-rubrica-y-defensa/), [3.4.4 · Volatilidad implícita: Newton-Raphson, Brent, py_vollib y QuantLib](https://pythonparatrading.com/curso-python-trading/nivel-3/volatilidad-implicita-newton-brent-py-vollib-y-quantlib/), [3.4.5 · Sonrisa, skew y estructura temporal; superficie de volatilidad con SVI](https://pythonparatrading.com/curso-python-trading/nivel-3/sonrisa-skew-y-superficie-de-volatilidad-svi/), [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/), [3.6.4 · Backtesting de opciones: datos, fills y ajustes por eventos](https://pythonparatrading.com/curso-python-trading/nivel-3/backtesting-de-opciones/), [4.2.6 · Riesgo de derivados: griegas de cartera, margen y liquidaciones](https://pythonparatrading.com/curso-python-trading/nivel-4/riesgo-de-derivados-griegas-margen-y-liquidaciones/), [2.5.4 · Deflated Sharpe Ratio y Probability of Backtest Overfitting (PBO)](https://pythonparatrading.com/curso-python-trading/nivel-2/deflated-sharpe-ratio-y-pbo/)

## Al terminar esta lección

- Delimitar el encargo de un libro de opciones sistemático: qué se vende, qué cobertura se compra, con qué límites y qué queda fuera del proyecto.
- Montar la columna vertebral del sistema con los datos del curso: volatilidad implícita de las cotizaciones, superficie SVI, selección por delta, tamaño por el peor escenario, griegas diarias y kill switch.
- Validar la elección de la estructura con un registro de pruebas, un walk-forward de la elección y el Deflated Sharpe, y escribir antes el criterio de parada.
- Planificar cuatro semanas de paper trading con XSP en IBKR que vean varias entradas y liquidaciones, con conciliación diaria y monitorización de griegas.
- Aplicar la rúbrica común de 7.6.1 a C2 y reconocer los errores que suspenden este proyecto.

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

Vender opciones expone a pérdidas grandes y rápidas; un ala comprada las limita, pero no evita perder la pérdida
máxima de un ciclo varias veces seguidas. Esta guía describe un proyecto de formación con datos sintéticos y una
cuenta de papel. No es una recomendación de inversión ni de producto.

</aside>

## El encargo

Una gestora pequeña quiere un libro sistemático de opciones sobre el índice que cobre la prima de riesgo de
volatilidad con el riesgo de cola acotado por construcción. El encargo, tal como lo recibirías:

- **Qué se construye.** Un sistema que cada mes vende una put del índice elegida por delta y compra en el mismo
  vencimiento una put más lejana (el ala) como cobertura de cola; ajusta la superficie de volatilidad a las
  cotizaciones, fija el número de contratos por la peor pérdida posible del ciclo, valora el libro y sus griegas cada
  día, aplica límites y un kill switch, envía las órdenes como combinación a una cuenta de papel y concilia cada día con
  el extracto del broker.
- **Para quién.** Un comité de riesgos que no va a leer el código: quiere un informe con la pérdida máxima por ciclo,
  las griegas en USD, los días fuera de límite, los cierres del kill switch y la evidencia estadística, con el número
  de pruebas que se hicieron.
- **Qué no entra.** Calls vendidas (la lección 3.6.1 midió que la pata de calls pierde en una muestra alcista y es una
  apuesta distinta), cobertura delta continua, opciones sobre acciones con ejercicio anticipado, opciones de 0 días y
  cualquier dinero real. Tampoco una mesa de volatilidad: el libro compra y vende siempre la misma estructura, con
  parámetros fijados antes de mirar el resultado.

Entregables, además de los comunes de 7.6.1: el paquete con el motor de valoración, la superficie, la selección, el
tamaño, el libro diario y el ejecutor; `informe_c2.json` regenerado con un comando; el registro de pruebas completo;
el diario de las cuatro semanas de papel con la conciliación diaria; y la memoria técnica.

El proyecto aplica lo que ya se enseñó; la guía no lo repite y remite por código. Volatilidad implícita (3.4.4),
superficie SVI (3.4.5), griegas (3.4.3), venta sistemática de prima y su cola (3.6.1), backtesting de opciones y
limpieza de la cadena (3.6.4), gestión de posiciones y pin risk (3.5.5), riesgo de derivados en un libro (4.2.6),
Interactive Brokers con `ib_async` (4.4.2), sistema en vivo (4.5.x) y monitorización y desactivación (7.2.3, 7.3.3).

## Arquitectura

El sistema es un ciclo: la cadena entra, pasa un control de calidad, se convierte en superficie, la superficie decide
los strikes y el tamaño, los límites aprueban o recortan, el ejecutor envía una orden combinada y la conciliación
cierra el día. El monitor vigila el libro entre ciclos y puede cerrar todo.

```mermaid
flowchart TB
  D[Cadena OPRA] --> Q[Control de calidad]
  Q --> V[Superficie SVI]
  V --> S[Strikes por delta]
  S --> T[Tamaño por estrés]
  T --> R[Límites de griegas]
  R --> E[Orden combo XSP]
  E --> C[Conciliación]
  C --> M[Kill switch]
  M --> R
```

| Pieza | Qué hace | Librería | Dónde se aprendió |
|---|---|---|---|
| Control de calidad | descarta cruzadas, congeladas, bids nulos y violaciones de paridad | pandas | 3.6.4 |
| Volatilidad implícita | de los precios medios, contrastada con vollib y QuantLib | numpy, vollib, QuantLib | 3.4.4 |
| Superficie | SVI por vencimiento, sin arbitraje de mariposa | scipy | 3.4.5 |
| Griegas | BSM vectorizado en USD por 1 % y por punto | numpy | 3.4.3, 4.2.6 |
| Tamaño | peor pérdida en una rejilla de caídas y en escenarios de estrés | numpy | 3.5.4, 3.6.1 |
| Ejecución | orden combinada al broker de papel | ib_async | 4.4.2, 4.5.2 |
| Conciliación | posiciones, ejecuciones y efectivo contra el extracto | pandas, SQLite | 4.5.5 |
| Monitor | límites diarios, kill switch, alertas | Prometheus o logs estructurados | 7.2.3, 7.3.3 |

Dos decisiones de diseño que el tribunal preguntará. La primera: el ala es del **mismo vencimiento**, así que la
pérdida máxima del ciclo es conocida al abrir (la anchura menos el crédito) y no depende de cómo se mueva la
volatilidad. Una cobertura de cola de otro vencimiento (puts a tres meses, futuros o calls sobre el VIX) protege mejor
en una crisis larga, pero su resultado depende de la estructura temporal; es una extensión, no el núcleo. La segunda:
la volatilidad con la que se eligen los strikes y se calculan las griegas sale de las cotizaciones, no de un modelo ni
de un proveedor, y el modelo (SVI) solo la suaviza.

## Datos

El proyecto arranca con los datos sintéticos del nivel 3, que imitan las opciones del S&P 500 sobre un índice ficticio,
ORX, de 2014 a 2025 (README de `public/curso/datos/nivel-3/`):

| Archivo | Qué aporta al proyecto |
|---|---|
| `opciones3_cadena.parquet` | cadena diaria de los tres vencimientos mensuales más cercanos: bid, ask, volumen e interés abierto |
| `opciones3_indice.csv` | cierre de ORX, rentabilidad por dividendo y curva de tipos de 1 mes a 2 años |
| `opciones3_verdad.parquet` | la superficie verdadera (SVI por día y vencimiento) y el forward: la respuesta conocida |
| `opciones3_superficie.parquet` | la superficie completa de los miércoles, hasta 2 años, para la cobertura de otro vencimiento |
| `opciones3_incidencias.csv` | defectos plantados (congeladas, cruzadas, paridad violada, un día sin cadena) para probar el control de calidad |
| `vol3_indices.csv`, `vol3_futuros.parquet` | el VORX y sus futuros, para una cobertura con volatilidad |

Las opciones de ORX son europeas, liquidan en efectivo al cierre del día de vencimiento y tienen un multiplicador de
100. El esqueleto usa un multiplicador de 10 para que el tamaño se parezca al de las XSP: según su ficha en Cboe, las
XSP son opciones europeas con liquidación en efectivo sobre un índice que vale la décima parte del S&P 500, con
multiplicador 100 (10 USD por punto del S&P 500), vencimientos estándar el tercer viernes y semanales de lunes a viernes,
y el mismo margen de opciones vendidas sin cubrir que las SPX[^1].

Para el proyecto real, tres fuentes documentadas, sin descargar nada en las celdas:

- **Cboe DataShop** vende históricos de opciones de EE. UU., entre ellos «Option EOD Summary», una foto del final del
  día y otra de las 15:45 ET con cotizaciones, precios y volumen y, opcionalmente, volatilidades implícitas y griegas,
  y «Option Quotes», resúmenes por minuto con el NBBO[^2]. Es de pago por conjunto de datos; la licencia de cada uno
  está en su ficha.
- **Databento** distribuye OPRA (todas las opciones cotizadas de EE. UU.) con la misma API para histórico y tiempo
  real. Su página dice que repercute sin recargo las cuotas mensuales de licencia del tiempo real y que los usuarios
  nuevos reciben 125 USD de crédito gratuito para histórico y suscripciones[^3].
- **Interactive Brokers** da la cadena y las griegas de su modelo a través de la API en la cuenta de papel, con las
  suscripciones de datos de mercado que tenga la cuenta; qué suscripción hace falta se comprueba en la cuenta antes de
  la semana 5 (lección 4.4.2).

Un dato real trae lo que los sintéticos solo imitan: sesiones con la cadena incompleta, strikes que aparecen a mitad de
ciclo, la liquidación de las SPX estándar con precios de apertura frente a la de las XSP con el cierre, y cotizaciones de cierre que no
son negociables. El README de datos del proyecto documenta, para cada fuente, licencia, coste, campos, hora de la foto
y huella del fichero versionado (7.6.1).

## Esqueleto ejecutable

El esqueleto es la columna vertebral mínima que funciona de verdad con los datos del curso: siete celdas que van de la
cadena al informe. La primera monta el motor de valoración (el de las lecciones 3.5.4 y 4.2.6, vectorizado) y una
volatilidad implícita por bisección para cadenas enteras, y la contrasta con vollib y con QuantLib en una put: es el
primer test del repositorio.

```python
import numpy as np
import pandas as pd
from scipy.special import ndtr

AÑO, MULT, CAPITAL = 365.0, 10, 1_000_000  # MULT 10, como las XSP
ind = pd.read_csv("opciones3_indice.csv", parse_dates=["fecha"])
ind = ind.set_index("fecha")
S = ind["cierre"]
q = ind["rentabilidad_dividendo"] / 100
CURVA = ind[["tipo_1m", "tipo_3m", "tipo_6m", "tipo_1a",
             "tipo_2a"]] / 100
PLAZOS = [30, 91, 182, 365, 730]
cad = pd.read_parquet("opciones3_cadena.parquet",
                      columns=["fecha", "vencimiento", "tipo",
                               "strike", "bid", "ask"])
cad["medio"] = (cad["bid"] + cad["ask"]) / 2
dias = dict(tuple(cad.groupby("fecha")))
vencs = sorted(cad["vencimiento"].unique())


def tipo(f, d):
    """Tipo continuo de la curva del día f a d días (tanto por uno)."""
    return float(np.interp(d, PLAZOS, CURVA.loc[f].to_numpy()))


def bsm(S, K, T, v, r, q, w=-1.0):
    """Precio y griegas BSM vectorizados (w = -1 put, +1 call).
    Vega por punto de volatilidad; theta por día natural."""
    S, K, T, v, r, q = np.broadcast_arrays(S, K, T, v, r, q)
    rt = v * np.sqrt(T)
    d1 = (np.log(S / K) + (r - q + v**2 / 2) * T) / rt
    d2 = d1 - rt
    dq, dr = np.exp(-q * T), np.exp(-r * T)
    n1 = np.exp(-d1**2 / 2) / np.sqrt(2 * np.pi)
    return {"precio": w * (S * dq * ndtr(w * d1)
                           - K * dr * ndtr(w * d2)),
            "delta": w * dq * ndtr(w * d1),
            "gamma": dq * n1 / (S * rt),
            "vega": S * dq * n1 * np.sqrt(T) / 100,
            "theta": (-S * dq * n1 * v / (2 * np.sqrt(T))
                      + w * q * S * dq * ndtr(w * d1)
                      - w * r * K * dr * ndtr(w * d2)) / AÑO}


def iv(precio, S, K, T, r, q, w=-1.0):
    """Volatilidad implícita por bisección vectorizada (3.4.4)."""
    lo = np.full(np.shape(precio), 1e-3)
    hi = np.full(np.shape(precio), 4.0)
    for _ in range(60):
        m = (lo + hi) / 2
        alto = bsm(S, K, T, m, r, q, w)["precio"] > precio
        hi, lo = np.where(alto, m, hi), np.where(alto, lo, m)
    return (lo + hi) / 2


# contraste: una put del 2019-12-20 con vollib y QuantLib
import QuantLib as ql
from vollib.black_scholes_merton.implied_volatility import (
    implied_volatility)

f0 = pd.Timestamp("2019-12-20")
g0 = dias[f0].set_index(["vencimiento", "tipo", "strike"])
v0 = pd.Timestamp("2020-01-17")
px, K0 = g0.at[(v0, "P", 2750.0), "medio"], 2750.0
T0 = (v0 - f0).days / AÑO
r0 = tipo(f0, (v0 - f0).days)
mio = float(iv(px, S[f0], K0, T0, r0, q[f0]))
vl = implied_volatility(px, S[f0], K0, T0, r0, q[f0], "p")
F0 = S[f0] * np.exp((r0 - q[f0]) * T0)
sd = ql.blackFormulaImpliedStdDev(ql.Option.Put, K0, F0, px,
                                  np.exp(-r0 * T0), 0.0, 0.3,
                                  1e-12, 200)
print(f"put {K0:.0f}: medio {px}, σ propia {mio:.6f}")
print(f"diferencia con vollib {abs(mio - vl):.1e}, "
      f"con QuantLib {abs(mio - sd / np.sqrt(T0)):.1e}")
```
```output
put 2750: medio 11.15, σ propia 0.143291
diferencia con vollib 3.6e-16, con QuantLib 1.5e-12
```

La volatilidad propia difiere de la de vollib en 3,6·10⁻¹⁶ y de la de QuantLib en 1,5·10⁻¹² (la tolerancia que se le
pasa). El multiplicador de 10 y el capital de un millón de USD son parámetros; el resto de la guía los da
por fijos.

La superficie se ajusta cada día de apertura con SVI (lección 3.4.5) sobre la volatilidad implícita de los medios de
las opciones fuera del dinero con bid. Como los datos traen la superficie verdadera, se puede medir cuánto se equivoca:

```python
from scipy.optimize import least_squares


def svi(k, a, b, rho, m, sig):
    return a + b * (rho * (k - m) + np.sqrt((k - m)**2 + sig**2))


LIM = ([-1, 1e-6, -0.999, -1, 1e-4], [1, 5, 0.999, 1, 2])
_cache = {}


def superficie(f, v):
    """SVI del vencimiento v el día f, ajustado a la volatilidad
    implícita de los medios de las OTM con bid > 0 (3.4.5)."""
    if (f, v) not in _cache:
        g = dias[f]
        g = g[(g["vencimiento"] == v) & (g["bid"] > 0)]
        d = (v - f).days
        T, r = d / AÑO, tipo(f, d)
        F = S[f] * np.exp((r - q[f]) * T)
        otm = g[np.where(g["tipo"] == "P", g["strike"] < F,
                         g["strike"] >= F)]
        w = np.where(otm["tipo"] == "P", -1.0, 1.0)
        K = otm["strike"].to_numpy()
        vol = iv(otm["medio"].to_numpy(), S[f], K, T, r, q[f], w)
        k = np.log(K / F)
        p = least_squares(
            lambda p: np.sqrt(np.maximum(svi(k, *p), 1e-10) / T) - vol,
            [0.5 * (vol**2 * T).min(), 0.1, -0.5, 0.0, 0.1],
            bounds=LIM).x
        _cache[(f, v)] = {"T": T, "r": r, "F": F, "p": p, "K": K,
                          "iv": vol}
    return _cache[(f, v)]


def vol_svi(s, K):
    return np.sqrt(svi(np.log(np.asarray(K) / s["F"]), *s["p"])
                   / s["T"])


ver = pd.read_parquet("opciones3_verdad.parquet")
ver = ver.set_index(["fecha", "vencimiento"])
COLS = ["svi_a", "svi_b", "svi_rho", "svi_m", "svi_sigma"]
filas = []
for f, v in [(f0, v0), (f0, pd.Timestamp("2020-02-21")),
             (pd.Timestamp("2020-03-20"), pd.Timestamp("2020-04-17"))]:
    s, x = superficie(f, v), ver.loc[(f, v)]
    k_v = np.log(s["K"] / x["forward"])
    verdad = np.sqrt(svi(k_v, *x[COLS].to_numpy(float)) / s["T"])
    filas.append({"fecha": f.date(), "días": (v - f).days,
                  "opciones": len(s["K"]),
                  "ATM_%": 100 * float(vol_svi(s, s["F"])),
                  "ruido_max": 100 * np.abs(s["iv"] - verdad).max(),
                  "error_svi_max": 100 * np.abs(
                      vol_svi(s, s["K"]) - verdad).max()})
pd.DataFrame(filas).round(3)
```
```output
        fecha  días  opciones   ATM_%  ruido_max  error_svi_max
0  2019-12-20    28        22  10.402      0.247          0.053
1  2019-12-20    63        26  11.158      0.270          0.064
2  2020-03-20    28        64  62.904      0.660          0.300
```

En calma (diciembre de 2019, volatilidad ATM del 10,4 %), las cotizaciones se separan de la verdad hasta 0,25 puntos
y el SVI ajustado, solo 0,05: el modelo filtra el ruido de la horquilla. En la crisis de marzo de 2020, con la
volatilidad ATM al 62,9 % y 64 opciones con bid, el error del ajuste sube a 0,30 puntos, menos de la mitad del ruido
(0,66). Es la prueba de que la pieza funciona; en datos reales no hay verdad y la prueba es la estabilidad del ajuste
día a día y su error frente a las cotizaciones.

El ciclo se abre el día de cada vencimiento con el siguiente que tenga al menos 20 días. La put vendida es la de delta
más cercana a −0,20 con la volatilidad del SVI; el ala, la de delta −0,05. Se vende al bid y se compra al ask, con una
comisión supuesta de 1 USD por contrato y lado (la del broker real va al fichero de configuración). El tamaño es el
menor de dos: el que deja la peor pérdida del ciclo en el 15 % del capital y el que deja el margen en el 50 %.

```python
COMISION = 1.0              # USD por contrato y lado (supuesto)
PERDIDA_MAX = 0.15          # del capital en el peor escenario
MARGEN_MAX = 0.50           # del capital comprometido en margen
ESTRES = ((0.10, 0.35), (0.20, 0.60), (0.30, 0.80))  # 3.6.1


def margen_put(prima, s, K):
    """Margen de Cboe de una put de índice vendida sin cubrir."""
    return MULT * max(prima + 0.15 * s - max(s - K, 0),
                      prima + 0.10 * K)


def abrir(f, corta, ala, capital):
    """Put de delta -corta vendida al bid y, si hay ala, put de delta
    -ala comprada al ask, del primer vencimiento con >= 20 días.
    Tamaño: el menor de la pérdida máxima y del margen."""
    v = next(x for x in vencs if (x - f).days >= 20)
    s = superficie(f, v)
    p = dias[f]
    p = p[(p["vencimiento"] == v) & (p["tipo"] == "P")]
    K = p["strike"].to_numpy()
    dl = bsm(S[f], K, s["T"], vol_svi(s, K), s["r"], q[f])["delta"]
    patas = [(p.iloc[int(np.argmin(np.abs(dl + corta)))], -1)]
    if ala:
        patas.append((p.iloc[int(np.argmin(np.abs(dl + ala)))], 1))
    credito = sum(x["bid"] if n < 0 else -x["ask"] for x, n in patas)
    # peor pérdida por contrato: caídas al vencimiento y escenarios
    # de estrés al día siguiente con la sonrisa escalada
    peor = estres = 0.0
    for mov in np.arange(0.05, 0.41, 0.05):
        pago = sum(n * max(x["strike"] - S[f] * (1 - mov), 0)
                   for x, n in patas)
        peor = max(peor, -(credito + pago))
    atm = float(vol_svi(s, s["F"]))
    for mov, va in ESTRES:
        valor = sum(n * float(bsm(
            S[f] * (1 - mov), x["strike"], s["T"] - 1 / AÑO,
            float(vol_svi(s, x["strike"])) * va / atm, s["r"],
            q[f])["precio"]) for x, n in patas)
        estres = max(estres, -(credito + valor))
    peor = max(peor, estres)
    perdida = MULT * peor + 2 * COMISION * len(patas)
    if ala:
        m = MULT * (patas[0][0]["strike"] - patas[1][0]["strike"])
    else:
        m = margen_put(patas[0][0]["bid"], S[f], patas[0][0]["strike"])
    n = int(min(PERDIDA_MAX * capital / perdida,
                MARGEN_MAX * capital / m))
    return {"venc": v, "n": n, "credito": credito,
            "perdida": perdida, "solo_estres": MULT * estres,
            "margen": m,
            "patas": [(float(x["strike"]), k) for x, k in patas]}


for ala in (None, 0.05):
    c = abrir(f0, 0.20, ala, CAPITAL)
    nom = c["n"] * MULT * c["patas"][0][0] / CAPITAL
    print("20Δ/5Δ:" if ala else "20Δ sola:", c["patas"],
          f"crédito {c['credito']:.2f}")
    print(f"  pérdida máx. {c['perdida']:,.0f} USD/contrato "
          f"(solo estrés: {c['solo_estres']:,.0f}); margen "
          f"{c['margen']:,.0f}; n = {c['n']}; nominal {nom:.2f} x")
```
```output
20Δ sola: [(2750.0, -1)] crédito 11.00
  pérdida máx. 10,290 USD/contrato (solo estrés: 7,938); margen 3,383; n = 14; nominal 0.39 x
20Δ/5Δ: [(2750.0, -1), (2625.0, 1)] crédito 7.70
  pérdida máx. 1,177 USD/contrato (solo estrés: 447); margen 1,250; n = 127; nominal 3.49 x
```

El 20 de diciembre de 2019, la put 2750 sola cobra 11 puntos; su peor pérdida, en la rejilla hasta un −40 %, es de
10.290 USD por contrato, así que el límite permite 14 contratos: un nominal de 0,39 veces el capital. Con el ala 2625,
el crédito baja a 7,70 puntos, pero la pérdida máxima es la anchura menos el crédito, 1.177 USD por contrato: 127
contratos y un nominal de 3,49 veces el capital. Es la forma en que un ala cambia un libro: menos crédito por
contrato, pérdida máxima conocida y mucho más nominal para el mismo riesgo de cola.

La cifra «solo estrés» enseña la trampa que motivó la rejilla. Los escenarios de la lección 3.6.1, revalorados al
día siguiente, dan una pérdida de 447 USD por contrato para el spread: al día siguiente de una caída del 20 % todavía
le queda valor temporal. Al vencimiento, una caída del 8 % (el índice por debajo del ala) ya cuesta la anchura entera. Dimensionar con los
escenarios al día siguiente habría permitido unas 2,6 veces más contratos y una pérdida de ciclo de casi el 40 % del
capital. En un spread, el peor punto del perfil no está en el escenario extremo.

Con la apertura escrita, el barrido aplica la misma lógica a cada ciclo de 2014 a 2025 para cada estructura candidata
y guarda el resultado en el registro de pruebas. Cada ciclo se mantiene hasta el vencimiento; el capital de cada
apertura incluye el resultado de los anteriores y los intereses de la caja.

```python
def aperturas():
    """Día de apertura de cada ciclo: el primero de la muestra y,
    después, cada vencimiento (o la sesión siguiente)."""
    f, out = S.index[0], []
    while True:
        v = next((x for x in vencs if (x - f).days >= 20), None)
        if v is None or v > S.index[-1]:
            return out
        out.append(f)
        f = v if v in dias else S.index[S.index > v][0]


APERTURAS = aperturas()


def liquidacion(v, patas):
    """Valor de las patas al cierre del vencimiento (en puntos)."""
    return sum(k * max(K - S[v], 0) for K, k in patas)


def ciclos(corta, ala):
    """Cada ciclo hasta el vencimiento: rendimiento en exceso de la
    caja sobre el capital al abrir (con interés compuesto)."""
    cap, filas = CAPITAL, []
    for f in APERTURAS:
        c = abrir(f, corta, ala, cap)
        pyg = c["n"] * (MULT * (c["credito"]
                                + liquidacion(c["venc"], c["patas"]))
                        - COMISION * len(c["patas"]))
        caja = cap * (np.exp(tipo(f, 30) * (c["venc"] - f).days
                             / AÑO) - 1)
        filas.append({"venc": c["venc"], "r_ex": pyg / cap,
                      "n": c["n"], "cap": cap, "apertura": f})
        cap += pyg + caja
    return pd.DataFrame(filas).set_index("venc")


def nombre(corta, ala):
    return f"{corta * 100:.0f}Δ" + (f"/{ala * 100:.0f}Δ" if ala
                                    else " sin ala")


REJILLA = [(c, a) for c in (0.30, 0.20, 0.10)
           for a in (None, 0.05, 0.02)]
R = pd.DataFrame({nombre(c, a): ciclos(c, a)["r_ex"]
                  for c, a in REJILLA})
registro = pd.DataFrame({
    "sharpe": R.mean() / R.std() * np.sqrt(12),
    "media_%": 100 * R.mean(), "peor_%": 100 * R.min(),
    "asim": R.skew()})
registro.index.name = "prueba"
registro.to_csv("registro_pruebas.csv")
print(f"{len(R)} ciclos de {R.index[0].date()} a "
      f"{R.index[-1].date()}; {len(registro)} pruebas registradas")
registro.round(2)
```
```output
143 ciclos de 2014-02-21 a 2025-12-19; 9 pruebas registradas
             sharpe  media_%  peor_%  asim
prueba
30Δ sin ala    0.59     0.19  -10.05 -6.12
30Δ/5Δ         0.62     0.55  -14.88 -3.85
30Δ/2Δ         0.58     0.40  -14.86 -4.55
20Δ sin ala    0.56     0.16   -9.65 -7.92
20Δ/5Δ         0.63     0.49  -14.95 -4.98
20Δ/2Δ         0.56     0.34  -14.89 -5.80
10Δ sin ala    0.36     0.09   -9.34 -9.82
10Δ/5Δ         0.36     0.25  -14.94 -5.87
10Δ/2Δ         0.34     0.18  -14.92 -7.00
```

Nueve estructuras, 143 ciclos cada una. La figura de la cabecera las ordena por Sharpe. Tres lecturas: las alas suben
el rendimiento medio por ciclo (0,49 % frente a 0,16 % en 20Δ) porque permiten más nominal, y con él la peor pérdida
de ciclo sube al límite de diseño del 15 %; las puts sin ala tienen la asimetría más negativa (hasta −9,8), y las de
delta 0,10, el peor Sharpe: cobran poco por lo que arriesgan. Las diferencias de Sharpe entre las seis mejores son
pequeñas (0,56 a 0,63) para 143 ciclos: es justo el caso en que elegir la mejor es sobreajustar.

La validación usa el registro entero: un walk-forward de la elección (cada enero se elige la estructura con mejor
Sharpe hasta diciembre y se opera ese año) y el Deflated Sharpe de la mejor con las 9 pruebas.

```python
from scipy.stats import norm


def psr(sr, umbral, t, asim, curt):
    """Probabilistic Sharpe Ratio (2.5.4), con Sharpe por periodo."""
    den = np.sqrt(1 - asim * sr + (curt - 1) / 4 * sr**2)
    return norm.cdf((sr - umbral) * np.sqrt(t - 1) / den)


def max_esperado(n):
    """Máximo esperado de n Sharpes de media 0 y varianza 1."""
    gm = np.euler_gamma
    return ((1 - gm) * norm.ppf(1 - 1 / n)
            + gm * norm.ppf(1 - 1 / (n * np.e)))


# walk-forward anclado: cada enero, la mejor hasta diciembre
anyo = R.index.year
oos, elegida = [], {}
for y in range(2017, 2026):
    pasado = R[anyo < y]
    elegida[y] = (pasado.mean() / pasado.std()).idxmax()
    oos.append(R.loc[anyo == y, elegida[y]])
oos = pd.concat(oos)
print("elegida:", "; ".join(f"{y}: {e}" for y, e in elegida.items()
                             if y in (2017, 2019, 2021, 2022)), "…")
sr_m = lambda x: x.mean() / x.std() * np.sqrt(12)
print(f"walk-forward 2017-2025: Sharpe {sr_m(oos):.2f}; "
      f"20Δ/5Δ fija en el mismo tramo: "
      f"{sr_m(R.loc[anyo >= 2017, '20Δ/5Δ']):.2f}")

# Deflated Sharpe de la mejor de las 9 en toda la muestra
srs = R.mean() / R.std()
mejor = srs.idxmax()
x = R[mejor]
sr0 = srs.std() * max_esperado(len(srs))
print(f"mejor: {mejor}, Sharpe {srs[mejor] * np.sqrt(12):.2f}; "
      f"umbral por 9 pruebas {sr0 * np.sqrt(12):.2f}")
mom = (len(x), x.skew(), x.kurt() + 3)
print(f"PSR(0) {psr(srs[mejor], 0, *mom):.3f}; "
      f"DSR {psr(srs[mejor], sr0, *mom):.3f}")
```
```output
elegida: 2017: 10Δ/5Δ; 2019: 20Δ sin ala; 2021: 30Δ/5Δ; 2022: 20Δ/5Δ …
walk-forward 2017-2025: Sharpe 0.36; 20Δ/5Δ fija en el mismo tramo: 0.46
mejor: 20Δ/5Δ, Sharpe 0.63; umbral por 9 pruebas 0.18
PSR(0) 0.931; DSR 0.853
```

El walk-forward cambia de estructura tres veces en nueve años y obtiene un Sharpe de 0,36 en 2017-2025, frente al
0,46 de quedarse con 20Δ/5Δ todo el tramo. La mejor de la muestra, 20Δ/5Δ, tiene un PSR(0) de 0,931 y un DSR de 0,853:
el umbral que exige el número de pruebas (0,18 de Sharpe anual con esta dispersión) y la asimetría de los ciclos se
comen buena parte de la evidencia. La sección de validación explica qué hace el proyecto con estas cifras.

El libro diario lleva la estructura del encargo, 20Δ/5Δ, fijada antes del barrido. Valora cada pata al medio cada día
(si falta la cotización, la última), lleva la caja al tipo a 1 mes y aplica el kill switch: si el capital cae más de un
20 % desde su máximo, cierra todo pagando la horquilla y no abre hasta pasadas 21 sesiones (mientras, el capital es la caja con sus
intereses). Las griegas se calculan
después, de forma vectorizada, con la volatilidad implícita de cada medio.

```python
mid = cad[cad["tipo"] == "P"].set_index(
    ["fecha", "vencimiento", "strike"])["medio"]
fac = np.exp(CURVA["tipo_1m"] / 252).cumprod()   # caja al tipo a 1 mes
LIM = {"vega": -0.004, "gamma": -0.003}  # × capital, por punto y 1 %


def libro(corta=0.20, ala=0.05, dd_max=0.20, pausa=21):
    """Abre cada ciclo, valora al medio cada día y cierra todo (al
    ask/bid) si la caída desde el máximo supera dd_max; tras un
    cierre, no abre hasta pasadas `pausa` sesiones."""
    caja, t_caja, pico, hasta = CAPITAL, APERTURAS[0], CAPITAL, None
    eq, patas, cierres = {}, [], []
    for f in APERTURAS:
        if hasta is not None and f < hasta:
            continue
        caja *= fac[f] / fac[t_caja]         # intereses hasta hoy
        c = abrir(f, corta, ala, caja)
        caja += c["n"] * (MULT * c["credito"]
                          - COMISION * len(c["patas"]))
        sesiones = S.index[(S.index > f) & (S.index <= c["venc"])]
        base = caja / fac[f]
        for d in sesiones:
            valor = 0.0
            for K, k in c["patas"]:
                px = mid.get((d, c["venc"], K), np.nan)
                if d == c["venc"]:
                    px = max(K - S[d], 0)
                u = k * c["n"] * MULT
                patas.append((d, K, u, px, (c["venc"] - d).days))
                valor += u * px
            e = base * fac[d] + valor
            if not np.isfinite(e):           # sin cotización: ayer
                e = eq[S.index[S.index < d][-1]]
            eq[d] = e
            pico = max(pico, e)
            if e < (1 - dd_max) * pico and d < c["venc"]:
                g = dias[d].set_index(["vencimiento", "tipo", "strike"])
                coste = sum(-k * c["n"] * MULT * g.at[
                    (c["venc"], "P", K), "ask" if k < 0 else "bid"]
                    for K, k in c["patas"])
                caja = base * fac[d] + coste - COMISION * c["n"] * len(
                    c["patas"])
                eq[d], pico, t_caja = caja, caja, d
                cierres.append(d)
                hasta = S.index[S.index.get_loc(d) + pausa]
                break
        else:
            caja = base * fac[c["venc"]] + c["n"] * MULT * liquidacion(
                c["venc"], c["patas"])
            t_caja = c["venc"]
    eq = pd.Series(eq)
    eq = eq.reindex(S.index[S.index >= eq.index[0]])
    k = fac.reindex(eq.index)
    eq = eq.fillna((eq / k).ffill() * k)     # en pausa: solo la caja
    return eq, pd.DataFrame(
        patas, columns=["fecha", "K", "u", "px", "dias"]), cierres


def griegas(p):
    """Griegas del libro en USD por día (delta y gamma por 1 %)."""
    p = p[(p["dias"] > 0) & (p["px"] > 0)].copy()
    f = p["fecha"]
    T = p["dias"].to_numpy() / AÑO
    r = np.array([tipo(a, b) for a, b in zip(f, p["dias"])])
    s, qq = S[f].to_numpy(), q[f].to_numpy()
    vol = iv(p["px"].to_numpy(), s, p["K"].to_numpy(), T, r, qq)
    b = bsm(s, p["K"].to_numpy(), T, vol, r, qq)
    u = p["u"].to_numpy()
    p["delta"] = u * b["delta"] * s * 0.01
    p["gamma"] = u * 0.5 * b["gamma"] * (0.01 * s)**2
    p["vega"], p["theta"] = u * b["vega"], u * b["theta"]
    return p.groupby("fecha")[["delta", "gamma", "vega", "theta"]].sum()


eq, patas, cierres = libro()
G = griegas(patas)
fuera = {k: int((G[k] < v * eq.reindex(G.index)).sum())
         for k, v in LIM.items()}
print(f"{len(eq)} sesiones; cierres por caída: "
      f"{[str(d.date()) for d in cierres]}")
print("días fuera de límite:", fuera)
(G.quantile([0.01, 0.5, 0.99]) / 1000).round(1)
```
```output
3017 sesiones; cierres por caída: ['2018-10-22']
días fuera de límite: {'vega': 0, 'gamma': 16}
      delta  gamma  vega  theta
0.01    0.0   -3.0  -3.4   -0.8
0.50    2.8   -0.5  -0.8    0.5
0.99   22.4   -0.0   0.0    3.4
```

El kill switch salta una vez, el 22 de octubre de 2018. Ningún día el libro supera el límite de vega (−0,4 % del
capital por punto); 16 días supera el de gamma (−0,3 % por cada 1 %), y el ejercicio 1 explica cuáles. La tabla da las
griegas en miles de USD: la mediana es un libro con theta positiva (500 USD al día), vega de −800 por punto y gamma
de −500 por 1 %; el percentil 99 de la delta, 22.400 USD por 1 %, son los días después de una caída, con la put vendida
en el dinero.

La última celda calcula las métricas con rendimientos mensuales (como el barrido) con y sin kill switch, y escribe el
artefacto: `informe_c2.json`, con la configuración, la huella de los datos, las métricas, la validación y el resumen de
riesgo. Es el fichero que el comando único del repositorio debe regenerar idéntico.

```python
import hashlib
import json


def metricas(eq):
    """Con rendimientos mensuales, como el barrido de ciclos."""
    mes = eq.resample("ME").last().pct_change().dropna()
    ex = mes - fac.resample("ME").last().pct_change().reindex(
        mes.index)
    anos = (eq.index[-1] - eq.index[0]).days / 365.25
    return {"cagr": (eq.iloc[-1] / eq.iloc[0]) ** (1 / anos) - 1,
            "vol": mes.std() * np.sqrt(12),
            "sharpe": ex.mean() / ex.std() * np.sqrt(12),
            "max_caida": (eq / eq.cummax() - 1).min(),
            "peor_mes": mes.min(), "asim_mensual": mes.skew()}


sin_kill, _, _ = libro(dd_max=1.0)
tabla = pd.DataFrame({"con kill switch": metricas(eq),
                      "sin kill switch": metricas(sin_kill)}).T
print(tabla.round(3).to_string())


def sha(ruta):
    return hashlib.sha256(open(ruta, "rb").read()).hexdigest()[:12]


m = metricas(eq)
informe = {
    "proyecto": "C2 libro de opciones", "config": "20Δ/5Δ",
    "datos": {a: sha(a) for a in ("opciones3_cadena.parquet",
                                  "opciones3_indice.csv")},
    "metricas": {k: round(float(v), 4) for k, v in m.items()},
    "validacion": {"n_pruebas": len(registro),
                   "dsr": round(float(psr(srs[mejor], sr0, *mom)), 3),
                   "sharpe_wf": round(float(sr_m(oos)), 3)},
    "riesgo": {"cierres_kill_switch": len(cierres),
               "dias_fuera_de_limite": fuera,
               "perdida_max_ciclo": PERDIDA_MAX}}
with open("informe_c2.json", "w") as fh:
    json.dump(informe, fh, ensure_ascii=False, indent=1)
print(json.dumps(informe["validacion"]), informe["datos"])
```
```output
                  cagr    vol  sharpe  max_caida  peor_mes  asim_mensual
con kill switch  0.078  0.087   0.706     -0.234    -0.141        -4.342
sin kill switch  0.074  0.090   0.636     -0.271    -0.144        -4.358
{"n_pruebas": 9, "dsr": 0.853, "sharpe_wf": 0.359} {'opciones3_cadena.parquet': 'b13d44a9b279', 'opciones3_indice.csv': 'e71e0fdee66b'}
```

*Fig. — Capstone C2: el libro 20Δ/5Δ día a día (capital a fin de mes y caída desde máximos), con el kill switch del 20 % y sin él, 2014-2025.* Arriba, el capital en múltiplos del inicial: con kill switch termina en 2,47 veces y sin él en 2,35. El punto destacado es el único cierre del kill switch, el 22 de octubre de 2018, cuando la caída desde el máximo pasa del 20 %. Abajo, la caída desde máximos: con kill switch (línea azul) llega a −23,4 %; sin él (línea gris), a −27,1 %.

Con kill switch, el libro rinde un 7,8 % anual con una volatilidad del 8,7 %, un Sharpe mensual de 0,71, una caída
máxima del −23,4 % y un peor mes del −14,1 %; sin él, 7,4 %, 0,64 y −27,1 %. La figura sigue las dos curvas. Una sola
activación no demuestra que el kill switch añada valor: con otra fecha podría haber cerrado en el mínimo y perdido la
recuperación. Lo que sí demuestra es que funciona, que su coste (la horquilla al cerrar) está contado y que sus dos
parámetros (20 % y 21 sesiones) se fijaron antes; si se ajustan mirando el resultado, entran en el registro como
pruebas.

## Validación

La pregunta del tribunal no es si el libro gana en la muestra, sino cuánta de su ventaja sobrevive a la forma en que
se eligió. En C2 hay tres fuentes de selección, y cada una tiene su control:

| Qué se elige | Riesgo | Control en el proyecto |
|---|---|---|
| Delta vendida y ala | elegir la mejor de muchas en la misma muestra | registro de pruebas, DSR con su N |
| Elección a lo largo del tiempo | que la mejor de hoy no lo sea mañana | walk-forward anclado de la elección |
| Límites y kill switch | ajustar umbrales hasta que el histórico quede bonito | parámetros fijados en la semana 3, registrados si cambian |
| Datos | decidir con información que no se tenía | volatilidad de las cotizaciones, nunca `iv_modelo`; superficie del mismo día |

El registro guarda todas las pruebas, también las descartadas, con su serie de rendimientos por ciclo (lección 2.2.4 y
7.6.1). El esqueleto registra 9; un proyecto real registrará más (anchuras, vencimientos, reglas de cierre anticipado,
coberturas de otro vencimiento), y el DSR se calcula con todas. Ocultar pruebas o elegir el periodo a posteriori
suspende por sí solo.

El **criterio de parada** se escribe en la semana 4, antes de calcular nada, y el de este esqueleto es:

- Si el Sharpe del walk-forward de la elección es ≤ 0 o la peor pérdida de un ciclo supera el límite de diseño, el
  libro no pasa: se hace el papel para evaluar el sistema, pero la memoria lo presenta como no operable.
- Si el DSR de la mejor estructura es < 0,95, la memoria dice que no hay evidencia estadística de ventaja después de
  las pruebas hechas, y el libro no se propone para dinero real con esta evidencia.
- En los datos del curso: Sharpe del walk-forward 0,36 > 0, pérdida máxima de ciclo del 14,95 % < 15 % y DSR 0,853
  < 0,95. El sistema pasa a papel; la memoria no lo presenta como operable con dinero.

Dos cautelas propias de este proyecto. La primera: 143 ciclos mensuales son pocos para una estrategia con asimetría de
−5; el PSR y el DSR ya la tienen en cuenta, pero el periodo contiene una sola crisis grande (2020), y el resultado
depende de cómo la atraviesa el libro. La segunda: los datos son sintéticos, con la prima de riesgo de volatilidad
plantada por el generador. Validan el código y el método, no la estrategia. El ejercicio 2 mide cuánto cuesta la
horquilla, y la extensión a CPCV o a la probabilidad de sobreajuste (PBO, lección 2.5.4) sobre las 9 series del
registro se hace con el mismo código del ejercicio 1 de C3 (7.6.4).

## Riesgos y paper trading

Los riesgos que de verdad tumban un libro de venta de prima no son los que mide su Sharpe:

- **Cola y encadenamiento.** El ala acota la pérdida de un ciclo, no la de tres seguidos. Con un 15 % por ciclo, tres
  ciclos malos seguidos son un −39 %. El límite de diseño se elige pensando en la racha, no en el ciclo.
- **Gap y liquidez.** Una caída de un día no da tiempo a ajustar; en una crisis la horquilla se abre y cerrar cuesta
  mucho más que en el backtest (la lección 3.6.4 midió la horquilla por distancia al dinero).
- **Pin risk.** Cerca del vencimiento, con el índice junto al strike vendido, la gamma se dispara (ejercicio 1).
- **Modelo y datos.** Una cotización congelada o cruzada mueve el SVI y el delta de selección; el control de calidad
  descarta antes de ajustar, y un ajuste con error grande frente a las cotizaciones bloquea la apertura.
- **Liquidación.** Las SPX estándar liquidan con un valor calculado con los precios de apertura de sus componentes el
  día de vencimiento (y dejan de negociarse la sesión anterior); las XSP, con el cierre oficial del S&P 500[^4]. El backtest y el papel deben usar la misma regla que el contrato que se opera.
- **Operativo.** Una orden combinada parcialmente ejecutada deja una put vendida sin ala; el ejecutor solo envía
  combinaciones y la conciliación busca patas sueltas cada día.

Los límites van en la configuración validada (7.1.1) y el monitor los comprueba cada día: pérdida máxima de ciclo
(15 % del capital), margen comprometido (50 %), vega (−0,4 % por punto), gamma (−0,3 % por 1 %) y delta, con una regla
de calendario para el pin risk. El kill switch tiene estados explícitos, como en 4.5.5 y 7.3.3:

```mermaid
stateDiagram-v2
  [*] --> CONCILIANDO
  CONCILIANDO --> OPERANDO
  CONCILIANDO --> BLOQUEADO
  OPERANDO --> REDUCIENDO
  REDUCIENDO --> OPERANDO
  OPERANDO --> CERRANDO
  CERRANDO --> PAUSA
  PAUSA --> CONCILIANDO
  BLOQUEADO --> CONCILIANDO
```

`REDUCIENDO` recorta contratos cuando una griega sale del límite; `CERRANDO` lo cierra todo (caída del 20 %, datos
rancios, conciliación imposible) y pasa a `PAUSA`, que solo sale con una persona que reconoce el evento; `BLOQUEADO`
es el arranque con algo sin explicar en la conciliación.

**Plan de paper trading.** Cuatro semanas de papel con un ciclo mensual verían una entrada. Para ejercitar el ciclo
completo varias veces, el libro de papel se escalona en cuatro sublibros semanales sobre XSP, cada uno con un cuarto
del capital y vencimientos de 4 a 5 semanas: cuatro aperturas, al menos una liquidación y la misma lógica de tamaño
por sublibro. El código de la decisión no cambia durante las cuatro semanas (7.6.1). La conexión es la de la lección
4.4.2, con IB Gateway en papel; esta celda es el ejecutor real y no se ejecuta aquí porque necesita la cuenta:

```python skip
from ib_async import IB, ComboLeg, Contract, Index, LimitOrder, Option

ib = IB()
ib.connect("127.0.0.1", 4002, clientId=23)       # IB Gateway, papel
xsp = Index("XSP", "CBOE", "USD")
ib.qualifyContracts(xsp)
venc = "20261127"                                  # el del sublibro
corta, ala = (Option("XSP", venc, k, "P", "SMART", tradingClass="XSP")
              for k in (655.0, 640.0))
ib.qualifyContracts(corta, ala)
for t in ib.reqTickers(corta, ala):                # griegas del broker
    g = t.modelGreeks
    print(t.contract.strike, t.bid, t.ask, g.impliedVol, g.delta)

# spread de compra (compra la corta, vende el ala) y se VENDE el
# combo: el precio límite positivo es el crédito que se cobra
combo = Contract(symbol="XSP", secType="BAG", currency="USD",
                 exchange="SMART", comboLegs=[
                     ComboLeg(conId=corta.conId, ratio=1,
                              action="BUY", exchange="SMART"),
                     ComboLeg(conId=ala.conId, ratio=1,
                              action="SELL", exchange="SMART")])
orden = LimitOrder("SELL", 3, 1.85, orderRef="C2-S2-20261023",
                   tif="DAY")
print(ib.whatIfOrder(combo, orden).initMarginChange)   # margen
trade = ib.placeOrder(combo, orden)
```

Tres detalles que el ejecutor real debe resolver y probar contra un broker simulado (7.1.1): la clave de idempotencia
en `orderRef` (sublibro y fecha, lección 4.5.5); qué hacer si el combo no se ejecuta al límite (repetir rebajando el
crédito hasta un mínimo escrito, nunca pasar a dos órdenes sueltas); y la comparación de las griegas de IBKR
(`modelGreeks`) con las propias, que es otra conciliación: una diferencia grande de delta dice que una de las dos
superficies está mal.

Cada día de papel deja en el diario (7.6.1): la cadena guardada con su huella, el SVI y su error, las griegas propias
y las del broker, las órdenes y ejecuciones, la conciliación de posiciones y efectivo contra el extracto (4.5.5), el
deslizamiento frente al medio en el momento de la orden (4.5.3) y las alertas. El monitor de 7.2.3 expone las
griegas y el estado del kill switch; el de 7.3.3 compara el P&L explicado por griegas con el real y avisa cuando el
residuo crece (una superficie que ha dejado de ajustar).

## Hitos y rúbrica

El calendario es el de 7.6.1 (diez semanas, de 40 a 80 horas), aplicado a C2:

| Semana | Entregable verificable |
|---|---|
| 1 | Repositorio desde la plantilla; README de datos con fuentes, licencias y huellas; auditor de 7.6.1 en verde |
| 2 | Carga y control de calidad de la cadena; tests contra `opciones3_incidencias.csv`; volatilidad implícita contrastada con vollib y QuantLib |
| 3 | Superficie con su error frente a `opciones3_verdad.parquet`; selección, tamaño por rejilla, costes; límites y kill switch fijados por escrito |
| 4 | Registro de pruebas, walk-forward de la elección, DSR; criterio de parada aplicado; `informe_c2.json` |
| 5 | Ejecutor de combos con `ib_async` contra un broker simulado y luego contra la cuenta de papel; Docker; monitor de griegas |
| 6-9 | Papel en cuatro sublibros semanales sobre XSP, con diario y conciliación diaria; memoria en paralelo |
| 10 | Informe de papel (deslizamiento, griegas propias frente a las del broker, incidencias); ensayo y defensa |

La rúbrica común de 7.6.1, con la evidencia concreta que pide en C2:

| Criterio (peso) | Evidencia en C2 |
|---|---|
| Reproducibilidad (15 %) | `make reproduce` regenera `informe_c2.json` con la huella de la memoria; uv.lock, imagen Docker, datos con huella |
| Datos (15 %) | origen, licencia y coste de cada fuente; hora de la foto; control de calidad con sus contadores; volatilidad de las cotizaciones, no de un modelo |
| Validación (20 %) | registro completo; walk-forward de la elección; DSR con el N del registro (o CPCV/PBO); criterio de parada escrito antes |
| Riesgo y ejecución (15 %) | horquilla y comisiones; tamaño por la peor pérdida del ciclo; límites de griegas en USD; kill switch probado; solo órdenes combinadas |
| Paper trading (15 %) | 28 días y al menos 20 sesiones; cuatro aperturas y una liquidación como mínimo; conciliación diaria sin diferencias sin explicar |
| Código (10 %) | tests del motor (paridad, contraste con vollib y QuantLib), de la superficie, del tamaño y del ejecutor; ruff y pre-commit |
| Memoria y defensa (10 %) | decisiones con cifras del informe; qué no demuestran 143 ciclos sintéticos; respuestas a las preguntas típicas |

Errores que suspenden por sí solos en este proyecto, además de los comunes: decidir con la volatilidad del generador
(`iv_modelo`) o con una superficie ajustada con cotizaciones posteriores; vender al medio sin decirlo (el backtest sin
horquilla es un backtest sin costes); dimensionar por el margen del broker; y patas vendidas sin su ala en el papel sin
detectarlas en la conciliación del día.

## Ejercicios

1. El libro diario sale 16 días del límite de gamma. Lista esos días con los días que faltaban al vencimiento y la
   distancia del índice al strike vendido. ¿Qué regla de gestión los evitaría?

**Solución.**

Los 16 días caen a 4 días naturales o menos del vencimiento (mediana 2), con el índice a una distancia mediana del 0,4 % del
strike vendido: es el pin risk de la lección 3.5.5. La gamma de una put en el dinero crece como $1/\sqrt{T}$ y el
límite deja de cumplirse aunque el tamaño fuera correcto al abrir. La regla es de calendario: cerrar o rodar el ciclo
unas sesiones antes del vencimiento cuando el índice está cerca del strike vendido, con su coste (una horquilla más)
medido en el barrido como una prueba nueva del registro.

```python
lim = LIM["gamma"] * eq.reindex(G.index)
malos = G.index[G["gamma"] < lim]
corto = patas[patas["u"] < 0].set_index("fecha")
filas = []
for d in malos:
    x = corto.loc[d]
    filas.append({"fecha": d.date(), "dias": int(x["dias"]),
                  "dist_%": 100 * (S[d] / x["K"] - 1),
                  "gamma_%": 100 * G.at[d, "gamma"] / eq[d]})
m = pd.DataFrame(filas)
print(f"{len(m)} días; días al vencimiento: mediana "
      f"{m['dias'].median():.0f}, máximo {m['dias'].max()}; "
      f"|distancia| mediana {m['dist_%'].abs().median():.1f} %")
m.sort_values("gamma_%").head(4).round(2)
```
```output
16 días; días al vencimiento: mediana 2, máximo 4; |distancia| mediana 0.4 %
         fecha  dias  dist_%  gamma_%
9   2015-05-14     1   -0.26    -0.93
8   2015-05-13     2    0.11    -0.63
5   2014-07-17     1    0.48    -0.61
13  2019-05-15     2   -0.11    -0.46
```

2. El barrido vende al bid y compra al ask. Calcula cuánto del capital se lleva la horquilla cada año en 20Δ/5Δ y qué
   Sharpe tendría el libro si las dos patas se ejecutaran al medio.

**Solución.**

La horquilla de apertura cuesta un 0,27 % del capital al año y baja el Sharpe de 0,66 (al medio) a 0,63 (al bid y al
ask). Es poco porque las dos patas son líquidas en los datos y el ciclo se mantiene hasta el vencimiento, sin pagar la
horquilla de cierre. En un libro con cierres anticipados o rodajes, y con la horquilla real de una crisis, el coste se
multiplica; por eso la ejecución del papel se compara siempre con el medio en el momento de la orden.

```python
base = ciclos(0.20, 0.05)
extra = []
for venc, fila in base.iterrows():
    f = fila["apertura"]
    c = abrir(f, 0.20, 0.05, fila["cap"])
    g = dias[f].set_index(["vencimiento", "tipo", "strike"])
    medio = sum(-k * g.at[(c["venc"], "P", K), "medio"]
                for K, k in c["patas"])
    extra.append(fila["n"] * MULT * (medio - c["credito"])
                 / fila["cap"])
al_medio = base["r_ex"] + np.array(extra)
print(f"horquilla pagada: {12 * np.mean(extra):.2%} del capital al año")
print(f"Sharpe al bid/ask {sr_m(base['r_ex']):.2f}, "
      f"al medio {sr_m(al_medio):.2f}")
```
```output
horquilla pagada: 0.27% del capital al año
Sharpe al bid/ask 0.63, al medio 0.66
```

3. Un miembro del tribunal pregunta por qué presentas 20Δ/5Δ si el walk-forward de la elección da un Sharpe de 0,36 y
   el DSR es de 0,853. ¿Qué respondes?

**Solución.**

Que 20Δ/5Δ no se eligió por ser la mejor: es la estructura del encargo, fijada antes del barrido, y su lugar en la
tabla es una coincidencia que el DSR descuenta. Que el registro tiene 9 pruebas y todas están en la memoria. Que con
un DSR de 0,853 no hay evidencia estadística suficiente de ventaja, y que el walk-forward muestra además que elegir la
estructura con el pasado no mejora a quedarse con una fija. Por eso el criterio de parada se aplicó tal como estaba
escrito: el sistema pasó a papel, donde se evalúa que funciona, se vigila y se concilia, y la memoria no lo presenta
como operable con dinero. Lo que haría falta para cambiar esa conclusión es más muestra fuera de la usada para
diseñar, no más pruebas sobre la misma.

[^1]: Cboe, «XSP Options Product Specifications» (https://www.cboe.com/tradable_products/sp_500/mini_spx_options/specifications/):
    subyacente «based on 1/10th the value of the Standard & Poor's 500 Index», multiplicador 100, ejercicio europeo,
    liquidación en efectivo con una décima parte del cierre oficial del S&P 500 y margen de las vendidas sin cubrir del
    100 % de la prima más el 15 % del valor del contrato menos lo que esté fuera del dinero. Consultada el 24 de octubre
    de 2026.

[^2]: Cboe DataShop (https://datashop.cboe.com/): fichas «Option EOD Summary» y «Option Quotes». Consultada el 24 de
    octubre de 2026.

[^3]: Databento, página del conjunto OPRA (https://databento.com/datasets/OPRA.PILLAR): «We pass through these license
    fees with no upcharge» y «New users get \$125 in free credits». Consultada el 24 de octubre de 2026.

[^4]: Cboe, «SPX Options Product Specifications» (https://www.cboe.com/tradable_products/sp_500/spx_options/specifications/):
    «The exercise-settlement value, SET, is calculated using the opening sales price in the primary market of each
    component security on the expiration date». Consultada el 24 de octubre de 2026.

## Autoevaluación

1. El esqueleto dimensiona el put spread 20Δ/5Δ con la peor pérdida de una rejilla de caídas al vencimiento, no solo con los escenarios de estrés al día siguiente. ¿Por qué?

   - A. Porque los escenarios al día siguiente no se pueden calcular con Black-Scholes
   - B. Porque la peor pérdida de un put spread (la anchura menos el crédito, 1.177 USD por contrato el 20-12-2019) llega con cualquier caída que lleve el índice por debajo del ala al vencimiento; los escenarios al día siguiente solo ven 447 USD y dejarían operar unas 2,6 veces más contratos
   - C. Porque la rejilla es más conservadora con la put sola, que es la que importa
   - D. Porque el margen del spread es mayor que su pérdida máxima

   *Respuesta B: Porque la peor pérdida de un put spread (la anchura menos el crédito, 1.177 USD por contrato el 20-12-2019) llega con cualquier caída que lleve el índice por debajo del ala al vencimiento; los escenarios al día siguiente solo ven 447 USD y dejarían operar unas 2,6 veces más contratos.* Al día siguiente de un −20 % al spread aún le queda valor temporal y la pérdida parece pequeña; al vencimiento, una caída del 8 % (el índice por debajo del ala) ya cuesta la anchura entera. El tamaño se fija con el peor punto del perfil de pérdidas, que en un spread no está en el escenario extremo.

2. La mejor de las 9 estructuras registradas, 20Δ/5Δ, tiene un Sharpe de 0,63 y un DSR de 0,853. El walk-forward de la elección da 0,36. ¿Qué dice la memoria?

   - A. Que la estrategia está validada, porque el PSR(0) es 0,931
   - B. Que hay que repetir el barrido con más estructuras hasta pasar el 0,95
   - C. Que no hay evidencia suficiente de ventaja después de 9 intentos y que elegir cada año con el pasado rinde menos que la fija; el paper trading se hace (se evalúa el sistema), pero el libro no se presenta como operable con dinero
   - D. Que el DSR no se aplica a opciones

   *Respuesta C: Que no hay evidencia suficiente de ventaja después de 9 intentos y que elegir cada año con el pasado rinde menos que la fija; el paper trading se hace (se evalúa el sistema), pero el libro no se presenta como operable con dinero.* El DSR descuenta el número de pruebas y la asimetría de los ciclos; 0,853 queda por debajo del 0,95 que fija el criterio de parada. Añadir pruebas para pasar el umbral es exactamente lo que el DSR penaliza.

3. El libro diario sale 16 días del límite de gamma (−0,3 % del capital por cada 1 %). En el ejercicio 1 todos caen a 4 días o menos del vencimiento, con el índice a un 0,4 % del strike vendido. ¿Qué regla lo corrige?

   - A. Subir el límite de gamma al −1 %
   - B. Cubrir la delta cada hora
   - C. Vender el spread más lejos del dinero
   - D. Cerrar o rodar el ciclo unas sesiones antes del vencimiento cuando el índice está cerca del strike vendido: es el pin risk de la lección 3.5.5

   *Respuesta D: Cerrar o rodar el ciclo unas sesiones antes del vencimiento cuando el índice está cerca del strike vendido: es el pin risk de la lección 3.5.5.* La gamma de una opción en el dinero crece sin límite al acercarse el vencimiento. Los días fuera del límite no son un fallo de tamaño sino de calendario: la regla se escribe en la configuración y se prueba antes del papel.

4. ¿Por qué el esqueleto calcula las deltas con la volatilidad implícita de los precios medios y no con la columna `iv_modelo` de la cadena?

   - A. Porque `iv_modelo` es la verdad del generador de los datos: ningún mercado la publica, y usarla para decidir es información que el sistema no tendría
   - B. Porque `iv_modelo` tiene errores
   - C. Porque los precios medios son más baratos
   - D. Porque QuantLib no acepta `iv_modelo`

   *Respuesta A: Porque `iv_modelo` es la verdad del generador de los datos: ningún mercado la publica, y usarla para decidir es información que el sistema no tendría.* Las columnas de verdad de los datos sintéticos sirven para comprobar el código (el SVI ajustado frente a la verdad), no para operar. Decidir con ellas es una anticipación demostrable, uno de los errores que suspenden.

5. Las cuatro semanas de papel con un ciclo mensual verían una sola entrada. ¿Qué propone la guía?

   - A. Alargar el papel a un año
   - B. Escalonar el libro en cuatro sublibros semanales sobre XSP, con vencimientos de 4 a 5 semanas: cuatro entradas, al menos una liquidación y la misma lógica de tamaño por sublibro
   - C. Simular las entradas que faltan con el backtest
   - D. Operar opciones de 0 días para tener más operaciones

   *Respuesta B: Escalonar el libro en cuatro sublibros semanales sobre XSP, con vencimientos de 4 a 5 semanas: cuatro entradas, al menos una liquidación y la misma lógica de tamaño por sublibro.* El papel tiene que ejercitar el ciclo completo (selección, orden combinada, ejecución, valoración, liquidación y conciliación) varias veces. Las XSP tienen vencimientos semanales y un nominal de una décima parte del S&P 500, adecuado para una cuenta pequeña.

## Ideas clave

- C2 es un libro de venta de prima con cobertura de cola: una put vendida por delta y un ala comprada del mismo vencimiento, con el tamaño fijado por la peor pérdida posible del ciclo y no por el margen.
- La superficie se ajusta a las cotizaciones (SVI sobre la volatilidad implícita de los medios) y se comprueba contra la verdad de los datos; decidir con la volatilidad del generador sería anticipación.
- El peor punto de un put spread está al vencimiento, en cualquier caída por debajo del ala: con solo escenarios al día siguiente, el tamaño sale unas 2,6 veces mayor.
- Con 9 estructuras registradas, la mejor tiene un Sharpe de 0,63 y un DSR de 0,853, y el walk-forward de la elección da 0,36: no hay evidencia suficiente para operar con dinero, y la memoria lo dice.
- Las griegas del libro se miden cada día en USD; los excesos de gamma se concentran en los últimos días antes del vencimiento, cerca del strike: el pin risk se gestiona con una regla de calendario.
- El papel con XSP escalonado en sublibros semanales ejercita varias entradas y liquidaciones en cuatro semanas, con conciliación diaria contra el extracto.

## 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 XSP (Mini-SPX)](https://www.cboe.com/tradable_products/sp_500/mini_spx_options/specifications/) · cboe.com
- [Cboe DataShop: datos históricos de opciones (Option EOD Summary, Option Quotes)](https://datashop.cboe.com/) · datashop.cboe.com
- [Databento: OPRA, datos de opciones de EE. UU. en tiempo real e históricos](https://databento.com/datasets/OPRA.PILLAR) · databento.com
- [ib_async (sucesor de ib_insync): repositorio y documentación](https://github.com/ib-api-reloaded/ib_async) · github.com

### Libros

- [Euan Sinclair, Positional Option Trading (Wiley, 2020)](https://www.wiley.com/en-us/Positional+Option+Trading%3A+An+Advanced+Guide-p-9781119583516) · wiley.com

### Datos

- [Datos del nivel 3: cadena diaria de opciones de ORX (Parquet)](https://pythonparatrading.com/curso/datos/nivel-3/opciones3_cadena.parquet) · Descarga · opciones3_cadena.parquet

Lección anterior: [7.6.2 · Capstone C1: un sistema de futuros multi-activo, de la previsión a la cuenta de papel](https://pythonparatrading.com/curso-python-trading/nivel-7/capstone-c1-futuros-multiactivo-nautilustrader/)

Lección siguiente: [7.6.4 · Capstone C3: cartera de acciones y ETFs con ML, HRP y fiscalidad española](https://pythonparatrading.com/curso-python-trading/nivel-7/capstone-c3-cartera-ml-hrp-y-fiscalidad/)
