---
title: "1.4.6 Series continuas de futuros"
description: "Del futuro que vence a una serie larga: roll por calendario, volumen o interés abierto y ajuste por diferencia o por ratio, con 31 contratos de FIX."
url: "https://pythonparatrading.com/curso-python-trading/nivel-1/series-continuas-de-futuros/"
lang: "es"
image: "https://pythonparatrading.com/og/curso-python-trading/nivel-1/series-continuas-de-futuros.png"
---

# 1.4.6 · 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.

- Curso: [Nivel 1: Datos financieros, mercados y análisis](https://pythonparatrading.com/curso-python-trading/nivel-1/)
- Módulo: 1.4 · Ingeniería de datos financieros
- Duración: 60 min
- Requisitos: [1.2.3 · Futuros: especificaciones, margen, roll y contango](https://pythonparatrading.com/curso-python-trading/nivel-1/futuros-especificaciones-margen-y-roll/), [0.5.3 · Selección, filtrado, groupby, merge, pivot, resample y ventanas móviles](https://pythonparatrading.com/curso-python-trading/nivel-0/pandas-groupby-merge-resample-y-rolling/)

## Al terminar esta lección

- Programar tres reglas de roll (días antes del vencimiento, cruce de volumen y cruce de interés abierto) sin usar información del futuro.
- Construir la serie empalmada sin ajustar, la ajustada por diferencia y la ajustada por ratio, y medir los huecos de cada roll.
- Comprobar qué conserva cada serie: los puntos ganados (diferencia), las rentabilidades porcentuales (ratio) o ninguna de las dos (sin ajustar).
- Explicar el riesgo de precios negativos del ajuste por diferencia y el coste de mantener un futuro en contango.

## 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:

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

```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())
```
```output
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:

```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"]]
```
```output
í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):

```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]
```
```output
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

```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 $t$ se ejecuta al cierre de $t+1$.

```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)
```
```output
   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.

<aside class="callout" data-kind="nota">

Dos proveedores, dos convenciones. Norgate Data publica sus series continuas sin ajustar y ajustadas por diferencia, y
hace el roll la sesión anterior al último día de negociación en los contratos liquidados en efectivo y la anterior al
primer día de aviso en los entregables. CSI Data deja elegir el roll por volumen y/o interés abierto, con una o con
varias sesiones de confirmación. Antes de comparar un backtest propio con uno hecho con datos de un proveedor, hay que
saber qué regla usa.

</aside>

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í:

```python
mal = sesiones_antes(roll_cruce(vol, solo_frontal=False))
print("sesiones antes del vencimiento:", mal.value_counts().to_dict())
```
```output
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.

```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"]
```
```output
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).* Dos líneas de interés abierto entre el 27 de mayo y el 24 de junio de 2025. FIXM25 se mantiene en torno a 780 000 contratos hasta el 5 de junio y cae hasta 44 111 el día del vencimiento; FIXU25 sube de unos 25 000 a más de 800 000. Las líneas se cruzan entre el 12 y el 13 de junio. Una línea vertical destacada marca el roll al cierre del 13 de junio, la sesión siguiente al cruce de volumen, y otra el vencimiento del 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:

```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)
```
```output
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.* 28 barras verticales, una por roll. Pequeñas y positivas en 2019 (hasta 4,75 puntos); negativas (backwardation) de diciembre de 2019 a marzo de 2022, de −1,75 a −17,5 puntos, cuando el rendimiento por dividendo superaba a unos tipos casi a cero; positivas (contango) desde junio de 2022, con un máximo de 53,5 puntos en marzo y junio de 2024 (destacados).

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.

```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")
```
```output
            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.

```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")
```
```output
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:

| Serie | Variación en puntos | Rentabilidad % | Nivel de precios | Uso |
|---|---|---|---|---|
| Sin ajustar | Falsa en los rolls (hasta 53,5) | Falsa en los rolls (1,04 pp) | Real | Gráficos de precios negociados, niveles |
| Por diferencia | **Exacta** | Falsa (hasta 1,08 pp) | Desplazado | Resultados en puntos y dólares |
| Por ratio | Falsa (hasta 40,36) | **Exacta** | Desplazado | Rentabilidades, 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.

<aside class="callout" data-kind="riesgo">

Ninguna serie ajustada contiene los precios que se negociaron. Un stop «en 3.000 puntos» en 2019, una comparación con
el nivel del índice o una orden limitada de un backtest tienen que usar el precio del contrato real de ese día, no el
de una serie que se ha movido hacia atrás.

</aside>

## 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:

```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))
```
```output
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:

```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)
```
```output
      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.

<aside class="callout" data-kind="aviso">

Si un backtest compara un sistema con futuros contra el índice de precios, la comparación está sesgada por el carry: a
favor del futuro con tipos bajos, en contra con tipos altos. La comparación justa es contra el índice de rentabilidad
total menos el tipo de interés, o directamente contra mantener el futuro continuo.

</aside>

## 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.**

```python
h_cal = mantenido(reglas["calendario_5"])
distintos = (h_cal != h).sum()
print(f"Sesiones con distinto contrato: {distintos}")
```
```output
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.

2. 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.**

```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))
```
```output
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.

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

## Autoevaluación

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

   - A. La rentabilidad porcentual de cada sesión
   - B. La variación en puntos de cada sesión, y por tanto el resultado en dólares
   - C. El nivel de precios de 2019
   - D. Ninguna

   *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?

   - A. La empalmada sin ajustar
   - B. La ajustada por diferencia
   - C. La ajustada por ratio
   - D. Da igual

   *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é?

   - A. Porque el exchange no deja operar el día del cruce
   - 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
   - C. Porque así los huecos son menores
   - D. Por costumbre de los proveedores

   *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?

   - A. Por un error de redondeo
   - 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
   - C. Porque los futuros pueden cotizar negativos
   - D. Porque el ratio puede ser negativo

   *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?

   - A. De un error en los datos
   - 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
   - C. De las comisiones del roll
   - D. De que el futuro vence

   *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

### Research

- [Estacionalidad en Python. Buscando Patrones Intradia](https://pythonparatrading.com/estacionalidad-intradia-preprocesado/) · Research-0024 · 9 min

### Documentación

- [Norgate Data: paquete de futuros (series continuas sin ajustar y ajustadas por diferencia; fecha de roll)](https://norgatedata.com/futurespackage.php) · norgatedata.com
- [CSI Data: opciones del ajuste de series continuas (roll por volumen o interés abierto, confirmación, precios no negativos)](https://www.csidata.com/custserv/onlinehelp/OnlineManual/back_adjustingsettings.htm) · csidata.com
- [exchange_calendars: calendarios de sesiones de mercado en Python](https://github.com/gerrymanoim/exchange_calendars) · github.com

### Libros

- [Marcos López de Prado: Advances in Financial Machine Learning (Wiley, 2018), sección 2.4.3, «Single Future Roll»](https://www.wiley.com/en-us/Advances+in+Financial+Machine+Learning-p-9781119482086) · wiley.com

### Datos

- [Datos del nivel 1: contratos del futuro FIX, 2019-2025 (Parquet)](https://pythonparatrading.com/curso/datos/nivel-1/fix_contratos.parquet) · Descarga · fix_contratos.parquet
- [Datos del nivel 1: índice FIX diario (CSV)](https://pythonparatrading.com/curso/datos/nivel-1/fix_indice_diario.csv) · Descarga · fix_indice_diario.csv

Lección anterior: [1.4.5 · Construcción de barras: tiempo, ticks, volumen, dólar y desequilibrio](https://pythonparatrading.com/curso-python-trading/nivel-1/barras-de-tiempo-ticks-volumen-y-dolar/)

Lección siguiente: [1.4.7 · Almacenamiento: CSV, Parquet, particionado, DuckDB, TimescaleDB y ArcticDB](https://pythonparatrading.com/curso-python-trading/nivel-1/almacenamiento-parquet-duckdb-y-bases-temporales/)
