Lección 7.6.3 · 70 min

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.

Fig. — Capstone C2: las 9 estructuras del registro de pruebas (delta de la put vendida y del ala), ordenadas por el Sharpe de sus 143 ciclos mensuales, frente al máximo esperado por azar con 9 pruebas
Lecciones del nivel 7
  1. 7.1 · Ingeniería de software para trading

    1. 7.1.1 Estructura de proyecto, configuración y calidad: del notebook al repositorio
    2. 7.1.2 Rendimiento: medir, perfilar y compilar solo lo que hace falta
    3. 7.1.3 Patrones: eventos, colas de mensajes y arquitectura hexagonal
  2. 7.2 · Infraestructura y despliegue

    1. 7.2.1 Docker y docker-compose: imágenes reproducibles con uv
    2. 7.2.2 VPS, nube y latencia; CI/CD con GitHub Actions
    3. 7.2.3 Orquestación y monitorización: grafos de tareas, logs estructurados, métricas, alertas y latidos
    4. 7.2.4 Seguridad y continuidad: 2FA, IP, copias y procedimientos ante fallos
  3. 7.3 · MLOps para modelos de trading

    1. 7.3.1 Registro de experimentos, modelos y datos: MLflow, DVC y una ejecución reproducida de principio a fin
    2. 7.3.2 Reentrenamiento programado, validación automática y champion/challenger
    3. 7.3.3 Monitorización del modelo en vivo y desactivación automática
    4. 7.3.4 Servir modelos: ONNX, inferencia en CPU o GPU y el coste de los LLM en producción
  4. 7.4 · Regulación, ética y fiscalidad

    1. 7.4.1 MiFID II: clientes, idoneidad y trading algorítmico
    2. 7.4.2 ESMA y CFD: apalancamiento, cierre y saldo negativo
    3. 7.4.3 PRIIPs, KID y ETFs estadounidenses
    4. 7.4.4 MiCA después del transitorio: comprobar un proveedor en los registros
    5. 7.4.5 Abuso de mercado: spoofing, layering y el bot que los imita sin querer
    6. 7.4.6 Fiscalidad del trading en España
    7. 7.4.7 Ética: datos personales, inteligencia artificial y la venta de señales
  5. 7.5 · Carrera profesional quant

    1. 7.5.1 Roles del quant: seis oficios alrededor de un mismo sistema
    2. 7.5.2 Dónde trabajar: qué hace cada empresa y qué dicen los registros
    3. 7.5.3 Portfolio público y entrevistas: enseñar criterio, no rentabilidades
    4. 7.5.4 Certificaciones, formación y prop firms: qué certifica cada cosa y qué mide un examen
  6. 7.6 · Capstone final

    1. 7.6.1 Capstone: requisitos comunes, rúbrica del tribunal y preparación de la defensa
    2. 7.6.2 Capstone C1: un sistema de futuros multi-activo, de la previsión a la cuenta de papel
    3. 7.6.3 Capstone C2: libro de opciones sistemático, superficie de volatilidad y griegas en tiempo real
    4. 7.6.4 Capstone C3: cartera de acciones y ETFs con ML, HRP y fiscalidad española
    5. 7.6.5 Capstone C4: una plataforma de investigación con LLMs y datos alternativos
    6. 7.6.6 Capstone C5: market making o arbitraje en cripto con control de contraparte

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.

Cadena OPRAControl de calidadSuperficie SVIStrikes por deltaTamaño por estrésLímites de griegasOrden combo XSPConciliaciónKill switch
Fuente 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
PiezaQué haceLibreríaDónde se aprendió
Control de calidaddescarta cruzadas, congeladas, bids nulos y violaciones de paridadpandas3.6.4
Volatilidad implícitade los precios medios, contrastada con vollib y QuantLibnumpy, vollib, QuantLib3.4.4
SuperficieSVI por vencimiento, sin arbitraje de mariposascipy3.4.5
GriegasBSM vectorizado en USD por 1 % y por puntonumpy3.4.3, 4.2.6
Tamañopeor pérdida en una rejilla de caídas y en escenarios de estrésnumpy3.5.4, 3.6.1
Ejecuciónorden combinada al broker de papelib_async4.4.2, 4.5.2
Conciliaciónposiciones, ejecuciones y efectivo contra el extractopandas, SQLite4.5.5
Monitorlímites diarios, kill switch, alertasPrometheus o logs estructurados7.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/):

ArchivoQué aporta al proyecto
opciones3_cadena.parquetcadena diaria de los tres vencimientos mensuales más cercanos: bid, ask, volumen e interés abierto
opciones3_indice.csvcierre de ORX, rentabilidad por dividendo y curva de tipos de 1 mes a 2 años
opciones3_verdad.parquetla superficie verdadera (SVI por día y vencimiento) y el forward: la respuesta conocida
opciones3_superficie.parquetla superficie completa de los miércoles, hasta 2 años, para la cobertura de otro vencimiento
opciones3_incidencias.csvdefectos plantados (congeladas, cruzadas, paridad violada, un día sin cadena) para probar el control de calidad
vol3_indices.csv, vol3_futuros.parquetel 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 SPX1Nota 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..

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 NBBO2Nota 2 · Cboe DataShop (https://datashop.cboe.com/): fichas «Option EOD Summary» y «Option Quotes». Consultada el 24 de octubre de 2026.. 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 suscripciones3Nota 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..
  • 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.

IN [01]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}")
OUT [01]Datos, motor de valoración y contraste
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:

IN [02]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)
OUT [02]Superficie: SVI por vencimiento y contraste con la verdad
        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 %.

IN [03]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")
OUT [03]Un ciclo: selección por delta, precios y tamaño
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.

IN [04]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)
OUT [04]El barrido de ciclos y el registro de pruebas
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.

IN [05]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}")
OUT [05]Walk-forward de la elección y Deflated Sharpe
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.

IN [06]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)
OUT [06]El libro día a día: griegas, límites y kill switch
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.

IN [07]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"])
OUT [07]Métricas e informe
                  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

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 eligeRiesgoControl en el proyecto
Delta vendida y alaelegir la mejor de muchas en la misma muestraregistro de pruebas, DSR con su N
Elección a lo largo del tiempoque la mejor de hoy no lo sea mañanawalk-forward anclado de la elección
Límites y kill switchajustar umbrales hasta que el histórico quede bonitoparámetros fijados en la semana 3, registrados si cambian
Datosdecidir con información que no se teníavolatilidad 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 5004Nota 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.. 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:

CONCILIANDOOPERANDOBLOQUEADOREDUCIENDOCERRANDOPAUSA
Fuente 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:

IN [08]python
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:

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

IN [09]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)
OUT [09]Los días fuera del límite de gamma
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
  1. 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.

IN [10]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}")
OUT [10]Lo que cuesta la horquilla
horquilla pagada: 0.27% del capital al año
Sharpe al bid/ask 0.63, al medio 0.66
  1. 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.

Footnotes

  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. ↩

Respuesta plegada en cada pregunta

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é?

    • APorque los escenarios al día siguiente no se pueden calcular con Black-Scholes
    • BPorque 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
    • CPorque la rejilla es más conservadora con la put sola, que es la que importa
    • DPorque el margen del spread es mayor que su pérdida máxima
    Ver respuesta

    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?

    • AQue la estrategia está validada, porque el PSR(0) es 0,931
    • BQue hay que repetir el barrido con más estructuras hasta pasar el 0,95
    • CQue 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
    • DQue el DSR no se aplica a opciones
    Ver respuesta

    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?

    • ASubir el límite de gamma al −1 %
    • BCubrir la delta cada hora
    • CVender el spread más lejos del dinero
    • DCerrar 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
    Ver respuesta

    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?

    • APorque 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
    • BPorque iv_modelo tiene errores
    • CPorque los precios medios son más baratos
    • DPorque QuantLib no acepta iv_modelo
    Ver respuesta

    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?

    • AAlargar el papel a un año
    • BEscalonar 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
    • CSimular las entradas que faltan con el backtest
    • DOperar opciones de 0 días para tener más operaciones
    Ver respuesta

    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

Datos