Lección 3.7.3 · 75 min

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.

Fig. — Pago al vencimiento, en XBQ, de una call y una put inversas de strike 64.000 USD
Lecciones del nivel 3
  1. 3.1 · Futuros en profundidad

    1. 3.1.1 Contratos de futuros principales
    2. 3.1.2 Curva de futuros: base, cost of carry, contango y roll yield
    3. 3.1.3 Gestión del roll, márgenes, SPAN y apalancamiento implícito
    4. 3.1.4 Sistemas de futuros diversificados: el enfoque de Carver
  2. 3.2 · Spreads de futuros

    1. 3.2.1 Spreads de calendario, butterflies y condors
    2. 3.2.2 Spreads intercommodity: crack, spark y crush
    3. 3.2.3 Spreads de tipos y de índices: 2s10s, NOB, NQ/ES y Europa frente a EE. UU.
    4. 3.2.4 Ratios de cobertura: por nominal, por DV01 y por volatilidad
    5. 3.2.5 Datos y backtesting de spreads: cotizados, sintéticos y por patas
    6. 3.2.6 Estacionalidad de spreads: análisis multi-año, patrones y trampas
  3. 3.3 · Forex avanzado

    1. 3.3.1 Microestructura FX: OTC, ECN, last look y sesiones
    2. 3.3.2 Carry y riesgo de crash, momentum y value en divisas; cestas
    3. 3.3.3 Riesgo de divisa en carteras multi-moneda; ejecución vía OANDA, IBKR o MT5
  4. 3.4 · Teoría y valoración de opciones

    1. 3.4.1 Payoffs, paridad put-call y límites de arbitraje
    2. 3.4.2 Árboles binomiales, Black-Scholes-Merton, Black-76 y Monte Carlo
    3. 3.4.3 Griegas: delta, gamma, vega, theta, rho, vanna, volga y charm
    4. 3.4.4 Volatilidad implícita: Newton-Raphson, Brent, py_vollib y QuantLib
    5. 3.4.5 Sonrisa, skew y estructura temporal; superficie de volatilidad con SVI
    6. 3.4.6 Volatilidad realizada frente a implícita; prima de riesgo de volatilidad; VIX y VSTOXX
    7. 3.4.7 Modelos avanzados: Heston, SABR y volatilidad local
  5. 3.5 · Estructuras de opciones en Python

    1. 3.5.1 Estructuras direccionales y verticales: call, put, covered call, collar y spreads
    2. 3.5.2 Estructuras de volatilidad y rango: straddles, strangles, cóndores y mariposas
    3. 3.5.3 Calendarios, diagonales, ratios, backspreads, risk reversals y jade lizard
    4. 3.5.4 Herramienta propia: payoff, griegas agregadas y escenarios
    5. 3.5.5 Gestión: ajustes, roll, cierre anticipado, asignación temprana y pin risk
  6. 3.6 · Estrategias sistemáticas de volatilidad y opciones

    1. 3.6.1 Venta sistemática de prima: put-write, strangles y su riesgo de cola
    2. 3.6.2 Delta hedging, gamma scalping y dispersión
    3. 3.6.3 Estrategias con VIX y VSTOXX: futuros y ETP de volatilidad
    4. 3.6.4 Backtesting de opciones: datos, fills y ajustes por eventos
  7. 3.7 · Cripto derivados y estrategias específicas

    1. 3.7.1 Perpetuos: mecánica del funding, mark price y liquidaciones
    2. 3.7.2 Arbitraje de funding, basis con futuros y arbitraje entre exchanges
    3. 3.7.3 Opciones cripto, datos on-chain y DeFi
    4. 3.7.4 ccxt para operar en varios exchanges
    5. 3.7.5 MiCA: el marco de la UE para criptoactivos y sus proveedores (CASP)

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 moneda1Nota 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, consultada el 22/10/2026.. 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 CC es el precio de Black-76 en dólares con forward FF y tipo cero, el precio en moneda y el pago al vencimiento son

V=C(F,K,T,σ)F,pago call=max⁡(ST−K,0)ST,pago put=max⁡(K−ST,0)ST,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 STS_T el índice al vencimiento. Multiplicado por STS_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 UTC1). El vencimiento elegido es el del 26 de abril, a 49 días:

IN [01]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)
OUT [01]Una foto de la cadena de boreal
í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:

IN [02]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)
OUT [02]Black-76 dividido por el forward
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):

IN [03]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")
OUT [03]Volatilidad implícita del punto medio
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:

IN [04]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")
OUT [04]El pago en moneda al vencimiento
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

∂V∂F=∂(C/F)∂F=N(d1)−VF,\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(d1)−VN(d_1) - V monedas de exposición lineal, no como N(d1)N(d_1). La prima pagada en moneda resta exposición: quien compró la opción ya entregó VV monedas.

IN [05]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)
OUT [05]Delta en dólares y delta en moneda
       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)2Nota 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, consultada el 22/10/2026.. 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?

IN [06]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")
OUT [06]DVOL frente a la volatilidad realizada después
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

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:

IN [07]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")
OUT [07]Recalcular el índice desde la cadena
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:

IN [08]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"])
OUT [08]Vender el straddle semanal ATM
        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 FIX2. 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:

IN [09]deribit_rest.pypython
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 otros2. 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 Deribit2:

IN [10]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])
OUT [10]Un Deribit local: cadena y DVOL paginado
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:

IN [11]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})}")
OUT [11]Paginar el DVOL hacia atrás con continuation
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)3Nota 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, consultada el 22/10/2026.. Convertirlos en columnas es el primer paso de cualquier análisis de la cadena:

IN [12]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
OUT [12]Del nombre del instrumento a columnas
                     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.

get_instrumentsbook_summaryNombre a columnasIV y griegasvolatility_indexcontinuationCadena diaria
Fuente 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 xx e yy 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 kk crece con cada operación4Nota 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 kk, docs.uniswap.org, consultada el 22/10/2026.. Con la comisión cc sobre la entrada, quien entrega Δx\Delta x recibe

Δy=y (1−c) Δxx+(1−c) Δx,\Delta y = \frac{y \, (1 - c)\, \Delta x}{x + (1 - c)\, \Delta x},

y el precio marginal del pool es y/xy/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:

IN [13]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")
OUT [13]El pool: x·y = k con comisión
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⋅10−74{,}2 \cdot 10^{-7}, es el redondeo a seis decimales de los datos), y kk 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:

IN [14]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})")
OUT [14]Deslizamiento según el tamaño
   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

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)5Nota 5 · ethereum.org, «Maximal extractable value (MEV)»: buscadores, constructores, separación proponente-constructor y sándwiches, ethereum.org, 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).. 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.

UsuarioMempool públicaCanal privadoBuscador MEVConstructorProponenteBloque
Fuente 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 ven6Nota 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, consultada el 22/10/2026.. 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:

Compra atacanteCompra víctimaVenta atacante
Fuente 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.

IN [15]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%}")
OUT [15]Dos detectores de sándwich
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:

IN [16]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}")
OUT [16]Quién se queda la pérdida de la víctima
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

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:

IN [17]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)
OUT [17]Flujos hacia exchanges y el rendimiento de mañana
  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 %

Con nn ≈ 2.190 días, el error típico de una correlación cercana a cero es 1/n1/\sqrt{n} ≈ 0,021, y t=ρnt = \rho\sqrt{n}. El flujo neto de XEQ (−0,097, tt = −4,5) se separa de todo. El de XBQ (−0,034, tt = −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 tt = 2 por azar.

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
IN [18]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))
OUT [18]Pérdida de la víctima en % de su operación
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.

  1. 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
IN [19]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))
OUT [19]La señal de XEQ frente a un control, con costes
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.

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

Footnotes

  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, consultada el 22/10/2026. ↩ ↩2

  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, consultada el 22/10/2026. ↩ ↩2 ↩3 ↩4 ↩5

  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, 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 kk, docs.uniswap.org, consultada el 22/10/2026. ↩

  5. ethereum.org, «Maximal extractable value (MEV)»: buscadores, constructores, separación proponente-constructor y sándwiches, ethereum.org, 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). ↩

  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, consultada el 22/10/2026. ↩

Respuesta plegada en cada pregunta

Autoevaluación

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

    • A192.000 USD en dólares
    • B0,75 XBQ, que valen 192.000 USD a ese precio
    • C3 XBQ
    • D1 XBQ, el máximo de la call
    Ver respuesta

    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?

    • APorque su prima es menor que la de la call
    • BPorque 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
    • CPorque las puts inversas no se pueden cerrar antes del vencimiento
    • DPorque se liquidan en dólares
    Ver respuesta

    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?

    • AVender volatilidad es una renta segura de 9,5 puntos
    • BHay 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
    • CEl índice está mal calculado
    • DLa prima solo existe en XEQ
    Ver respuesta

    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?

    • AEncuentra los 54 sándwiches y nada más
    • BEncuentra los 54 y otros 54 falsos positivos: las idas y vueltas de un arbitrajista sin víctima entre medias
    • CNo encuentra ninguno
    • DEncuentra la mitad
    Ver respuesta

    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?

    • AEs una señal significativa al 1 %
    • BNo se distingue de los controles (variación de direcciones y de transacciones), que dan correlaciones de la misma magnitud por azar
    • CPrueba que las entradas a exchanges no anticipan nada
    • DQue la señal plantada era cero
    Ver respuesta

    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