Lección 3.2.4 · 60 min
Ratios de cobertura: por nominal, por DV01 y por volatilidad
Un spread no es solo «largo de uno y corto de otro»: el número de contratos de cada pata decide qué riesgo queda. Esta lección calcula los ratios por valor nominal, por DV01, por volatilidad y por mínima varianza en los futuros de tipos, de índices y de energía del nivel, y mide con los datos lo que cada cobertura deja dentro.
- Módulo
- 3.2 · Spreads de futuros
- Duración
- 60 min
Lecciones del nivel 3
3.1 · Futuros en profundidad
3.2 · Spreads de futuros
- 3.2.1 Spreads de calendario, butterflies y condors
- 3.2.2 Spreads intercommodity: crack, spark y crush
- 3.2.3 Spreads de tipos y de índices: 2s10s, NOB, NQ/ES y Europa frente a EE. UU.
- 3.2.4 Ratios de cobertura: por nominal, por DV01 y por volatilidad
- 3.2.5 Datos y backtesting de spreads: cotizados, sintéticos y por patas
- 3.2.6 Estacionalidad de spreads: análisis multi-año, patrones y trampas
3.3 · Forex avanzado
3.4 · Teoría y valoración de opciones
- 3.4.1 Payoffs, paridad put-call y límites de arbitraje
- 3.4.2 Árboles binomiales, Black-Scholes-Merton, Black-76 y Monte Carlo
- 3.4.3 Griegas: delta, gamma, vega, theta, rho, vanna, volga y charm
- 3.4.4 Volatilidad implícita: Newton-Raphson, Brent, py_vollib y QuantLib
- 3.4.5 Sonrisa, skew y estructura temporal; superficie de volatilidad con SVI
- 3.4.6 Volatilidad realizada frente a implícita; prima de riesgo de volatilidad; VIX y VSTOXX
- 3.4.7 Modelos avanzados: Heston, SABR y volatilidad local
3.5 · Estructuras de opciones en Python
- 3.5.1 Estructuras direccionales y verticales: call, put, covered call, collar y spreads
- 3.5.2 Estructuras de volatilidad y rango: straddles, strangles, cóndores y mariposas
- 3.5.3 Calendarios, diagonales, ratios, backspreads, risk reversals y jade lizard
- 3.5.4 Herramienta propia: payoff, griegas agregadas y escenarios
- 3.5.5 Gestión: ajustes, roll, cierre anticipado, asignación temprana y pin risk
3.6 · Estrategias sistemáticas de volatilidad y opciones
3.7 · Cripto derivados y estrategias específicas
Qué iguala cada ratio
Un spread de futuros tiene dos patas: se compra una y se vende la otra. La pregunta práctica es cuántos contratos de cada una. Esa cifra, el ratio de cobertura (contratos de una pata por cada contrato de la otra), decide qué riesgo se cancela y cuál queda. Con el precio, el multiplicador del contrato (lo que vale un punto de precio), la variación en dinero de un contrato por un punto básico de tipos y la volatilidad del rendimiento, los cuatro ratios de esta lección, para cubrir un contrato de la pata con contratos de la pata , son:
con el cambio diario del valor de un contrato en dinero. El primero iguala el dinero invertido; el segundo, la sensibilidad a un desplazamiento de tipos de un punto básico; el tercero, el dinero en riesgo con la volatilidad reciente; el cuarto, el de mínima varianza, es la pendiente de una regresión: el que habría dejado el menor riesgo total en la ventana de estimación (la lección 2.1.6 lo hace dinámico con un filtro de Kalman).
| Ratio | Qué iguala | Qué necesita | Cuándo tiene sentido |
|---|---|---|---|
| Nominal | Dinero de cada pata | Precio y multiplicador | Rendimiento relativo de dos activos parecidos |
| DV01 | Sensibilidad a 1 pb | DV01 por contrato | Spreads de tipos: aislar la pendiente |
| Volatilidad | Dinero en riesgo reciente | σ estimada | Patas sin relación estructural |
| Mínima varianza | Riesgo total pasado | Regresión de P&L | Reducir riesgo, sin decir cuál |
| Unidades | Cantidad física | Ficha del contrato | Márgenes físicos: crack, crush, spark |
Ninguno es «el correcto» en abstracto. La elección depende de qué se quiere dejar fuera del spread:
Fuente Mermaid
flowchart TB
Q{¿Qué cancelas?} -- tipos --> D[DV01]
Q -- mercado --> B[Beta]
Q -- margen físico --> U[Unidades]
Q -- riesgo total --> V[Volatilidad o MV]Los datos de esta lección son sintéticos: futuros de tipos, índices y energía ficticios que imitan a los reales
(TPA al de 2 años, TPC al de 10, TPD al bono a 30 años, RVA y RVB a los mini del S&P 500 y del Nasdaq-100,
PEC y PEG al crudo y a la gasolina). Su ventaja es que la respuesta se conoce: el archivo tipos3_dv01.csv trae el
DV01 exacto de cada contrato con el modelo que generó los precios, y la beta del Nasdaq ficticio sobre el factor de
mercado está fijada en 1,15.
Tres patas de tipos y su DV01
En los futuros de bonos, el DV01 de un contrato es la variación de su precio por un punto básico de rendimiento del
bono que lo determina (en los contratos reales, el más barato de entregar; aquí, un bono estándar por contrato), en
dinero. La lección 1.2.7 lo calculó para un bono; tipos3_dv01.csv lo da para el contrato que se tiene cada sesión:
import numpy as np
import pandas as pd
dv = pd.read_csv("tipos3_dv01.csv", parse_dates=["fecha"])
esp = pd.read_csv("futuros3_especificaciones.csv",
index_col="mercado")
mult = esp["multiplicador"]
fin = dv[dv["fecha"] == "2025-12-31"].set_index("mercado")
fin["nominal"] = fin["liquidacion"] * mult.reindex(fin.index)
fin[["contrato", "liquidacion", "rendimiento_ctd",
"duracion_modificada", "dv01", "nominal"]].round(2) contrato liquidacion rendimiento_ctd duracion_modificada dv01 nominal
mercado
TPA TPAH26 104.27 3.67 2.04 42.55 208546.88
TPB TPBH26 108.75 3.73 3.97 43.21 108750.00
TPC TPCH26 112.55 3.83 5.73 64.47 112546.88
TPD TPDH26 121.62 4.13 11.06 134.46 121625.00
TPE TPEH26 126.91 2.51 7.24 91.88 126910.00El contrato a 2 años (TPA) tiene el doble de nominal que los demás (el multiplicador es 2.000 USD por punto de
precio, frente a 1.000), pero su duración es de 2 años: su DV01, 42,55 USD por pb, es menos de un tercio del del bono
a 30 años (134,46). Igualar nominales e igualar sensibilidades a los tipos dan, por tanto, cifras muy distintas.
Para medir el P&L de cada pata hace falta el cambio de precio del mismo contrato de un día a otro: el de la serie
del contrato que se tiene salta en cada roll, cuando se pasa de un vencimiento al siguiente (lección 1.4.6). La función
siguiente toma el contrato que se tenía ayer y busca su precio de ayer y de hoy en futuros3_curvas.parquet:
cur = pd.read_parquet("futuros3_curvas.parquet")
px = cur.set_index(["fecha", "contrato"])["liquidacion"]
def cambio_mismo_contrato(contratos):
"""Cambio de hoy del contrato que se tenía ayer: sin el salto
del roll (lección 1.4.6)."""
ayer = contratos.shift(1).iloc[1:]
f, f0 = contratos.index[1:], contratos.index[:-1]
hoy = px.reindex(list(zip(f, ayer))).to_numpy()
antes = px.reindex(list(zip(f0, ayer))).to_numpy()
return pd.Series(hoy - antes, index=f)
D = dv.pivot(index="fecha", columns="mercado", values="dv01")
F = dv.pivot(index="fecha", columns="mercado", values="liquidacion")
Y = dv.pivot(index="fecha", columns="mercado",
values="rendimiento_ctd")
C = dv.pivot(index="fecha", columns="mercado", values="contrato")
dP = pd.DataFrame({k: cambio_mismo_contrato(C[k])
for k in ["TPA", "TPB", "TPC", "TPD"]})
usd = dP * mult[dP.columns] # USD por contrato y sesión
ret = dP / F[dP.columns].shift(1) # rendimiento del contrato
usd.loc["2025-12-24":].round(1) TPA TPB TPC TPD
fecha
2025-12-24 70.3 70.3 15.6 -312.5
2025-12-26 265.6 85.9 -109.4 -843.8
2025-12-29 328.1 140.6 0.0 -468.8
2025-12-30 507.8 656.2 1062.5 2250.0
2025-12-31 -500.0 -437.5 -578.1 -1000.0Las cifras son múltiplos del valor del tick de cada contrato (15,625 USD en TPC, 31,25 en TPD, 7,8125 en TPA):
las liquidaciones se redondean al tick, como en un mercado real.
Cuatro ratios para el 2s10s y el NOB
Dos spreads de tipos clásicos sirven de laboratorio. El 2s10s opone la nota a 2 años a la de 10; el NOB (notes over bonds), la de 10 años al bono a 30. La lección 3.2.3 los construye y estudia su comportamiento; aquí se trata solo de cuántos contratos lleva cada pata. En los dos, el ratio se expresa como contratos del plazo corto por cada contrato del largo:
def ratios(corto, largo):
"""Contratos de la pata corta por contrato de la larga."""
nominal = (F[largo] * mult[largo]) / (F[corto] * mult[corto])
dv01 = D[largo] / D[corto]
sig = ret.rolling(63).std()
vol = nominal * sig[largo] / sig[corto]
mv = (usd[largo].rolling(252).cov(usd[corto])
/ usd[corto].rolling(252).var())
return pd.DataFrame({"nominal": nominal, "dv01": dv01,
"volatilidad": vol, "min_varianza": mv})
R = {"2s10s": ratios("TPA", "TPC"), "nob": ratios("TPC", "TPD")}
pd.DataFrame({k: v.iloc[-1] for k, v in R.items()}).round(2) 2s10s nob
nominal 0.54 1.08
dv01 1.52 2.09
volatilidad 1.41 2.15
min_varianza 1.18 2.03En el 2s10s, el nominal pide medio contrato a 2 años por cada contrato a 10 (el de 2 años tiene el doble de nominal) y el DV01, uno y medio: casi el triple. En el NOB, el nominal pide algo más de uno y el DV01 algo más de dos. La volatilidad de 63 sesiones y la regresión de 252 sesiones dan, a fin de 2025, valores cercanos al de DV01, porque en 2025 los dos plazos se mueven de forma parecida en puntos básicos. No siempre es así, como se ve más abajo.
Lo que deja cada cobertura
Un empinador (steepener) gana si la curva se empina: compra contratos del plazo corto y vende uno del largo. Su P&L diario por contrato largo es , con el ratio de la sesión anterior (el de hoy usaría el cierre de hoy: anticipación). Para saber qué riesgo queda, se regresa ese P&L sobre el cambio del rendimiento de la pata larga (el nivel) y el de la pendiente (largo menos corto), en puntos básicos:
La respuesta conocida: con la cobertura por DV01, un desplazamiento paralelo no gana ni pierde, así que , y es el DV01 de la pata larga (un punto básico más de pendiente, con el plazo corto quieto, es un punto básico más en la pata vendida). Con la beta al nivel sola, sin la pendiente en la regresión, no tiene por qué salir 0: en el modelo de los datos, nivel y pendiente están correlados.
PATAS = {"2s10s": ("TPA", "TPC"), "nob": ("TPC", "TPD")}
def betas(nombre, ratio, desde, hasta):
corto, largo = PATAS[nombre]
h = R[nombre][ratio].shift(1) # el de la sesión anterior
pnl = h * usd[corto] - usd[largo] # empinador
nivel = Y[largo].diff() * 100 # pb
pend = (Y[largo] - Y[corto]).diff() * 100
m = pd.concat([pnl, nivel, pend], axis=1).loc[desde:hasta]
m = m.dropna()
m.columns = ["pnl", "nivel", "pend"]
X = np.c_[np.ones(len(m)), m["nivel"], m["pend"]]
b = np.linalg.lstsq(X, m["pnl"], rcond=None)[0]
sola = np.polyfit(m["nivel"], m["pnl"], 1)[0]
return {"beta_sola": sola, "beta_nivel": b[1],
"beta_pend": b[2], "desv_dia": m["pnl"].std()}
filas = {(n, r, e): betas(n, r, *p)
for n in PATAS
for e, p in {"2010-15": ("2010", "2015"),
"2016-25": ("2016", "2025")}.items()
for r in R[n].columns}
tabla = pd.DataFrame(filas).T
tabla.index.names = ["spread", "ratio", "periodo"]
tabla.round(1) beta_sola beta_nivel beta_pend desv_dia
spread ratio periodo
2s10s nominal 2010-15 57.9 45.5 26.0 365.6
dv01 2010-15 34.0 -0.2 71.7 269.4
volatilidad 2010-15 11.9 -33.4 93.1 257.9
min_varianza 2010-15 19.8 -22.7 88.0 254.9
nominal 2016-25 50.1 44.8 26.3 373.6
dv01 2016-25 15.1 1.1 69.5 300.7
volatilidad 2016-25 11.8 -0.5 61.1 300.3
min_varianza 2016-25 20.8 9.0 58.4 296.5
nob nominal 2010-15 82.8 76.2 80.2 557.7
dv01 2010-15 13.0 0.1 156.6 361.0
volatilidad 2010-15 11.1 -2.0 160.2 370.7
min_varianza 2010-15 23.0 10.9 148.9 372.2
nominal 2016-25 86.7 79.7 83.1 676.7
dv01 2016-25 14.2 1.0 158.2 412.7
volatilidad 2016-25 9.3 -4.1 160.2 420.2
min_varianza 2016-25 18.1 5.2 154.5 421.0La tabla tiene cuatro lecturas:
- DV01 recupera la respuesta conocida. La beta al nivel es −0,2 y 1,1 USD por pb en el 2s10s y 0,1 y 1,0 en el
NOB, en los dos periodos; la beta a la pendiente, 71,7 y 69,5 en el 2s10s y 156,6 y 158,2 en el NOB, es el DV01
medio de la pata larga (unos 70 en
TPCy unos 160 enTPD, segúntipos3_dv01.csv). Es lo que se busca: un spread que solo responde a la pendiente. - El nominal deja una apuesta de duración. Con 0,54 contratos a 2 años, el empinador tiene una beta al nivel de 45 USD por pb (80 en el NOB) y solo 26 a la pendiente: responde más a un desplazamiento paralelo que a lo que se quería operar.
- Volatilidad y mínima varianza cambian de signo. En 2010-2015 la cobertura por volatilidad tiene una beta al nivel de −33,4 y en 2016-2025, de −0,5; la de mínima varianza pasa de −22,7 a +9,0. Reducen el riesgo diario (su desviación es la más baja: 257,9 y 254,9 USD en 2010-2015 frente a 269,4 con DV01), pero lo hacen mezclando una apuesta de nivel que nadie pidió, y de signo variable.
- La beta «sola» engaña. Sin la pendiente en la regresión, incluso el empinador por DV01 parece tener una beta al nivel de 34 USD por pb en 2010-2015. No es un fallo de la cobertura: en esos años, cerca de tipos cero, el plazo de 2 años apenas se mueve, y cuando el nivel sube la curva se empina a la vez. Una regresión que omite una variable correlada atribuye su efecto a la que queda.
El ratio se mueve
La figura de la cabecera sigue los ratios del 2s10s mes a mes de 2010 a 2025. El de nominal apenas cambia (entre 0,52 y 0,63) y el de DV01 tampoco mucho (entre 1,42 y 1,83): la duración de cada contrato varía despacio, con el nivel de los tipos y con el bono que lo determina en cada vencimiento. El de volatilidad, en cambio, salta:
anual = R["2s10s"].resample("YE").last()
anual.index = anual.index.year
anual.loc[[2010, 2011, 2012, 2015, 2019, 2020, 2021, 2025]].T.round(2)fecha 2010 2011 2012 2015 2019 2020 2021 2025
nominal 0.54 0.58 0.55 0.56 0.58 0.58 0.59 0.54
dv01 1.50 1.63 1.55 1.59 1.66 1.64 1.67 1.52
volatilidad 3.21 4.99 3.88 1.70 1.62 13.17 2.27 1.41
min_varianza NaN 2.07 3.56 1.60 1.29 2.29 2.24 1.18La razón está en la pata corta. Con el tipo a 2 años cerca de cero, el banco central lo ancla y apenas se mueve:
vol_pb = Y.diff().mul(100).rolling(252).std().resample("YE").last()
vol_pb.index = vol_pb.index.year
vol_pb.loc[[2011, 2013, 2015, 2018, 2020, 2021, 2023, 2025],
["TPA", "TPC"]].T.round(1)fecha 2011 2013 2015 2018 2020 2021 2023 2025
mercado
TPA 3.8 2.9 5.7 6.6 3.6 2.8 9.7 7.1
TPC 6.3 5.9 6.7 6.6 7.2 5.4 9.2 6.6En 2011 el rendimiento a 2 años se movió 3,8 pb diarios y el de 10 años, 6,3; en 2021, 2,8 frente a 5,4. A la vez, la duración del contrato a 2 años es un tercio de la del de 10: su precio se mueve muy poco y su volatilidad de 63 sesiones cae aún más en los meses de tipos anclados. El ratio por volatilidad compensa pidiendo 3, 5 o 13 contratos a 2 años por cada uno a 10 (3,21 a fin de 2010, 4,99 a fin de 2011, 13,17 a fin de 2020). Si después el tipo corto vuelve a moverse, como en 2023 (9,7 pb diarios en el modelo), ese exceso de contratos convierte el spread en un largo de duración varias veces mayor que el que se quería. El DV01 no depende de cuánto se movió el precio: es una sensibilidad, y por eso no salta. La mínima varianza tiene el mismo problema, algo amortiguado por su ventana de 252 sesiones (3,56 a fin de 2012).
Contratos enteros y reequilibrio
Un ratio de 1,52 no se puede comprar. Hay que redondear, y el error depende del tamaño:
def enteros(nombre, maximo=6):
corto, largo = PATAS[nombre]
d_c, d_l = D[corto].iloc[-1], D[largo].iloc[-1]
filas = []
for n_l in range(1, maximo + 1):
n_c = round(n_l * d_l / d_c)
resto = n_c * d_c - n_l * d_l
filas.append({"largos": n_l, "cortos": n_c,
"dv01_resto": resto,
"resto_pct": 100 * resto / (n_l * d_l)})
return pd.DataFrame(filas).set_index("largos")
enteros("2s10s").round(1) cortos dv01_resto resto_pct
largos
1 2 20.6 32.0
2 3 -1.3 -1.0
3 5 19.3 10.0
4 6 -2.6 -1.0
5 8 18.0 5.6
6 9 -3.9 -1.0Con un contrato a 10 años y dos a 2 años sobra un 32 % de DV01 en la pata corta: 20,6 USD por pb netos, un tercio de un contrato a 10 años apostado a que bajen los tipos. Con dos y tres, el error es del 1 %. El tamaño mínimo razonable no es «un spread»: es el número de contratos que deja el resto por debajo de lo que se tolera. En el NOB, con , el redondeo a 2 por contrato deja un 4 % de 1 a 5 contratos largos; hacen falta 6 o más para que la parte decimal del ratio cuente. Cuando existen contratos más pequeños (los micro de índices o de crudo, una décima parte del original), sirven para ajustar el resto.
El ratio también hay que mantenerlo. Recalcularlo cada sesión cuesta comisiones; dejarlo fijo entre rolls deja que el DV01 se desalinee:
corto, largo = PATAS["2s10s"]
h_diario = R["2s10s"]["dv01"].shift(1)
roll = C[largo] != C[largo].shift(1)
h_roll = R["2s10s"]["dv01"].where(roll).ffill().shift(1)
desfase = (h_roll * D[corto] - D[largo]) / D[largo]
print(f"DV01 sin cubrir con el ratio fijo entre rolls: "
f"mediana {100 * desfase.abs().median():.1f} %, "
f"p95 {100 * desfase.abs().quantile(0.95):.1f} %")
for nombre, h in {"diario": h_diario, "en el roll": h_roll}.items():
pnl = h * usd[corto] - usd[largo]
nivel = Y[largo].diff() * 100
pend = (Y[largo] - Y[corto]).diff() * 100
m = pd.concat([pnl, nivel, pend], axis=1).loc["2016":].dropna()
X = np.c_[np.ones(len(m)), m.iloc[:, 1], m.iloc[:, 2]]
b = np.linalg.lstsq(X, m.iloc[:, 0], rcond=None)[0]
print(f"{nombre:>10}: beta al nivel {b[1]:5.1f} USD/pb, "
f"a la pendiente {b[2]:5.1f}")DV01 sin cubrir con el ratio fijo entre rolls: mediana 3.5 %, p95 8.8 %
diario: beta al nivel 1.1 USD/pb, a la pendiente 69.5
en el roll: beta al nivel 3.6 USD/pb, a la pendiente 66.9Con el ratio fijado el día del roll y sin tocar durante el trimestre, el DV01 sin cubrir es del 3,5 % en la mediana y del 8,8 % en el 5 % de los peores días; la beta al nivel desde 2016 sube de 1,1 a 3,6 USD por pb. Es un desfase pequeño frente al del redondeo de una cuenta con un solo spread, y lleva a una regla práctica: reequilibrar en el roll y cuando el desfase pase de un umbral (por ejemplo, medio contrato de la pata corta), no cada día.
Índices y materias primas: beta y unidades
Fuera de los tipos no hay DV01, pero la pregunta sigue en pie. En un spread entre índices como NQ/ES (lección
3.2.3), lo que se quiere cancelar suele ser el mercado: el Nasdaq ficticio RVB carga 1,15 en el factor común y el
S&P ficticio RVA, 1. La función continuo reconstruye el contrato que se tiene de cada mercado con la regla de roll
de futuros3_contratos.csv:
ctr = pd.read_csv("futuros3_contratos.csv",
parse_dates=["fecha_roll"])
def continuo(cod):
"""Contrato que se tiene (roll en `fecha_roll`), su precio y su
cambio de precio con el mismo contrato."""
fechas = pd.DatetimeIndex(
cur.loc[cur["mercado"] == cod, "fecha"].unique()).sort_values()
cm = ctr[ctr["mercado"] == cod].sort_values("fecha_roll")
i = np.searchsorted(cm["fecha_roll"], fechas, side="right")
tiene = pd.Series(cm["contrato"].to_numpy()[i], index=fechas)
precio = px.reindex(list(zip(fechas, tiene))).to_numpy()
return pd.DataFrame({"precio": precio,
"dP": cambio_mismo_contrato(tiene)},
index=fechas)
A, B = continuo("RVA"), continuo("RVB")
ra = A["dP"] / A["precio"].shift(1)
rb = B["dP"] / B["precio"].shift(1)
nom = (B["precio"] * mult["RVB"]) / (A["precio"] * mult["RVA"])
h_ind = pd.DataFrame({
"nominal": nom,
"beta": nom * rb.rolling(252).cov(ra) / ra.rolling(252).var(),
"volatilidad": nom * rb.rolling(63).std() / ra.rolling(63).std(),
})
nominal_b = B["precio"].shift(1) * mult["RVB"]
filas = {}
for k in h_ind:
cob = h_ind[k].shift(1) * A["dP"] * mult["RVA"]
pnl = (B["dP"] * mult["RVB"] - cob) / nominal_b
m = pd.concat([pnl, ra], axis=1).loc["2011":].dropna()
filas[k] = {
"ratio_medio": h_ind[k].loc["2011":].mean(),
"beta_resto": np.polyfit(m.iloc[:, 1], m.iloc[:, 0], 1)[0],
"vol_anual_pct": 100 * m.iloc[:, 0].std() * np.sqrt(252)}
vol_b = 100 * rb.loc["2011":].std() * np.sqrt(252)
print(f"Vol. anual de RVB sola: {vol_b:.1f} %")
pd.DataFrame(filas).T.round(3)Vol. anual de RVB sola: 24.0 %
ratio_medio beta_resto vol_anual_pct
nominal 0.903 0.151 10.486
beta 1.035 0.007 10.123
volatilidad 1.198 -0.122 10.730Las tres coberturas bajan la volatilidad del 24 % de RVB sola a algo más del 10 %, y las tres se parecen en riesgo
total. La diferencia está en la exposición al mercado. Con el mismo nominal, el spread sigue largo un 15 % del índice
de referencia (beta 0,151): es la respuesta conocida, la beta de 1,15 menos uno. La cobertura por beta lo deja en
0,007. La de volatilidad se pasa, −0,122: iguala , que es la beta dividida por la correlación
(, con en los datos), así que vende más índice del necesario. Es
el mismo error del 2s10s en pequeño: la volatilidad iguala riesgos, no exposiciones.
En materias primas aparece un quinto criterio: las unidades. El crack spread de la gasolina mide el margen de un refinador, que se cuenta por barril:
G, P = continuo("PEG"), continuo("PEC")
barriles = mult["PEG"] / 42 / mult["PEC"] # 42 galones por barril
valor = (G["precio"] * mult["PEG"]) / (P["precio"] * mult["PEC"])
rg = G["dP"] / G["precio"].shift(1)
rc = P["dP"] / P["precio"].shift(1)
vol = valor * rg.rolling(63).std() / rc.rolling(63).std()
cr = pd.DataFrame({"valor": valor, "volatilidad": vol}).loc["2011":]
print(f"Barriles: {barriles:.2f} PEC por PEG, siempre")
cr.describe().loc[["mean", "min", "50%", "max"]].round(2)Barriles: 1.00 PEC por PEG, siempre
valor volatilidad
mean 1.26 1.43
min 0.91 0.96
50% 1.25 1.42
max 1.63 2.02Un contrato de gasolina de los datos (42.000 galones) son 1.000 barriles, igual que uno de crudo: el crack en barriles es 1:1 y no cambia nunca. Igualar el valor pide entre 0,91 y 1,63 contratos de crudo por cada uno de gasolina (1,26 de media), porque la gasolina suele valer más por barril que el crudo; igualar el riesgo, todavía más (1,43 de media). Son spreads distintos: el de barriles es el margen de refino, lo que cubre una refinería y lo que estudia la lección 3.2.2; el de valor, el rendimiento relativo de los dos precios (el spread en logaritmos que usa la lección 3.2.6); el de volatilidad, una apuesta de riesgo igualado sin significado físico.
Errores típicos
- Ratio de precios en vez de ratio de valores. Dividir 112,55 entre 104,27 no da nada útil: hay que multiplicar
cada precio por su multiplicador (2.000 en
TPA, 1.000 enTPC). Con divisas distintas, convertir antes a la misma moneda (lección 3.3.3). - El ratio de hoy en el P&L de hoy. El ratio se calcula al cierre y se aplica desde la sesión siguiente. Usar el de la misma sesión es anticipación (lección 2.5.1).
- Regresión en niveles de precio. Una regresión del precio de una pata sobre el de otra da una pendiente sin sentido si los precios no cointegran (lección 2.1.5). La de mínima varianza se hace en cambios de valor, en dinero.
- El resto del redondeo. Un spread de 1 contra 2 con un ratio de 1,52 lleva un tercio de su riesgo en tipos sin saberlo. Se mide el DV01 neto de la cartera, no el de un spread ideal.
- Confiar en la volatilidad reciente de una pata anclada. Tipos cerca de cero, una divisa con banda, un contrato ilíquido: la volatilidad baja no es ausencia de riesgo, es riesgo aplazado.
Ejercicios
- Monta el spread 5s10s (
TPBfrente aTPC, el que en CBOT se lista como FYT) y repite la tabla de betas de 2016-2025 con los cuatro ratios. ¿Cuál deja la beta al nivel en cero? ¿Por qué aquí el de volatilidad se parece más al de DV01 que en el 2s10s?
Solución
PATAS["5s10s"] = ("TPB", "TPC")
R["5s10s"] = ratios("TPB", "TPC")
print(R["5s10s"].iloc[-1].round(2).to_string())
pd.DataFrame({r: betas("5s10s", r, "2016", "2025")
for r in R["5s10s"]}).T.round(1)nominal 1.03
dv01 1.49
volatilidad 1.50
min_varianza 1.43
beta_sola beta_nivel beta_pend desv_dia
nominal 24.0 21.6 49.2 191.5
dv01 3.8 0.4 68.8 120.4
volatilidad 1.9 -1.3 62.8 115.8
min_varianza 3.4 0.2 64.8 118.5Con DV01 (1,49 contratos de TPB por cada TPC), la beta al nivel es 0,4 USD por pb y la beta a la pendiente, 68,8,
el DV01 de TPC: la respuesta conocida otra vez. Aquí el ratio por volatilidad (1,50) casi coincide con el de DV01 y
deja una beta al nivel de −1,3, y el de mínima varianza, 0,2. La razón es que el 5 años no está anclado como el 2
años: con tipos cerca de cero sigue moviéndose casi tanto como el 10 años en puntos básicos, y entonces igualar riesgo
y sensibilidad son casi lo mismo. El nominal (1,03, porque los dos contratos tienen el mismo multiplicador) vuelve a
dejar una apuesta de duración: 21,6 USD por pb.
TPEes un futuro sobre un bono a 10 años en evros (QEV), la otra divisa del nivel. Calcula su P&L diario en USD con el tipo de cambio defx3_diario.parquety cúbrelo conTPCpor DV01 (con el DV01 deTPEpasado a USD). ¿Cuánta varianza quita la cobertura desde 2016? Corrige el ratio con la beta de los cambios del rendimiento deTPEsobre los deTPC, estimada en 2010-2015, y compara.
Solución
fx = pd.read_parquet("fx3_diario.parquet")
x = fx[fx["divisa"] == "QEV"].set_index("fecha")["usd_por_unidad"]
x = x.reindex(D.index).ffill()
usd_e = cambio_mismo_contrato(C["TPE"]) * mult["TPE"] * x.shift(1)
dy = Y[["TPE", "TPC"]].diff().loc[:"2015"]
beta_y = dy.cov().loc["TPE", "TPC"] / dy["TPC"].var()
h_dv = (D["TPE"] * x / D["TPC"]).shift(1)
for nombre, h in {"DV01": h_dv, "DV01 x beta": h_dv * beta_y}.items():
cub = usd_e - h * usd["TPC"]
m = pd.concat([usd_e, cub], axis=1).loc["2016":].dropna()
quita = 1 - m.iloc[:, 1].var() / m.iloc[:, 0].var()
print(f"{nombre:>12}: ratio final {h.iloc[-1]:.2f}, "
f"varianza que quita {100 * quita:.0f} %")
print(f"Beta de los rendimientos (2010-2015): {beta_y:.2f}") DV01: ratio final 1.97, varianza que quita 16 %
DV01 x beta: ratio final 1.06, varianza que quita 49 %
Beta de los rendimientos (2010-2015): 0.54Pasado a USD, el DV01 de TPE pide 1,97 contratos de TPC por cada uno, y esa cobertura solo quita el 16 % de la
varianza desde 2016. El motivo: el DV01 supone que los dos rendimientos se mueven lo mismo en puntos básicos, y el
bono de Vesperia se mueve, de media, 0,54 pb por cada pb del de EE. UU. (estimado en 2010-2015, antes del periodo en el
que se prueba). Con el DV01 multiplicado por esa beta (1,06 contratos), la cobertura quita el 49 %, casi el máximo que
permite una correlación de 0,70 entre los cambios de los dos rendimientos (). Entre curvas
distintas, el DV01 es necesario pero no suficiente: falta la beta de los tipos, que se estima y cambia (y el riesgo de
divisa, que la lección 3.3.3 trata aparte).
- Una cuenta quiere un spread NQ/ES neutral al mercado con unos 300.000 USD de nominal en la pata de
RVB. Con los datos de 2025-12-31, ¿cuántos contratos de cada pata necesita? ¿Y si usa los micro (RVAM,RVBM, una décima parte del multiplicador)? ¿Qué resto de beta queda en cada caso?
Solución
pB, pA = B["precio"].iloc[-1], A["precio"].iloc[-1]
beta_fin = h_ind["beta"].iloc[-1] / nom.iloc[-1]
print(f"Nominal de un contrato: RVB {pB * mult['RVB']:,.0f} USD, "
f"RVA {pA * mult['RVA']:,.0f} USD; beta {beta_fin:.2f}")
for etiqueta, k in {"normales": 1, "micro": 10}.items():
nom_b, nom_a = pB * mult["RVB"] / k, pA * mult["RVA"] / k
n_b = max(1, round(300_000 / nom_b))
objetivo = n_b * nom_b * beta_fin # nominal de RVA que cubre
n_a = round(objetivo / nom_a)
resto = (objetivo - n_a * nom_a) / objetivo
print(f"{etiqueta:>8}: {n_b} RVB contra {n_a} RVA, "
f"beta sin cubrir {100 * resto:+.1f} %")Nominal de un contrato: RVB 437,815 USD, RVA 337,350 USD; beta 1.17
normales: 1 RVB contra 2 RVA, beta sin cubrir -31.7 %
micro: 7 RVB contra 11 RVA, beta sin cubrir -3.5 %Un contrato de RVB tiene casi 438.000 USD de nominal: no se puede hacer un spread de 300.000 con contratos normales.
Con 1 contrato de RVB y 2 de RVA (la beta de 1,17 pide 1,52), la cuenta vende un 31,7 % más de mercado del que
cubre: un corto de unos 160.000 USD de índice. Con los micro (una décima parte), 7 contratos de RVBM (306.000 USD)
contra 11 de RVAM dejan un −3,5 %. Los micro no cambian el ratio, cambian la resolución: lo mismo que pasaba con el
número de contratos en el 2s10s.
Respuesta plegada en cada pregunta
Autoevaluación
Un empinador 2s10s se monta con 0,54 contratos a 2 años por cada contrato a 10 años, el ratio por nominal. ¿Qué exposición le queda?
- ANinguna: el nominal está igualado
- BUna posición neta en tipos: el contrato a 10 años se mueve mucho más por punto básico, así que el spread gana o pierde con un desplazamiento paralelo casi tanto como con la pendiente
- CSolo riesgo de divisa
- DSolo el riesgo del roll
Ver respuesta
Respuesta B. Una posición neta en tipos: el contrato a 10 años se mueve mucho más por punto básico, así que el spread gana o pierde con un desplazamiento paralelo casi tanto como con la pendiente
Igualar el nominal no iguala la sensibilidad a los tipos. En los datos, la cobertura por nominal deja una beta al nivel de unos 45 USD por punto básico y por contrato en los dos periodos, más de la mitad del DV01 de la pata larga (unos 70): media apuesta de duración.
A finales de 2020 el ratio por volatilidad del 2s10s vale 13,2 y el de DV01, 1,64. ¿Qué ha pasado?
- AQue el DV01 está mal calculado
- BQue el tipo a 2 años, cerca de cero, casi no se mueve: su volatilidad se hunde y el ratio por volatilidad pide muchos más contratos, que dejan un largo en tipos en cuanto el 2 años vuelve a moverse
- CQue el 10 años tiene más DV01 que nunca
- DQue el spread está en contango
Ver respuesta
Respuesta B. Que el tipo a 2 años, cerca de cero, casi no se mueve: su volatilidad se hunde y el ratio por volatilidad pide muchos más contratos, que dejan un largo en tipos en cuanto el 2 años vuelve a moverse
El ratio por volatilidad iguala el dinero en riesgo de las dos patas con lo que se movieron en los últimos 63 días. Cuando una pata está anclada (tipos cerca de cero), su volatilidad tiende a cero y el ratio explota. En 2010-2015, con esa cobertura, el empinador tuvo una beta al nivel de −33 USD por pb: un largo en duración con forma de spread.
El ratio por DV01 del 2s10s es 1,52 y una cuenta pequeña opera 1 contrato a 10 años contra 2 a 2 años. ¿Qué tiene?
- AUn 32 % de DV01 de más en la pata corta: con 2 contratos a 10 años y 3 a 2 años el error baja al 1 %
- BUn spread casi perfecto
- CUn spread neutral en volatilidad
- DUn 50 % de DV01 de menos
Ver respuesta
Respuesta A. Un 32 % de DV01 de más en la pata corta: con 2 contratos a 10 años y 3 a 2 años el error baja al 1 %
2 × 42,55 = 85,1 USD por pb frente a 64,47 de la pata larga: sobran 20,6 USD por pb, un 32 %. El redondeo de un ratio no entero solo se arregla con más contratos o con contratos más pequeños, si existen.
En NQ/ES, un spread igualado en nominal deja una beta de 0,15 al índice de referencia. ¿Qué ratio la anula?
- AEl de nominal
- BEl de volatilidad: 1,20 contratos de RVA por cada uno de RVB de media
- CEl de beta: el nominal multiplicado por la beta del Nasdaq ficticio sobre el S&P ficticio, 1,15 en los datos
- DNinguno
Ver respuesta
Respuesta C. El de beta: el nominal multiplicado por la beta del Nasdaq ficticio sobre el S&P ficticio, 1,15 en los datos
El índice tecnológico de los datos carga 1,15 en el factor de mercado: con el mismo nominal, el spread sigue largo un 15 % de mercado. El de volatilidad se pasa (beta −0,12), porque la correlación no es 1: σ_B / σ_A = β / ρ.
Un refinador quiere cubrir su margen de gasolina. ¿Qué ratio usa?
- AEl de volatilidad
- BEl de valor nominal
- CEl de mínima varianza, siempre
- DEl de unidades: un contrato de gasolina (42.000 galones, 1.000 barriles) por cada contrato de crudo (1.000 barriles), porque su margen se mide por barril
Ver respuesta
Respuesta D. El de unidades: un contrato de gasolina (42.000 galones, 1.000 barriles) por cada contrato de crudo (1.000 barriles), porque su margen se mide por barril
El margen de refino es el precio de los productos menos el del crudo, por barril. Cubrirlo exige el mismo número de barriles en cada pata. Igualar el valor (1,26 contratos de crudo por cada uno de gasolina de media en los datos) o el riesgo es otra apuesta: la del rendimiento relativo de los dos precios.
Ideas clave
- El ratio de cobertura es el número de contratos de una pata por contrato de la otra, y decide qué riesgo queda: no hay un ratio «correcto» sin decir qué se quiere neutralizar.
- Por nominal se iguala el dinero; por DV01, la sensibilidad a un punto básico; por volatilidad, el dinero en riesgo reciente; por mínima varianza, el riesgo total estimado. En el 2s10s de los datos, a fin de 2025, dan 0,54, 1,52, 1,41 y 1,18.
- Solo la cobertura por DV01 deja la beta al nivel de tipos en cero en los dos periodos (−0,2 y 1,1 USD por pb) y la beta a la pendiente igual al DV01 de la pata larga: la respuesta conocida.
- El ratio por volatilidad y el de mínima varianza dependen de lo que se movió hace poco: con el tipo a 2 años anclado cerca de cero llegan a 3, 5 y hasta 13 contratos, y convierten el spread en una apuesta de duración.
- Redondear importa: 1 contra 2 deja un 32 % de DV01 sin cubrir; 2 contra 3, un 1 %. Reequilibrar solo en el roll deja un 3,5 % de desfase mediano.
- En índices, el ratio que neutraliza el mercado es el de beta (NQ/ES: 1,15 veces el nominal); en materias primas, el de unidades cuando lo que se opera es un margen físico.