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.
- Módulo
- 1.4 · Ingeniería de datos financieros
- Duración
- 60 min
Lecciones del nivel 1
1.1 · Cómo funcionan los mercados
1.2 · Clases de activos
- 1.2.1 Acciones: splits, dividendos y eventos corporativos
- 1.2.2 ETFs: réplica, tracking error, apalancados y UCITS
- 1.2.3 Futuros: especificaciones, margen, roll y contango
- 1.2.4 Forex: pares, pips, lotes, rollover y CFD
- 1.2.5 Opciones: call, put, valor intrínseco y valor temporal
- 1.2.6 Cripto: spot, perpetuos, funding y custodia
- 1.2.7 Renta fija y tipos de interés; CFDs y productos apalancados
1.3 · Fuentes de datos de mercado
- 1.3.1 Tipos de datos de mercado: EOD, barras, ticks, libro y referencia
- 1.3.2 Fuentes de datos gratuitas
- 1.3.3 APIs de datos retail y prosumer
- 1.3.4 Proveedores de datos profesionales
- 1.3.5 Datos de cripto y on-chain
- 1.3.6 Brókeres como fuente de datos
- 1.3.7 Licencias y términos de uso de datos
- 1.3.8 Cómo elegir proveedor de datos
1.4 · Ingeniería de datos financieros
- 1.4.1 Descarga por REST y websockets: paginación, límites y reintentos
- 1.4.2 Limpieza de datos: faltantes, ticks erróneos, huecos y zonas horarias
- 1.4.3 Ajustes por splits y dividendos: precios ajustados y sin ajustar
- 1.4.4 Universos sin sesgo de supervivencia: deslistados, fusiones y cambios de símbolo
- 1.4.5 Construcción de barras: tiempo, ticks, volumen, dólar y desequilibrio
- 1.4.6 Series continuas de futuros: reglas de roll, ajuste por diferencia y ajuste por ratio
- 1.4.7 Almacenamiento: CSV, Parquet, particionado, DuckDB, TimescaleDB y ArcticDB
- 1.4.8 Pipeline reproducible: descarga, validación, actualización incremental y tests de calidad
1.5 · Datos fundamentales y macro
1.6 · Análisis exploratorio financiero
1.7 · Análisis técnico programado
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:
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]
- Qué contratos entran: todos los vencimientos o solo los de un ciclo (en FIX, los trimestrales H, M, U y Z).
- 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.
- 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.
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())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:
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"]]í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 3201El 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):
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]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
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 se ejecuta al cierre de .
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) calendario_5 volumen interes_abierto
3 0 14 28
4 0 14 0
5 28 0 0La 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í:
mal = sesiones_antes(roll_cruce(vol, solo_frontal=False))
print("sesiones antes del vencimiento:", mal.value_counts().to_dict())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.
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"]fecha
2025-06-10 FIXM25
2025-06-11 FIXM25
2025-06-12 FIXM25
2025-06-13 FIXU25
2025-06-16 FIXU25
dtype: strEl 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:
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)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.9957Los 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.
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") 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 puntosLas 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.
# 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")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 ppCada 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.
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:
# 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))ajuste por diferencia, primer precio: -76.0
ajuste por ratio, primer precio: 0.1807Ciento 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:
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) 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.752019 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
- 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
h_cal = mantenido(reglas["calendario_5"])
distintos = (h_cal != h).sum()
print(f"Sesiones con distinto contrato: {distintos}")Sesiones con distinto contrato: 42Son 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.
- 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
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))Puntos ganados en 2022: -573.50
Resultado de 2 contratos: -57,350 USD
con la serie ajustada por diferencia: -573.5Dos 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.
- 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
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.
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.
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.
¿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ó.
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.