---
title: "3.7.3 Opciones cripto, on-chain y DeFi"
description: "Opciones inversas al estilo de Deribit, el DVOL y su prima, su API con un simulador local, un pool x·y = k con sándwiches MEV y señales on-chain."
url: "https://pythonparatrading.com/curso-python-trading/nivel-3/opciones-cripto-deribit-on-chain-y-defi/"
lang: "es"
image: "https://pythonparatrading.com/og/curso-python-trading/nivel-3/opciones-cripto-deribit-on-chain-y-defi.png"
---

# 3.7.3 · Opciones cripto, datos on-chain y DeFi

> Las opciones de bitcoin más negociadas cotizan y se liquidan en bitcoin, no en dólares, y eso cambia el pago, la delta y el riesgo del vendedor. Esta lección las valora sobre la cadena sintética de un exchange que imita a Deribit, mide la prima de su índice de volatilidad y baja después a la cadena de bloques: un pool de producto constante con ataques sándwich plantados y métricas on-chain con una señal débil y sus controles.

- Curso: [Nivel 3: Derivados: futuros, spreads, forex y opciones](https://pythonparatrading.com/curso-python-trading/nivel-3/)
- Módulo: 3.7 · Cripto derivados y estrategias específicas
- Duración: 75 min
- Requisitos: [3.7.1 · Perpetuos: mecánica del funding, mark price y liquidaciones](https://pythonparatrading.com/curso-python-trading/nivel-3/perpetuos-funding-mark-price-y-liquidaciones/), [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/), [1.3.5 · Datos de cripto y on-chain](https://pythonparatrading.com/curso-python-trading/nivel-1/datos-de-cripto-y-on-chain/)

## Al terminar esta lección

- Valorar una opción inversa (precio en moneda = Black-76 / forward), recuperar su volatilidad implícita y explicar por qué su pago y su delta no son los de una opción en dólares.
- Medir la prima de un índice de volatilidad implícita a 30 días tipo DVOL frente a la volatilidad realizada después, y presentar la venta de straddles con su peor semana.
- Consumir la API JSON-RPC de Deribit (cadena, DVOL paginado con `continuation`, nombres de instrumentos) contra un servidor local que la imita.
- Reproducir un pool x·y = k con comisión, medir su deslizamiento y detectar sándwiches MEV sin confundirlos con operaciones de ida y vuelta legítimas.
- Contrastar una señal on-chain contra controles sin efecto y con su error típico antes de tratarla como fuente de alfa.

## Tres capas del mercado cripto

Las lecciones 3.7.1 y 3.7.2 trataron los perpetuos, el funding y el arbitraje entre exchanges. Esta lección recorre
tres piezas más que un sistema cripto encuentra tarde o temprano, cada una con su lógica propia:

- **Opciones en exchanges centralizados.** Las opciones clásicas de Deribit sobre bitcoin y ether son **inversas**:
  se cotizan y se liquidan en la moneda subyacente. Las griegas, el margen y el
  riesgo del vendedor cambian respecto a una opción en dólares (lección 3.4.2).
- **DeFi.** En la cadena de bloques, un exchange descentralizado no tiene libro: un contrato con dos reservas fija el
  precio con una fórmula. El orden de las transacciones dentro de un bloque es negociable, y de ahí sale el MEV.
- **Datos on-chain.** La cadena publica flujos, direcciones y comisiones. Son datos abundantes y gratuitos, y por eso
  mismo una fuente fértil de correlaciones espurias.

Todo lo que se ejecuta usa los datos sintéticos del nivel 3. Los activos (XBQ imita a BTC, XEQ a ETH) y los exchanges
(boreal imita a Deribit) son ficticios; los efectos (la prima de volatilidad, 54 sándwiches, una correlación de −0,06
entre flujos y rendimiento) están **plantados** con parámetros conocidos, y cada método se contrasta con esa verdad y
con controles sin efecto. Ninguna cifra de estos datos es evidencia de que una estrategia funcione en el mercado real.

## Opciones inversas: precio en moneda, riesgo en dólares

En la API de Deribit, los contratos inversos (`reversed`) se liquidan en la moneda base, BTC para BTC-PERPETUAL, y en
las opciones la cantidad se expresa en la moneda subyacente: una opción es un contrato sobre una moneda[^1]. Conviven con
opciones lineales liquidadas en USDC, que se valoran como las de la lección 3.4.2. Las opciones de boreal en los datos
siguen el esquema inverso. Si $C$ es el precio de Black-76 en dólares con forward $F$ y tipo cero, el
precio en moneda y el pago al vencimiento son

$$
V = \frac{C(F, K, T, \sigma)}{F}, \qquad
\text{pago call} = \frac{\max(S_T - K, 0)}{S_T}, \qquad
\text{pago put} = \frac{\max(K - S_T, 0)}{S_T},
$$

con $S_T$ el índice al vencimiento. Multiplicado por $S_T$, el pago en dólares es el de siempre. Lo que cambia es la
unidad en la que se cobra, se paga la prima y se deposita el margen.

La foto es la del viernes 8 de marzo de 2024 a las 08:00 UTC, justo después del vencimiento semanal (las fotos de los
datos son semanales; en Deribit los vencimientos diarios son a las 08:00 UTC[^1]). El vencimiento elegido es el del 26
de abril, a 49 días:

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

op = pd.read_parquet("cripto3_opciones.parquet")
foto = pd.Timestamp("2024-03-08 08:00", tz="UTC")
venc = pd.Timestamp("2024-04-26 08:00")
c = op[(op["activo"] == "XBQ") & (op["ts"] == foto)
       & (op["vencimiento"] == venc)].copy()
F = c["precio_subyacente"].iloc[0]
S = c["precio_indice"].iloc[0]
T = (venc - foto.tz_localize(None)) / pd.Timedelta(days=365)
print(f"índice {S:,.1f} USD, forward {F:,.1f} USD, T = {T:.4f}")
cerca = c["strike"].between(56_000, 76_000)
c.loc[cerca, ["tipo", "strike", "bid", "ask", "mark_iv"]].head(8)
```
```output
índice 63,355.9 USD, forward 64,277.7 USD, T = 0.1342
       tipo   strike     bid     ask  mark_iv
152020    C  56000.0  0.1770  0.1805    74.65
152021    C  58000.0  0.1560  0.1595    73.38
152022    C  60000.0  0.1370  0.1400    72.29
152023    C  62000.0  0.1195  0.1225    71.40
152024    C  64000.0  0.1035  0.1065    70.72
152025    C  66000.0  0.0895  0.0925    70.23
152026    C  68000.0  0.0775  0.0800    69.95
152027    C  70000.0  0.0665  0.0690    69.84
```

Los precios son fracciones de XBQ: la call de 64.000, casi en el dinero, cuesta unos 0,10 XBQ, unos 6.700 USD. El
forward cotiza un 1,5 % por encima del índice: es el basis de los futuros (lección 3.7.2), y por eso la valoración usa
Black-76 sobre el forward y no Black-Scholes sobre el contado. Con la volatilidad del modelo (`mark_iv`) la fórmula
reproduce la cadena:

```python
from scipy.special import ndtr

def b76_usd(F, K, T, sigma, tipo):
    sd = sigma * np.sqrt(T)
    d1 = (np.log(F / K) + sd**2 / 2) / sd
    d2 = d1 - sd
    call = F * ndtr(d1) - K * ndtr(d2)
    put = K * ndtr(-d2) - F * ndtr(-d1)
    return np.where(tipo == "C", call, put)

c["medio"] = (c["bid"] + c["ask"]) / 2
c["modelo"] = b76_usd(F, c["strike"], T, c["mark_iv"] / 100,
                      c["tipo"]) / F
err = (c["medio"] - c["modelo"]).abs()
print(f"{len(c)} opciones; error máximo {err.max():.5f} XBQ")
print(f"horquilla mediana {(c['ask'] - c['bid']).median():.4f} XBQ")
c.loc[c["strike"] == 64_000,
      ["tipo", "bid", "ask", "modelo"]].round(5)
```
```output
46 opciones; error máximo 0.00062 XBQ
horquilla mediana 0.0030 XBQ
       tipo     bid     ask   modelo
152024    C  0.1035  0.1065  0.10504
152047    P  0.0990  0.1020  0.10072
```

El modelo y el punto medio difieren como mucho en 0,00062 XBQ, una quinta parte de la horquilla mediana (0,0030 XBQ):
lo que queda es el ruido del creador de mercado y el redondeo al tick (0,0001 por debajo de 0,005 XBQ y 0,0005 por
encima). En la dirección contraria, la volatilidad implícita se obtiene pasando el punto medio a dólares y usando el
inversor de Black de `vollib`, la librería a la que `py_vollib` 1.0.12 redirige sus importaciones (importarla
directamente evita el `DeprecationWarning` que emite `py_vollib`; la lección 3.4.4 trata los métodos de inversión):

```python
from vollib.black.implied_volatility import implied_volatility

def iv_inversa(fila):
    usd = fila["medio"] * F          # de moneda a USD
    return 100 * implied_volatility(usd, F, fila["strike"], 0.0, T,
                                    fila["tipo"].lower())

c["iv_medio"] = c.apply(iv_inversa, axis=1)
dif = (c["iv_medio"] - c["mark_iv"])[cerca]
print(f"diferencia con mark_iv: media {dif.mean():+.2f}, "
      f"máxima {dif.abs().max():.2f} puntos")
```
```output
diferencia con mark_iv: media -0.02, máxima 0.20 puntos
```

Entre 56.000 y 76.000 la implícita del medio se separa de la del modelo como mucho 0,20 puntos. Fuera de esa zona, con
primas de pocos ticks, el redondeo pesa más y la implícita de un precio medio es menos fiable: es el mismo problema de
las alas de la lección 3.4.5, agravado por un tick fijo en moneda.

### El pago en moneda

La figura de la cabecera dibuja el pago en XBQ de una call y una put de strike 64.000. La asimetría salta a la vista:

```python
def pago_moneda(S_T, K, tipo):
    usd = np.maximum(S_T - K, 0) if tipo == "C" \
        else np.maximum(K - S_T, 0)
    return usd / S_T

K = 64_000
for S_T in [32_000, 48_000, 64_000, 96_000, 256_000]:
    print(f"S_T = {S_T:>7,}: call {pago_moneda(S_T, K, 'C'):.3f}"
          f"  put {pago_moneda(S_T, K, 'P'):.3f} XBQ")
```
```output
S_T =  32,000: call 0.000  put 1.000 XBQ
S_T =  48,000: call 0.000  put 0.333 XBQ
S_T =  64,000: call 0.000  put 0.000 XBQ
S_T =  96,000: call 0.333  put 0.000 XBQ
S_T = 256,000: call 0.750  put 0.000 XBQ
```

La call inversa se acerca a 1 XBQ sin llegar nunca: a 256.000 USD paga 0,75 XBQ (192.000 USD). La put, en cambio,
paga 1 XBQ con el índice a 32.000 y crecería sin límite si siguiera cayendo. Para el comprador da igual en dólares;
para el **vendedor de puts con el margen en moneda**, es el peor caso posible: debe más monedas justo cuando cada
moneda de su colateral vale menos. Por eso los exchanges de opciones inversas piden márgenes que crecen deprisa con la
caída, y por eso un vendedor de puts que piensa en dólares cubre el valor en dólares de su colateral.

### La delta cambia de unidad

Si el resultado se mide en moneda, la sensibilidad del valor en moneda al forward es

$$
\frac{\partial V}{\partial F} = \frac{\partial (C/F)}{\partial F} = \frac{N(d_1) - V}{F},
$$

es decir, la posición se comporta como $N(d_1) - V$ monedas de exposición lineal, no como $N(d_1)$. La prima pagada en
moneda resta exposición: quien compró la opción ya entregó $V$ monedas.

```python
def delta_b76(F, K, T, sigma, tipo):
    sd = sigma * np.sqrt(T)
    d1 = (np.log(F / K) + sd**2 / 2) / sd
    return np.where(tipo == "C", ndtr(d1), ndtr(d1) - 1)

c["delta_usd"] = delta_b76(F, c["strike"], T, c["mark_iv"] / 100,
                           c["tipo"])
c["delta_moneda"] = c["delta_usd"] - c["modelo"]
sel = c["strike"].isin([48_000, 64_000, 80_000])
c.loc[sel, ["tipo", "strike", "modelo", "delta_usd",
            "delta_moneda"]].round(3)
```
```output
       tipo   strike  modelo  delta_usd  delta_moneda
152018    C  48000.0   0.275      0.871         0.595
152024    C  64000.0   0.105      0.558         0.453
152030    C  80000.0   0.033      0.240         0.207
152041    P  48000.0   0.022     -0.129        -0.152
152047    P  64000.0   0.101     -0.442        -0.543
152053    P  80000.0   0.277     -0.760        -1.037
```

La call de 64.000 pasa de una delta de 0,558 a 0,453; la put de 80.000, muy en el dinero, de −0,760 a −1,037: más de
una moneda de exposición corta por contrato. Una cobertura calculada con la delta de los libros de texto deja, en una
cuenta en moneda, una exposición residual del tamaño de la prima. Las griegas que publica un exchange pueden seguir una
u otra convención: hay que leer en su documentación cuál usa antes de cubrir con ellas.

## El DVOL y la prima de volatilidad

Deribit publica un índice de volatilidad de sus opciones, el DVOL, que su API sirve con
`public/get_volatility_index_data` como velas de cinco valores (marca de tiempo, apertura, máximo, mínimo y cierre)[^2].
En los datos, `cripto3_dvol.csv` imita ese índice con una definición conocida: a las 00:00 UTC de cada día, 100 veces
la raíz de la varianza implícita a 30 días. La pregunta útil es la de la lección 3.4.6: ¿cuánto se paga, de media, por encima de la
volatilidad que luego se realiza?

```python
dv = pd.read_csv("cripto3_dvol.csv", parse_dates=["fecha"])
dia = pd.read_parquet("cripto3_diario.parquet")
indice = (dia.groupby(["fecha", "activo"])["cierre_spot"]
          .median().unstack())          # el índice: mediana de spots
r = np.log(indice).diff()
rv_sig = r.rolling(30).std().shift(-30) * math.sqrt(365) * 100

for a in ["XBQ", "XEQ"]:
    x = dv[dv["activo"] == a].set_index("fecha")["dvol"]
    prima = (x - rv_sig[a].reindex(x.index)).dropna()
    print(f"{a}: DVOL medio {x.mean():.1f}, prima media "
          f"{prima.mean():.1f} puntos, negativa el "
          f"{(prima < 0).mean():.0%} de los días")
```
```output
XBQ: DVOL medio 64.0, prima media 9.5 puntos, negativa el 17% de los días
XEQ: DVOL medio 80.8, prima media 14.4 puntos, negativa el 19% de los días
```

*Fig. — Índice de volatilidad implícita a 30 días de XBQ (tipo DVOL) y volatilidad realizada en los 30 días siguientes.* Dos líneas semanales de 2020 a 2025 en puntos de volatilidad anual. El índice implícito, destacado, va casi siempre por encima de la realizada después: la prima media es de 9,5 puntos. La realizada supera al índice en los choques (marzo de 2020, mayo de 2021, junio y noviembre de 2022), cuando sube hasta 131.

La prima media es de 9,5 puntos en XBQ y 14,4 en XEQ, la respuesta plantada. El índice no es una predicción sesgada
por error: es el precio de un seguro, y la prima es lo que cobra quien lo vende. La figura enseña el coste: en marzo de
2020, mayo de 2021 y junio y noviembre de 2022 la realizada supera al índice con holgura, y la prima es negativa el
17 % de los días en XBQ y el 19 % en XEQ.

Para un índice publicado, la comprobación natural es recalcularlo desde la cadena. Con la fórmula de varianza de Cboe
de la lección 3.4.6, sobre opciones fuera del dinero y con interpolación entre los vencimientos que rodean a los 30
días, sobre uno de cada ocho viernes:

```python
def var_cboe(g, T, F):
    """Varianza al estilo Cboe con opciones OTM, r = 0 (USD)."""
    piv = g.pivot_table(index="strike", columns="tipo",
                        values="usd")
    piv = piv.dropna()
    ks = piv.index.to_numpy()
    K0 = ks[ks <= F].max()
    q = np.where(ks < K0, piv["P"], piv["C"])
    q[ks == K0] = piv.loc[K0].mean()
    dk = np.gradient(ks)
    return 2 / T * np.sum(dk / ks**2 * q) - (F / K0 - 1)**2 / T

def indice_30d(cadena, t0):
    vs = []
    for v, g in cadena.groupby("vencimiento"):
        Tv = (v - t0) / pd.Timedelta(days=365)
        vs.append((Tv, var_cboe(g, Tv, g["precio_subyacente"].iloc[0])))
    T30 = 30 / 365
    (T1, v1), = [x for x in vs if x[0] <= T30][-1:]
    (T2, v2), = [x for x in vs if x[0] > T30][:1]
    w = (T2 - T30) / (T2 - T1)
    return 100 * math.sqrt((T1 * v1 * w + T2 * v2 * (1 - w)) / T30)

xbq = op[op["activo"] == "XBQ"].copy()
xbq["usd"] = (xbq["bid"] + xbq["ask"]) / 2 * xbq["precio_subyacente"]
filas = []
for ts in sorted(xbq["ts"].unique())[::8]:
    t0 = ts.tz_localize(None)
    filas.append((t0, indice_30d(xbq[xbq["ts"] == ts], t0)))
calc = pd.DataFrame(filas, columns=["t", "calculado"]).set_index("t")
oficial = dv[dv["activo"] == "XBQ"].set_index("fecha")["dvol"]
calc["dvol"] = oficial.reindex(calc.index.normalize()).to_numpy()
d = calc["calculado"] - calc["dvol"]
print(f"{len(calc)} viernes: diferencia media {d.mean():+.2f}, "
      f"rango {d.min():+.2f} a {d.max():+.2f} puntos")
```
```output
33 viernes: diferencia media -0.90, rango -3.80 a +0.27 puntos
```

El recálculo queda de media 0,9 puntos por debajo del índice, con una diferencia de hasta 3,8 puntos. Hay tres razones,
y las tres aparecen también con datos reales: la cadena se corta a unas tres desviaciones típicas del forward y la
fórmula pierde la contribución de las alas; los precios son puntos medios con horquilla; y la foto es de las 08:00
UTC, ocho horas después del dato del índice. Un índice de proveedor y uno propio no coinciden al decimal; lo que hay
que comprobar es que el sesgo sea pequeño y estable.

### Vender la prima, con su cola

La forma más directa de cobrar la prima es vender cada viernes el straddle ATM que vence a la semana siguiente, al bid,
y liquidarlo al vencimiento:

```python
res = []
for a in ["XBQ", "XEQ"]:
    o = op[op["activo"] == a]
    fotos = sorted(o["ts"].unique())
    for d0, d1 in zip(fotos[:-1], fotos[1:]):
        sub = o[(o["ts"] == d0)
                & (o["vencimiento"] == d1.tz_localize(None))]
        if sub.empty:
            continue
        S0 = sub["precio_indice"].iloc[0]
        Fw = sub["precio_subyacente"].iloc[0]
        k = sub["strike"].iloc[(sub["strike"] - Fw).abs().argmin()]
        prima = sub.loc[sub["strike"] == k, "bid"].sum() * S0
        S1 = o.loc[o["ts"] == d1, "precio_indice"].iloc[0]
        res.append((a, d0, (prima - abs(S1 - k)) / S0))
st = pd.DataFrame(res, columns=["activo", "ts", "pnl"])
resumen = st.groupby("activo")["pnl"].agg(
    semanas="size", media="mean", peor="min",
    ganadoras=lambda s: (s > 0).mean())
(100 * resumen.drop(columns="semanas")).round(2).assign(
    semanas=resumen["semanas"])
```
```output
        media   peor  ganadoras  semanas
activo
XBQ      0.03 -18.93      56.54      260
XEQ     -0.13 -26.93      56.54      260
```

El resultado es casi nulo: un 0,03 % del subyacente por semana en XBQ y un −0,13 % en XEQ, con un 56,5 % de semanas
ganadoras en los dos (147 de 260). Las peores semanas pierden un 18,9 % y un 26,9 % del subyacente. La prima de 9,5
puntos existe, pero la horquilla de la venta al bid y las semanas de choque se la comen. Vender opciones semanales
cripto sin cobertura no es una renta: es una exposición corta a los saltos, y los saltos de este mercado son grandes
(la lección 3.6.1 trata la venta sistemática de prima y su gestión del riesgo de cola).

## La API de Deribit, contra un servidor local

Deribit ofrece la misma API por tres vías: JSON-RPC sobre WebSocket (la recomendada para casi todo), JSON-RPC sobre
HTTP y FIX[^2]. Los métodos públicos de datos se pueden pedir con un GET a `https://www.deribit.com/api/v2/public/<método>`;
la respuesta es un sobre JSON-RPC con `result` o `error`. Este es el código real, que necesita red:

```python skip title="deribit_rest.py"
import json
import urllib.parse
import urllib.request

BASE = "https://www.deribit.com/api/v2"

def publico(metodo, **params):
    url = f"{BASE}/public/{metodo}?" + urllib.parse.urlencode(params)
    with urllib.request.urlopen(url, timeout=10) as r:
        resp = json.load(r)
    if "error" in resp:
        raise RuntimeError(resp["error"])
    return resp["result"]

cadena = publico("get_book_summary_by_currency",
                 currency="BTC", kind="option")
dvol = publico("get_volatility_index_data", currency="BTC",
               start_timestamp=1_735_689_600_000,
               end_timestamp=1_738_368_000_000, resolution="1D")
```

`get_book_summary_by_currency` devuelve un resumen por instrumento: `instrument_name`, `bid_price`, `ask_price`,
`mark_price`, `mark_iv`, `underlying_price`, `open_interest` e `interest_rate`, entre otros[^2]. Los límites de
peticiones se cuentan en créditos: con la configuración por defecto, cada petición que no toca el motor de casamiento
cuesta 500 créditos, la reserva máxima es de 50.000 y se recargan 10.000 por segundo (20 peticiones por segundo
sostenidas); al agotarlos, la respuesta es el error `too_many_requests` (código 10028)[^2]. Para seguir una cadena
entera en tiempo real, la documentación recomienda las suscripciones por WebSocket en lugar de consultar en bucle.

Para ejecutar el cliente sin red, un servidor local en un hilo imita esos dos métodos con la foto de boreal y el
índice de los datos, incluida una regla de paginación: un máximo de 100 velas por respuesta y `continuation` como el
`end_timestamp` de la petición siguiente, que es como lo describe la documentación de Deribit[^2]:

```python
import json
import threading
import urllib.parse
import urllib.request
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer

MESES = "JAN FEB MAR APR MAY JUN JUL AUG SEP OCT NOV DEC".split()

def nombre(activo, venc, strike, tipo):
    f = f"{venc.day}{MESES[venc.month - 1]}{venc:%y}"
    return f"{activo}-{f}-{strike:.0f}-{tipo}"

cad = op[(op["activo"] == "XBQ") & (op["ts"] == foto)]
libro = [{"instrument_name": nombre("XBQ", v, k, t),
          "bid_price": b, "ask_price": a, "mark_iv": iv,
          "underlying_price": u, "open_interest": oi,
          "interest_rate": 0.0}
         for v, k, t, b, a, iv, u, oi in cad[
             ["vencimiento", "strike", "tipo", "bid", "ask",
              "mark_iv", "precio_subyacente", "interes_abierto"]
         ].itertuples(index=False)]
x = dv[dv["activo"] == "XBQ"]
velas = [[int(t.timestamp() * 1000), v, v, v, v]
         for t, v in zip(x["fecha"], x["dvol"])]

class Deribit(BaseHTTPRequestHandler):
    def log_message(self, *a):
        pass

    def do_GET(self):
        u = urllib.parse.urlparse(self.path)
        q = dict(urllib.parse.parse_qsl(u.query))
        if u.path.endswith("get_book_summary_by_currency"):
            res = libro if q.get("kind") == "option" else []
        elif u.path.endswith("get_volatility_index_data"):
            t0, t1 = int(q["start_timestamp"]), int(q["end_timestamp"])
            dentro = [v for v in velas if t0 <= v[0] <= t1]
            pagina = dentro[-100:]           # máximo 100 por respuesta
            sigue = pagina[0][0] - 1 if len(dentro) > 100 else None
            res = {"data": pagina, "continuation": sigue}
        else:
            res = None
        cuerpo = json.dumps({"jsonrpc": "2.0", "result": res}).encode()
        self.send_response(200)
        self.send_header("Content-Type", "application/json")
        self.end_headers()
        self.wfile.write(cuerpo)

srv = ThreadingHTTPServer(("127.0.0.1", 0), Deribit)
threading.Thread(target=srv.serve_forever, daemon=True).start()
BASE = f"http://127.0.0.1:{srv.server_address[1]}/api/v2"

def publico(metodo, **params):
    url = f"{BASE}/public/{metodo}?" + urllib.parse.urlencode(params)
    with urllib.request.urlopen(url, timeout=10) as r:
        resp = json.load(r)
    if "error" in resp:
        raise RuntimeError(resp["error"])
    return resp["result"]

cadena = pd.DataFrame(publico("get_book_summary_by_currency",
                              currency="XBQ", kind="option"))
print(len(cadena), "opciones;", cadena["instrument_name"].iloc[0])
```
```output
358 opciones; XBQ-15MAR24-48000-C
```

El cliente es el mismo de antes: solo cambia `BASE`. La paginación avanza hacia atrás en el tiempo, porque
`continuation` es un nuevo final, no un nuevo principio:

```python
def dvol_historico(desde_ms, hasta_ms):
    filas, fin, llamadas = [], hasta_ms, 0
    while fin is not None:
        r = publico("get_volatility_index_data", currency="XBQ",
                    start_timestamp=desde_ms, end_timestamp=fin,
                    resolution="1D")
        llamadas += 1
        filas = r["data"] + filas
        fin = r["continuation"]
    return filas, llamadas

ms = lambda s: int(pd.Timestamp(s, tz="UTC").timestamp() * 1000)
filas, n = dvol_historico(ms("2020-01-01"), ms("2025-12-31"))
print(f"{len(filas)} velas diarias en {n} llamadas; "
      f"duplicadas: {len(filas) - len({f[0] for f in filas})}")
```
```output
2192 velas diarias en 22 llamadas; duplicadas: 0
```

Veintidós llamadas para seis años de velas diarias, sin duplicados porque el servidor corta justo antes de la primera
vela entregada. Con velas de un minuto y páginas de 100, como en el simulador, serían más de 30.000 llamadas: a 20 por segundo, casi
media hora de descarga.

Los nombres de los instrumentos siguen la plantilla `MONEDA-DMMMAA-STRIKE-TIPO` (día de uno o dos dígitos, mes en
tres letras inglesas, `C` o `P`)[^3]. Convertirlos en columnas es el primer paso de cualquier análisis de la cadena:

```python
partes = cadena["instrument_name"].str.split("-", expand=True)
cadena["vencimiento"] = pd.to_datetime(partes[1], format="%d%b%y") \
    + pd.Timedelta(hours=8)
cadena["strike"] = partes[2].astype(float)
cadena["tipo"] = partes[3]
resumen = cadena.groupby("vencimiento").agg(
    strikes=("strike", "nunique"), oi=("open_interest", "sum"))
resumen
```
```output
                     strikes      oi
vencimiento
2024-03-15 08:00:00       15   485.4
2024-03-22 08:00:00       16   604.6
2024-03-29 08:00:00       18   690.2
2024-04-26 08:00:00       23   977.7
2024-05-31 08:00:00       29  1358.1
2024-06-28 08:00:00       32  1564.8
2024-09-27 08:00:00       46  2613.8
```

Siete vencimientos: dos semanales, los últimos viernes de marzo, abril y mayo, y los trimestrales de junio y
septiembre. El número de strikes y el interés
abierto crecen con el plazo, porque la ventana de strikes se abre con la raíz del tiempo. El formato `%d%b%y` de pandas
lee `15MAR24` con la configuración regional en inglés por defecto; en un sistema con otra configuración regional, lo
seguro es traducir el mes con un diccionario.

```mermaid
flowchart TB
  A[get_instruments] --> B[book_summary]
  B --> C[Nombre a columnas]
  C --> D[IV y griegas]
  E[volatility_index] --> F[continuation]
  F --> E
  D --> G[Cadena diaria]
  F --> G
```

El diagrama resume la recogida diaria de una cadena: instrumentos vivos, resumen de precios, nombres convertidos en
vencimiento, strike y tipo, implícitas y griegas recalculadas con la convención propia, y el índice de volatilidad
paginado hasta el último dato guardado.

## DeFi: un pool de producto constante

Uniswap v2, uno de los diseños de referencia de exchange descentralizado, mantiene dos reservas $x$ e $y$ y solo acepta swaps
que no reduzcan su producto. Cada swap paga una comisión fija del 0,30 % que se añade a las reservas, de modo que $k$
crece con cada operación[^4]. Con la comisión $c$ sobre la entrada, quien entrega $\Delta x$ recibe

$$
\Delta y = \frac{y \, (1 - c)\, \Delta x}{x + (1 - c)\, \Delta x},
$$

y el precio marginal del pool es $y/x$. El pool de los datos, XEQ/USDQ, sigue esa fórmula durante siete días desde el
4 de marzo de 2024, con un bloque cada 12 segundos, minoristas, arbitrajistas que devuelven el precio a la banda de la
comisión alrededor del precio de los exchanges centralizados y tres buscadores de MEV:

```python
sw = pd.read_parquet("defi3_swaps.parquet")
sw = sw.sort_values(["bloque", "posicion"]).reset_index(drop=True)
COM = 0.003

def salida(dx, x, y, com=COM):
    dxc = dx * (1 - com)
    return y * dxc / (x + dxc)

x0 = sw["reserva_xeq"].shift()
y0 = sw["reserva_usdq"].shift()
entra_xeq = sw["token_entrada"] == "XEQ"
pred = salida(sw["importe_entrada"], np.where(entra_xeq, x0, y0),
              np.where(entra_xeq, y0, x0))
err = (pred - sw["importe_salida"]).abs() / sw["importe_salida"]
k = sw["reserva_xeq"] * sw["reserva_usdq"]
print(f"{len(sw)} swaps en {sw['bloque'].nunique()} bloques")
print(f"error relativo mediano {err.iloc[1:].median():.1e}")
print(f"k sube un {k.iloc[-1] / k.iloc[0] - 1:.3%} en 7 días")
```
```output
18489 swaps en 15438 bloques
error relativo mediano 4.2e-07
k sube un 2.557% en 7 días
```

Cada salida se reproduce con la fórmula desde las reservas del swap anterior (el error relativo mediano, $4{,}2 \cdot 10^{-7}$, es el
redondeo a seis decimales de los datos), y $k$ crece un 2,6 % en una semana: son las comisiones que se quedan los
proveedores de liquidez. El coste de operar contra el pool depende del tamaño:

```python
x, y = sw[["reserva_xeq", "reserva_usdq"]].iloc[0]
p0 = y / x
for usd in [1_000, 5_000, 25_000, 100_000, 500_000]:
    recibe = salida(usd, y, x)               # USDQ entra, XEQ sale
    pagado = usd / recibe
    print(f"{usd:>8,} USDQ: precio {pagado:,.2f} "
          f"({pagado / p0 - 1:+.2%} sobre {p0:,.2f})")
```
```output
   1,000 USDQ: precio 3,356.11 (+0.31% sobre 3,345.80)
   5,000 USDQ: precio 3,357.11 (+0.34% sobre 3,345.80)
  25,000 USDQ: precio 3,362.12 (+0.49% sobre 3,345.80)
 100,000 USDQ: precio 3,380.88 (+1.05% sobre 3,345.80)
 500,000 USDQ: precio 3,480.92 (+4.04% sobre 3,345.80)
```

*Fig. — Sobreprecio de una compra de XEQ en el pool x·y = k según su tamaño, comisión del 0,30 % incluida.* Curva creciente con el tamaño en escala logarítmica, de 1.000 a 1.000.000 USDQ. Hasta unos 10.000 USDQ el sobreprecio apenas pasa de la comisión (0,30 %); a 5.000 USDQ, el umbral que miran los buscadores de MEV, es 0,34 %; a 1.000.000 USDQ llega al 7,8 %.

Hasta unos miles de USDQ el coste es casi solo la comisión; a partir de ahí crece deprisa: un 1,05 % por encima del
precio para 100.000 USDQ y un 4 % para 500.000. Un libro de órdenes (lección 1.1.3) tiene profundidad que se repone;
un pool tiene una curva fija hasta que alguien añade liquidez o un arbitrajista lo devuelve al precio de fuera.

## MEV: sándwiches dentro de un bloque

Una transacción enviada a la red espera en la mempool pública hasta que un constructor la incluye en un bloque. Los
**buscadores** vigilan esa mempool y envían transacciones que ganan dinero con el orden; los **constructores** ordenan
las transacciones y montan los bloques; el **proponente** (el validador de turno) publica el bloque. A lo que se puede
extraer reordenando se le llama MEV (maximal extractable value)[^5]. Un **sándwich** es el caso de manual: el buscador
ve una compra grande, compra justo antes, deja que la víctima compre más cara y vende justo después.

```mermaid
flowchart TB
  U[Usuario] --> MP[Mempool pública]
  U --> PR[Canal privado]
  MP --> BU[Buscador MEV]
  BU --> CO[Constructor]
  MP --> CO
  PR --> CO
  CO --> PO[Proponente]
  PO --> BL[Bloque]
```

El canal privado es la defensa: servicios como Flashbots Protect envían la transacción a una mempool privada donde los
bots de sándwich no la ven[^6]. En los datos, el 20 % de los minoristas lo usa y no puede ser atacado. Dentro del
bloque, el sándwich deja una firma de tres transacciones seguidas:

```mermaid
flowchart LR
  A[Compra atacante] --> V[Compra víctima]
  V --> B[Venta atacante]
```

Un detector que solo busca «el mismo remitente compra y vende en el mismo bloque» encuentra también otras cosas. El
generador plantó 54 bloques de control en los que un arbitrajista compra y deshace la compra en dos transacciones
seguidas, sin nadie en medio: misma dirección de entrada y salida que un sándwich, sin víctima.

```python
verdad = pd.read_csv("defi3_sandwich_verdad.csv")

def detectar(sw, estricto):
    hallados = []
    for b, g in sw.groupby("bloque"):
        g = g.reset_index(drop=True)
        for i in range(len(g) - 1):
            a = g.iloc[i]
            for j in range(i + 1, len(g)):
                z = g.iloc[j]
                vuelta = (a["remitente"] == z["remitente"]
                          and a["token_entrada"] == z["token_salida"])
                if not vuelta:
                    continue
                if not estricto:
                    hallados.append((b, a["posicion"]))
                elif j == i + 2:
                    m = g.iloc[i + 1]
                    if (m["remitente"] != a["remitente"] and
                            m["token_entrada"] == a["token_entrada"]):
                        hallados.append((b, a["posicion"]))
    return set(hallados)

real = set(zip(verdad["bloque"], verdad["pos_frontrun"]))
for estricto in (False, True):
    h = detectar(sw[sw.duplicated("bloque", keep=False)], estricto)
    acierta = len(h & real)
    print(f"estricto={estricto}: {len(h)} marcados, {acierta} reales,"
          f" precisión {acierta / len(h):.0%}, "
          f"cobertura {acierta / len(real):.0%}")
```
```output
estricto=False: 108 marcados, 54 reales, precisión 50%, cobertura 100%
estricto=True: 54 marcados, 54 reales, precisión 100%, cobertura 100%
```

El detector laxo marca 108 casos: los 54 sándwiches y los 54 controles, con una precisión del 50 %. El estricto exige
la tríada (atacante, otro remitente en el mismo sentido, atacante deshaciendo justo después) y acierta los 54 sin un
solo falso positivo. Con datos reales la regla estricta tampoco basta: hay sándwiches con varias víctimas, atacantes
que usan contratos distintos para cada pata y ataques repartidos entre bloques. La lección es la de siempre: un
detector se mide contra una verdad conocida y contra controles que se le parecen.

Con la verdad del generador se puede seguir el dinero de cada ataque:

```python
filas = []
for t in verdad.itertuples():
    g = sw[sw["bloque"] == t.bloque].set_index("posicion")
    f, v, bk = (g.loc[t.pos_frontrun], g.loc[t.pos_victima],
                g.loc[t.pos_backrun])
    px = f["precio_cex"]
    a_usd = 1.0 if f["token_entrada"] == "USDQ" else px
    b_usd = px if f["token_entrada"] == "USDQ" else 1.0
    lp = COM * (f["importe_entrada"] * a_usd
                + bk["importe_entrada"] * b_usd)
    filas.append((lp, t.soborno_usd, t.gas_usd,
                  t.beneficio_neto_usd, t.perdida_victima_usd))
rep = pd.DataFrame(filas, columns=["comision_lp", "propina",
                                   "gas", "neto_atacante",
                                   "perdida_victima"]).sum()
print(rep.round(0))
print(f"suma de destinos / pérdida: "
      f"{rep.iloc[:4].sum() / rep['perdida_victima']:.3f}")
```
```output
comision_lp        20972.0
propina            11036.0
gas                 2099.0
neto_atacante       5966.0
perdida_victima    39543.0
dtype: float64
suma de destinos / pérdida: 1.013
```

*Fig. — Adónde van los 39.543 USD que pierden las víctimas de los 54 sándwiches.* Barras horizontales en miles de USD: comisiones del pool 21,0; propina al constructor del bloque 11,0; beneficio neto del atacante 6,0 (destacado); gas 2,1. El atacante se queda alrededor de un 15 % de lo que pierden las víctimas.

Las víctimas pierden 39.543 USD frente a ejecutar sin ataque, un 0,9 % de lo que operaban. De esa pérdida, unos
21.000 USD acaban en las comisiones que el atacante paga al pool en sus dos patas (los proveedores de liquidez cobran
el sándwich), 11.036 en propinas al constructor para conseguir el orden, 2.099 en gas y solo 5.966 en el bolsillo del
atacante. Los cuatro destinos suman la pérdida con un 1,3 % de diferencia, porque la comisión de las patas también
cambia el estado final del pool. La competencia entre buscadores empuja la propina hacia el beneficio bruto: el MEV
lo cobra sobre todo quien controla el orden.

Para un sistema de trading en DeFi, la consecuencia práctica es fijar una tolerancia de deslizamiento ajustada (el
generador la toma de 0,5, 1 o 3 %, y el atacante compra lo máximo que deja a la víctima justo en su mínimo) y enviar
las órdenes grandes por un canal privado.

## On-chain como fuente de señales

Las métricas on-chain (lección 1.3.5) tienen una historia larga y atractiva: entradas netas en exchanges como anuncio
de ventas, direcciones activas como medida de uso. `onchain3_metricas.csv` tiene una relación plantada: la correlación
entre el flujo neto hacia exchanges de hoy y el rendimiento de mañana es −0,06 en los dos activos. La variación de las
direcciones activas y de las transacciones son controles sin relación con el rendimiento. El rendimiento es el del
índice, la mediana de los contados de los exchanges al cierre de las 23:00 UTC:

```python
oc = pd.read_csv("onchain3_metricas.csv", parse_dates=["fecha"])
filas = []
for a in ["XBQ", "XEQ"]:
    o = oc[oc["activo"] == a].set_index("fecha")
    r1 = np.log(indice[a].reindex(o.index)).diff().shift(-1)
    cand = {"flujo_neto": o["flujo_neto_exchanges"],
            "d_direcciones": np.log(o["direcciones_activas"]).diff(),
            "d_transacciones": np.log(o["transacciones"]).diff()}
    for nombre_s, s in cand.items():
        ok = s.notna() & r1.notna()
        rho = s[ok].corr(r1[ok])
        filas.append((a, nombre_s, rho, rho * math.sqrt(ok.sum())))
ic = pd.DataFrame(filas, columns=["activo", "serie", "corr", "t"])
ic.round(3)
```
```output
  activo            serie   corr      t
0    XBQ       flujo_neto -0.034 -1.594
1    XBQ    d_direcciones -0.033 -1.538
2    XBQ  d_transacciones -0.035 -1.658
3    XEQ       flujo_neto -0.097 -4.530
4    XEQ    d_direcciones  0.035  1.643
5    XEQ  d_transacciones  0.023  1.097
```

*Fig. — Correlación entre cada métrica on-chain de hoy y el rendimiento de mañana, con su intervalo del 95 %.* Seis puntos con barras de ±0,042. La línea discontinua marca la correlación plantada en el flujo neto, −0,06. Solo el flujo neto de XEQ (−0,097, destacado) queda lejos del cero; el de XBQ (−0,034) no se distingue de los cuatro controles, que dan entre −0,035 y 0,035.

Con $n$ ≈ 2.190 días, el error típico de una correlación cercana a cero es $1/\sqrt{n}$ ≈ 0,021, y $t = \rho\sqrt{n}$.
El flujo neto de XEQ (−0,097, $t$ = −4,5) se separa de todo. El de XBQ (−0,034, $t$ = −1,6) es indistinguible de los
controles: la variación de transacciones de XBQ da −0,035 y la de direcciones de XEQ, 0,035, sin ninguna relación
plantada. La verdad es la misma en los dos activos (−0,06); en una muestra de seis años, una sale casi el doble de
fuerte y la otra a mitad de camino del cero. Si se probaran veinte métricas, alguna de las de control pasaría de $t$ = 2
por azar.

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

Antes de usar una métrica on-chain como señal: comprobar que es point-in-time (los proveedores reetiquetan direcciones
de exchanges y recalculan la historia, lección 1.3.5), contrastarla con controles y con el número de métricas
probadas, y medirla neta de costes. Una correlación de −0,06 explica un 0,36 % de la varianza del día siguiente.

</aside>

## Ejercicios

1. Con `defi3_sandwich_verdad.csv` y los swaps, calcula la pérdida de cada víctima en porcentaje de su operación.
   ¿Cuál es la mediana, cuál la máxima y crece la pérdida con el tamaño?

**Solución.**

```python
pos = verdad.merge(sw, left_on=["bloque", "pos_victima"],
                   right_on=["bloque", "posicion"])
usd = np.where(pos["token_entrada"] == "USDQ",
               pos["importe_entrada"],
               pos["importe_entrada"] * pos["precio_cex"])
pos["perdida_pct"] = 100 * pos["perdida_victima_usd"] / usd
pos["tamano_usd"] = usd
print(pos["perdida_pct"].describe()[["50%", "max"]].round(2))
print("corr. de rangos con el tamaño:",
      round(pos["perdida_pct"].corr(pos["tamano_usd"],
                                    method="spearman"), 2))
```
```output
50%    0.50
max    2.98
Name: perdida_pct, dtype: float64
corr. de rangos con el tamaño: -0.41
```

La mediana es un 0,50 % y la máxima un 2,98 %: casi exactamente las tolerancias de deslizamiento que usan los
minoristas del generador (0,5 %, 1 % y 3 %, con el 0,5 % en el 60 % de los casos). El atacante compra lo máximo que
deja a la víctima en su mínimo aceptable, así que la víctima pierde casi toda su tolerancia. La correlación de rangos
con el tamaño es negativa (−0,41): en una operación grande, parte de la tolerancia ya se la come el propio
deslizamiento y queda menos margen para el atacante. La tolerancia no es un detalle de la interfaz: es lo que el
usuario regala si alguien le hace un sándwich.

2. Construye una regla diaria sobre XEQ: posición −1 (corta) si el flujo neto de hoy está por encima de su mediana de
   los 90 días anteriores y +1 (larga) si está por debajo, aplicada al rendimiento de mañana. Mide el Sharpe anual
   bruto y con un coste de 10 pb por unidad negociada (pasar de largo a corto son 20 pb), y haz lo mismo con el
   control de direcciones activas.

**Solución.**

```python
def regla(s, r1, coste=0.001):
    med = s.rolling(90).median().shift(1)
    pos = np.sign(med - s).where(med.notna())
    bruto = pos * r1
    neto = bruto - coste * pos.diff().abs()
    sh = lambda x: x.mean() / x.std() * math.sqrt(365)
    return (round(float(sh(bruto.dropna())), 2),
            round(float(sh(neto.dropna())), 2))

o = oc[oc["activo"] == "XEQ"].set_index("fecha")
r1 = np.log(indice["XEQ"].reindex(o.index)).diff().shift(-1)
print("flujo neto (bruto, neto):",
      regla(o["flujo_neto_exchanges"], r1))
print("control direcciones:",
      regla(np.log(o["direcciones_activas"]).diff(), r1))
```
```output
flujo neto (bruto, neto): (1.5, 0.97)
control direcciones: (-0.75, -1.31)
```

Con la señal plantada, la regla de XEQ da un Sharpe bruto de 1,5 y de 0,97 con los costes. El
control, sin ninguna relación con el rendimiento, da −0,75 bruto: la mitad de la magnitud de la señal real, por puro
azar y con signo contrario. Si el signo hubiera salido positivo, habría parecido una segunda señal. Son datos
sintéticos con un efecto plantado del tamaño que eligió el generador: el ejercicio enseña la escala de los falsos
positivos, no que el flujo neto prediga el rendimiento de ether. La regla gira de posición casi un día de cada dos
(el 48 % de los días), y con 20 pb por giro pierde un tercio de su Sharpe.

3. Vendes una put inversa de strike 64.000 sobre XBQ al bid de la foto (0,0990 XBQ, con el índice en 63.355,9 USD) y
   guardas 1 XBQ como margen. Al vencimiento el índice vale 32.000 USD. Calcula lo que debes en moneda y el resultado
   en dólares, y compáralo con una put lineal en USD de la misma prima en dólares.

**Solución.**

La put inversa debe (64.000 − 32.000)/32.000 = 1 XBQ. La prima cobrada, 0,0990 XBQ, valía 6.272 USD al venderla y
3.168 USD al vencimiento, así que el resultado de la opción es −0,901 XBQ, unos −28.832 USD. Una put lineal con la
misma prima en dólares (6.272 USD) pagaría 32.000 USD y perdería 25.728 USD: la diferencia, unos 3.100 USD, es la
pérdida de valor en dólares de la prima cobrada en moneda. A eso se suma el margen: 1 XBQ que valía 63.356 USD vale
32.000, y además se entrega casi entero para pagar la put. Un vendedor que piensa en dólares tiene que cubrir la
moneda del margen y de la prima, no solo la delta de la opción.

[^1]: Documentación de la API de Deribit: `public/get_instruments` (contratos `reversed` liquidados en la moneda base,
    `contract_size` de las opciones en la moneda), «Options Data Collection Best Practices» (las cantidades de las
    opciones van en la moneda subyacente) y `public/get_expirations` (vencimientos diarios a las 08:00 UTC),
    [docs.deribit.com](https://docs.deribit.com/api-reference/market-data/public-get_instruments), consultada el
    22/10/2026.
[^2]: Documentación de la API de Deribit: introducción (interfaces JSON-RPC sobre WebSocket y HTTP y FIX; servidores de
    producción y de pruebas), `public/get_book_summary_by_currency`, `public/get_volatility_index_data` y «Rate
    Limits» (créditos por defecto de las peticiones que no tocan el motor de casamiento),
    [docs.deribit.com/articles/rate-limits](https://docs.deribit.com/articles/rate-limits), consultada el 22/10/2026.
[^3]: Deribit, «JSON-RPC Overview», tabla de nombres de instrumentos (`BTC-25MAR23-420-C`; en las opciones lineales,
    `d` como separador decimal del strike),
    [docs.deribit.com/articles/json-rpc-overview](https://docs.deribit.com/articles/json-rpc-overview), consultada el
    22/10/2026.
[^4]: Uniswap, «Fees»: en v2 cada pool cobra una comisión fija del 0,30 % que se añade a las reservas y hace crecer
    $k$, [docs.uniswap.org](https://docs.uniswap.org/contracts/v2/concepts/advanced-topics/fees), consultada el
    22/10/2026.
[^5]: ethereum.org, «Maximal extractable value (MEV)»: buscadores, constructores, separación proponente-constructor y
    sándwiches, [ethereum.org](https://ethereum.org/en/developers/docs/mev/), consultada el 22/10/2026. El artículo de
    referencia sobre la reordenación de transacciones en DEX es Daian et al., «Flash Boys 2.0»
    ([arXiv:1904.05234](https://arxiv.org/abs/1904.05234)).
[^6]: Flashbots, «Flashbots Protect»: las transacciones se envían a una mempool privada, ocultas a los bots de
    frontrunning y de sándwich, [docs.flashbots.net](https://docs.flashbots.net/flashbots-protect/overview), consultada
    el 22/10/2026.

## Autoevaluación

1. Compras una call inversa de strike 64.000 USD y al vencimiento el índice vale 256.000 USD. ¿Cuánto cobras?

   - A. 192.000 USD en dólares
   - B. 0,75 XBQ, que valen 192.000 USD a ese precio
   - C. 3 XBQ
   - D. 1 XBQ, el máximo de la call

   *Respuesta B: 0,75 XBQ, que valen 192.000 USD a ese precio.* El pago de una call inversa es (S − K)/S monedas: (256.000 − 64.000)/256.000 = 0,75 XBQ. En dólares equivale a S − K, igual que una call lineal; en moneda, el pago de una call nunca llega a 1 XBQ.

2. ¿Por qué la put inversa vendida es especialmente peligrosa para quien guarda su margen en la propia moneda?

   - A. Porque su prima es menor que la de la call
   - B. Porque su pago en moneda, (K − S)/S, crece sin límite cuando el precio cae, a la vez que el colateral en moneda pierde valor en dólares
   - C. Porque las puts inversas no se pueden cerrar antes del vencimiento
   - D. Porque se liquidan en dólares

   *Respuesta B: Porque su pago en moneda, (K − S)/S, crece sin límite cuando el precio cae, a la vez que el colateral en moneda pierde valor en dólares.* Con S = 32.000 y K = 64.000 la put debe 1 XBQ; con S = 16.000, 3 XBQ. El vendedor paga más monedas justo cuando cada moneda de su colateral vale menos: el riesgo se multiplica por dos vías.

3. En los datos, el índice tipo DVOL de XBQ supera de media en 9,5 puntos a la volatilidad realizada en los 30 días siguientes. ¿Qué conclusión es correcta?

   - A. Vender volatilidad es una renta segura de 9,5 puntos
   - B. Hay una prima media, pero el straddle semanal vendido al bid gana un 0,03 % de media y pierde un 18,9 % en su peor semana: la prima se paga con colas
   - C. El índice está mal calculado
   - D. La prima solo existe en XEQ

   *Respuesta B: Hay una prima media, pero el straddle semanal vendido al bid gana un 0,03 % de media y pierde un 18,9 % en su peor semana: la prima se paga con colas.* La prima de la varianza implícita no es beneficio neto: hay que vender al bid, cubrir o asumir el movimiento, y los choques (2020, 2021, 2022) se llevan en una semana lo que se gana en meses. Además, el 17 % de los días la realizada supera al índice.

4. Un detector marca como sándwich cualquier par de swaps del mismo remitente en el mismo bloque y en sentidos opuestos. En el pool de los datos, ¿qué pasa?

   - A. Encuentra los 54 sándwiches y nada más
   - B. Encuentra los 54 y otros 54 falsos positivos: las idas y vueltas de un arbitrajista sin víctima entre medias
   - C. No encuentra ninguno
   - D. Encuentra la mitad

   *Respuesta B: Encuentra los 54 y otros 54 falsos positivos: las idas y vueltas de un arbitrajista sin víctima entre medias.* La firma del sándwich es una tríada: compra del atacante, swap de otro remitente en el mismo sentido y venta del atacante justo después. Sin exigir la víctima en medio, el detector marca 108 casos con una precisión del 50 %.

5. El flujo neto hacia exchanges de XBQ tiene una correlación de −0,034 con el rendimiento del día siguiente (t ≈ −1,6). ¿Qué se puede decir?

   - A. Es una señal significativa al 1 %
   - B. No se distingue de los controles (variación de direcciones y de transacciones), que dan correlaciones de la misma magnitud por azar
   - C. Prueba que las entradas a exchanges no anticipan nada
   - D. Que la señal plantada era cero

   *Respuesta B: No se distingue de los controles (variación de direcciones y de transacciones), que dan correlaciones de la misma magnitud por azar.* Con 2.190 días el error típico es ≈ 0,021, así que −0,034 está a 1,6 errores del cero, igual que los controles (−0,035 y 0,035). La señal plantada era −0,06: el estimador de XBQ es compatible con ella y con cero. Solo en XEQ (−0,097, t ≈ −4,5) se distingue.

## Ideas clave

- Una opción inversa cotiza en moneda: precio = Black-76(F, K, T, σ) / F y pago = max(S − K, 0)/S o max(K − S, 0)/S monedas. La call nunca paga 1 moneda; la put vendida puede deber varias.
- La delta en moneda es la delta de Black-76 menos el precio en moneda: un largo de call ATM pasa de 0,56 a 0,45, y una put en el dinero, de −0,76 a −1,04.
- El índice tipo DVOL de los datos supera a la realizada posterior en 9,5 puntos (XBQ) y 14,4 (XEQ), pero el straddle semanal vendido al bid apenas gana y su peor semana pierde un 18,9 % y un 26,9 % del subyacente.
- La API de Deribit es JSON-RPC sobre HTTP o WebSocket; el DVOL se pagina hacia atrás con `continuation` y los nombres `XBQ-26APR24-70000-C` llevan vencimiento, strike y tipo.
- En un pool x·y = k la comisión del 0,30 % se queda en las reservas y el deslizamiento crece con el tamaño; un sándwich es una tríada en el mismo bloque, y de los 39.543 USD que pierden las víctimas el atacante solo se queda unos 6.000.
- Una métrica on-chain se contrasta con controles y con su error típico: la señal plantada de −0,06 solo se ve con claridad en uno de los dos activos.

## Recursos

### Research

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

### Documentación

- [Deribit: public/get_book_summary_by_currency (campos de la cadena de opciones)](https://docs.deribit.com/api-reference/market-data/public-get_book_summary_by_currency) · docs.deribit.com
- [Deribit: public/get_volatility_index_data (DVOL, velas y continuation)](https://docs.deribit.com/api-reference/market-data/public-get_volatility_index_data) · docs.deribit.com
- [Uniswap v2: cómo funciona el producto constante y sus comisiones](https://docs.uniswap.org/contracts/v2/concepts/protocol-overview/how-uniswap-works) · docs.uniswap.org
- [ethereum.org: maximal extractable value (MEV), buscadores, constructores y sándwiches](https://ethereum.org/en/developers/docs/mev/) · ethereum.org

### Libros

- [Daian et al. (2019): Flash Boys 2.0, frontrunning y reordenación de transacciones en DEX](https://arxiv.org/abs/1904.05234) · arxiv.org

### Datos

- [Datos del nivel 3: opciones inversas semanales de boreal (Parquet)](https://pythonparatrading.com/curso/datos/nivel-3/cripto3_opciones.parquet) · Descarga · cripto3_opciones.parquet

Lección anterior: [3.7.2 · Arbitraje de funding, basis con futuros y arbitraje entre exchanges](https://pythonparatrading.com/curso-python-trading/nivel-3/arbitraje-de-funding-basis-y-entre-exchanges/)

Lección siguiente: [3.7.4 · ccxt para operar en varios exchanges](https://pythonparatrading.com/curso-python-trading/nivel-3/ccxt-multi-exchange/)
