Lección 1.4.6 · 60 min

Series continuas de futuros: reglas de roll, ajuste por diferencia y ajuste por ratio

Un contrato de futuros vive unos meses y después vence; un backtest necesita años. La serie continua empalma contratos sucesivos, y cada forma de hacerlo conserva una cosa y falsea otra. Esta lección construye las tres versiones habituales con los 31 contratos del futuro ficticio FIX y comprueba cuál dice la verdad sobre los resultados.

Fig. — Futuro continuo de FIX, 2019–2025, con roll por volumen: las tres construcciones (arriba) y su distancia a la serie ajustada por ratio (abajo)
Lecciones del nivel 1
  1. 1.1 · Cómo funcionan los mercados

    1. 1.1.1 Participantes y centros de negociación
    2. 1.1.2 Sesiones, subastas, festivos y calendarios
    3. 1.1.3 Libro de órdenes: bid, ask, spread y profundidad
    4. 1.1.4 Tipos de órdenes
    5. 1.1.5 Costes, liquidación, apalancamiento y margen
  2. 1.2 · Clases de activos

    1. 1.2.1 Acciones: splits, dividendos y eventos corporativos
    2. 1.2.2 ETFs: réplica, tracking error, apalancados y UCITS
    3. 1.2.3 Futuros: especificaciones, margen, roll y contango
    4. 1.2.4 Forex: pares, pips, lotes, rollover y CFD
    5. 1.2.5 Opciones: call, put, valor intrínseco y valor temporal
    6. 1.2.6 Cripto: spot, perpetuos, funding y custodia
    7. 1.2.7 Renta fija y tipos de interés; CFDs y productos apalancados
  3. 1.3 · Fuentes de datos de mercado

    1. 1.3.1 Tipos de datos de mercado: EOD, barras, ticks, libro y referencia
    2. 1.3.2 Fuentes de datos gratuitas
    3. 1.3.3 APIs de datos retail y prosumer
    4. 1.3.4 Proveedores de datos profesionales
    5. 1.3.5 Datos de cripto y on-chain
    6. 1.3.6 Brókeres como fuente de datos
    7. 1.3.7 Licencias y términos de uso de datos
    8. 1.3.8 Cómo elegir proveedor de datos
  4. 1.4 · Ingeniería de datos financieros

    1. 1.4.1 Descarga por REST y websockets: paginación, límites y reintentos
    2. 1.4.2 Limpieza de datos: faltantes, ticks erróneos, huecos y zonas horarias
    3. 1.4.3 Ajustes por splits y dividendos: precios ajustados y sin ajustar
    4. 1.4.4 Universos sin sesgo de supervivencia: deslistados, fusiones y cambios de símbolo
    5. 1.4.5 Construcción de barras: tiempo, ticks, volumen, dólar y desequilibrio
    6. 1.4.6 Series continuas de futuros: reglas de roll, ajuste por diferencia y ajuste por ratio
    7. 1.4.7 Almacenamiento: CSV, Parquet, particionado, DuckDB, TimescaleDB y ArcticDB
    8. 1.4.8 Pipeline reproducible: descarga, validación, actualización incremental y tests de calidad
  5. 1.5 · Datos fundamentales y macro

    1. 1.5.1 Estados financieros, ratios, XBRL y SEC EDGAR
    2. 1.5.2 Datos macro, calendarios económicos, estimaciones y transcripciones
  6. 1.6 · Análisis exploratorio financiero

    1. 1.6.1 Rentabilidades, volatilidad, correlaciones móviles y drawdown
    2. 1.6.2 Hechos estilizados: colas gruesas, agrupamiento, autocorrelación y apalancamiento
    3. 1.6.3 Estacionalidad: día de la semana, mes, cambio de mes y sesiones intradía
  7. 1.7 · Análisis técnico programado

    1. 1.7.1 Indicadores técnicos en Python
    2. 1.7.2 Implementación propia frente a TA-Lib: validar resultados
    3. 1.7.3 Patrones de velas y soportes y resistencias
    4. 1.7.4 Multi-timeframe sin anticipación

Por qué hace falta una serie continua

Un contrato de futuros tiene fecha de caducidad. El de FIX con vencimiento en junio de 2025, FIXM25, empieza a cotizar unos nueve meses antes, concentra casi toda la negociación durante los tres últimos y deja de existir el 20 de junio, liquidado contra el cierre del índice (lección 1.2.3). Quien quiere estar expuesto al índice durante años no mantiene un contrato: va pasando de uno al siguiente antes de cada vencimiento. Eso es el roll.

Para investigar hace falta lo mismo en forma de datos: una serie larga, sin cortes, que represente «tener el futuro de FIX». No existe en la bolsa; se fabrica empalmando contratos. Hay que tomar tres decisiones, y cada proveedor de datos las toma a su manera:

Contratos sueltosRegla de rollContrato del díaSerie empalmadaAjuste diferenciaAjuste ratioSin ajustar
Fuente Mermaid
flowchart TB
  K[Contratos sueltos] --> R[Regla de roll]
  R --> M[Contrato del día]
  M --> E[Serie empalmada]
  E --> D[Ajuste diferencia]
  E --> T[Ajuste ratio]
  E --> S[Sin ajustar]
  1. Qué contratos entran: todos los vencimientos o solo los de un ciclo (en FIX, los trimestrales H, M, U y Z).
  2. Cuándo se cambia de contrato: un número fijo de sesiones antes del vencimiento, o cuando el siguiente pasa a tener más volumen o más interés abierto.
  3. Qué se hace con el salto de precio del cambio: dejarlo, restarlo (ajuste por diferencia) o dividirlo (ajuste por ratio).

Los datos son los del módulo de futuros del nivel 1: liquidaciones diarias, volumen e interés abierto de 31 contratos de FIX entre 2019 y 2025, sus especificaciones y el índice.

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

c = pd.read_parquet("fix_contratos.parquet")
spec = pd.read_csv("fix_especificaciones.csv").iloc[0]
liq = c.pivot(index="fecha", columns="contrato", values="liquidacion")
vol = c.pivot(index="fecha", columns="contrato", values="volumen")
oi = c.pivot(index="fecha", columns="contrato",
             values="interes_abierto")
venc = c.groupby("contrato")["vencimiento"].first().sort_values()
orden = list(venc.index)
sesiones = liq.index
print(len(orden), "contratos,", len(sesiones), "sesiones")
print("multiplicador:", spec["multiplicador"], "USD por punto")
print("contratos vivos por sesión:",
      liq.notna().sum(axis=1).value_counts().to_dict())
OUT [01]Los contratos de FIX
31 contratos, 1760 sesiones
multiplicador: 50 USD por punto
contratos vivos por sesión: {3: 1706, 4: 45, 2: 9}

Tres tablas anchas, una columna por contrato y una fila por sesión, con NaN cuando el contrato no cotiza. Casi siempre hay tres contratos vivos; alrededor de cada vencimiento, dos o cuatro. Una foto de un día enseña cómo se reparten:

IN [02]python
indice = pd.read_csv("fix_indice_diario.csv", parse_dates=["fecha"])
indice = indice.set_index("fecha")["indice"]
dia = c[c["fecha"] == "2025-06-02"].set_index("contrato")
dia["base"] = dia["liquidacion"] - indice["2025-06-02"]
print("índice:", indice["2025-06-02"])
dia[["vencimiento", "liquidacion", "base", "volumen",
     "interes_abierto"]]
OUT [02]Un día de la curva: 2 de junio de 2025
índice: 6359.1
         vencimiento  liquidacion    base  volumen  interes_abierto
contrato
FIXM25    2025-06-20      6367.75    8.65   315854           786283
FIXU25    2025-09-19      6419.00   59.90     4406            24548
FIXZ25    2025-12-19      6462.50  103.40      729             3201

El contrato cercano, FIXM25, se lleva casi todo el volumen y el interés abierto; los otros dos apenas se negocian. Cada vencimiento más lejano cotiza más caro: la base (futuro menos índice) es de 8,65 puntos a 18 días y de 103,40 a algo más de seis meses. Es contango, porque el tipo de interés supera al rendimiento por dividendo del índice (el carry de la lección 1.2.3). Esa escalera de precios es la que produce los saltos al empalmar.

Antes de programar reglas de roll hay que tener bien los vencimientos. La especificación dice «tercer viernes del mes del contrato; si no hay sesión, la anterior», y se comprueba con el calendario real de la NYSE de exchange_calendars (lección 1.1.2):

IN [03]python
import exchange_calendars as xcals

nyse = xcals.get_calendar("XNYS")
def tercer_viernes(v):
    viernes = pd.date_range(v.replace(day=1), periods=31,
                            freq="D")
    viernes = viernes[(viernes.dayofweek == 4)
                      & (viernes.month == v.month)]
    return viernes[2]

esperado = venc.map(lambda v: nyse.date_to_session(
    tercer_viernes(v), direction="previous"))
print("vencimientos según la regla:", (esperado == venc).all())
venc[venc.dt.dayofweek != 4]
OUT [03]Vencimientos: tercer viernes o la sesión anterior
vencimientos según la regla: True
contrato
FIXM26   2026-06-18
Name: vencimiento, dtype: datetime64[us]

Los 31 vencimientos cumplen la regla, y uno solo no cae en viernes: FIXM26, el 18 de junio de 2026, porque el viernes 19 es Juneteenth y la NYSE cierra. Un calendario «a mano» que solo mire el día de la semana se equivocaría ahí.

Cuándo cambiar de contrato

sínosínoCierre de sesión¿Cruce ayer?Roll al cierre¿Vence en 2 días?Mantener
Fuente Mermaid
flowchart TB
  S[Cierre de sesión] --> C{¿Cruce ayer?}
  C -- sí --> R[Roll al cierre]
  C -- no --> N{¿Vence en 2 días?}
  N -- sí --> R
  N -- no --> M[Mantener]

Las tres reglas habituales:

  • Calendario. Cambiar un número fijo de sesiones antes del vencimiento. Es predecible y no necesita datos: se sabe con meses de antelación. Su riesgo es cambiar demasiado pronto o demasiado tarde si la liquidez se mueve otro día.
  • Volumen. Cambiar cuando el contrato siguiente negocia más que el cercano. Sigue a la liquidez, que es donde se ejecuta mejor.
  • Interés abierto. Lo mismo con el número de contratos abiertos, que cambia más despacio y con menos ruido que el volumen.

El diagrama combina las dos ideas: roll por cruce y, si no llega, una red de seguridad por calendario para no llegar nunca al vencimiento con la posición abierta. Las dos funciones siguientes implementan cada regla por separado. El detalle importante está en la segunda: el volumen de una sesión solo se conoce cuando termina, así que el cruce observado al cierre del día tt se ejecuta al cierre de t+1t+1.

IN [04]python
def roll_calendario(n):
    """Cambiar al cierre de la sesión n anterior al vencimiento."""
    return {a: sesiones[sesiones.get_loc(venc[a]) - n]
            for a in orden if venc[a] <= sesiones[-1]}

def roll_cruce(tabla, solo_frontal=True):
    """Sesión siguiente a la primera en que el contrato siguiente
    supera al actual: el volumen del día se conoce al cierre."""
    rolls = {}
    for i, (a, b) in enumerate(zip(orden, orden[1:])):
        if venc[a] > sesiones[-1]:
            break
        par = tabla[[a, b]].dropna()
        par = par[par.index <= venc[a]]
        if solo_frontal and i > 0:  # a ya es el contrato cercano
            par = par[par.index > venc[orden[i - 1]]]
        cruces = par.index[par[b] > par[a]]
        rolls[a] = sesiones[sesiones.get_loc(cruces[0]) + 1]
    return rolls

def sesiones_antes(rolls):
    return pd.Series({a: sesiones.get_loc(venc[a])
                      - sesiones.get_loc(d) for a, d in rolls.items()})

reglas = {"calendario_5": roll_calendario(5),
          "volumen": roll_cruce(vol),
          "interes_abierto": roll_cruce(oi)}
antes = pd.DataFrame({k: sesiones_antes(v) for k, v in reglas.items()})
antes.apply(pd.Series.value_counts).fillna(0).astype(int)
OUT [04]Tres reglas de roll
   calendario_5  volumen  interes_abierto
3             0       14               28
4             0       14                0
5            28        0                0

La tabla cuenta, para los 28 rolls de 2019 a 2025, cuántas sesiones antes del vencimiento cambia cada regla. Por volumen, tres o cuatro (14 veces cada una); por interés abierto, siempre tres; el calendario, cinco por construcción. En FIX la liquidez pasa al siguiente contrato en la última semana y media, como describe el README de los datos, y las tres reglas caen a una o dos sesiones de distancia. En mercados reales la diferencia puede ser mayor: hay futuros cuyo roll empieza semanas antes, y entregables (bonos, materias primas) en los que hay que salir antes del primer día de aviso para no recibir la entrega.

El parámetro solo_frontal evita un error que no se ve en el código y sí en los resultados. Si el cruce se busca en toda la vida del contrato, y no solo mientras es el cercano, aparece algo así:

IN [05]python
mal = sesiones_antes(roll_cruce(vol, solo_frontal=False))
print("sesiones antes del vencimiento:", mal.value_counts().to_dict())
OUT [05]El cruce buscado en toda la vida del contrato
sesiones antes del vencimiento: {3: 8, 4: 6, 126: 6, 124: 4, 127: 2, 123: 1, 125: 1}

En 14 de los 28 rolls, el «cruce» sale entre 123 y 127 sesiones antes del vencimiento: medio año. Cuando un contrato todavía es el segundo de la curva, su volumen y el del tercero son pequeños y ruidosos, y el tercero lo supera algún día por casualidad. Ese día no significa nada: la liquidez está en otro contrato. La comparación solo tiene sentido entre el contrato cercano y el siguiente.

Con las fechas de roll, cada sesión tiene su contrato. El cambio ocurre al cierre de la sesión de roll: ese día la posición termina en el contrato nuevo.

IN [06]python
def mantenido(rolls):
    """Contrato en cartera al cierre de cada sesión: el roll se
    hace al cierre de la sesión del cruce."""
    out, k = [], 0
    for t in sesiones:
        while orden[k] in rolls and t >= rolls[orden[k]]:
            k += 1
        out.append(orden[k])
    return pd.Series(out, index=sesiones)

rolls = reglas["volumen"]
h = mantenido(rolls)
h["2025-06-10":"2025-06-16"]
OUT [06]Qué contrato se tiene al cierre de cada sesión
fecha
2025-06-10    FIXM25
2025-06-11    FIXM25
2025-06-12    FIXM25
2025-06-13    FIXU25
2025-06-16    FIXU25
dtype: str
Fig. — Roll de FIX de junio a septiembre de 2025: interés abierto de cada contrato, el roll por volumen (cierre del 13 de junio) y el vencimiento (20 de junio)

El volumen de FIXU25 supera al de FIXM25 el 12 de junio de 2025 (341.427 contratos frente a 330.646); el roll se hace al cierre del 13. La figura enseña el interés abierto alrededor de esas fechas: el del contrato de junio pasa de unos 780.000 contratos a 44.111 el día del vencimiento, y el de septiembre sube a más de 800.000. Las dos curvas se cruzan entre el 12 y el 13 de junio.

Empalmar: la serie sin ajustar y sus huecos

La forma más simple de serie continua es tomar, cada sesión, el precio del contrato que se tiene al cierre. Funciona hasta el día del roll: ese día la serie salta del precio del contrato viejo al del nuevo, y el salto no es una ganancia ni una pérdida de nadie. Es la diferencia entre dos contratos distintos en el mismo instante, el hueco del roll:

IN [07]python
precio_h = pd.Series([liq.at[t, k] for t, k in h.items()],
                     index=sesiones)
huecos = pd.DataFrame([
    {"fecha": d, "viejo": liq.at[d, a],
     "nuevo": liq.at[d, orden[orden.index(a) + 1]]}
    for a, d in rolls.items()]).set_index("fecha")
huecos["hueco"] = huecos["nuevo"] - huecos["viejo"]
huecos["ratio"] = huecos["nuevo"] / huecos["viejo"]
print(f"{len(huecos)} rolls; huecos de {huecos['hueco'].min()} "
      f"a {huecos['hueco'].max()} puntos; "
      f"{(huecos['hueco'] < 0).sum()} negativos")
huecos.loc["2020-03":"2020-12"].round(4)
OUT [07]La serie empalmada y sus huecos
28 rolls; huecos de -17.5 a 53.5 puntos; 10 negativos
              viejo    nuevo  hueco   ratio
fecha
2020-03-16  2553.25  2542.75 -10.50  0.9959
2020-06-16  3050.25  3038.00 -12.25  0.9960
2020-09-14  3211.75  3200.00 -11.75  0.9963
2020-12-14  3831.50  3815.00 -16.50  0.9957
Fig. — Hueco de cada roll del futuro de FIX, 2019–2025: precio del contrato nuevo menos el del viejo al cierre del día del roll, en puntos

Los 28 huecos van de −17,5 a 53,5 puntos. Su signo cuenta la historia de los tipos de interés de los datos: de diciembre de 2019 a marzo de 2022, con tipos casi a cero y por debajo del rendimiento por dividendo, el contrato siguiente cotizaba más barato que el cercano (backwardation) y los diez huecos negativos están ahí. Desde junio de 2022, con tipos altos, contango y huecos de 30 a 50 puntos. En dólares, un hueco de 53,5 puntos son 2.675 USD por contrato (× 50): un sistema que leyera la serie sin ajustar vería esa «ganancia» cada trimestre sin que nadie la cobrara.

Ajustar: por diferencia o por ratio

Hay dos formas de quitar los saltos, y las dos reescriben toda la historia anterior a cada roll para que encaje con el contrato nuevo:

  • Por diferencia (back-adjusted, aditivo): a todos los precios anteriores al roll se les suma el hueco.
  • Por ratio (proporcional): todos los precios anteriores se multiplican por el cociente nuevo / viejo.

En las dos, el último tramo (el contrato vivo hoy) conserva sus precios reales y todo lo anterior se desplaza. Por eso la serie ajustada cambia hacia atrás cada vez que se añade un roll: un precio de 2019 en la serie de hoy no es el mismo que en la serie de hace un año.

IN [08]python
dif = precio_h.copy()
rat = precio_h.copy()
for d, fila in huecos.iterrows():
    antes_del_roll = dif.index < d
    dif[antes_del_roll] += fila["hueco"]
    rat[antes_del_roll] *= fila["ratio"]

series = pd.DataFrame({"sin_ajustar": precio_h, "diferencia": dif,
                       "ratio": rat})
print(series.iloc[[0, -1]].round(2))
print("suma de huecos:", huecos["hueco"].sum(), "puntos")
OUT [08]Ajuste por diferencia y ajuste por ratio
            sin_ajustar  diferencia    ratio
fecha
2019-01-02      2536.50     2981.50  2748.05
2025-12-31      7027.75     7027.75  7027.75
suma de huecos: 445.0 puntos

Las tres series terminan en 7027,75, el precio real del contrato vivo a final de 2025, y empiezan en tres sitios distintos: 2536,50 la sin ajustar (lo que se negoció de verdad el 2 de enero de 2019), 2981,50 la ajustada por diferencia (2536,50 más los 445 puntos de huecos acumulados) y 2748,05 la ajustada por ratio. La figura de la cabecera dibuja las tres y, abajo, la distancia de las otras dos a la de ratio: hasta un 9,5 % la de diferencia y un −10,6 % la sin ajustar.

Cuál de las tres series dice la verdad

La prueba es comparar cada serie con lo que gana de verdad una posición de un contrato. Cada sesión, la posición es el contrato que se tenía al cierre anterior, y gana la variación de ese contrato entre los dos cierres. El día del roll no hay excepción: hasta el cierre se tiene el viejo.

IN [09]python
# lo que gana de verdad la posición: el contrato que se tenía al
# cierre anterior, de un cierre al siguiente
antes_h = h.shift(1).iloc[1:]
p0 = pd.Series([liq.at[t, k] for t, k in
                zip(sesiones[:-1], antes_h)], index=sesiones[1:])
p1 = pd.Series([liq.at[t, k] for t, k in antes_h.items()],
               index=sesiones[1:])
real_pts, real_pct = p1 - p0, p1 / p0 - 1

for nombre, s in series.items():
    err_pts = (s.diff().iloc[1:] - real_pts).abs().max()
    err_pct = (s.pct_change().iloc[1:] - real_pct).abs().max()
    print(f"{nombre:12s} error máx.: {err_pts:6.2f} puntos, "
          f"{100 * err_pct:.3f} pp")
OUT [09]Cuál de las tres series dice la verdad
sin_ajustar  error máx.:  53.50 puntos, 1.039 pp
diferencia   error máx.:   0.00 puntos, 1.084 pp
ratio        error máx.:  40.36 puntos, 0.000 pp

Cada serie acierta en una cosa como mucho:

SerieVariación en puntosRentabilidad %Nivel de preciosUso
Sin ajustarFalsa en los rolls (hasta 53,5)Falsa en los rolls (1,04 pp)RealGráficos de precios negociados, niveles
Por diferenciaExactaFalsa (hasta 1,08 pp)DesplazadoResultados en puntos y dólares
Por ratioFalsa (hasta 40,36)ExactaDesplazadoRentabilidades, volatilidad, stops en %

El error de la serie por diferencia en porcentaje no está concentrado en los rolls: es en todas las sesiones antiguas, porque divide variaciones reales entre un nivel que no es el real. Una sesión de 2019 con una variación de 30 puntos sobre un precio real de 2536,50 es un 1,18 %; sobre el nivel desplazado de 2981,50, un 1,01 %. Con la de ratio pasa lo contrario: el porcentaje es exacto y los puntos no.

Precios negativos y el coste de mantener el futuro

En FIX los huecos se compensan en parte (negativos de 2019 a 2022, positivos después) y la serie ajustada por diferencia no baja de 2.700. En mercados con contango persistente (materias primas con coste de almacenamiento, futuros de volatilidad en épocas tranquilas) los huecos son casi siempre del mismo signo y se acumulan durante años. Un esquema muestra a dónde lleva:

IN [10]python
# esquema: el contado no se mueve (20 USD) y cada contrato mensual
# cotiza un 4 % por encima del anterior; 10 años de rolls
contado, contango, meses = 20.0, 0.04, 120
hueco_mes = contado * contango
print("ajuste por diferencia, primer precio:",
      round(contado - hueco_mes * meses, 2))
print("ajuste por ratio, primer precio:",
      round(contado / (1 + contango) ** meses, 4))
OUT [10]Precios negativos: un futuro en contango fuerte
ajuste por diferencia, primer precio: -76.0
ajuste por ratio, primer precio: 0.1807

Ciento veinte huecos de 0,80 USD suman 96 USD, y el primer precio ajustado por diferencia sale en −76. Con un precio negativo no se puede calcular una rentabilidad ni un logaritmo, y cualquier indicador que divida por el precio se rompe. La opción de CSI Data para este caso es sumar una cantidad fija a toda la serie para que no haya negativos, lo que arregla el síntoma y desplaza todavía más el nivel. El ajuste por ratio no da negativos (0,18), pero un precio de 18 céntimos en una serie de un activo que siempre ha cotizado a 20 USD deforma igual cualquier regla basada en niveles.

Lo que de verdad importa de una serie continua son sus rentabilidades, y la de ratio las tiene exactas. Comparadas con las del índice, enseñan el coste de mantener el futuro:

IN [11]python
curva = pd.read_csv("curva_tipos_diaria.csv", parse_dates=["fecha"])
y = curva.set_index("fecha")["3m"]
anios = range(2019, 2026)
fut = rat.groupby(rat.index.year).last()
fut = fut / fut.shift(1).fillna(rat.iloc[0]) - 1
ind = indice.groupby(indice.index.year).last()
ind = ind / ind.shift(1).fillna(indice.iloc[0]) - 1
tabla = pd.DataFrame({
    "indice_pct": 100 * ind, "futuro_pct": 100 * fut,
    "diferencia": 100 * (fut - ind),
    "tipo_3m": y.groupby(y.index.year).mean(),
    "dividendo": [1.9 - 0.1 * (a - 2019) for a in anios],
}, index=anios)
tabla["div_menos_tipo"] = tabla["dividendo"] - tabla["tipo_3m"]
tabla.round(2)
OUT [11]Lo que cuesta tener el futuro, año a año
      indice_pct  futuro_pct  diferencia  tipo_3m  dividendo  div_menos_tipo
2019       30.58       29.99       -0.59     2.10        1.9           -0.20
2020       14.42       15.90        1.48     0.33        1.8            1.47
2021       11.40       13.29        1.89     0.20        1.7            1.50
2022      -13.52      -13.69       -0.17     2.37        1.6           -0.77
2023       23.09       18.98       -4.10     5.04        1.5           -3.54
2024       36.32       31.28       -5.04     4.94        1.4           -3.54
2025       14.30       11.13       -3.17     4.05        1.3           -2.75

2019 empieza el 2 de enero, primera sesión de los datos. La columna dividendo aproxima por años el rendimiento por dividendo del índice, que según el README de los datos baja de forma lineal del 1,9 % en 2019 al 1,3 % en 2025.

El futuro no rinde lo mismo que el índice de precios. Un largo en futuros equivale a tener el índice financiado con deuda: no cobra dividendos y no paga intereses por el dinero que no ha puesto. Su rentabilidad frente al índice de precios se parece a dividendo − tipo: +1,48 puntos en 2020 frente a +1,47, +1,89 en 2021 frente a +1,50, y −4,10, −5,04 y −3,17 de 2023 a 2025 frente a −3,54, −3,54 y −2,75. No coinciden exactamente porque el coste se aplica sobre un nocional que cambia durante el año (en 2024 el índice sube un 36,32 %) y porque los tipos y el dividendo de la tabla son medias. Ese coste o ingreso del carry es lo que en la jerga se llama rendimiento del roll: en contango, cada roll vende barato el contrato que converge y compra caro el siguiente.

Lo que esto cambia en tus datos

  • Pregunta qué serie te dan. «Continuo» no basta: regla de roll, método de ajuste y fecha del último ajuste. Dos series continuas del mismo futuro de dos proveedores no tienen los mismos precios antiguos.
  • La historia ajustada se reescribe con cada roll. Si guardas una serie ajustada y la vuelves a descargar meses después, los precios antiguos habrán cambiado. Guarda los contratos sueltos y construye la serie tú (lección 1.4.7).
  • Rentabilidades con ratio, dólares con diferencia, niveles con los contratos. Y nunca la serie sin ajustar para calcular rentabilidades.
  • El roll se decide con datos ya publicados. El volumen de hoy sirve para mañana; el interés abierto, según la fuente, a veces llega incluso más tarde.

Ejercicios

  1. Compara la regla de calendario (cinco sesiones antes) con la de volumen: ¿en cuántas sesiones se tiene un contrato distinto? ¿Por qué el resultado de 2019 a 2025 apenas cambia?
Solución
IN [12]python
h_cal = mantenido(reglas["calendario_5"])
distintos = (h_cal != h).sum()
print(f"Sesiones con distinto contrato: {distintos}")
OUT [12]Roll por calendario frente a roll por volumen
Sesiones con distinto contrato: 42

Son 42 sesiones de 1.760: una o dos por roll, las que separan el cambio por calendario (cinco sesiones antes) del cambio por volumen (tres o cuatro). En esas sesiones se tiene el contrato siguiente en lugar del cercano, y los dos se mueven casi igual: la diferencia de resultado es solo la de la base entre ellos, que cambia poco en un par de sesiones. Donde las reglas sí importan es en mercados con roll lento o con entrega física, y en la ejecución: cambiar cuando el contrato nuevo aún no tiene liquidez cuesta más horquilla.

  1. Calcula el resultado en puntos y en dólares de mantener dos contratos largos de FIX durante 2022 y comprueba que coincide con el de la serie ajustada por diferencia.
Solución
IN [13]python
pts_2022 = real_pts["2022"].sum()
print(f"Puntos ganados en 2022: {pts_2022:.2f}")
print(f"Resultado de 2 contratos: "
      f"{2 * spec['multiplicador'] * pts_2022:,.0f} USD")
print("con la serie ajustada por diferencia:",
      round(dif["2022"].iloc[-1] - dif[:"2021"].iloc[-1], 2))
OUT [13]El resultado de 2022 en dólares
Puntos ganados en 2022: -573.50
Resultado de 2 contratos: -57,350 USD
con la serie ajustada por diferencia: -573.5

Dos contratos largos pierden 573,50 puntos en 2022, 57.350 USD con un multiplicador de 50. La serie ajustada por diferencia da exactamente los mismos puntos entre el último cierre de 2021 y el de 2022, porque conserva todas las variaciones diarias. Con la serie sin ajustar saldría otra cifra: los cuatro huecos de 2022 se sumarían como si fueran ganancias o pérdidas.

  1. Un compañero calcula la volatilidad anualizada del futuro de FIX con la serie empalmada sin ajustar y otro con la ajustada por diferencia. ¿Qué error comete cada uno y qué serie usarías tú?
Solución

El primero mete en la volatilidad 28 saltos falsos, uno por roll, de hasta 53,5 puntos (más de un 1 % en 2024): días de «rentabilidad» que nadie obtuvo y que inflan la volatilidad y las colas. El segundo no tiene saltos, pero calcula rentabilidades dividiendo variaciones reales entre niveles desplazados: en los años antiguos, con un nivel más alto que el real (hasta un 9,5 % en 2020), subestima las rentabilidades y por tanto la volatilidad de esos años. La serie correcta para cualquier medida porcentual es la ajustada por ratio, cuyas rentabilidades coinciden exactamente con las de mantener los contratos.

Respuesta plegada en cada pregunta

Autoevaluación

  1. En la serie de FIX ajustada por diferencia, ¿qué medida coincide exactamente con la de mantener los contratos de verdad?

    • ALa rentabilidad porcentual de cada sesión
    • BLa variación en puntos de cada sesión, y por tanto el resultado en dólares
    • CEl nivel de precios de 2019
    • DNinguna
    Ver respuesta

    Respuesta B. La variación en puntos de cada sesión, y por tanto el resultado en dólares

    El ajuste por diferencia suma a toda la historia anterior el hueco de cada roll, así que las diferencias entre sesiones no cambian: el error máximo en puntos es 0,00. Pero el nivel sí cambia (2981,50 frente a 2536,50 en 2019) y con él las rentabilidades porcentuales, que llegan a equivocarse en 1,08 puntos porcentuales.

  2. Un sistema usa stops del 2 % y rentabilidades logarítmicas. ¿Qué serie continua le conviene?

    • ALa empalmada sin ajustar
    • BLa ajustada por diferencia
    • CLa ajustada por ratio
    • DDa igual
    Ver respuesta

    Respuesta C. La ajustada por ratio

    El ajuste por ratio multiplica la historia anterior por el cociente nuevo/viejo de cada roll, y así conserva las rentabilidades porcentuales exactamente (error 0,000 pp en FIX). A cambio, los puntos de cada sesión ya no son los reales, y para calcular resultados en dólares hay que volver a los contratos o a la serie por diferencia.

  3. La regla de roll de la lección cambia de contrato al cierre de la sesión siguiente al cruce de volumen, no al cierre del propio día del cruce. ¿Por qué?

    • APorque el exchange no deja operar el día del cruce
    • BPorque el volumen de una sesión solo se conoce cuando termina: decidir con él a su propio cierre sería usar un dato que todavía no existía
    • CPorque así los huecos son menores
    • DPor costumbre de los proveedores
    Ver respuesta

    Respuesta B. Porque el volumen de una sesión solo se conoce cuando termina: decidir con él a su propio cierre sería usar un dato que todavía no existía

    Es la misma idea que en cualquier señal: un dato del día t sirve para actuar después de t. El cruce de volumen de FIXM25 a FIXU25 fue el 12 de junio de 2025; el roll se hace al cierre del 13.

  4. ¿Por qué puede dar precios negativos el ajuste por diferencia?

    • APor un error de redondeo
    • BPorque suma o resta a la historia antigua la suma de todos los huecos posteriores: en un mercado en contango fuerte durante años, esa suma puede superar el precio antiguo
    • CPorque los futuros pueden cotizar negativos
    • DPorque el ratio puede ser negativo
    Ver respuesta

    Respuesta B. Porque suma o resta a la historia antigua la suma de todos los huecos posteriores: en un mercado en contango fuerte durante años, esa suma puede superar el precio antiguo

    En el esquema de la lección, un contado plano de 20 USD con un 4 % de contango mensual acumula 96 USD de huecos en 120 rolls, y el primer precio ajustado sale en −76. El ajuste por ratio da 0,18: positivo, aunque igual de alejado de lo que se negoció.

  5. De 2023 a 2025 el futuro de FIX rinde entre 3,2 y 5,0 puntos menos al año que su índice de precios. ¿De dónde sale esa diferencia?

    • ADe un error en los datos
    • BDel carry: con tipos por encima del rendimiento por dividendo el futuro cotiza por encima del índice y converge a él, así que el largo en futuros paga la diferencia en cada roll
    • CDe las comisiones del roll
    • DDe que el futuro vence
    Ver respuesta

    Respuesta B. Del carry: con tipos por encima del rendimiento por dividendo el futuro cotiza por encima del índice y converge a él, así que el largo en futuros paga la diferencia en cada roll

    En esos años el tipo a 3 meses (entre el 4,05 % y el 5,04 % de media) supera al rendimiento por dividendo (1,3 % a 1,5 %): contango. El precio del futuro baja hacia el índice a medida que se acerca el vencimiento, y esa caída es un coste para el largo. De 2020 a 2021, con tipos casi a cero, ocurría lo contrario y el futuro rendía más.

Ideas clave

  • Una serie continua es una cadena de contratos más una regla de roll y un método de ajuste; cada proveedor elige los suyos y hay que saber cuáles.
  • El roll por volumen o interés abierto se decide con datos del cierre y se ejecuta después; en FIX cae tres o cuatro sesiones antes del vencimiento.
  • La serie empalmada sin ajustar tiene saltos falsos en cada roll; la ajustada por diferencia conserva los puntos (y el resultado en dólares) y la ajustada por ratio conserva las rentabilidades porcentuales.
  • Ninguna serie ajustada muestra los precios que se negociaron: para niveles, stops en precio y comparaciones con el índice, los contratos reales.
  • El ajuste por diferencia puede dar precios negativos en mercados con contango persistente; el de ratio no, pero deforma igual los niveles antiguos.
  • En contango, mantener el futuro cuesta frente al índice (de −3,2 a −5,0 puntos al año en FIX de 2023 a 2025); en backwardation, suma.

Recursos