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.
- Módulo
- 3.7 · Cripto derivados y estrategias específicas
- Duración
- 75 min
Lecciones del nivel 3
3.1 · Futuros en profundidad
3.2 · Spreads de futuros
- 3.2.1 Spreads de calendario, butterflies y condors
- 3.2.2 Spreads intercommodity: crack, spark y crush
- 3.2.3 Spreads de tipos y de índices: 2s10s, NOB, NQ/ES y Europa frente a EE. UU.
- 3.2.4 Ratios de cobertura: por nominal, por DV01 y por volatilidad
- 3.2.5 Datos y backtesting de spreads: cotizados, sintéticos y por patas
- 3.2.6 Estacionalidad de spreads: análisis multi-año, patrones y trampas
3.3 · Forex avanzado
3.4 · Teoría y valoración de opciones
- 3.4.1 Payoffs, paridad put-call y límites de arbitraje
- 3.4.2 Árboles binomiales, Black-Scholes-Merton, Black-76 y Monte Carlo
- 3.4.3 Griegas: delta, gamma, vega, theta, rho, vanna, volga y charm
- 3.4.4 Volatilidad implícita: Newton-Raphson, Brent, py_vollib y QuantLib
- 3.4.5 Sonrisa, skew y estructura temporal; superficie de volatilidad con SVI
- 3.4.6 Volatilidad realizada frente a implícita; prima de riesgo de volatilidad; VIX y VSTOXX
- 3.4.7 Modelos avanzados: Heston, SABR y volatilidad local
3.5 · Estructuras de opciones en Python
- 3.5.1 Estructuras direccionales y verticales: call, put, covered call, collar y spreads
- 3.5.2 Estructuras de volatilidad y rango: straddles, strangles, cóndores y mariposas
- 3.5.3 Calendarios, diagonales, ratios, backspreads, risk reversals y jade lizard
- 3.5.4 Herramienta propia: payoff, griegas agregadas y escenarios
- 3.5.5 Gestión: ajustes, roll, cierre anticipado, asignación temprana y pin risk
3.6 · Estrategias sistemáticas de volatilidad y opciones
3.7 · Cripto derivados y estrategias específicas
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 es el precio de Black-76 en dólares con forward y tipo cero, el
precio en moneda y el pago al vencimiento son
con el índice al vencimiento. Multiplicado por , 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:
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)í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.84Los 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:
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)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.10072El 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):
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")diferencia con mark_iv: media -0.02, máxima 0.20 puntosEntre 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:
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")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 XBQLa 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
es decir, la posición se comporta como monedas de exposición lineal, no como . La prima pagada en moneda resta exposición: quien compró la opción ya entregó monedas.
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) 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.037La 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?
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")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íasLa 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:
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")33 viernes: diferencia media -0.90, rango -3.80 a +0.27 puntosEl 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:
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"]) media peor ganadoras semanas
activo
XBQ 0.03 -18.93 56.54 260
XEQ -0.13 -26.93 56.54 260El 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:
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:
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])358 opciones; XBQ-15MAR24-48000-CEl 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:
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})}")2192 velas diarias en 22 llamadas; duplicadas: 0Veintidó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:
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 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.8Siete 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.
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 e 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 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 , docs.uniswap.org, consultada el 22/10/2026.. Con la comisión sobre la entrada, quien entrega recibe
y el precio marginal del pool es . 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:
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")18489 swaps en 15438 bloques
error relativo mediano 4.2e-07
k sube un 2.557% en 7 díasCada salida se reproduce con la fórmula desde las reservas del swap anterior (el error relativo mediano, , es el redondeo a seis decimales de los datos), y 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:
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})") 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)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.
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:
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.
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%}")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:
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}")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.013Las 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:
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) 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.097Con ≈ 2.190 días, el error típico de una correlación cercana a cero es ≈ 0,021, y . El flujo neto de XEQ (−0,097, = −4,5) se separa de todo. El de XBQ (−0,034, = −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 = 2 por azar.
Ejercicios
- Con
defi3_sandwich_verdad.csvy 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
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))50% 0.50
max 2.98
Name: perdida_pct, dtype: float64
corr. de rangos con el tamaño: -0.41La 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.
- 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
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))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.
- 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
-
Documentación de la API de Deribit:
public/get_instruments(contratosreversedliquidados en la moneda base,contract_sizede las opciones en la moneda), «Options Data Collection Best Practices» (las cantidades de las opciones van en la moneda subyacente) ypublic/get_expirations(vencimientos diarios a las 08:00 UTC), docs.deribit.com, 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_datay «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 -
Deribit, «JSON-RPC Overview», tabla de nombres de instrumentos (
BTC-25MAR23-420-C; en las opciones lineales,dcomo separador decimal del strike), docs.deribit.com/articles/json-rpc-overview, consultada el 22/10/2026. ↩ -
Uniswap, «Fees»: en v2 cada pool cobra una comisión fija del 0,30 % que se añade a las reservas y hace crecer , docs.uniswap.org, consultada el 22/10/2026. ↩
-
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). ↩
-
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
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.
¿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.
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.
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 %.
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
continuationy los nombresXBQ-26APR24-70000-Cllevan 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.