Lección 7.6.6 · 65 min

Capstone C5: market making o arbitraje en cripto con control de contraparte

El encargo es una mesa pequeña de arbitraje cripto que cobra el funding de los perpetuos sin apostar por el precio, o que cotiza a los dos lados, y que sobrevive a la quiebra de un exchange. Esta guía fija el alcance, ejecuta el esqueleto con los datos del nivel 3, incluida la quiebra plantada de dedalo, y aplica la rúbrica común a lo que se defiende ante el tribunal.

Fig. — Capital final del carry de funding del esqueleto de C5 según cómo se reparte entre exchanges, con la quiebra plantada de dedalo en noviembre de 2022 (datos sintéticos del nivel 3, 2020-2025)
Lecciones del nivel 7
  1. 7.1 · Ingeniería de software para trading

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

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

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

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

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

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

El encargo

Una mesa de tesorería con capital propio quiere un rendimiento que no dependa de la dirección del precio de las criptomonedas. Encarga un sistema de una de estas dos familias, a elegir por el alumno:

  • Arbitraje de funding y basis. Comprar contado y vender perpetuo (o futuro trimestral) en la misma cantidad, cobrar el funding o fijar el basis, y repartir el capital entre varios exchanges (lecciones 2.3.7, 3.7.1 y 3.7.2).
  • Market making. Cotizar a los dos lados de un libro, cobrar la horquilla y controlar el inventario y la selección adversa, con un modelo tipo Avellaneda-Stoikov (lección 2.3.6).

En las dos, el encargo pone por delante lo mismo: el capital está depositado en exchanges que pueden caer. El sistema tiene que decir cuánto hay en cada uno, por qué, qué lo saca de allí y cuánto se pierde si uno quiebra. Una estrategia que rinde mucho con todo el capital en un solo exchange no cumple el encargo, aunque el backtest sea bueno.

Queda fuera del encargo: DeFi y MEV (lección 3.7.3), opciones, estrategias direccionales, apalancamiento por encima de lo que el esqueleto justifica, operar con dinero real y cualquier exchange que no pase la comprobación de la lección 7.4.4 (autorización como proveedor de servicios de criptoactivos con MiCA, o registro equivalente fuera de la UE).

EntregableQué contieneCómo se comprueba
RepositorioPlantilla de 7.6.1, uv y Docker, un comando por etapamake todo reproduce el informe en una máquina limpia
Registro de contrapartesExchanges, autorización, límites, segregación, pruebas de retiradaCada exchange con capital tiene su ficha y su límite
Registro de pruebasCada configuración probada, con su serieEl número de pruebas del DSR sale de este fichero
Informe de validaciónWalk-forward, DSR, escenarios de quiebra y de liquidaciónLas reglas de parada están fechadas antes de los resultados
EjecutorDos patas con identificadores propios, deshacer, conciliarPruebas con fallos inyectados en el simulador
Diario de paper tradingCuatro semanas en cuentas de demostraciónCada periodo de funding tiene su fila y su conciliación
Memoria y defensaPlantilla de 7.6.1, 20 minutos de exposiciónAnte el tribunal

Arquitectura

Datos de exchangesSeñal de carryAsignaciónContrapartesRiesgo y margenEjecutor ccxtExchange 1Exchange 2ConciliaciónMonitor
Fuente Mermaid
flowchart TB
  D[Datos de exchanges] --> S[Señal de carry]
  S --> A[Asignación]
  C[Contrapartes] --> A
  A --> R[Riesgo y margen]
  R --> E[Ejecutor ccxt]
  E --> X1[Exchange 1]
  E --> X2[Exchange 2]
  X1 --> K[Conciliación]
  X2 --> K
  K --> M[Monitor]

El flujo es el de cualquier sistema en vivo (lección 4.5.1), con dos piezas propias del proyecto. El monitor de contrapartes es una entrada de la asignación al mismo nivel que la señal: puede sacar a un exchange del reparto aunque sea el que más paga. Y la asignación no reparte por rendimiento esperado sin más: reparte con límites por exchange que se fijan con el escenario de quiebra, no con el backtest.

PiezaLibreríaDónde se aprendió
Funding, mark price y liquidaciónpandas1.2.6, 3.7.1
Cash-and-carry, basis, diferenciales y contrapartepandas2.3.7, 3.7.2
Market making e inventarionumpy2.3.6
API unificada, errores, límites e idempotenciaccxt, ccxt.pro3.7.4
Firmas, nonces, relojes y streams de usuarioccxt, APIs nativas4.4.4
Credenciales y permisos mínimosvariables de entorno, gestor de secretos4.4.5
Riesgo de contraparte y estréspandas4.2.4
Margen y liquidaciones en derivadospandas4.2.6
Órdenes, reconexiones y conciliaciónmáquina de estados, ccxt4.5.2, 4.5.5
Pruebas múltiples y DSRregistro de pruebas2.2.4, 2.5.4
Monitorización y desactivaciónmétricas, permiso con caducidad7.2.3, 7.3.3
Proveedores autorizados con MiCAregistro de ESMA y de la CNMV3.7.5, 7.4.4

Datos

El esqueleto usa los datos del nivel 3 (public/curso/datos/nivel-3/); la variante de market making, los del nivel 2:

FicheroQué esPara qué sirve aquí
cripto3_funding.parquetFunding cada 8 h, índice, mark y último precio del perpetuo; 4 exchanges, 2 activos, 2020-2025Señal, cobro y cobertura
cripto3_exchanges.csvFichas y cronología de la quiebra de dedaloEscenario de contraparte y alarma
cripto3_1h.parquetBarras de 1 h del perpetuo de alfa con el markMargen y liquidaciones
cripto3_futuros.parquet, cripto3_diario.parquetTrimestrales y cierres diarios por exchangeBasis y dislocaciones (3.7.2)
xsn_funding_8h.csv, xsn_ccxt_ohlcv.jsonFunding de otro token y el formato de ccxtPruebas del cargador de datos
mm2_medio.parquet, mm2_flujo.parquetUna sesión segundo a segundo con flujo informadoVariante de market making

La respuesta conocida, según el README del nivel 3: la fórmula del funding es la de Binance (prima más un término de interés recortado), con un suelo de 0,01 % por periodo, el 10,95 % anual; la prima alcista se estrecha con una vida media de 2,5 años, y dedalo suspende retiradas el 8 de noviembre de 2022 a las 12:00 y deja los saldos a cero el 11. La lección 3.7.2 midió el carry por exchange y año y la prima de dedalo frente a alfa.

Con datos reales, el historial de funding se descarga de cada exchange con ccxt (fetch_funding_rate_history) o con su API nativa; por ejemplo, la documentación de la API v5 de Bybit describe la ruta del historial de funding. Las comisiones por nivel, los topes de funding, el intervalo de cada contrato y las reglas de margen se leen en la documentación de cada exchange el día que se escribe la ficha, con fecha: cambian, y no se copian de un tutorial. El intervalo de funding no se supone de 8 horas: se calcula con las marcas de tiempo del historial. Las comprobaciones de autorización con MiCA (registro provisional de ESMA, registro de la CNMV) son las de la lección 7.4.4.

Esqueleto ejecutable

El esqueleto es la columna vertebral del arbitraje de funding, de punta a punta, en el entorno principal del curso y sin red: datos, señal, asignación con límites y alarma, margen, ejecución de dos patas con ccxt, validación e informe. La variante de market making se monta sobre el simulador de 2.3.6 (ejercicio 2).

Datos con huella y la cronología de la quiebra

IN [01]python
import hashlib
import json

import numpy as np
import pandas as pd
from scipy.stats import norm

def huella(ruta):
    """SHA-256 abreviado de un fichero de datos (va al informe)."""
    with open(ruta, "rb") as f:
        return hashlib.sha256(f.read()).hexdigest()[:12]

FICHEROS = ["cripto3_funding.parquet", "cripto3_exchanges.csv",
            "cripto3_1h.parquet"]
HUELLAS = {f: huella(f) for f in FICHEROS}

fu = pd.read_parquet("cripto3_funding.parquet")
def tabla(col):
    return fu.pivot(index="ts", columns=["exchange", "activo"],
                    values=col)
F, I, P = (tabla("tasa_funding"), tabla("precio_indice"),
           tabla("ultimo"))
ev = pd.read_csv("cripto3_exchanges.csv").dropna(subset=["ts_utc"])
EV = dict(zip(ev["evento"], pd.to_datetime(ev["ts_utc"], utc=True)))
print(F.shape, "periodos de 8 h x pares;", F.index[0].date(), "a",
      F.index[-1].date())
print(ev[["exchange", "ts_utc", "evento"]].to_string(index=False))
OUT [01]Datos, huellas y la cronología de dedalo
(6575, 8) periodos de 8 h x pares; 2020-01-01 a 2025-12-31
exchange           ts_utc                evento
  dedalo 2022-11-06 00:00               rumores
  dedalo 2022-11-08 12:00 retiradas_suspendidas
  dedalo 2022-11-11 14:00      cese_negociacion
  dedalo 2022-11-11 23:00               quiebra

6.575 periodos de 8 horas por par, de 2020 a 2025, y la cronología de dedalo: rumores el 6 de noviembre de 2022, retiradas suspendidas el 8 a las 12:00, cese de la negociación el 11 a las 14:00 y quiebra esa noche. En los datos de funding, dedalo deja de tener filas tras el último periodo antes del cese.

Señal, asignación y simulador

La señal es el carry de 7 días: la media de los últimos 21 funding publicados, anualizada. Un par entra cuando su carry supera un umbral y sale cuando baja de otro (histéresis, para no pagar comisiones en cada oscilación). El capital se reparte por igual entre los pares activos, con un límite por exchange, y cada unidad de nominal pide 1+1/L1 + 1/L de capital: el contado y el margen del corto a L=3L = 3. El simulador decide en cada periodo con lo publicado hasta ese momento y cobra en el siguiente el funding y la cobertura: la diferencia entre lo que hace el contado (el índice) y lo que hace el perpetuo, que no es cero aunque la posición sea neutral. Cada cambio de nominal paga dos lados de 5 pb.

IN [02]python
PER = 3 * 365                       # periodos de funding por año
carry7 = F.rolling(21).mean() * PER  # carry anual de 7 días, en t
cobertura = (I.pct_change() - P.pct_change()).to_numpy()
pares = list(F.columns)
exch = np.array([e for e, _ in pares])
ts, Fv, Cv = F.index, F.to_numpy(), carry7.to_numpy()

def simular(entrada=0.11, salida=0.0, limite=0.30, alarma=True,
            modo="igual", coste=5e-4, L=3, vetos=()):
    """Largo contado + corto perpetuo en cada par activo. Decide en t
    con lo publicado hasta t; cobra funding y cobertura en t + 1.
    `vetos`: (exchange, desde, hasta) en que se retira el capital."""
    vetos = list(vetos) + ([("dedalo", EV["rumores"], None)]
                           if alarma else [])
    activo, nom = np.zeros(len(pares), bool), np.zeros(len(pares))
    cap_u = 1 + 1 / L          # capital por unidad de nominal
    capital, congelado, perdida, giro = 1.0, 0.0, 0.0, 0.0
    atrapado, serie = False, []
    for t in range(len(ts) - 1):
        fuera = {e for e, d, h in vetos
                 if ts[t] >= d and (h is None or ts[t] < h)}
        if ts[t] >= EV["retiradas_suspendidas"] and not atrapado:
            atrapado = True                # lo que queda, atrapado
            if "dedalo" not in fuera:
                congelado = nom[exch == "dedalo"].sum() * cap_u
        if atrapado:
            fuera.add("dedalo")
        if ts[t] >= EV["cese_negociacion"] and congelado:
            perdida = congelado / capital
            capital, congelado = capital - congelado, 0.0
        c = Cv[t]
        ok = ~np.isnan(c) & ~np.isin(exch, list(fuera))
        activo = np.where(activo, c > salida, c > entrada) & ok
        w = np.zeros(len(pares))
        if activo.any() and modo == "mejor":
            w[np.where(activo, c, -np.inf).argmax()] = 1.0
        elif activo.any():
            w[activo] = 1 / activo.sum()
        for e in set(exch[w > 0]):        # límite por contraparte
            m = exch == e
            w[m] *= min(1.0, limite / w[m].sum())
        nuevo = (capital - congelado) * w / cap_u
        cambio = np.abs(nuevo - nom).sum()
        giro += cambio
        nom = nuevo
        cobro = (np.nan_to_num(Fv[t + 1])
                 + np.nan_to_num(cobertura[t + 1]))
        capital += (nom * cobro).sum() - 2 * coste * cambio
        serie.append(capital)
    return pd.Series(serie, index=ts[1:]), perdida, giro

def metricas(s):
    r = s.pct_change().dropna()
    anos = (s.index[-1] - s.index[0]).days / 365.25
    return {"final": s.iloc[-1],
            "anual %": 100 * (max(s.iloc[-1], 1e-9) ** (1 / anos) - 1),
            "Sharpe": r.mean() / r.std() * np.sqrt(PER),
            "caída máx. %": 100 * (s / s.cummax() - 1).min()}

base, perdida, giro = simular()
print({k: round(float(v), 2) for k, v in metricas(base).items()})
anos = 100 * base.resample("YE").last().pct_change().dropna()
print("por año %:", {d.year: round(v, 1) for d, v in anos.items()})
OUT [02]Señal y simulador: carry, costes, límites y contraparte
{'final': 2.29, 'anual %': 14.84, 'Sharpe': 14.22, 'caída máx. %': -0.75}
por año %: {2021: 27.2, 2022: 7.6, 2023: 7.9, 2024: 7.4, 2025: 6.2}

Con entrada en el 11 % (justo por encima del suelo de la fórmula), salida cuando el carry de la semana se vuelve negativo, límite del 30 % por exchange y alarma de contraparte, el capital pasa de 1 a 2,29 en seis años: un 14,8 % anual, con una caída máxima del 0,75 %. Por año, un 27,2 % en 2021 y entre el 6,2 % y el 7,9 % desde 2022: la prima se estrecha y el carry se acerca al suelo dividido por el capital que pide cada unidad de nominal (10,95 % / 1,33 ≈ 8,2 %), menos los costes. El Sharpe de 14,2 no es una errata: es lo que da un carry sin la cola dentro de la muestra.

Contraparte: concentración, límites y alarma

La alarma de la celda anterior saca a dedalo del reparto en cuanto aparece el evento de rumores de insolvencia en el registro de incidencias (en real, el monitor de contrapartes: noticias, retrasos en las retiradas, descuento del contado frente al índice, salidas de fondos). Sin ella, y con otras formas de repartir:

IN [03]python
VARIANTES = {
    "todo en el que más paga": dict(limite=1.0, alarma=False,
                                    modo="mejor"),
    "repartido, sin límite": dict(limite=1.0, alarma=False),
    "repartido, límite 30 %": dict(limite=0.30, alarma=False),
    "repartido, límite 15 %": dict(limite=0.15, alarma=False),
    "límite 30 % y alarma": dict(limite=0.30, alarma=True),
}
filas = {}
for nombre, kw in VARIANTES.items():
    s, perd, _ = simular(**kw)
    filas[nombre] = metricas(s) | {"pérdida en dedalo %": 100 * perd}
pd.DataFrame(filas).T.round(2)
OUT [03]Contraparte: concentración, límites y alarma
                         final  anual %  Sharpe  caída máx. %  pérdida en dedalo %
todo en el que más paga   0.01   -50.59   -0.20        -99.18                99.12
repartido, sin límite     1.42     5.98    0.47        -40.15                39.91
repartido, límite 30 %    1.61     8.25    0.72        -30.04                29.95
repartido, límite 15 %    1.38     5.50    0.90        -15.04                14.99
límite 30 % y alarma      2.29    14.84   14.22         -0.75                 0.00

La figura de la cabecera dibuja la primera columna. Con todo el capital en el exchange que más paga, que en noviembre de 2022 era dedalo, se pierde el 99 % del capital de ese momento. Repartido por igual sin límite, el 39,9 %; con un límite del 30 %, el 30 %; con el 15 %, el 15 %. El límite convierte una pérdida total en una pérdida acotada, pero no la evita: lo que estaba en dedalo cuando se suspendieron las retiradas se queda allí. Solo la alarma, que actúa 60 horas antes, deja la pérdida en cero.

Dos advertencias para la memoria. La alarma está escrita después de conocer el caso FTX: en el tribunal hay que decirlo y medir también lo que cuestan las falsas alarmas (ejercicio 1). Y un límite del 15 % reduce la pérdida a la mitad que el del 30 %, pero también el rendimiento, porque con pocos exchanges activos parte del capital se queda sin usar.

señal débilse aclaraseñal fuerterumor o retrasocapital fuerarevisión manualNORMALVIGILANCIARETIRADAVETADO
Fuente Mermaid
stateDiagram-v2
  [*] --> NORMAL
  NORMAL --> VIGILANCIA: señal débil
  VIGILANCIA --> NORMAL: se aclara
  VIGILANCIA --> RETIRADA: señal fuerte
  NORMAL --> RETIRADA: rumor o retraso
  RETIRADA --> VETADO: capital fuera
  VETADO --> NORMAL: revisión manual

Margen de la pata corta

El corto del perpetuo se liquida si el mark sube hasta su precio de liquidación, aunque el contado haya ganado lo mismo (lecciones 3.7.1 y 4.2.6). Con el margen repuesto cada día, la pregunta es cuántos días la subida del mark desde el inicio del día habría llegado a ese precio:

IN [04]python
h1 = pd.read_parquet("cripto3_1h.parquet")
h1["dia"] = h1["ts"].dt.floor("D")
ref = h1.groupby(["activo", "dia"])["mark_cierre"].first()
techo = h1.groupby(["activo", "dia"])["mark_maximo"].max()
subida = techo / ref - 1               # peor subida del mark en el día
MMR = 0.005                            # margen de mantenimiento
margen = {}
for L in (2, 3, 5, 10):
    umbral = (1 + 1 / L) / (1 + MMR) - 1
    margen[f"{L}x"] = {"subida que liquida %": 100 * umbral,
                      "días XBQ": int((subida["XBQ"] >= umbral).sum()),
                      "días XEQ": int((subida["XEQ"] >= umbral).sum())}
peor = 100 * subida.groupby(level=0).max()
print(f"peor subida del mark en un día: XBQ {peor['XBQ']:.1f} %, "
      f"XEQ {peor['XEQ']:.1f} %")
pd.DataFrame(margen).T.round(1)
OUT [04]Margen de la pata corta: días que liquidarían
peor subida del mark en un día: XBQ 15.9 %, XEQ 18.1 %
     subida que liquida %  días XBQ  días XEQ
2x                   49.3       0.0       0.0
3x                   32.7       0.0       0.0
5x                   19.4       0.0       0.0
10x                   9.5      16.0      37.0

La peor subida del mark en un día fue del 15,9 % en XBQ y del 18,1 % en XEQ. A 3x hace falta un 32,7 %: ningún día. A 5x, un 19,4 %: ninguno, por poco en XEQ. A 10x basta un 9,5 %, y eso pasó 16 días en XBQ y 37 en XEQ. El esqueleto usa 3x con reposición diaria; la memoria tiene que justificar la frecuencia real de reposición, que depende de cuánto se tarda en mover fondos entre cuentas.

Ejecución de dos patas con ccxt

El ejecutor habla con los exchanges a través de la API unificada de ccxt. En el esqueleto, cada exchange es una clase en memoria que hereda de ccxt.Exchange: mercados unificados (XBQ/USDQ y el perpetuo lineal XBQ/USDQ:USDQ), precisión, órdenes, saldo y posiciones con las estructuras de ccxt. En cenit, el perpetuo está caído:

IN [05]python
import ccxt
from ccxt.base.errors import ExchangeNotAvailable

class Simulado(ccxt.Exchange):
    """Exchange en memoria con la API unificada de ccxt: mercados,
    órdenes, saldo y posiciones. Sin red (la versión HTTP con firma
    y límites de peticiones está en la lección 3.7.4)."""
    def describe(self):
        return self.deep_extend(super().describe(), {
            "id": "simulado", "has": {"createOrder": True,
                                      "fetchPositions": True}})

    def __init__(self, nombre, precio, caidos=()):
        super().__init__({})
        self.id, self.px, self.caidos = nombre, precio, set(caidos)
        self.reloj, self.n = 1_704_067_200_000, 0   # 2024-01-01 UTC
        self.saldo, self.pos = {"USDQ": 100_000.0, "XBQ": 0.0}, 0.0
        comun = {"base": "XBQ", "quote": "USDQ", "baseId": "XBQ",
                 "quoteId": "USDQ", "active": True, "taker": 5e-4,
                 "maker": 2e-4, "limits": {"cost": {"min": 10}},
                 "precision": {"amount": 0.001, "price": 0.1}}
        mercados = (
            {"id": "XBQUSDQ", "symbol": "XBQ/USDQ", "type": "spot",
             "spot": True},
            {"id": "XBQ-PERP", "symbol": "XBQ/USDQ:USDQ",
             "type": "swap", "swap": True, "linear": True,
             "contract": True, "settle": "USDQ", "contractSize": 1})
        self.set_markets([self.safe_market_structure(comun | m)
                          for m in mercados])

    def create_order(self, symbol, type, side, amount, price=None,
                     params={}):
        if symbol in self.caidos:
            raise ExchangeNotAvailable(f"{self.id} 503 {symbol}")
        m, s = self.market(symbol), 1 if side == "buy" else -1
        q = float(self.amount_to_precision(symbol, amount))
        px = round(self.px * (1 + s * 2e-4), 1)    # media horquilla
        fee = round(q * px * m["taker"], 4)
        if m["spot"]:
            self.saldo["XBQ"] += s * q
            self.saldo["USDQ"] -= s * q * px + fee
        else:
            self.pos += s * q
            self.saldo["USDQ"] -= fee
        self.reloj, self.n = self.reloj + 250, self.n + 1
        return self.safe_order({
            "id": f"{self.id}-{self.n}", "timestamp": self.reloj,
            "clientOrderId": params.get("clientOrderId"),
            "symbol": symbol, "type": type, "side": side, "amount": q,
            "filled": q, "average": px, "status": "closed",
            "fee": {"cost": fee, "currency": "USDQ"}}, m)

    def fetch_positions(self, symbols=None, params={}):
        return [self.safe_position({
            "symbol": "XBQ/USDQ:USDQ", "contracts": abs(self.pos),
            "side": "short" if self.pos < 0 else "long", "info": {}})]

def abrir_par(ex, q, cid):
    """Compra contado y vende perpetuo; si la segunda pata falla,
    deshace la primera. Devuelve las órdenes ejecutadas."""
    hechas = [ex.create_order("XBQ/USDQ", "market", "buy", q,
                              params={"clientOrderId": f"{cid}-c"})]
    try:
        hechas.append(ex.create_order(
            "XBQ/USDQ:USDQ", "market", "sell", q,
            params={"clientOrderId": f"{cid}-p"}))
    except ExchangeNotAvailable as e:
        print(f"{ex.id}: {e}; se deshace el contado")
        hechas.append(ex.create_order(
            "XBQ/USDQ", "market", "sell", q,
            params={"clientOrderId": f"{cid}-x"}))
    return hechas

def conciliar(ex, ordenes):
    """Posiciones según las órdenes propias frente al exchange."""
    libro = {"XBQ/USDQ": 0.0, "XBQ/USDQ:USDQ": 0.0}
    for o in ordenes:
        libro[o["symbol"]] += o["filled"] * (1 if o["side"] == "buy"
                                             else -1)
    p = ex.fetch_positions()[0]
    real = {"XBQ/USDQ": ex.saldo["XBQ"], "XBQ/USDQ:USDQ":
            p["contracts"] * (-1 if p["side"] == "short" else 1)}
    coste = 100_000 - ex.saldo["USDQ"] - ex.saldo["XBQ"] * ex.px
    return libro == real, real, round(coste, 2)

for ex in (Simulado("alfa", 42_000.0),
           Simulado("cenit", 42_010.0, caidos=["XBQ/USDQ:USDQ"])):
    ordenes = abrir_par(ex, 0.5, f"c5-{ex.id}-0001")
    print(ex.id, [(o["clientOrderId"], o["side"], o["average"])
                  for o in ordenes])
    print("  cuadra, posiciones, coste:", conciliar(ex, ordenes))
OUT [05]Dos patas contra exchanges simulados con ccxt
alfa [('c5-alfa-0001-c', 'buy', 42008.4), ('c5-alfa-0001-p', 'sell', 41991.6)]
  cuadra, posiciones, coste: (True, {'XBQ/USDQ': 0.5, 'XBQ/USDQ:USDQ': -0.5}, 25.2)
cenit: cenit 503 XBQ/USDQ:USDQ; se deshace el contado
cenit [('c5-cenit-0001-c', 'buy', 42018.4), ('c5-cenit-0001-x', 'sell', 42001.6)]
  cuadra, posiciones, coste: (True, {'XBQ/USDQ': 0.0, 'XBQ/USDQ:USDQ': 0.0}, 29.4)

En alfa entran las dos patas y la conciliación cuadra: 0,5 XBQ de contado y −0,5 de perpetuo, con un coste de 25,2 USDQ entre comisiones y horquilla. En cenit, el contado entra, el perpetuo devuelve un 503 y el ejecutor deshace el contado con su propio identificador (-x): la posición queda a cero y el intento cuesta 29,4 USDQ. Los identificadores propios (clientOrderId) son los que permiten saber, tras un timeout, si una orden existe sin duplicarla (lección 3.7.4).

Contra un exchange real, el mismo código usa la clase de ccxt del exchange y su cuenta de demostración. Va con skip: necesita red y claves, que se leen del entorno y nunca tienen permiso de retirada (lección 4.4.5).

IN [06]ejecutor_demo.pypython
import os

import ccxt

ex = ccxt.bybit({"apiKey": os.environ["BYBIT_KEY"],
                 "secret": os.environ["BYBIT_SECRET"]})
ex.enable_demo_trading(True)        # cuenta de demostración
ex.load_markets()
hist = ex.fetch_funding_rate_history("BTC/USDT:USDT", limit=21)
horas = (hist[-1]["timestamp"] - hist[0]["timestamp"]) / 3.6e6 / 20
carry7 = (sum(h["fundingRate"] for h in hist) / len(hist)
          * 24 / horas * 365)
if carry7 > 0.11:
    q = float(ex.amount_to_precision("BTC/USDT:USDT", 0.01))
    ex.create_order("BTC/USDT", "market", "buy", q,
                    params={"clientOrderId": "c5-0001-c"})
    ex.create_order("BTC/USDT:USDT", "market", "sell", q,
                    params={"clientOrderId": "c5-0001-p"})
print(ex.fetch_positions(["BTC/USDT:USDT"]))

enable_demo_trading existe en la versión 4.5.89 de ccxt para Bybit y para Binance. Antes de usarlo hay que leer en la documentación de cada exchange qué cubre su entorno de demostración (mercados, saldos ficticios, límites): no es idéntico al real.

Registro de pruebas, validación e informe

Las ocho combinaciones de umbrales (entrada en el 8, 11, 15 y 20 %; salida en el 0 y el 5 %) quedan en experimentos/registro_pruebas.jsonl, solo de añadir y con los campos de la guía 7.6.1, y el número de pruebas del Deflated Sharpe se cuenta en ese fichero. El walk-forward elige cada año con los anteriores y no opera si la elegida no gana en entrenamiento.

IN [07]python
REJILLA = [(e, s) for e in (0.08, 0.11, 0.15, 0.20)
           for s in (0.0, 0.05)]
import os

REG = "experimentos/registro_pruebas.jsonl"    # solo de añadir (7.6.1)
os.makedirs("experimentos", exist_ok=True)

def sharpe(x):
    return float(x.mean() / x.std() * np.sqrt(PER))

series = {}
for e, s in REJILLA:
    x = simular(e, s)[0].pct_change().dropna()
    series[(e, s)] = x
    cfg = {"señal": "carry7", "entrada": e, "salida": s,
           "limite": 0.30, "apalancamiento": 3, "coste_pb": 5}
    with open(REG, "a", encoding="utf-8") as f:
        f.write(json.dumps({
            "id": hashlib.sha256(json.dumps(cfg, sort_keys=True)
                                 .encode()).hexdigest()[:10],
            "hipotesis": "el carry de 7 días persiste",
            "config": cfg, "datos": HUELLAS["cripto3_funding.parquet"],
            "resultado": {"sharpe": round(sharpe(x), 4), "n": len(x)}},
            ensure_ascii=False) + "\n")
with open(REG, encoding="utf-8") as f:
    N = sum(1 for _ in f)

oos = []
for ano in range(2021, 2026):
    ent = {k: sharpe(x[x.index.year < ano]) for k, x in series.items()}
    elegida = max(ent, key=ent.get)
    x = series[elegida][series[elegida].index.year == ano]
    oos.append(x if ent[elegida] > 0 else 0 * x)
    print(ano, "elegida", elegida, f"| fuera: {100 * x.sum():.1f} %"
          f" (Sharpe {sharpe(x):.1f})")
oos = pd.concat(oos)

def max_esperado(n):
    g = np.euler_gamma
    return ((1 - g) * norm.ppf(1 - 1 / n)
            + g * norm.ppf(1 - 1 / (n * np.e)))

def psr(x, umbral):
    sr = x.mean() / x.std()
    a, k = x.skew(), x.kurt() + 3
    return float(norm.cdf((sr - umbral) * np.sqrt(len(x) - 1) / np.sqrt(
        1 - a * sr + (k - 1) / 4 * sr ** 2)))

sr = pd.Series({k: x.mean() / x.std() for k, x in series.items()})
mejor = max(series, key=lambda k: sharpe(series[k]))
sr0 = sr.std() * max_esperado(N)
print(f"N = {N}; mejor {mejor}: Sharpe {sharpe(series[mejor]):.1f},"
      f" DSR {psr(series[mejor], sr0):.3f}; walk-forward: Sharpe "
      f"{sharpe(oos):.1f}")
anual = 100 * oos[oos.index.year >= 2023].sum() / 3
print(f"carry neto 2023-2025: {anual:.1f} % al año; una quiebra con "
      f"el 30 % del capital dentro se come {30 / anual:.1f} años")
OUT [07]Registro de pruebas, walk-forward y Deflated Sharpe
2021 elegida (0.08, 0.0) | fuera: 23.9 % (Sharpe 21.2)
2022 elegida (0.08, 0.0) | fuera: 7.4 % (Sharpe 11.2)
2023 elegida (0.08, 0.0) | fuera: 7.6 % (Sharpe 13.7)
2024 elegida (0.08, 0.0) | fuera: 7.2 % (Sharpe 14.9)
2025 elegida (0.08, 0.0) | fuera: 6.0 % (Sharpe 12.8)
N = 8; mejor (0.08, 0.0): Sharpe 14.3, DSR 1.000; walk-forward: Sharpe 14.3
carry neto 2023-2025: 6.9 % al año; una quiebra con el 30 % del capital dentro se come 4.3 años
IN [08]python
informe = {
    "proyecto": "C5", "datos": HUELLAS,
    "estrategia": {"entrada": mejor[0], "salida": mejor[1],
                   "limite_por_exchange": 0.30, "apalancamiento": 3,
                   "coste_pb": 5, "n_pruebas": N},
    "validacion": {"sharpe_walk_forward": round(sharpe(oos), 1),
                   "dsr": round(psr(series[mejor], sr0), 3),
                   "carry_neto_2023_2025_pct": round(anual, 1)},
    "riesgo": {"perdida_dedalo_sin_alarma_pct": round(
                   100 * simular(alarma=False)[1], 1),
               "anos_de_carry_por_quiebra_al_limite": round(
                   30 / anual, 1),
               "dias_liquidacion_3x": margen["3x"]["días XBQ"]
                                      + margen["3x"]["días XEQ"]},
}
informe["decision"] = (
    "paper trading" if informe["validacion"]["dsr"] >= 0.95
    and informe["validacion"]["sharpe_walk_forward"] > 0
    else "parar o rediseñar")
with open("informe_c5.json", "w", encoding="utf-8") as f:
    json.dump(informe, f, ensure_ascii=False, indent=1)
for parte in ("estrategia", "validacion", "riesgo", "decision"):
    print(parte, json.dumps(informe[parte], ensure_ascii=False))
OUT [08]El informe de la ejecución
estrategia {"entrada": 0.08, "salida": 0.0, "limite_por_exchange": 0.3, "apalancamiento": 3, "coste_pb": 5, "n_pruebas": 8}
validacion {"sharpe_walk_forward": 14.3, "dsr": 1.0, "carry_neto_2023_2025_pct": 6.9}
riesgo {"perdida_dedalo_sin_alarma_pct": 30.0, "anos_de_carry_por_quiebra_al_limite": 4.3, "dias_liquidacion_3x": 0}
decision "paper trading"

El walk-forward elige cada año la entrada del 8 % sin umbral de salida, y el Sharpe fuera de muestra es de 14,3; el Deflated Sharpe con 8 pruebas es 1,000. La regla automática dice «paper trading». La memoria tiene que decir también la otra línea del informe: con el 30 % del capital en un exchange, una quiebra se come 4,3 años de carry neto al ritmo de 2023-2025 (6,9 % anual). Ese es el número que decide el tamaño de los límites, y no sale de ningún contraste estadístico.

Validación

La validación del carry tiene dos partes que no se pueden sustituir la una por la otra.

La parte estadística, como en cualquier proyecto del curso: walk-forward anual con ventana creciente (lección 2.5.2), registro de pruebas y Deflated Sharpe (2.5.4), o CPCV (2.5.3) si hay más parámetros. Aquí sirve para lo que sirve: comprobar que los umbrales no están ajustados a la muestra y que el carry sobrevive a los costes en cada año. En la variante de market making, los parámetros (γ\gamma, kk, límite de inventario) se eligen en unas sesiones y se miden en otras, y la métrica incluye la selección adversa medida aparte (2.3.6).

La parte de escenarios, que es la que decide. Un carry tiene una distribución de rendimientos con muchos días pequeños y positivos y un salto raro: su Sharpe muestral es enorme mientras el salto no está en la muestra. Lo que se valida es la pérdida en cada escenario y su relación con el carry:

EscenarioCómo se mideQué se registra
Quiebra de un exchangeCapital en él cuando se cierran las retiradasPérdida y años de carry que se come
Liquidación de la pata cortaSubida del mark entre reposiciones de margenDías que liquidarían a cada apalancamiento
Funding negativo prolongadoSemanas seguidas pagando (2022 en los datos)Pérdida y giro de la histéresis
Pata huérfanaFallo de la segunda ordenCoste de deshacer y frecuencia en paper trading
Dislocación del contadoDescuento de un exchange frente al índice (3.7.2)Pérdida de la cobertura si se usa su contado

Las reglas de parada, fechadas en la memoria antes de la semana 4: carry neto fuera de muestra no positivo en el último año; DSR por debajo de 0,95 con el número de pruebas registrado; una quiebra en el límite que se come más de cinco años de carry neto, salvo que se baje el límite; cualquier día de liquidación al apalancamiento elegido en la historia disponible. Los umbrales son criterio del curso, no una norma.

Riesgos y paper trading

Lo que de verdad tumba una mesa de arbitraje cripto:

  • Contraparte. Quiebra, congelación de retiradas, mezcla de fondos de clientes. El caso FTX, según las demandas de la CFTC y la SEC, lo resume la lección 3.7.2. Defensas: límites, retirada de beneficios, exchanges autorizados con MiCA (7.4.4), pruebas de retirada periódicas y la alarma.
  • Liquidación de la pata corta por una subida rápida con el margen sin reponer, o por una mecha del último precio en un exchange que liquida con él en lugar de con el mark (3.7.1).
  • Patas huérfanas y duplicadas. Una orden que entra en un exchange y no en el otro, o un reintento que duplica la orden tras un timeout (3.7.4).
  • Funding que cambia de signo y deja a la mesa pagando mientras la histéresis decide.
  • Claves. Una clave con permiso de retirada filtrada vacía la cuenta; las claves del ejecutor solo operan, y las direcciones de retirada están en una lista blanca configurada a mano (4.4.5).
  • Market making: selección adversa, inventario que se acumula en una caída y cotizaciones que siguen vivas cuando el proceso se cae (cancel-on-disconnect, si el exchange lo ofrece; 4.5.2).

Los límites y el kill switch: capital máximo por exchange, apalancamiento máximo, distancia mínima al precio de liquidación, número máximo de órdenes por minuto, pérdida diaria máxima (4.2.5), y el estado de cada contraparte del diagrama de arriba. Un exchange en RETIRADA no recibe órdenes que aumenten la exposición.

El plan de las cuatro semanas de paper trading, en las cuentas de demostración de al menos dos exchanges:

  1. Cada periodo de funding: descarga del funding publicado, señal, asignación, órdenes de las dos patas y conciliación de órdenes, saldos y posiciones (4.5.5). Una diferencia sin explicar bloquea el periodo siguiente.
  2. Cada día: margen y distancia a la liquidación por posición, estado de cada contraparte, cobertura (contado frente a perpetuo) y deslizamiento frente al supuesto de 5 pb (4.5.3).
  3. Cada semana: carry cobrado frente al simulado para las mismas fechas, incidencias del ejecutor (patas huérfanas, rechazos, límites de peticiones) y una prueba de fallo provocada (cortar la conexión a mitad de una apertura).
  4. Al final: informe con las diferencias entre el simulador y lo ejecutado. Cuatro semanas no dicen nada de la cola; dicen si el ejecutor, la conciliación y el monitor funcionan.

Hitos y rúbrica

El calendario es el de la guía 7.6.1: de 40 a 80 horas en unas 10 semanas, con las cuatro de paper trading en paralelo a la memoria.

SemanaEntregable verificable
1Repositorio de la plantilla; cargador de funding con huellas; registro de contrapartes con la comprobación de 7.4.4
2Señal, simulador con costes, margen y cobertura; respuesta conocida del nivel 3 reproducida
3Asignación con límites y alarma; escenario de quiebra; reglas de parada escritas y fechadas
4Walk-forward, DSR y escenarios; decisión de paso a paper trading
5Ejecutor con ccxt contra el simulador: dos patas, deshacer, idempotencia, conciliación, con pruebas
6Cuentas de demostración, claves sin retirada, monitor de contrapartes y alertas
7-10Paper trading, conciliación por periodo y monitor; memoria técnica; ensayo de la defensa

La rúbrica es la común del curso (criterio propio, no una norma externa), con la evidencia que pide en C5:

CriterioPesoEvidencia en C5
Reproducibilidad15 %uv.lock, Docker, un comando; huellas de datos; simulador determinista con reloj propio
Datos15 %Funding con su hora de publicación, exchanges desaparecidos incluidos (sin sesgo de supervivencia), fichas fechadas
Validación anti-sobreajuste20 %Walk-forward, DSR con el registro, escenarios de quiebra y de liquidación con su pérdida
Riesgo y ejecución15 %Costes, margen, límites por contraparte, alarma, ejecutor de dos patas con deshacer y conciliación
Paper trading y monitorización15 %Cuatro semanas en demostración, conciliación por periodo, estado de contrapartes
Calidad del código10 %Pruebas del ejecutor con fallos inyectados, del simulador y de la asignación; lint; configuración fuera del código
Memoria técnica y defensa10 %Plantilla de 7.6.1; límites justificados con el escenario de quiebra

Errores que suspenden por sí solos, con su forma en C5: anticipación demostrable (usar el funding de un periodo para decidir la posición que lo cobra); resultados que no se reproducen con el comando documentado; backtest sin costes o sin margen; claves de API en el repositorio o en su historial, más aún con permiso de retirada; ocultar el número de pruebas o elegir el periodo a posteriori (por ejemplo, empezar en 2023 para no ver la quiebra, o quitar dedalo del universo, que es sesgo de supervivencia); menos de cuatro semanas de paper trading.

Ejercicios

  1. La alarma de contraparte salta por un rumor falso sobre alfa el 4 de marzo de 2024 y el capital se retira una semana. ¿Cuánto cuesta esa falsa alarma frente al esqueleto?
Solución
IN [09]python
falsa = [("alfa", pd.Timestamp("2024-03-04", tz="UTC"),
          pd.Timestamp("2024-03-11", tz="UTC"))]
s_f, _, _ = simular(vetos=falsa)
coste = 1 - s_f.iloc[-1] / base.iloc[-1]
b24 = base[base.index.year == 2024]
diario = (b24.iloc[-1] / b24.iloc[0] - 1) / 365
print(f"capital final: {base.iloc[-1]:.4f} sin la falsa alarma, "
      f"{s_f.iloc[-1]:.4f} con ella")
print(f"coste: {100 * coste:.3f} % del capital, unos "
      f"{coste / diario:.1f} días de carry de 2024")
OUT [09]Lo que cuesta una falsa alarma
capital final: 2.2940 sin la falsa alarma, 2.2920 con ella
coste: 0.087 % del capital, unos 4.3 días de carry de 2024

La falsa alarma cuesta un 0,087 % del capital, unos 4,3 días de carry de 2024: se cierran y se vuelven a abrir las patas de alfa (cuatro lados con costes) y durante una semana parte del capital se queda sin usar, porque el límite del 30 % impide llevar a los otros exchanges lo que sale de alfa. Frente al 30 % que se pierde en una quiebra al límite, una alarma que salta por error varias veces al año sigue siendo barata. Lo que hay que vigilar es que no salte tan a menudo que el sistema pase más tiempo fuera que dentro, y registrar cada disparo con su motivo para calibrarla.

  1. Si eliges market making: al simulador de Avellaneda-Stoikov de la lección 2.3.6 añádele un límite duro de inventario (no cotizar el lado que lo supera) y una comisión de maker. ¿Cuánto queda del resultado?
Solución
IN [10]python
flujo = pd.read_parquet("mm2_flujo.parquet")
medio = pd.read_parquet("mm2_medio.parquet")
inicio = pd.Timestamp("2025-03-12 13:30:00", tz="UTC")
T = len(medio)
m = np.r_[medio["medio"].iloc[0], medio["medio"].to_numpy()]
seg = np.floor((flujo["ts"] - inicio).dt.total_seconds()).astype(int)
seg = seg.clip(0, T - 1).to_numpy()
signo = np.where(flujo["lado"] == "compra", 1, -1)
alcance = flujo["alcance"].to_numpy()
por_seg = [[] for _ in range(T)]
for i, s in enumerate(seg):
    por_seg[s].append(i)

def creador(q_max, comision, gamma=1e-4, k=35.4, sigma=0.0065):
    """Avellaneda-Stoikov (2.3.6) con límite duro de inventario y
    comisión de maker sobre el efectivo de cada ejecución."""
    q, caja, n = 0, 0.0, 0
    for s in range(T):
        resto = T - s
        r = m[s] - q * gamma * sigma ** 2 * resto
        semi = 0.5 * (gamma * sigma ** 2 * resto
                      + 2 / gamma * np.log(1 + gamma / k))
        bid = np.floor((r - semi) / 0.01 + 1e-9) * 0.01
        ask = np.ceil((r + semi) / 0.01 - 1e-9) * 0.01
        for i in por_seg[s]:
            if (signo[i] == 1 and q - 100 >= -q_max
                    and alcance[i] >= ask - m[s] - 1e-9):
                q, caja = q - 100, caja + 100 * ask * (1 - comision)
                n += 1
            elif (signo[i] == -1 and q + 100 <= q_max
                    and alcance[i] >= m[s] - bid - 1e-9):
                q, caja = q + 100, caja - 100 * bid * (1 + comision)
                n += 1
    return {"resultado": caja + q * m[T], "ejecuciones": n}

pd.DataFrame({
    "sin límite, sin comisión": creador(10 ** 9, 0.0),
    "límite 1000, sin comisión": creador(1000, 0.0),
    "límite 1000, maker 1 pb": creador(1000, 1e-4),
    "límite 1000, maker 2 pb": creador(1000, 2e-4),
}).T.round(1)
OUT [10]Market making con límite de inventario y comisión
                           resultado  ejecuciones
sin límite, sin comisión     38061.0      13169.0
límite 1000, sin comisión    38135.0      13163.0
límite 1000, maker 1 pb      31748.4      13163.0
límite 1000, maker 2 pb      25361.7      13163.0

Con Avellaneda-Stoikov el inventario ya se mantiene pequeño (en 2.3.6 no pasó de 1.700 acciones), así que el límite de 1.000 casi no cambia el resultado: 38.135 USD frente a 38.061. La comisión sí: cada punto básico de maker sobre el efectivo negociado (unos 64 millones de USD en 13.163 ejecuciones de 100 acciones) se lleva unos 6.400 USD, un 17 % del resultado. La estructura de comisiones de cada exchange (maker, taker, descuentos por volumen) decide si el market making tiene sentido, y se lee en su página oficial con fecha. El límite duro sigue siendo obligatorio: protege de lo que el modelo no prevé, como un salto del precio o un flujo informado que no se detiene.

  1. Antes de depositar capital en un exchange nuevo, ¿qué va en su ficha del registro de contrapartes y qué señales alimentan su estado en el monitor?
Solución

En la ficha: entidad legal y LEI, país, autorización como proveedor de servicios de criptoactivos con MiCA y su comprobación en el registro de ESMA y de la CNMV con fecha (lecciones 3.7.5 y 7.4.4), segregación de los activos de clientes según los términos del contrato, servicios que se usarán, límite de capital y su justificación con el escenario de quiebra, permisos de cada clave (sin retirada), lista blanca de direcciones, resultado de la última prueba de retirada y fecha de la próxima revisión. En el monitor: retrasos o fallos en las retiradas de prueba, descuento del contado frente al índice de los demás exchanges (3.7.2), funding muy distinto del resto sin causa de mercado, caídas de la API fuera de lo normal, noticias de solvencia o actuaciones de supervisores y cambios en el registro de ESMA o en la lista de entidades no autorizadas (7.4.4). Las señales débiles pasan a VIGILANCIA; una fuerte, o varias débiles a la vez, a RETIRADA.

Respuesta plegada en cada pregunta

Autoevaluación

  1. La mejor configuración del esqueleto tiene un Sharpe de 14,3 y un Deflated Sharpe de 1,000 con 8 pruebas. ¿Qué concluyes?

    • AQue la estrategia está validada y se puede operar con apalancamiento alto
    • BQue la validación estadística no ve el riesgo que importa: la muestra sin la quiebra no tiene saltos, y el riesgo de contraparte se valida con escenarios y límites
    • CQue hay anticipación en el código
    • DQue hay que probar más configuraciones para que el DSR baje
    Ver respuesta

    Respuesta B. Que la validación estadística no ve el riesgo que importa: la muestra sin la quiebra no tiene saltos, y el riesgo de contraparte se valida con escenarios y límites

    Un carry cobra poco y casi siempre; su pérdida llega de golpe y rara vez (una quiebra, una liquidación). El Sharpe de la muestra mide la parte tranquila. Con la quiebra de dedalo dentro y sin alarma, el mismo sistema pierde el 30 % del capital en un día: eso es lo que hay que defender ante el tribunal.

  2. En cenit, la pata corta del perpetuo devuelve un error 503 después de haber comprado el contado. ¿Qué hace el ejecutor del esqueleto?

    • AReintenta la venta del perpetuo hasta que entre
    • BDeja el contado comprado y espera al siguiente periodo
    • CAbre el corto en otro exchange
    • DDeshace la pata del contado, anota la incidencia y concilia: posición neta cero con el coste de la ida y la vuelta
    Ver respuesta

    Respuesta D. Deshace la pata del contado, anota la incidencia y concilia: posición neta cero con el coste de la ida y la vuelta

    Una pata sola es una apuesta direccional que la estrategia no quiere tener. Reintentar a ciegas tras un error puede duplicar órdenes (lección 3.7.4); cubrirse en otro exchange cambia la contraparte sin decisión previa. Deshacer cuesta 29,4 USDQ en el ejemplo y deja el libro cuadrado.

  3. Con el capital repartido y un límite del 30 % por exchange, pero sin alarma, ¿qué pasa en noviembre de 2022?

    • ASe pierde la parte que estaba en dedalo cuando se suspendieron las retiradas: un 30 % del capital, el límite entero
    • BNada: el límite evita cualquier pérdida
    • CSe pierde todo el capital
    • DSe pierde solo el funding de una semana
    Ver respuesta

    Respuesta A. Se pierde la parte que estaba en dedalo cuando se suspendieron las retiradas: un 30 % del capital, el límite entero

    El límite acota la pérdida, no la evita: el capital que estaba en dedalo el 8 de noviembre a las 12:00 quedó atrapado. Lo que la evita en el esqueleto es la alarma de contraparte, que retira el capital con los rumores del día 6, antes de la suspensión.

  4. Con reposición diaria del margen, ¿cuántos días del periodo habrían liquidado la pata corta a 3x y a 10x?

    • ANinguno en los dos casos
    • BMuchos a 3x y ninguno a 10x
    • CNinguno a 3x (haría falta una subida del 32,7 % del mark en un día) y 53 entre XBQ y XEQ a 10x
    • DUno a 3x y uno a 10x
    Ver respuesta

    Respuesta C. Ninguno a 3x (haría falta una subida del 32,7 % del mark en un día) y 53 entre XBQ y XEQ a 10x

    La peor subida del mark en un día fue del 15,9 % en XBQ y del 18,1 % en XEQ, lejos del 32,7 % que liquida a 3x. A 10x basta un 9,5 %, y eso pasó 16 días en XBQ y 37 en XEQ. Los últimos puntos de rendimiento del apalancamiento se pagan con liquidaciones.

  5. ¿Cuál de estos hechos suspende el proyecto por sí solo, según la rúbrica común de 7.6.1?

    • AQue el carry neto de 2023 a 2025 sea solo del 6,9 % anual
    • BUna clave de API con permiso de retirada guardada en el historial de git del repositorio
    • CElegir market making en lugar de arbitraje
    • DQue el paper trading muestre un deslizamiento mayor que el supuesto
    Ver respuesta

    Respuesta B. Una clave de API con permiso de retirada guardada en el historial de git del repositorio

    Credenciales en el repositorio o en su historial suspenden por sí solas, y una clave con permiso de retirada es la peor de todas (lección 4.4.5). Un carry modesto o un deslizamiento mayor del esperado son resultados que se documentan y se defienden.

Ideas clave

  • C5 es una mesa de arbitraje o de market making en cripto cuyo riesgo principal no es el precio, sino las contrapartes: los exchanges guardan el capital.
  • El esqueleto cobra el funding con contado largo y perpetuo corto a 3x en los exchanges que pagan más que el suelo de la fórmula, con 5 pb por lado y reposición diaria del margen: un 14,8 % anual, que baja al 6-8 % desde 2022.
  • Ante la quiebra de dedalo, concentrar en el que más paga pierde todo; repartir con un límite del 30 % pierde el 30 %; solo la alarma que retira el capital con los rumores evita la pérdida.
  • El ejecutor envía las dos patas con identificadores propios, deshace una pata huérfana y concilia contra lo que dice el exchange antes del siguiente periodo.
  • Un Sharpe de 14 y un DSR de 1,000 no validan un carry: miden la parte tranquila. Con el 30 % del capital en un exchange, una quiebra se come 4,3 años de carry neto.
  • El paper trading va en cuentas de demostración de los exchanges, con las claves sin permiso de retirada y la monitorización de cada contraparte.

Recursos