Lección 3.2.5 · 70 min

Datos y backtesting de spreads: cotizados, sintéticos y por patas

Un spread de futuros se puede operar como un instrumento propio, con su libro, o montar pata a pata. Las dos cosas dejan huellas distintas en los datos y en el resultado. Esta lección mide la diferencia entre el libro del spread y el que se construye con sus patas, escribe un backtest que lleva cada pata con sus contratos y sus costes, cuantifica el riesgo de ejecutar una pata sin la otra y enseña a pedir los datos reales a Databento con un simulador local de su API.

Fig. — Legging risk en el calendario diciembre-enero del crudo ficticio: desviación típica del coste de comprar el spread pata a pata (la cercana ahora, la lejana k minutos después) frente al cambio del ask del propio spread en el mismo tiempo
Lecciones del nivel 3
  1. 3.1 · Futuros en profundidad

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

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

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

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

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

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

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

Cotizado o sintético

Un spread de calendario (comprar el vencimiento cercano y vender el siguiente, lección 3.2.1) se puede ejecutar de dos maneras. La primera, como spread cotizado: las bolsas de futuros listan los spreads más usados como instrumentos propios, con su libro de órdenes, su precio (cercano menos lejano) y su volumen; una orden compra el spread entero, las dos patas a la vez, a un solo precio. La segunda, como spread sintético: comprar una pata en su libro y vender la otra en el suyo, dos operaciones.

cotizadosintéticosintéticoOrden de spread¿Cómo?Libro del spreadPata cercanaPata lejanaUna ejecuciónDos ejecuciones
Fuente Mermaid
flowchart TB
  O[Orden de spread] --> C{¿Cómo?}
  C -- cotizado --> L[Libro del spread]
  C -- sintético --> P1[Pata cercana]
  C -- sintético --> P2[Pata lejana]
  L --> E[Una ejecución]
  P1 --> D[Dos ejecuciones]
  P2 --> D

La diferencia se ve en los precios. Con el mejor bid y ask de cada pata, el spread sintético se compra pagando el ask de la cercana y cobrando el bid de la lejana, y se vende al revés:

bidsint=bidcerca−asklejos,asksint=askcerca−bidlejos\text{bid}_{\text{sint}} = \text{bid}_{\text{cerca}} - \text{ask}_{\text{lejos}}, \qquad \text{ask}_{\text{sint}} = \text{ask}_{\text{cerca}} - \text{bid}_{\text{lejos}}

Su ancho es la suma de los anchos de las dos patas. El libro del spread, en cambio, lo cotizan quienes quieren el spread entero, y su ancho no tiene por qué ser la suma de nada.

spreads3_cotizados.parquet tiene, para cada sesión de 2010 a 2025, el cierre del libro del spread de calendario (contrato que se tiene menos el siguiente) de seis mercados del nivel, y el mejor bid y ask de cada pata. Son datos sintéticos con una respuesta conocida: el libro del spread cotiza a 1 tick el 75 % de los días y a 2 el resto; la pata cercana, a 1 o 2 ticks, y la lejana, a 1, 2 o 3. En RVA (índice) y TPC (bono) el spread solo tiene filas en las 10 sesiones anteriores al roll, que es cuando se negocia en esos mercados.

IN [01]python
import numpy as np
import pandas as pd

cot = pd.read_parquet("spreads3_cotizados.parquet")
esp = pd.read_csv("futuros3_especificaciones.csv",
                  index_col="mercado")
tick = cot["mercado"].map(esp["tick"])
cot["ancho_libro"] = (cot["ask"] - cot["bid"]) / tick
sint_bid = cot["bid_cerca"] - cot["ask_lejos"]
sint_ask = cot["ask_cerca"] - cot["bid_lejos"]
cot["ancho_sint"] = (sint_ask - sint_bid) / tick
ultimo = cot[cot["mercado"] == "PEC"].iloc[-1]
print(ultimo[["fecha", "spread", "liquidacion", "bid", "ask"]]
      .to_string())
print(f"Sintético: bid {sint_bid[ultimo.name]:.2f}, "
      f"ask {sint_ask[ultimo.name]:.2f}")
OUT [01]El libro del spread y el de sus patas
fecha          2025-12-31 00:00:00
spread               PECG26-PECH26
liquidacion                   0.78
bid                           0.77
ask                           0.78
Sintético: bid 0.77, ask 0.80

El último día de los datos, el spread febrero-marzo del crudo ficticio cotiza 0,77-0,78 en su libro: un tick (0,01 USD por barril, 10 USD por contrato de 1.000 barriles). El sintético del mismo cierre, montado con las patas, cotiza 0,77-0,80: tres ticks. La liquidación del spread (0,78) es la diferencia de las liquidaciones de las patas y, en todas las filas del archivo, cae dentro del libro del spread.

IN [02]python
mas_ancho = cot["ancho_sint"] > cot["ancho_libro"]
anchos = pd.DataFrame({
    "filas": cot.groupby("mercado").size(),
    "libro": cot.groupby("mercado")["ancho_libro"].mean(),
    "sintetico": cot.groupby("mercado")["ancho_sint"].mean(),
    "sint_mas_ancho": mas_ancho.groupby(cot["mercado"]).mean(),
})
print(f"Media: libro {cot['ancho_libro'].mean():.2f} ticks, "
      f"sintético {cot['ancho_sint'].mean():.2f}")
anchos.round(2)
OUT [02]Anchos en ticks por mercado
Media: libro 1.25 ticks, sintético 2.78
         filas  libro  sintetico  sint_mas_ancho
mercado
GNA       4024   1.25       2.81            0.94
GRM       4024   1.25       2.79            0.89
MTO       4024   1.24       2.76            0.96
PEC       4024   1.25       2.77            0.93
RVA        650   1.22       2.83            0.91
TPC        650   1.25       2.78            0.90

En los seis mercados, el libro del spread tiene un ancho medio de 1,25 ticks y el sintético, de 2,78: más del doble, como dice la respuesta conocida. El sintético es más ancho entre el 89 % y el 96 % de las sesiones y en el resto empata; nunca es más estrecho. En un mercado real la relación depende de la liquidez de cada libro, pero el mecanismo de las órdenes implícitas de la bolsa (más abajo) empuja en la misma dirección: los precios de las patas se trasladan al libro del spread.

IN [03]python
mk = esp.loc[["RVA", "TPC", "PEC", "GNA", "MTO", "GRM"]]
media = cot.groupby("mercado")[["ancho_libro", "ancho_sint"]].mean()
coste = pd.DataFrame({
    "medio_libro": media["ancho_libro"] / 2 * mk["valor_tick"],
    "medio_sint": media["ancho_sint"] / 2 * mk["valor_tick"],
    "comision_2_patas": 2 * mk["comision"],
    "margen_spread": mk["margen_spread_calendario"],
    "margen_2_simples": 2 * mk["margen_inicial"],
})
coste.round(1)
OUT [03]Coste de cruzar y margen, en USD por spread
         medio_libro  medio_sint  comision_2_patas  margen_spread  margen_2_simples
mercado
GNA              6.2        14.0               4.5         3300.0           17000.0
GRM              7.8        17.4               4.5          150.0            1680.0
MTO              6.2        13.8               4.5          290.0           19200.0
PEC              6.3        13.9               4.5          670.0           11200.0
RVA              7.7        17.7               4.2          610.0           40400.0
TPC              9.8        21.7               3.0          140.0            5400.0

Medio ancho es lo que cuesta cruzar una vez: por el libro, de 6,2 a 9,8 USD por spread; pata a pata, de 13,8 a 21,7, algo más del doble. A eso se suman las comisiones de las dos patas, que se pagan igual por las dos vías. El margen añade otra diferencia, esta vez a favor de cualquier spread: la cámara reconoce que las dos patas se compensan y pide 670 USD por un calendario de crudo frente a 11.200 por dos posiciones simples, un 6 %; en el índice, 610 frente a 40.400 (lección 3.1.3).

El backtest lleva las patas

La forma tentadora de hacer el backtest de un spread es tomar la serie de su precio (la liquidacion del libro del spread, o la diferencia de las dos primeras posiciones de la curva) y tratarla como un activo más. Falla por la misma razón que una serie continua de futuros sin ajustar (lección 1.4.6): en cada roll la serie pasa de un par de contratos al siguiente, y la diferencia de ese día no es lo que ganó nadie. La lección 3.2.1 enseña el salto; aquí se mide su efecto en un backtest completo.

El backtest correcto es por patas: la posición son contratos concretos, y el P&L de cada día es el cambio de precio de los contratos que se tenían al cierre anterior. Para un spread largo (cerca − lejos), con mm el multiplicador:

P&Lt=m[(Ft c−Ft−1 c)−(Ft l−Ft−1 l)]\mathrm{P\&L}_t = m\left[\left(F^{\,c}_{t} - F^{\,c}_{t-1}\right) - \left(F^{\,l}_{t} - F^{\,l}_{t-1}\right)\right]

con cc y ll los contratos del par de t−1t - 1, aunque tt sea el día del roll.

IN [04]python
cur = pd.read_parquet("futuros3_curvas.parquet")
px = cur.set_index(["fecha", "contrato"])["liquidacion"]

def backtest_calendario(mercado):
    """Largo 1 spread (cerca - lejos) todos los días, rolado cuando
    cambia el par. P&L por patas (mismos contratos que ayer) y P&L
    ingenuo (diferencia de la serie de liquidaciones del spread)."""
    s = cot[cot["mercado"] == mercado].set_index("fecha")
    m = esp.at[mercado, "multiplicador"]
    f, f0 = s.index[1:], s.index[:-1]
    cerca = s["pata_cerca"].iloc[:-1]       # el par de ayer
    lejos = s["pata_lejos"].iloc[:-1]

    def cambio(patas):
        hoy = px.reindex(list(zip(f, patas))).to_numpy()
        antes = px.reindex(list(zip(f0, patas))).to_numpy()
        return hoy - antes

    out = pd.DataFrame(index=f)
    out["patas"] = (cambio(cerca) - cambio(lejos)) * m
    out["ingenuo"] = s["liquidacion"].diff().iloc[1:] * m
    out["roll"] = (s["spread"] != s["spread"].shift(1)).iloc[1:]
    return out

bt = {k: backtest_calendario(k) for k in ["PEC", "GNA", "MTO", "GRM"]}
resumen = {}
for k, v in bt.items():
    salto = (v["ingenuo"] - v["patas"])[v["roll"]]
    resumen[k] = {"patas": v["patas"].sum(),
                  "ingenuo": v["ingenuo"].sum(),
                  "saltos_roll": salto.sum(),
                  "rolls": v["roll"].sum()}
pd.DataFrame(resumen).T.round(0)
OUT [04]Un calendario largo y rolado: por patas o con la serie
       patas  ingenuo  saltos_roll  rolls
PEC  11850.0    790.0     -11060.0  192.0
GNA   6910.0   1960.0      -4950.0  192.0
MTO  -1230.0  -1450.0       -220.0   96.0
GRM   5400.0   -262.0      -5662.0   80.0
Fig. — Calendario largo del crudo ficticio (cercano menos siguiente, 1 spread, rolado cada mes), 2010-2025, sin costes: P&L acumulado llevando las patas frente a la diferencia de la serie de liquidaciones del spread

Llevando las patas, el calendario largo del crudo gana 11.850 USD en 16 años sin costes; con la serie del spread, 790. La diferencia, −11.060 USD, sale entera de los 192 días de roll: es la suma de los saltos entre el par viejo y el nuevo, que cotizan a niveles distintos porque están en tramos distintos de la curva. En el gas, 6.910 frente a 1.960; en el maíz, 5.400 frente a −262: cambia hasta el signo. Solo en el oro, con una curva casi recta (su pendiente es la financiación), el error es pequeño (−220 USD). La figura enseña cómo se separan las dos curvas: el backtest con la serie no se equivoca en un día concreto, se equivoca un poco en cada roll, siempre en el mismo sentido.

Los costes del roll

Un calendario mantenido en el tiempo hace en cada roll dos operaciones de spread: cerrar el par viejo y abrir el nuevo. Cruzando medio ancho en cada una, el coste es un ancho completo más cuatro comisiones (dos patas por spread), por el libro del spread o, si se monta pata a pata, con el ancho de las dos patas:

IN [05]python
def neto(mercado):
    s = cot[cot["mercado"] == mercado].set_index("fecha").iloc[1:]
    v = bt[mercado]
    vt, t = esp.at[mercado, "valor_tick"], esp.at[mercado, "tick"]
    com = esp.at[mercado, "comision"]
    libro = (s["ask"] - s["bid"]) / t * vt           # USD
    patas = ((s["ask_cerca"] - s["bid_cerca"])
             + (s["ask_lejos"] - s["bid_lejos"])) / t * vt
    c_libro = (libro + 4 * com).where(v["roll"], 0)
    c_patas = (patas + 4 * com).where(v["roll"], 0)
    anos = len(v) / 252
    sr = lambda x: x.mean() / v["patas"].std() * np.sqrt(252)
    return {"bruto_anual": v["patas"].sum() / anos,
            "coste_libro_anual": c_libro.sum() / anos,
            "coste_patas_anual": c_patas.sum() / anos,
            "sharpe_bruto": sr(v["patas"]),
            "sharpe_libro": sr(v["patas"] - c_libro),
            "sharpe_patas": sr(v["patas"] - c_patas)}

pd.DataFrame({k: neto(k) for k in bt}).T.round(2)
OUT [05]Costes del roll: por el libro o pata a pata
     bruto_anual  coste_libro_anual  coste_patas_anual  sharpe_bruto  sharpe_libro  sharpe_patas
PEC       742.28             261.08             428.96          0.21          0.14          0.09
GNA       432.84             262.96             432.09          0.06          0.02          0.00
MTO       -77.05             130.54             216.98         -0.35         -0.95         -1.35
GRM       338.26             122.62             222.84          0.24          0.15          0.08

En el crudo, el calendario largo gana 742 USD al año en bruto. Rolar por el libro cuesta 261 USD al año (doce rolls de un ancho completo más cuatro comisiones): un 35 % del bruto, y el Sharpe pasa de 0,21 a 0,14. Pata a pata cuesta 429 USD al año, el 58 %, y el Sharpe baja a 0,09. En el gas, el resultado neto desaparece. En el oro, que pierde en bruto, los costes multiplican la pérdida: su spread se mueve poco (unos 220 USD de volatilidad anual) y 131 USD al año de costes le llevan el Sharpe de −0,35 a −0,95. El maíz, con cinco vencimientos al año, rola menos y paga menos: 123 USD. La lección 2.4.7 trata el modelado de costes en general; aquí lo propio de un spread es que el coste depende de cómo se ejecuta.

Legging risk

Montar el spread pata a pata tiene un coste además de la horquilla: entre la primera ejecución y la segunda, la posición no es un spread sino un futuro solo. Si el precio se mueve en ese intervalo, la segunda pata se ejecuta a otro nivel. Es el legging risk, y su tamaño depende de cuánto se mueve una pata frente a cuánto se mueve el spread.

spreads3_intradia_1min.parquet tiene cinco sesiones de noviembre de 2025 del crudo ficticio PEC, minuto a minuto: el mejor bid y ask de la pata de diciembre (PECZ25), de la de enero (PECF26) y del libro del spread entre las dos (PECZ25-PECF26). Cada sesión va de las 23:00 UTC del día anterior a las 22:00 UTC.

IN [06]python
im = pd.read_parquet("spreads3_intradia_1min.parquet")
im["medio"] = (im["bid"] + im["ask"]) / 2
med = im.pivot(index="ts", columns="instrumento", values="medio")
d1 = med.diff()
print(f"Correlación de los cambios de 1 minuto de las patas: "
      f"{d1['PECZ25'].corr(d1['PECF26']):.3f}")
d5 = med.diff(5).std()
print(f"Desv. típica del cambio en 5 minutos: pata cercana "
      f"{d5['PECZ25']:.3f} USD, spread {d5['PECZ25-PECF26']:.3f} USD")
im.groupby("instrumento").agg(minimo=("bid", "min"),
                              maximo=("ask", "max"),
                              volumen=("volumen", "sum"))
OUT [06]Cinco sesiones de las dos patas y del spread, minuto a minuto
Correlación de los cambios de 1 minuto de las patas: 0.987
Desv. típica del cambio en 5 minutos: pata cercana 0.097 USD, spread 0.011 USD
               minimo  maximo  volumen
instrumento
PECF26          67.09   73.99  1992508
PECZ25          68.74   76.54  1509831
PECZ25-PECF26    1.53    2.59   896527
Fig. — Las dos patas del calendario diciembre-enero del crudo ficticio y el libro del spread, precio medio cada 30 minutos, del 3 al 7 de noviembre de 2025

Las dos patas se mueven casi a la vez (correlación de 0,987 entre los cambios de un minuto) y mucho: la cercana, con una desviación típica de 0,097 USD en 5 minutos, 97 USD por contrato de 1.000 barriles. El spread entre las dos, 0,011 USD: nueve veces menos. La figura lo enseña en las cinco sesiones: las patas suben de unos 70 a unos 76 USD, con caídas de más de un dólar dentro de una sesión; el spread va de 1,56 a 2,58 sin saltos.

IN [07]python
w = im.pivot(index="ts", columns="instrumento", values=["bid", "ask"])
sesion = (w.index + pd.Timedelta(hours=1)).date   # 23:00 a 22:00 UTC
SP, CERCA, LEJOS = "PECZ25-PECF26", "PECZ25", "PECF26"

def legging(k):
    """Comprar el spread pata a pata: cerca al ask en t, lejos al bid
    en t + k (misma sesión). Coste frente al ask del libro del spread
    en t, en USD por spread (1.000 barriles)."""
    lejos_bid = w[("bid", LEJOS)].groupby(sesion).shift(-k)
    pagado = w[("ask", CERCA)] - lejos_bid
    return ((pagado - w[("ask", SP)]) * 1000).dropna()

filas = {}
for k in [0, 1, 2, 5, 10, 15, 30, 60]:
    c = legging(k)
    filas[k] = {"media": c.mean(), "desv": c.std(),
                "p1": c.quantile(0.01), "p99": c.quantile(0.99)}
tab_leg = pd.DataFrame(filas).T.rename_axis("minutos")
tab_leg.round(1)
OUT [07]Legging: una pata ahora y la otra k minutos después
         media   desv      p1    p99
minutos
0         10.9    5.7    -0.0   20.0
1         10.2   43.9  -100.0  120.0
2          9.5   61.3  -150.0  160.0
5          7.3   96.3  -250.0  240.0
10         3.5  135.2  -370.0  320.0
15        -0.4  165.7  -440.0  380.0
30       -12.7  238.8  -675.1  530.0
60       -37.9  350.5 -1080.0  820.0

Con k = 0, las dos patas en el mismo minuto, el sintético cuesta 10,9 USD más que el ask del libro del spread (algo más de un tick), con poca dispersión (5,7). En cuanto se espera, la dispersión crece como la raíz del tiempo, la de una pata sola: 96,3 USD con 5 minutos, 165,7 con 15 y 350,5 con 60. En el 1 % de los peores casos, esperar 15 minutos cuesta 380 USD por spread y esperar una hora, 820: más que el resultado bruto de un año del calendario. La media se vuelve negativa con esperas largas (−37,9 con 60 minutos) porque en estas cinco sesiones el crudo sube dentro de la sesión y la pata lejana se vende más cara más tarde: es la tendencia de cinco días, no un coste esperado (el ejercicio 1 lo comprueba al revés).

IN [08]python
espera = {k: (w[("ask", SP)].groupby(sesion).shift(-k)
              - w[("ask", SP)]).mul(1000).std()
          for k in [1, 5, 15, 30, 60]}
pd.Series(espera, name="desv_usd").rename_axis("minutos").round(1)
OUT [08]Esperar con el spread entero
minutos
1      6.2
5     10.3
15    16.5
30    23.0
60    32.0
Name: desv_usd, dtype: float64

Esperar con el spread entero también tiene riesgo, pero el del spread: 10,3 USD de desviación en 5 minutos y 32,0 en 60, entre nueve y once veces menos que pata a pata. La figura de la cabecera pone las dos curvas juntas. De ahí la regla práctica: si el spread cotiza, se ejecuta por su libro; si hay que montarlo pata a pata, primero la menos líquida (la que más cuesta llenar) y la otra inmediatamente, con una orden que cruce. Y en el backtest, el coste del legging (la horquilla de las dos patas y un margen por el retraso) se cobra en cada entrada y en cada roll.

Órdenes combinadas e implícitas

Hay tres formas de llevar una orden de spread al mercado, con riesgos distintos:

FormaQuién cruzaLegging riskPrecio
Spread de la bolsaEl motor de la bolsa, en el libro del spreadNo: una sola operaciónUno, el del spread
Combinación garantizadaLa bolsa o el bróker, todas las patas a la vezNoEl del conjunto
Combinación no garantizadaEl bróker, pata a pataSíEl de cada pata

En CME Globex, el libro del spread y los de las patas no están aislados. La bolsa calcula órdenes implícitas: una orden en el libro del spread que deduce de órdenes en las patas (implied IN) o una orden en una pata que deduce de órdenes en el spread y en la otra pata (implied OUT). El ejemplo de su documentación: una venta de 10 contratos de septiembre a 9500 y una compra de 5 de diciembre a 9450 crean una venta implícita de 5 spreads septiembre-diciembre a 50.

Venta sep. 9500Venta implícita 50Compra dic. 9450Libro sep-dic
Fuente Mermaid
flowchart LR
  A[Venta sep. 9500] --> I[Venta implícita 50]
  B[Compra dic. 9450] --> I
  I --> S[Libro sep-dic]

La misma página fija algunas reglas: hacen falta al menos dos órdenes en mercados relacionados, las implícitas de compra no se cruzan con las implícitas de venta, en futuros no tienen prioridad temporal y no existen cuando el mercado no está en negociación (la preapertura, por ejemplo). Para quien hace el backtest, la consecuencia es que el libro del spread de un contrato líquido no suele ser peor que el sintético: si lo fuera, la propia bolsa lo mejoraría con las implícitas.

En un bróker, una combinación se define con las patas. Con la API de Interactive Brokers (aquí con ib_async, la continuación mantenida de ib_insync: versión 2.1.0, licencia BSD, en PyPI), una combinación es un contrato BAG con una ComboLeg por pata, cada una con su conId, su proporción y su sentido. La documentación de la API distingue los spreads intercommodity que lista la bolsa, que IBKR enruta directamente y son siempre garantizados, de las combinaciones de futuros por SMART, que se envían como no garantizadas con la etiqueta NonGuaranteed: el bróker no asegura que todas las patas se ejecuten a la vez. El bloque necesita TWS o IB Gateway y no se ejecuta aquí:

IN [09]python
from ib_async import IB, Bag, ComboLeg, Future, LimitOrder, TagValue

ib = IB()
ib.connect("127.0.0.1", 7497, clientId=7)   # TWS en cuenta de papel

cerca = Future("CL", "202512", "NYMEX")
lejos = Future("CL", "202601", "NYMEX")
ib.qualifyContracts(cerca, lejos)           # rellena los conId

spread = Bag(symbol="CL", exchange="SMART", currency="USD",
             comboLegs=[
                 ComboLeg(conId=cerca.conId, ratio=1, action="BUY",
                          exchange="NYMEX"),
                 ComboLeg(conId=lejos.conId, ratio=1, action="SELL",
                          exchange="NYMEX")])

# Precio límite del spread (cerca - lejos), no de cada pata.
orden = LimitOrder("BUY", 1, 0.55)
orden.smartComboRoutingParams = [TagValue("NonGuaranteed", "1")]
operacion = ib.placeOrder(spread, orden)
ib.sleep(5)
print(operacion.orderStatus.status, operacion.fills)

Si la orden se llena solo en parte o una pata se ejecuta antes que la otra, operacion.fills lo enseña ejecución a ejecución. Un sistema que opere así necesita, además, una regla para el caso de una pata colgada: cuánto esperar, cuándo cruzar la otra a mercado, cuándo deshacer la primera.

Los datos reales: CME con Databento

Databento distribuye los datos de CME Globex (el conjunto GLBX.MDP3, el que presenta la lección 1.3.4) con los spreads de la bolsa como instrumentos propios. Según su documentación de simbología, el símbolo padre ES.FUT incluye «todos los futuros y spreads de futuros» del E-mini, y en las definiciones el campo instrument_class distingue un futuro (F) de un spread de futuros (S); los símbolos de los spreads de calendario tienen la forma ESU3-ESZ3. Los mejores precios por minuto están en el esquema bbo-1m (con ts_recv al final de cada minuto) y las liquidaciones oficiales, en statistics, con stat_type 31Nota 1 · Databento, «Statistics» (esquema statistics: stat_type 3 = precio de liquidación, 6 = volumen compensado, 9 = interés abierto): https://databento.com/docs/schemas-and-data-formats/statistics. Con el cliente oficial (databento 0.88.0, licencia Apache-2.0, en PyPI) y una clave en la variable de entorno DATABENTO_API_KEY, la descarga de una semana del calendario de diciembre-enero del crudo sería:

IN [10]python
import databento as db

cliente = db.Historical()          # lee DATABENTO_API_KEY

# 1. Qué instrumentos hay: futuros y spreads de toda la familia CL
defs = cliente.timeseries.get_range(
    dataset="GLBX.MDP3", schema="definition", symbols=["CL.FUT"],
    stype_in="parent", start="2025-11-03", end="2025-11-04").to_df()
spreads = defs[defs["instrument_class"] == "S"]
calendarios = spreads[spreads["raw_symbol"].str.fullmatch(
    r"CL[FGHJKMNQUVXZ]\d-CL[FGHJKMNQUVXZ]\d")]

# 2. Lo que costaría antes de pedirlo (USD)
simbolos = ["CLZ5", "CLF6", "CLZ5-CLF6"]
coste = cliente.metadata.get_cost(
    dataset="GLBX.MDP3", schema="bbo-1m", symbols=simbolos,
    start="2025-11-03", end="2025-11-08")

# 3. Mejor bid y ask de las patas y del spread, minuto a minuto
bbo = cliente.timeseries.get_range(
    dataset="GLBX.MDP3", schema="bbo-1m", symbols=simbolos,
    start="2025-11-03", end="2025-11-08").to_df()

# 4. Liquidaciones oficiales
st = cliente.timeseries.get_range(
    dataset="GLBX.MDP3", schema="statistics", symbols=simbolos,
    start="2025-11-03", end="2025-11-08").to_df()
liquidaciones = st[st["stat_type"] == 3]

Debajo del cliente hay una API HTTP: POST https://hist.databento.com/v0/timeseries.get_range, con la clave como usuario de autenticación básica y parámetros de formulario (dataset, symbols, schema, start, end, encoding, pretty_px, pretty_ts, map_symbols, limit). Sin pretty_px, los precios llegan como enteros en unidades de 10−910^{-9}; sin pretty_ts, las marcas de tiempo, en nanosegundos. Su página de límites admite por dirección IP hasta 100 conexiones simultáneas y 100 peticiones de series por segundo, y responde 429 con una cabecera Retry-After al pasarse. Todo eso se puede probar sin red ni clave con un simulador local que imite la API: un servidor en un hilo, en un puerto libre que elige el sistema, que sirve las cinco sesiones del crudo ficticio en el formato del esquema bbo-1m (con menos columnas que el real) y rechaza la primera petición con un 429.

IN [11]python
import base64
import io
import threading
import time
import urllib.error
import urllib.parse
import urllib.request
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer

CLAVE = "db-" + "x" * 29            # 32 caracteres, como una real
IDS = {"PECZ25": 101, "PECF26": 102, "PECZ25-PECF26": 9001}
peticiones = []

def a_csv(d, mapear):
    """Filas de `im` en el formato bbo-1m (simplificado)."""
    fin_minuto = d["ts"] + pd.Timedelta(minutes=1)
    out = pd.DataFrame({
        "ts_recv": fin_minuto.dt.as_unit("ns").astype("int64"),
        "rtype": 196,
        "instrument_id": d["instrumento"].map(IDS),
        "bid_px_00": (d["bid"] * 1e9).round().astype("int64"),
        "ask_px_00": (d["ask"] * 1e9).round().astype("int64"),
        "bid_sz_00": d["tam_bid"], "ask_sz_00": d["tam_ask"]})
    if mapear:
        out["symbol"] = d["instrumento"].to_numpy()
    return out.to_csv(index=False)

class Simulador(BaseHTTPRequestHandler):
    def log_message(self, *args):      # sin registro en stderr
        pass

    def responder(self, codigo, cuerpo, cabeceras=()):
        self.send_response(codigo)
        for k, v in cabeceras:
            self.send_header(k, v)
        self.end_headers()
        self.wfile.write(cuerpo.encode())

    def do_POST(self):
        peticiones.append(self.path)
        clave = base64.b64encode(f"{CLAVE}:".encode()).decode()
        if self.headers.get("Authorization") != f"Basic {clave}":
            return self.responder(401, '{"detail":"Not authenticated"}')
        if len(peticiones) == 1:      # la primera choca con el límite
            return self.responder(429, '{"detail":"Too Many Requests"}',
                                  [("Retry-After", "1")])
        largo = int(self.headers["Content-Length"])
        p = urllib.parse.parse_qs(self.rfile.read(largo).decode())
        if (self.path != "/v0/timeseries.get_range"
                or p["schema"] != ["bbo-1m"]
                or p.get("encoding", ["csv"]) != ["csv"]):
            return self.responder(422, '{"detail":"no simulado"}')
        ini = pd.Timestamp(p["start"][0], tz="UTC")
        fin = pd.Timestamp(p["end"][0], tz="UTC")
        d = im[im["instrumento"].isin(p["symbols"][0].split(","))
               & (im["ts"] >= ini) & (im["ts"] < fin)]
        mapear = p.get("map_symbols", ["false"]) == ["true"]
        self.responder(200, a_csv(d, mapear),
                       [("Content-Type", "text/csv")])

servidor = ThreadingHTTPServer(("127.0.0.1", 0), Simulador)
threading.Thread(target=servidor.serve_forever, daemon=True).start()
BASE = f"http://127.0.0.1:{servidor.server_port}"
print("Simulador en marcha:", BASE.startswith("http://127.0.0.1:"))
OUT [11]Un simulador local de timeseries.get_range
Simulador en marcha: True

El cliente es el de la lección 1.4.1 con lo propio de esta API: autenticación básica con la clave como usuario y la contraseña vacía, parámetros de formulario y, ante un 429, esperar lo que diga Retry-After antes de reintentar.

IN [12]python
def get_range(base, clave, intentos=3, **params):
    """POST /v0/timeseries.get_range con encoding=csv. Devuelve un
    DataFrame con precios en USD y marcas de tiempo UTC."""
    datos = urllib.parse.urlencode({"encoding": "csv", **params})
    auth = base64.b64encode(f"{clave}:".encode()).decode()
    for i in range(intentos):
        req = urllib.request.Request(
            f"{base}/v0/timeseries.get_range", data=datos.encode(),
            headers={"Authorization": f"Basic {auth}"})
        try:
            with urllib.request.urlopen(req, timeout=10) as r:
                df = pd.read_csv(io.StringIO(r.read().decode()))
            break
        except urllib.error.HTTPError as e:
            if e.code != 429 or i == intentos - 1:
                raise
            espera = float(e.headers.get("Retry-After", "1"))
            print(f"HTTP 429: espero {espera:.0f} s y reintento")
            time.sleep(espera)
    df["ts_recv"] = pd.to_datetime(df["ts_recv"], unit="ns", utc=True)
    for c in ["bid_px_00", "ask_px_00"]:
        df[c] = df[c] / 1e9
    return df

bbo = get_range(BASE, CLAVE, dataset="GLBX.MDP3", schema="bbo-1m",
                symbols="PECZ25,PECF26,PECZ25-PECF26",
                start="2025-11-04T13:00", end="2025-11-04T13:05",
                map_symbols="true")
print(f"{len(peticiones)} peticiones, {len(bbo)} filas")
bbo.loc[bbo["symbol"] == "PECZ25-PECF26",
        ["ts_recv", "bid_px_00", "ask_px_00", "bid_sz_00", "ask_sz_00"]]
OUT [12]Cliente con autenticación, límites y reintentos
HTTP 429: espero 1 s y reintento
2 peticiones, 15 filas
                     ts_recv  bid_px_00  ask_px_00  bid_sz_00  ask_sz_00
2  2025-11-04 13:01:00+00:00       1.69       1.70         42         90
5  2025-11-04 13:02:00+00:00       1.70       1.71         24         33
8  2025-11-04 13:03:00+00:00       1.70       1.71         33        123
11 2025-11-04 13:04:00+00:00       1.70       1.71         51         63
14 2025-11-04 13:05:00+00:00       1.70       1.71        111        165

La primera petición recibe un 429; el cliente espera el segundo que pide Retry-After y la segunda trae las 15 filas: cinco minutos de tres instrumentos. Las marcas son el final de cada minuto, como en bbo-1m: la fila de las 13:01 es el minuto que empieza a las 13:00 en el Parquet del curso.

IN [13]python
get_range(BASE, "db-clave-equivocada", dataset="GLBX.MDP3",
          schema="bbo-1m", symbols="PECZ25",
          start="2025-11-04", end="2025-11-05")
OUT [13]Una clave equivocada
urllib.error.HTTPError: HTTP Error 401: Unauthorized

Un 401 no se reintenta: es un error de configuración, no de carga. El cliente solo repite ante el 429; ante un 5xx real convendría repetir también, con espera exponencial (lección 1.4.1).

IN [14]python
dia = get_range(BASE, CLAVE, dataset="GLBX.MDP3", schema="bbo-1m",
                symbols="PECZ25,PECF26,PECZ25-PECF26",
                start="2025-11-03T23:00", end="2025-11-04T22:00",
                map_symbols="true")
ref = im[(im["ts"] >= "2025-11-03T23:00Z")
         & (im["ts"] < "2025-11-04T22:00Z")]
iguales = (np.allclose(dia["bid_px_00"], ref["bid"])
           and np.allclose(dia["ask_px_00"], ref["ask"])
           and (dia["ts_recv"] - pd.Timedelta(minutes=1)
                == ref["ts"].dt.as_unit("ns").to_numpy()).all())
print(f"{len(dia)} filas; coinciden con el Parquet: {iguales}")
servidor.shutdown()
OUT [14]Las mismas filas que el archivo
4140 filas; coinciden con el Parquet: True

Una sesión completa (de las 23:00 a las 22:00 UTC, 1.380 minutos por instrumento) llega con 4.140 filas y coincide con el archivo, desplazada un minuto. Con la URL real y una clave, el mismo cliente pide los datos de verdad; el simulador sirve para probar autenticación, límites, errores y paginación sin gastar uso (Databento cobra por volumen descargado, lección 1.3.4) y sin depender de la red en los tests (lección 0.4.5).

Errores típicos

  • La serie del spread como si fuera un activo. Su diferencia en el día del roll mezcla dos pares. El P&L se calcula con los contratos de ayer; la serie sirve para la señal, no para el resultado.
  • Ejecutar en el backtest a la liquidación del spread. La liquidación de los datos cae siempre dentro del libro del spread, pero no es un precio al que se pueda operar en volumen: el coste mínimo realista es medio ancho del libro.
  • Costes de un spread con el ancho del sintético, o al revés. Si el sistema opera por el libro, el coste es el del libro; si opera pata a pata, el de las dos patas más el legging. Confundirlos cambia el resultado neto en un factor de dos en el calendario de crudo.
  • Ignorar los días sin libro. En índices y bonos, el spread de calendario solo se negocia de verdad alrededor del roll (en los datos, las 10 sesiones anteriores). Un backtest que lo opera todo el trimestre supone una liquidez que no existe.
  • Marcas de tiempo del proveedor. En bbo-1m de Databento la marca es el final del minuto; en el Parquet del curso, el inicio. Alinear mal un minuto entre la señal y el precio es anticipación (lección 1.7.4).
  • Margen. El margen de un spread de calendario es una fracción del de dos posiciones simples (670 USD frente a 11.200 en el crudo ficticio): un rendimiento «sobre margen» parece enorme y esconde que la pérdida posible no se reduce en la misma proporción (lección 3.1.3).

Ejercicios

  1. Repite el estudio de legging al revés: vender primero la pata lejana al bid y comprar la cercana al ask k minutos después. ¿Cambia la desviación? ¿Y la media? Explica por qué la media del legging de la tabla se vuelve negativa con esperas largas en estas cinco sesiones.
Solución
IN [15]python
def legging_inverso(k):
    cerca_ask = w[("ask", CERCA)].groupby(sesion).shift(-k)
    pagado = cerca_ask - w[("bid", LEJOS)]
    return ((pagado - w[("ask", SP)]) * 1000).dropna()

pd.DataFrame({k: {"media": legging_inverso(k).mean(),
                  "desv": legging_inverso(k).std()}
              for k in [0, 5, 15, 60]}).T.round(1)
OUT [15]Legging empezando por la pata lejana
    media   desv
0    10.9    5.7
5    15.1   96.4
15   23.8  166.3
60   66.8  353.0

Empezando por la lejana, la desviación es la misma (96,4 USD con 5 minutos, 353,0 con 60): el riesgo de una pata sola no depende de cuál se ejecute primero. La media cambia de signo: +15,1 USD con 5 minutos y +66,8 con 60, frente a 7,3 y −37,9. En estas cinco sesiones el crudo sube dentro de la sesión (el contrato de diciembre pasa de unos 70 a unos 76 USD, como se ve en la figura): quien compra después la pata cercana la paga más cara, y quien vende después la lejana la vende más cara. Con cinco sesiones, esa media es la tendencia de esos días; la desviación, cinco veces mayor, es lo que hay que presupuestar.

  1. Las sesiones de Asia (de 23:00 a 08:00 UTC) tienen horquillas más anchas en los datos. Mide el ancho medio en ticks del libro del spread y del sintético por franja (Asia, Londres de 08:00 a 13:00, Nueva York desde las 13:00) y el coste de comprar el spread por cada vía en USD.
Solución
IN [16]python
h = w.index.hour
franja = np.select([(h >= 23) | (h < 8), h < 13],
                   ["asia", "londres"], "nueva_york")
libro = (w[("ask", SP)] - w[("bid", SP)]) / 0.01
sint = ((w[("ask", CERCA)] - w[("bid", CERCA)])
        + (w[("ask", LEJOS)] - w[("bid", LEJOS)])) / 0.01
t = pd.DataFrame({"libro": libro, "sintetico": sint})
t = t.groupby(franja).mean()
t["coste_libro_usd"] = t["libro"] / 2 * 10
t["coste_sint_usd"] = t["sintetico"] / 2 * 10
t.round(2)
OUT [16]Anchos por franja horaria
            libro  sintetico  coste_libro_usd  coste_sint_usd
asia         1.43       2.89             7.16           14.44
londres      1.00       2.00             5.00           10.00
nueva_york   1.00       2.00             5.00           10.00

En Asia, el libro del spread tiene 1,43 ticks de ancho medio y el sintético, 2,89; en Londres y Nueva York, 1,00 y 2,00 exactos. Comprar por el libro cuesta 7,16 USD en Asia y 5,00 después; pata a pata, 14,44 y 10,00. En los datos el horario solo ensancha el mercado en Asia (la mitad de los minutos con un tick más en cada libro). Un sistema que ejecuta al cierre europeo o americano paga lo mínimo; uno que reacciona a noticias de madrugada paga un 43 % más por el libro.

  1. Amplía el simulador para que pagine: con un parámetro limit, que devuelva como mucho ese número de filas, y que el cliente pida el siguiente tramo a partir del último ts_recv recibido. ¿Qué problema aparece si varias filas comparten el mismo ts_recv justo en el corte?
Solución

El parámetro limit de la API real limita el número de registros, pero no devuelve un cursor: el cliente tiene que volver a pedir desde la última marca recibida. Con start = último ts_recv (incluido), las filas de ese instante se duplican; con start = último ts_recv + 1 ns, se pierden las que no cupieron en el tramo anterior y compartían esa marca (en bbo-1m, las tres series de un mismo minuto tienen el mismo ts_recv). La solución robusta es pedir desde la última marca incluida y descartar los duplicados por (ts_recv, instrument_id), o bien no paginar por tiempo sino por bloques de tiempo cerrados (un día por petición), que es lo que hace la descarga por lotes de Databento (batch.submit_job). En el simulador basta con añadir d = d.head(int(p["limit"][0])) si viene limit, y en el cliente un bucle que avance start hasta que una respuesta llegue con menos filas que limit.

Footnotes

  1. Databento, «Statistics» (esquema statistics: stat_type 3 = precio de liquidación, 6 = volumen compensado, 9 = interés abierto): https://databento.com/docs/schemas-and-data-formats/statistics ↩

Respuesta plegada en cada pregunta

Autoevaluación

  1. En los datos, el libro del spread de calendario cotiza con 1,25 ticks de ancho medio y el spread montado con las patas, con 2,78. ¿Por qué el sintético es más ancho?

    • APorque los datos tienen errores
    • BPorque comprar el sintético es comprar la pata cercana al ask y vender la lejana al bid: se pagan los dos anchos, y la suma de dos horquillas es mayor que la horquilla de un instrumento que se negocia entero
    • CPorque el spread cotizado no se puede operar
    • DPorque el sintético incluye la comisión
    Ver respuesta

    Respuesta B. Porque comprar el sintético es comprar la pata cercana al ask y vender la lejana al bid: se pagan los dos anchos, y la suma de dos horquillas es mayor que la horquilla de un instrumento que se negocia entero

    El bid del sintético es bid_cerca − ask_lejos y su ask, ask_cerca − bid_lejos: su ancho es la suma de los anchos de las dos patas. El libro del spread lo cotizan quienes quieren el spread entero, y con las órdenes implícitas de la bolsa nunca es peor que el sintético.

  2. Un backtest de un calendario largo y rolado de crudo da 790 USD en 16 años con la diferencia de la serie de liquidaciones del spread, y 11.850 USD llevando las patas. ¿Cuál es el bueno y por qué?

    • AEl de la serie, porque es el precio del spread
    • BLos dos, según el día
    • CEl de las patas: la serie del spread cambia de par en cada roll y su diferencia de ese día mezcla dos instrumentos distintos; esos saltos suman −11.060 USD que nadie pagó
    • DNinguno: hay que usar la serie ajustada por ratio
    Ver respuesta

    Respuesta C. El de las patas: la serie del spread cambia de par en cada roll y su diferencia de ese día mezcla dos instrumentos distintos; esos saltos suman −11.060 USD que nadie pagó

    El P&L de un día es el cambio del par que se tenía ayer. En el roll, la serie pasa del par viejo al nuevo y su diferencia no es una ganancia ni una pérdida: es el cambio de instrumento. Es el mismo error que el de una serie continua de futuros sin ajustar (lección 1.4.6), multiplicado por 192 rolls.

  3. Se compra la pata cercana al ask y se espera 15 minutos para vender la lejana. ¿Qué dice la tabla de legging de los datos?

    • AQue no cambia nada: el spread apenas se mueve
    • BQue siempre se gana porque se espera a un precio mejor
    • CQue cuesta exactamente un tick
    • DQue el coste medio es casi cero, pero su desviación típica es de 166 USD por spread y el percentil 1, −440 USD: el riesgo es el de una pata sola, no el del spread
    Ver respuesta

    Respuesta D. Que el coste medio es casi cero, pero su desviación típica es de 166 USD por spread y el percentil 1, −440 USD: el riesgo es el de una pata sola, no el del spread

    Mientras solo hay una pata, la posición es un futuro de crudo de 1.000 barriles, no un spread. Su precio se mueve 0,097 USD en 5 minutos (97 USD por contrato) frente a 0,011 del spread. Esperar con el spread entero cuesta una décima parte de ese riesgo.

  4. ¿Qué es una orden implícita «implied IN» en CME Globex?

    • AUna orden en el libro del spread que la bolsa deduce de órdenes en los libros de las patas
    • BUna orden que el bróker parte en dos
    • CUna orden oculta de un creador de mercado
    • DUna orden que solo existe en la subasta
    Ver respuesta

    Respuesta A. Una orden en el libro del spread que la bolsa deduce de órdenes en los libros de las patas

    Según la documentación de CME, una venta de 10 en septiembre a 9500 y una compra de 5 en diciembre a 9450 crean una venta implícita de 5 a 50 en el spread septiembre-diciembre. Al revés, de órdenes en el spread y en una pata la bolsa deduce órdenes en la otra pata (implied OUT). Por eso el libro del spread no suele ser peor que el sintético.

  5. Una combinación de futuros enviada por SMART como no garantizada a IBKR, ¿qué riesgo tiene?

    • ANinguno: el bróker la ejecuta como un solo instrumento
    • BEl de que se ejecuten las patas por separado y no a la vez: legging risk, con el resultado de una pata sola si la otra no se llena
    • CEl de pagar el doble de comisiones
    • DEl de que la bolsa la rechace siempre
    Ver respuesta

    Respuesta B. El de que se ejecuten las patas por separado y no a la vez: legging risk, con el resultado de una pata sola si la otra no se llena

    La documentación de la API de IBKR distingue los spreads que lista la bolsa (intercommodity, enrutados directamente y siempre garantizados) de las combinaciones por SMART, que se marcan como no garantizadas. No garantizada quiere decir que el bróker no asegura que todas las patas se ejecuten juntas.

Ideas clave

  • El spread cotizado es un instrumento con su libro; el sintético se monta con las patas y paga sus dos horquillas. En los datos, 1,25 ticks frente a 2,78 de ancho medio: un 93 % de las sesiones de crudo, más ancho el sintético.
  • El backtest de un spread lleva las patas: el P&L de cada día es el cambio del par que se tenía ayer. La diferencia de la serie del spread mezcla pares en cada roll; en el calendario de crudo, 790 USD frente a 11.850 en 16 años.
  • Cada roll de un calendario es cerrar un spread y abrir otro. Por el libro cuesta 261 USD al año en el calendario de crudo, un 35 % del resultado bruto; pata a pata, 429 USD, un 58 %.
  • El legging risk es el riesgo de una pata sola: con 5 minutos de espera, 96 USD de desviación por spread de crudo; con 60, 350. Esperar con el spread entero, 10 y 32.
  • Databento sirve los spreads de CME como instrumentos propios (instrument_class S, símbolos como CLZ5-CLF6), con mejores precios por minuto (bbo-1m) y liquidaciones (statistics, stat_type 3); metadata.get_cost dice lo que cuesta antes de descargar.
  • Orden combinada de bolsa: una sola operación, sin legging. Combinación no garantizada del bróker: pata a pata. Las implícitas de CME conectan los libros de las patas y del spread.

Recursos

Datos