Lección 3.3.2 · 75 min
Carry y riesgo de crash, momentum y value en divisas; cestas
La lección 2.3.4 midió el carry de divisas y sus crashes. Esta va más lejos: saca el carry de los forwards y separa la base de la paridad cubierta, descompone la prima por carteras, mide cada crash contra el que está plantado, construye momentum y value con el tipo de cambio real y los combina en una cesta neta de costes. Los datos son doce divisas ficticias con los tres efectos plantados y un control sin ninguno.
- Módulo
- 3.3 · Forex avanzado
- Duración
- 75 min
- Requisitos
- 1.2.4 · Forex: pares, pips, lotes, rollover y CFD, 2.3.1 · Seguimiento de tendencia y momentum, 2.3.4 · Carry en divisas, futuros y bonos; estacionalidad y efectos calendario, 2.5.6 · Sensibilidad de parámetros: mesetas frente a picos, robustez entre activos y periodos, 2.6.1 · CAGR, Sharpe, Sortino, Calmar, Omega, drawdown y Ulcer Index
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
Doce divisas y lo que tienen plantado
La lección 2.3.4 definió el carry, construyó la cartera clásica de tres contra tres con ejecución al día siguiente,
estimó la regresión de Fama y miró los crashes. Aquí se da por sabido todo eso y se trabaja con un conjunto de datos
más rico: doce divisas ficticias frente al USD, en días laborables de 2006 a 2025, cada una con su tipo de cambio,
su forward a un mes, sus tipos de depósito y su IPC mensual. Cada divisa imita el perfil de una real: el evro (QEV)
al euro, el yen de Nara (QYN) al yen, el real de Brava (QBR) al real brasileño. Los datos son sintéticos y tres
efectos están plantados con parámetros conocidos:
| Efecto | Qué hace en los datos | Parámetro plantado |
|---|---|---|
| Carry | Largo en la divisa rinde el 80 % del diferencial de tipos (paridad descubierta incompleta) | θ = 0,20 |
| Crash | En ocho tramos de estrés, cada divisa cae k · (su diferencial − la media) | k de 0,6 a 3,0 |
| Momentum | Deriva anual en la dirección del rendimiento en exceso de 63 sesiones | λ = 0,15 volatilidades |
| Value | El componente propio de cada divisa revierte a cero | Vida media de 3 años |
| Control | sorpresas_macro: una serie que no entra en ninguna ecuación | Sin efecto |
Además hay un factor dólar común, un factor de riesgo que sube las divisas de tipo alto y baja las refugio (QYN,
QFR) en las crisis, y una base de la paridad cubierta en cinco divisas. Todo está en
public/curso/datos/nivel-3/README.md y en fx3_verdad.csv. La lección mide cada efecto como si no lo conociera y
compara.
Fuente Mermaid
flowchart TB D[fx3_diario] --> C[Carry] D --> M[Momentum] I[fx3_ipc_mensual] --> V[Value] D --> V C --> R[Rangos fin de mes] M --> R V --> R R --> P[3 largas, 3 cortas] P --> E[Escala por vol] E --> K[Cesta neta]
El carry en los forwards y la base de la paridad cubierta
Un gestor de divisas no suele pedir prestado en una divisa para depositar en otra: compra a plazo. El forward a un mes ya lleva dentro el diferencial de tipos, por la paridad cubierta de intereses (lección 1.2.4):
con y en USD por unidad de la divisa, el tipo de la divisa y . Si la paridad se cumpliera, el tipo en USD implícito en el forward sería el del depósito. Desde 2008 no se cumple en las divisas reales: Du, Tepper y Verdelhan (2018) documentan desviaciones persistentes (la base) en las divisas del G10, que atribuyen a que arbitrarlas consume balance de los bancos. En los datos hay una base plantada en cinco divisas. Se recupera con la fórmula exacta; la aproximación logarítmica habitual, , deja varios puntos básicos de error en las de tipo alto, donde ya no se parece a .
import numpy as np
import pandas as pd
import statsmodels.api as sm
fx = pd.read_parquet("fx3_diario.parquet")
def tabla(col):
return fx.pivot(index="fecha", columns="divisa", values=col)
X = tabla("usd_por_unidad")
i_div = tabla("tipo_deposito_1m")
i_usd = fx.groupby("fecha")["tipo_deposito_1m_usd"].first()
dif = i_div.sub(i_usd, axis=0) # % anual
cotiza_usd = fx.groupby("divisa")["par"].first().str.startswith("USD")
F = tabla("forward_1m")
F.loc[:, cotiza_usd] = 1 / F.loc[:, cotiza_usd] # USD por unidad
TAU = 30 / 360
i_usd_impl = ((1 + i_div / 100 * TAU) * F / X - 1) / TAU * 100
base_pb = 100 * i_usd_impl.sub(i_usd, axis=0)
base_pb.median().round(1).sort_values().to_frame("base_pb").Tdivisa QYN QEV QFR QLB QSK QBR QCD QAD QNK QMP QZD QZR
base_pb -30.3 -15.4 -9.9 -5.4 -5.4 -0.0 0.0 -0.0 -0.0 -0.0 -0.0 -0.0Las cinco bases plantadas salen casi exactas: −30 pb en QYN, −15 en QEV, −10 en QFR y −5 en QLB y QSK
(las cifras plantadas en calma; en los tramos de estrés se amplían, y la mediana los ignora). El resto, cero. Una base
negativa significa que conseguir dólares a plazo con esa divisa sale más caro que pedirlos prestados: el tipo en USD
implícito en el forward está por debajo del depósito, y quien vende la divisa a plazo para cubrirse paga esa
diferencia (lección 3.3.3).
¿Importa la base para el carry? Para ordenar las divisas, casi nada: 30 pb frente a diferenciales de varios puntos. ¿Predice algo? En el modelo no entra en ninguna ecuación de rentabilidad: es un control. La regresión del rendimiento en exceso del mes siguiente sobre la base, con errores agrupados por mes (lección 2.1.1), lo confirma:
dias = X.index.to_series().diff().dt.days
interes = dif.shift(1).div(100).mul(dias / 360, axis=0)
d_spot = np.log(X).diff()
r = d_spot + interes # rendimiento en exceso
fin = X.index.to_series().groupby(X.index.to_period("M")).max()
sig_mes = r.groupby(r.index.to_period("M")).sum().shift(-1)
sig_mes.index = fin.values # el mes siguiente a cada fin
panel = pd.DataFrame({"y": 1200 * sig_mes.stack(), # % anual
"base": base_pb.loc[fin].stack(),
"dif": dif.loc[fin].stack()}).dropna()
grupos = panel.index.get_level_values(0).astype(str)
for xs in (["base"], ["dif", "base"]):
m = sm.OLS(panel["y"], sm.add_constant(panel[xs])).fit(
cov_type="cluster", cov_kwds={"groups": grupos})
print(xs, "t =", m.tvalues[xs].round(2).to_dict())['base'] t = {'base': 0.3}
['dif', 'base'] t = {'dif': 1.75, 'base': -0.39}Sola, la base tiene un t de 0,30; con el diferencial en la regresión, −0,39. El diferencial tiene un t de 1,75: ni siquiera el carry, que sí está plantado, es significativo divisa a divisa y mes a mes. Es el mismo problema de potencia que la regresión de Fama de la lección 2.3.4. La medida útil es la cartera.
La prima, cartera a cartera
Lustig, Roussanov y Verdelhan (2011) ordenan las divisas por su diferencial de tipos (o su descuento a plazo) en carteras y documentan dos factores: el dólar (la media de todas las divisas frente al USD) y el HML de divisas (la cartera de tipo alto menos la de tipo bajo). Con doce divisas salen cuatro carteras de tres. Cada fin de mes se ordenan por diferencial y se mantienen desde la sesión siguiente:
puesto = dif.loc[fin].rank(axis=1)
grupo = ((puesto - 1) // 3 + 1).reindex(X.index, method="ffill")
grupo = grupo.shift(1) # desde la sesión siguiente
filas = {}
for g in range(1, 5):
en_g = grupo == g
filas[f"C{g}"] = {
"dif_medio": dif.where(en_g).mean(axis=1).mean(),
"interes": 260 * 100 * interes.where(en_g).mean(axis=1).mean(),
"spot": 260 * 100 * d_spot.where(en_g).mean(axis=1).mean(),
"total": 260 * 100 * r.where(en_g).mean(axis=1).mean(),
}
pd.DataFrame(filas).T.round(2) dif_medio interes spot total
C1 -1.40 -1.41 1.39 -0.02
C2 -0.29 -0.29 0.35 0.06
C3 1.24 1.26 -0.69 0.57
C4 6.18 6.25 -0.31 5.95Es la figura de la cabecera. La cartera de tipo más alto (C4, 6,18 puntos sobre el USD de media) cobra un 6,25 %
anual de interés y pierde solo un 0,31 % en el tipo de cambio: se queda con un 5,95 % de rendimiento en exceso. La de
tipo más bajo (C1) paga un 1,41 % de interés y lo recupera casi entero en el tipo de cambio (+1,39 %): su total es
cero. La paridad descubierta predice que el tipo de cambio compensa el diferencial en todas; lo hace en las de tipo
bajo y no en las de tipo alto, donde el modelo cobra el 80 % del diferencial y pierde, además, los crashes. Que la
C4 solo pierda un 0,31 % anual en el spot incluye los crashes: se pierden de golpe, pero entre uno y otro la divisa
de tipo alto ni siquiera se deprecia lo que dice la paridad.
La diferencia C4 − C1 es el HML de divisas, y con doce divisas en cuatro grupos de tres es exactamente la cartera de
carry de tres contra tres. La media de todas frente al USD es el factor dólar:
dolar = r.mean(axis=1)
hml = (r.where(grupo == 4).mean(axis=1)
- r.where(grupo == 1).mean(axis=1)).dropna()
def sharpe(x, n=260):
return x.mean() / x.std() * np.sqrt(n)
pd.DataFrame({"media_pct": [260 * 100 * dolar.mean(),
260 * 100 * hml.mean()],
"sharpe": [sharpe(dolar), sharpe(hml)]},
index=["dolar", "hml"]).round(2) media_pct sharpe
dolar 1.50 0.20
hml 5.97 0.52El factor dólar rinde un 1,50 % anual con un Sharpe de 0,20: la media de las doce divisas incluye las de tipo alto y cobra algo de carry. El HML, un 5,97 % con un Sharpe de 0,52. En la literatura, el dólar explica la mayor parte de la variación común de las divisas y el HML la diferencia de rentabilidad media entre carteras; en los datos, por construcción, el factor dólar es un AR(1) acotado sin deriva y casi toda la prima está en el HML.
El riesgo de crash, tramo a tramo
La cartera de carry de tres contra tres, con la convención del mercado monetario (360 días) y 260 días laborables para anualizar:
def cartera(senal, n=3):
"""Largo en las n de señal más alta y corto en las n más bajas,
señal a fin de mes, posición desde la sesión siguiente."""
s = senal.loc[fin]
k = s.notna().sum(axis=1)
pos = s.rank(axis=1)
w = (pos.gt(k - n, axis=0).astype(float)
- (pos <= n).astype(float)) / n
w = w.where(k >= 2 * n, 0.0, axis=0)
w = w.reindex(X.index, method="ffill").shift(1)
vivo = w.abs().sum(axis=1) > 0
return (w * r).sum(axis=1)[vivo], w[vivo]
def resumen(x, n=260):
m = x.resample("ME").sum()
curva = x.cumsum()
return pd.Series({
"sharpe": x.mean() / x.std() * np.sqrt(n),
"media_pct": 100 * x.mean() * n,
"vol_pct": 100 * x.std() * np.sqrt(n),
"asim_mensual": m.skew(),
"max_dd_pct": 100 * (curva - curva.cummax()).min(),
"peor_mes_pct": 100 * m.min(),
})
carry, w_carry = cartera(dif)
print(resumen(carry).round(2).to_string())
mensual = carry.resample("ME").sum()
print((100 * mensual.nsmallest(4)).round(1).to_string())sharpe 0.52
media_pct 5.97
vol_pct 11.50
asim_mensual -1.49
max_dd_pct -51.80
peor_mes_pct -27.61
fecha
2008-10-31 -27.6
2010-05-31 -8.7
2020-03-31 -8.3
2014-05-31 -8.1Sharpe de 0,52, un 5,97 % anual con un 11,50 % de volatilidad, asimetría mensual de −1,49, una caída máxima del 51,8 % (en log) y un peor mes de −27,6 %, octubre de 2008. El README de los datos da 0,55 y 6,2 % porque anualiza con 252 días y cobra el interés con 365: las convenciones mueven la segunda cifra decimal del Sharpe, no la conclusión. Los otros tres peores meses (mayo de 2010, marzo de 2020, mayo de 2014) son mucho menores: el riesgo del carry está concentrado en un episodio.
Cada tramo, contra el plantado
En cada tramo de estrés el generador hace caer cada divisa en log, repartido en el tramo. Una regresión transversal de la caída de cada divisa en el tramo sobre su diferencial respecto a la media, al empezar el tramo, debería dar una pendiente de :
tramos = pd.DataFrame({
"desde": ["2008-09-29", "2010-05-03", "2011-08-01", "2013-05-22",
"2015-08-11", "2018-04-16", "2020-02-24", "2024-07-31"],
"hasta": ["2008-10-27", "2010-05-25", "2011-09-23", "2013-06-24",
"2015-08-25", "2018-09-07", "2020-03-23", "2024-08-05"],
"k": [3.0, 0.8, 1.2, 1.0, 0.8, 0.6, 2.5, 0.6]})
filas = []
for t in tramos.itertuples():
antes = X.index[X.index < t.desde][-1]
caida = np.log(X.loc[t.hasta] / X.loc[antes])
x = (dif.loc[antes] - dif.loc[antes].mean()) / 100
fit = sm.OLS(caida, sm.add_constant(x)).fit()
filas.append({"desde": t.desde[:7],
"sesiones": len(X.loc[t.desde:t.hasta]),
"k": t.k, "pendiente": fit.params.iloc[1],
"error": fit.bse.iloc[1],
"carry_pct": 100 * carry[t.desde:t.hasta].sum()})
pd.DataFrame(filas).round(2) desde sesiones k pendiente error carry_pct
0 2008-09 21 3.0 -3.14 0.17 -28.41
1 2010-05 17 0.8 -1.12 0.09 -7.98
2 2011-08 40 1.2 -1.22 0.32 -8.28
3 2013-05 24 1.0 -0.17 0.16 0.64
4 2015-08 11 0.8 -0.81 0.12 -7.33
5 2018-04 105 0.6 2.62 0.96 28.18
6 2020-02 21 2.5 -2.38 0.22 -11.38
7 2024-07 4 0.6 -0.60 0.07 -4.46Los dos tramos grandes y cortos se recuperan bien: octubre de 2008 (k = 3,0) da −3,14 con un error de 0,17, y la pandemia (k = 2,5), −2,38 con 0,22. En ellos la cartera pierde un 28,4 % y un 11,4 %. Los tramos pequeños se recuperan peor o nada: 2013 (k = 1,0) da −0,17, y 2018 (k = 0,6) da +2,62: la cartera gana un 28,2 % en un tramo plantado de estrés. Ese tramo dura 105 sesiones y la caída plantada, 0,6 veces un diferencial de unos 5 puntos, es de un 3 %; en esos cinco meses las divisas de tipo alto se apreciaron un 20–30 % por los otros componentes del modelo (el factor de riesgo, el momentum, el ruido). La lección general: un efecto pequeño repartido en un periodo largo no se ve en ese periodo; un efecto grande y concentrado, sí. Y una lectura de los datos reales que diga «en el episodio X el carry no cayó, luego el riesgo de crash es un mito» comete el mismo error.
La volatilidad global de las divisas
Menkhoff, Sarno, Schmeling y Schrimpf (2012) relacionan las rentabilidades del carry con las innovaciones de la volatilidad global de las divisas: las divisas de tipo alto pierden cuando la volatilidad sube por sorpresa, y las de tipo bajo hacen de cobertura. Su medida es la media de los cambios absolutos diarios de todas las divisas. En los datos, la volatilidad del factor de riesgo se duplica en los tramos de estrés, así que la relación debería aparecer:
vol_global = (d_spot.abs().mean(axis=1).resample("ME").mean()
* np.sqrt(260))
ar1 = sm.OLS(vol_global, sm.add_constant(vol_global.shift(1)),
missing="drop").fit()
sorpresa = ar1.resid # lo que el AR(1) no esperaba
j = pd.concat({"carry": 100 * mensual, "sorpresa": 100 * sorpresa},
axis=1).dropna()
m = sm.OLS(j["carry"], sm.add_constant(j["sorpresa"])).fit(
cov_type="HAC", cov_kwds={"maxlags": 3})
print("correlación:", round(j.corr().iloc[0, 1], 2))
print("pendiente:", round(m.params["sorpresa"], 2),
" t:", round(m.tvalues["sorpresa"], 2))correlación: -0.21
pendiente: -0.6 t: -1.37La correlación es de −0,21, con el signo de la literatura: el carry pierde en los meses en que la volatilidad global sube más de lo esperado. Pero la pendiente, con errores robustos a la autocorrelación (HAC), tiene un t de −1,37: con 240 meses, no es significativa. El riesgo está ahí, en los datos y en el modelo, pero concentrado en pocos meses; una regresión sobre todos los meses lo diluye.
Escalar por volatilidad: lo que hace y lo que no
La respuesta habitual al riesgo de crash es escalar la posición por la inversa de su volatilidad reciente (lección 2.2.2 y el research de volatility targeting del sitio). Con un objetivo del 10 % anual y la volatilidad de las 63 sesiones anteriores, con un máximo de 2 veces:
vol_63 = carry.rolling(63).std().shift(1) * np.sqrt(260)
escala = (0.10 / vol_63).clip(upper=2.0)
carry_esc = (carry * escala).dropna()
pd.DataFrame({"carry": resumen(carry[carry_esc.index]),
"escalado": resumen(carry_esc)}).round(2) carry escalado
sharpe 0.51 0.42
media_pct 5.82 4.41
vol_pct 11.52 10.43
asim_mensual -1.49 -1.53
max_dd_pct -51.80 -45.34
peor_mes_pct -27.61 -23.72El escalado reduce la caída máxima del 51,8 % al 45,3 % y el peor mes del 27,6 % al 23,7 %, pero baja el Sharpe de 0,51 a 0,42 y deja la asimetría igual (−1,53). No protege del primer golpe porque los crashes del carry llegan después de periodos tranquilos: en septiembre de 2008 la volatilidad de los tres meses anteriores era baja y la posición estaba en su tamaño máximo. Ayuda en lo que viene después, cuando la volatilidad ya ha subido. El seguro contra el primer golpe es otro: un tamaño que lo sobreviva, o opciones fuera de dinero sobre las divisas de tipo alto, que cuestan prima (módulos 3.4 y 3.5).
Fuente Mermaid
flowchart TB A[Calma: vol baja] --> B[Posición máxima] B --> C[Crash: primer mes] C --> D[La vol sube] D --> E[Escalado reduce] E --> F[Menos pérdida]
Momentum: transversal y serial
El momentum de divisas compra las que más han subido (en exceso del depósito) en los últimos meses y vende las que menos. Menkhoff, Sarno, Schmeling y Schrimpf (2012, en el Journal of Financial Economics) lo documentan en divisas con un rasgo propio: es más fuerte en divisas con costes altos y difíciles de arbitrar. En los datos está plantado como una deriva de λ = 0,15 volatilidades al año en la dirección del rendimiento en exceso de las 63 sesiones anteriores. Dos versiones: transversal (tres contra tres por rango, como el carry) y serial (cada divisa larga o corta según su propio signo, lección 2.3.1).
exceso_63 = r.rolling(63).sum()
mom, w_mom = cartera(exceso_63)
ts_w = (np.sign(exceso_63.loc[fin]) / 12).reindex(
X.index, method="ffill").shift(1)
tsmom = (ts_w * r).sum(axis=1)[ts_w.abs().sum(axis=1) > 0]
print(pd.DataFrame({"transversal": resumen(mom),
"serial": resumen(tsmom)}).round(2).to_string())
# rendimiento del mes siguiente en volatilidades anuales
vol = d_spot.rolling(63).std() * np.sqrt(260)
y = r.rolling(21).sum().shift(-21) * 12 / vol
pan = pd.DataFrame({"y": y.loc[fin].stack(),
"senal": np.sign(exceso_63).loc[fin].stack()}
).dropna()
m = sm.OLS(pan["y"], sm.add_constant(pan["senal"])).fit(
cov_type="cluster",
cov_kwds={"groups": pan.index.get_level_values(0).astype(str)})
print(f"lambda = {m.params['senal']:.3f} "
f"(error típico {m.bse['senal']:.3f}; plantada 0,15)") transversal serial
sharpe 0.20 0.16
media_pct 1.69 0.79
vol_pct 8.60 5.05
asim_mensual -0.24 -0.03
max_dd_pct -44.83 -28.49
peor_mes_pct -11.64 -5.77
lambda = 0.060 (error típico 0.109; plantada 0,15)La cartera transversal tiene un Sharpe de 0,20 y la serial de 0,16. Las dos, con una asimetría cercana a cero, sin el perfil de crash del carry; pero el peor mes del momentum transversal es también octubre de 2008 (−11,6 %): en un crash las divisas que venían subiendo eran las de tipo alto. La estimación directa del parámetro plantado da λ = 0,060 con un error típico de 0,109: el intervalo incluye el 0,15 plantado y el cero. Con doce divisas y 240 meses, un efecto de 0,15 volatilidades al año está por debajo de lo que se puede medir con precisión, aunque esté.
Value: el tipo de cambio real
La señal de value en divisas compara el tipo de cambio con un valor de equilibrio. El más simple es la paridad del poder adquisitivo (PPP): a largo plazo, el tipo de cambio real, , debería volver a su media. Una divisa cuyo tipo real ha subido mucho en cinco años está cara; una que ha caído, barata. Asness, Moskowitz y Pedersen (2013) usan esa señal (menos el cambio del tipo real en cinco años) en divisas, junto con value en otras clases de activos. Menkhoff, Sarno, Schmeling y Schrimpf (2017) refinan la medida ajustando por productividad y otros fundamentales1Nota 1 · Menkhoff, Sarno, Schmeling y Schrimpf (2017), Currency Value, Review of Financial Studies 30(2)..
Dos detalles de anticipación. El IPC de un mes se publica con retraso: a fin del mes solo se conoce el del mes , y eso es lo que se usa. Y la señal necesita cinco años de historia, así que el value empieza en 2011.
ipc = pd.read_csv("fx3_ipc_mensual.csv", parse_dates=["mes"])
ipc = ipc.set_index("mes")
rel = ipc[X.columns].div(ipc["USD"], axis=0).shift(1) # mes anterior
rel.index = rel.index.to_period("M")
q_real = np.log(X.loc[fin]) + np.log(rel.loc[fin.index].to_numpy())
ventanas = {}
for meses in (36, 48, 60, 72):
senal = -(q_real - q_real.shift(meses)).reindex(X.index)
v, _ = cartera(senal)
ventanas[meses] = resumen(v)[["sharpe", "media_pct",
"max_dd_pct"]]
val, w_val = cartera(-(q_real - q_real.shift(60)).reindex(X.index))
pd.DataFrame(ventanas).T.round(2) sharpe media_pct max_dd_pct
36 0.05 0.40 -35.41
48 0.34 2.99 -28.72
60 0.18 1.66 -30.46
72 0.60 5.19 -27.12Con 60 meses, la ventana de Asness, Moskowitz y Pedersen, el value tiene un Sharpe de 0,18. Con 48, de 0,34; con 36, de 0,05; con 72, de 0,60. El README de los datos, con 1.260 sesiones (unos 58 meses) y anualizando con 252 días, da 0,30. Cada ventana empieza en una fecha distinta y deja fuera años distintos, y con unos quince años de señal el error típico de un Sharpe es de alrededor de . Las cuatro cifras son compatibles con una prima real pero débil; elegir la de 72 meses porque sale mejor sería sobreajuste (lecciones 2.5.4 y 2.5.6).
¿Recupera el tipo real la vida media plantada de tres años? El value plantado es el componente propio de cada divisa, sin el factor dólar; el tipo real lo mezcla con el factor dólar (vida media de cinco años), la deriva del carry y las diferencias de inflación:
vida = {}
for c in q_real:
z = q_real[c].dropna()
z = z - z.mean()
ar = sm.OLS(z.diff().iloc[1:], z.shift(1).iloc[1:]).fit()
vida[c] = np.log(2) / -ar.params.iloc[0] / 12 # años
pd.Series(vida).round(1).sort_values().to_frame("vida_anos").T QLB QFR QSK QEV QCD QAD QZD QNK QZR QMP QYN QBR
vida_anos 0.9 1.1 1.1 1.2 1.2 1.7 1.8 2.2 2.5 3.2 4.9 5.1Las vidas medias van de 0,9 años (QLB) a 5,1 (QBR), con una mediana de 1,75: casi todas por debajo de los tres
plantados y con una dispersión enorme. Las cortas lo son porque el tipo real lleva encima ruido que revierte deprisa
(el del IPC, los movimientos del momentum), y el AR(1) mezcla las dos velocidades. Las más largas son las de divisas con diferenciales extremos (QBR muy alto, QYN muy bajo): la deriva del carry, que
mueve el tipo real año tras año en una dirección, parece persistencia. Es el mismo problema que tiene la PPP en los
datos reales, donde las estimaciones de la vida media de las desviaciones del tipo real son de varios años y muy
imprecisas: una señal de value en divisas trabaja con una reversión lenta y medida con mucho ruido.
El control
sorpresas_macro es una serie AR(1) por divisa que no entra en ninguna ecuación. Una cartera de tres contra tres con
ella como señal debería no ganar nada:
sorpresas = tabla("sorpresas_macro")
control, _ = cartera(sorpresas)
resumen(control).round(2).to_frame("control").T sharpe media_pct vol_pct asim_mensual max_dd_pct peor_mes_pct
control 0.1 0.71 6.99 0.45 -28.69 -5.17Sharpe de 0,10: un 0,71 % anual con una caída máxima del 28,7 %. Sin ningún efecto plantado, veinte años de una cartera de divisas rinden algo por azar. Es la vara de medir para el momentum (0,20) y el value (0,18): están por encima, pero no muy lejos. El carry (0,52) sí se separa.
Costes: el giro decide
Las tres señales cambian a ritmos muy distintos. El carry depende de los tipos de los bancos centrales, que se mueven
poco; el momentum de 63 sesiones cambia de orden casi cada mes. Con un coste por lado de 3 pb del nominal en las
divisas del G10 y de 10 pb en las emergentes (QMP, QBR, QZR), un supuesto razonable para un cliente
institucional que habrá que contrastar con los precios propios (lecciones 2.4.7 y 3.3.1):
coste_lado = pd.Series(3e-4, index=X.columns)
coste_lado[["QMP", "QBR", "QZR"]] = 10e-4
brutas = {"carry": (carry, w_carry), "momentum": (mom, w_mom),
"value": (val, w_val)}
netas, filas = {}, {}
for nom, (bruta, w) in brutas.items():
giro = w.diff().abs().fillna(w.abs())
coste = (giro * coste_lado).sum(axis=1)
netas[nom] = bruta - coste
anos = len(w) / 260
filas[nom] = {"giro_anual": giro.sum(axis=1).sum() / anos,
"coste_pct": 100 * 260 * coste.mean(),
"sharpe_bruto": resumen(bruta)["sharpe"],
"sharpe_neto": resumen(netas[nom])["sharpe"]}
pd.DataFrame(filas).T.round(2) giro_anual coste_pct sharpe_bruto sharpe_neto
carry 1.60 0.06 0.52 0.51
momentum 20.38 1.02 0.20 0.08
value 5.69 0.26 0.18 0.15El carry gira 1,6 veces al año y paga un 0,06 % anual: su Sharpe pasa de 0,52 a 0,51. El value, 5,7 veces y un 0,26 % (de 0,18 a 0,15). El momentum gira 20 veces al año y paga un 1,02 % anual, más de la mitad de su rendimiento bruto: su Sharpe baja de 0,20 a 0,08, por debajo del control. Una prima de momentum que en un backtest sin costes parece pequeña pero real se queda en nada con costes razonables; en divisas emergentes, con horquillas más anchas, peor.
Una cesta de carry, momentum y value
Las tres primas tienen poco que ver entre sí:
N = pd.DataFrame(netas).dropna()
N.resample("ME").sum().corr().round(2) carry momentum value
carry 1.00 0.12 0.02
momentum 0.12 1.00 -0.23
value 0.02 -0.23 1.00Correlación de 0,12 entre carry y momentum, de 0,02 entre carry y value, y de −0,23 entre momentum y value. La correlación negativa entre value y momentum es la que Asness, Moskowitz y Pedersen (2013) documentan en todas las clases de activos: una divisa que ha subido mucho (momentum alto) se ha encarecido (value bajo). La cesta las combina con el mismo riesgo: cada estrategia se escala a un 10 % de volatilidad con su desviación típica de las 252 sesiones anteriores (no la de toda la muestra, que sería anticipación) y la cesta es la media de las tres.
vol_ant = N.rolling(252, min_periods=126).std().shift(1)
escaladas = (N * 0.10 / (vol_ant * np.sqrt(260))).dropna()
cesta = escaladas.mean(axis=1)
tabla_cesta = pd.DataFrame({c: resumen(escaladas[c])
for c in escaladas})
tabla_cesta["cesta"] = resumen(cesta)
print("desde", cesta.index[0].date())
tabla_cesta.round(2)desde 2011-08-24
carry momentum value cesta
sharpe 0.82 0.32 0.31 0.91
media_pct 8.38 3.24 3.16 4.93
vol_pct 10.16 10.25 10.30 5.39
asim_mensual 0.28 0.09 -0.09 -0.02
max_dd_pct -24.67 -35.12 -23.63 -12.31
peor_mes_pct -8.79 -10.30 -6.98 -4.78Desde agosto de 2011 (cuando las tres tienen al menos medio año de historia para estimar su volatilidad), la cesta tiene un Sharpe de 0,91 con un 5,39 % de volatilidad y una caída máxima del 12,3 %. El carry escalado solo, en el mismo periodo, 0,82 y un 24,7 % de caída; el momentum, 0,32; el value, 0,31. La cesta mejora poco el Sharpe del carry, porque desde 2011 el carry ha sido la mejor de las tres y no ha tenido un octubre de 2008; donde se nota es en la caída máxima, que se divide por dos, y en la asimetría, que pasa a ser casi nula.
periodos = [("2011", "2015"), ("2016", "2020"), ("2021", "2025")]
pd.DataFrame({f"{a}-{b}": {**{c: resumen(escaladas[c][a:b])["sharpe"]
for c in escaladas},
"cesta": resumen(cesta[a:b])["sharpe"]}
for a, b in periodos}).round(2) 2011-2015 2016-2020 2021-2025
carry 1.12 0.18 1.23
momentum 0.04 0.31 0.55
value 0.77 0.19 0.03
cesta 0.95 0.53 1.28Ninguna estrategia gana en todos los subperiodos. El carry tiene un Sharpe de 0,18 en 2016-2020 y de 1,23 en 2021-2025; el value, de 0,77 en 2011-2015 y de 0,03 en 2021-2025; el momentum, de 0,04 al principio y 0,55 al final. La cesta tiene el Sharpe más estable: 0,95, 0,53 y 1,28. Esa estabilidad es lo que se compra al diversificar entre primas que no dependen de lo mismo, más que un Sharpe medio mayor.
Lo que una cesta de divisas exige en real
- Dimensionar por el peor episodio, no por la volatilidad. El carry tiene una volatilidad del 11 % y una caída del 52 %. La cesta reduce la segunda, pero no la elimina.
- Costes por divisa. El momentum muere por costes; en divisas emergentes la horquilla es varias veces la del G10, y en los crashes se abre más (lección 3.3.1).
- Ventanas y fechas de rebalanceo. El value cambia de 0,05 a 0,60 con la ventana. Cualquier parámetro que no se pueda justificar antes de mirar el resultado es un grado de libertad que infla el backtest (lección 2.5.4).
- Anticipación. IPC con retraso de publicación, señal al cierre de fin de mes y posición desde la sesión siguiente, pesos de volatilidad con datos pasados. Los tres errores aparecen en backtests publicados.
- Implementación. Las posiciones se toman con forwards o swaps de divisas, no con depósitos: el carry se cobra en los puntos forward, con la base incluida. La lección 3.3.3 trata la ejecución y la cobertura del riesgo de divisa de una cartera.
Ejercicios
- Construye una cesta solo de carry y momentum netos, que se puede medir desde 2006, con el mismo escalado por volatilidad pasada. Compara con el carry solo escalado: Sharpe, caída máxima y resultado en octubre de 2008.
Solución
dos = pd.DataFrame({k: netas[k] for k in ("carry", "momentum")})
dos = dos.dropna()
v2 = dos.rolling(252, min_periods=126).std().shift(1)
esc2 = (dos * 0.10 / (v2 * np.sqrt(260))).dropna()
cesta2 = esc2.mean(axis=1)
oct08 = {c: 100 * esc2[c]["2008-10"].sum() for c in esc2}
oct08["cesta"] = 100 * cesta2["2008-10"].sum()
t = pd.DataFrame({"carry": resumen(esc2["carry"]),
"cesta": resumen(cesta2)}).T
t["oct_2008_pct"] = [oct08["carry"], oct08["cesta"]]
t.round(2) sharpe media_pct vol_pct asim_mensual max_dd_pct peor_mes_pct oct_2008_pct
carry 0.53 5.48 10.32 -1.93 -46.06 -27.05 -27.05
cesta 0.37 3.01 8.06 -2.08 -43.39 -20.84 -20.84La cesta de dos estrategias empieza en septiembre de 2006 (medio año de historia para la volatilidad) e incluye octubre de 2008. Su Sharpe es peor que el del carry solo (0,37 frente a 0,53): el momentum neto de costes apenas rinde y ocupa la mitad del riesgo. En octubre de 2008 pierde un 20,8 % frente al 27,1 % del carry, porque solo la mitad está en carry, pero el momentum también pierde ese mes: en un crash, las divisas que venían subiendo son las de tipo alto. La caída máxima casi no cambia (−43,4 % frente a −46,1 %) y la asimetría empeora (−2,08). Diversificar con una prima débil y correlada en el peor mes no protege: la caída del 12 % de la cesta de tres, medida desde 2011, no dice nada de 2008.
- El factor dólar mezcla la prima del carry con la exposición al USD. Construye una cartera de carry «contra el dólar»: larga en las tres de diferencial más alto, sin cortos. Compara su correlación con el factor dólar y con la cartera de tres contra tres.
Solución
puesto_d = dif.loc[fin].rank(axis=1)
w_d = ((puesto_d > 9).astype(float) / 3).reindex(
X.index, method="ffill").shift(1)
carry_usd = (w_d * r).sum(axis=1)[w_d.sum(axis=1) > 0]
comp = pd.DataFrame({"carry_usd": carry_usd, "carry_3x3": carry,
"dolar": dolar}).dropna()
print(comp.resample("ME").sum().corr().round(2).to_string())
resumen(carry_usd).round(2).to_frame("carry_usd").T carry_usd carry_3x3 dolar
carry_usd 1.00 0.77 0.80
carry_3x3 0.77 1.00 0.28
dolar 0.80 0.28 1.00
sharpe media_pct vol_pct asim_mensual max_dd_pct peor_mes_pct
carry_usd 0.51 5.95 11.59 0.12 -53.44 -14.65La cartera larga solo en las de tipo alto tiene una correlación mensual de 0,80 con el factor dólar y de 0,77 con el carry de tres contra tres, que a su vez solo tiene 0,28 con el dólar. Su rendimiento es casi el mismo (5,95 % anual, Sharpe de 0,51) y su caída máxima algo mayor (53,4 %), pero su peor mes es de −14,7 %, no de −27,6 %, y su asimetría es positiva (+0,12). La diferencia está en los cortos: en un crash, las divisas de tipo bajo son refugios que suben, y la cartera de tres contra tres pierde por los dos lados; la larga contra el USD solo por uno, a cambio de cargar con el riesgo del dólar todo el tiempo. Por eso los estudios separan el factor dólar del HML: son dos riesgos distintos con dos perfiles de cola distintos.
- Un compañero propone la ventana de 72 meses para el value porque da un Sharpe de 0,60. ¿Qué tres comprobaciones le pedirías antes de aceptarla?
Solución
Primero, que la ventana se justifique antes de mirar el resultado: la vida media plantada de tres años o la convención de cinco años de la literatura, no el Sharpe. Segundo, que el resultado sea estable alrededor de la ventana elegida (66 y 78 meses deberían dar algo parecido) y por subperiodos; una cifra que salta de 0,34 a 0,18 y a 0,60 en ventanas vecinas es ruido. Tercero, que se corrija por el número de ventanas probadas: el Sharpe deflactado de la lección 2.5.4 con cuatro pruebas y unos quince años de datos deja 0,60 lejos de ser concluyente. Y una cuarta, específica: que la ventana de 72 meses empieza en 2012, un año después que la de 60, y no es la misma muestra.
Footnotes
-
Menkhoff, Sarno, Schmeling y Schrimpf (2017), Currency Value, Review of Financial Studies 30(2). ↩
Respuesta plegada en cada pregunta
Autoevaluación
En los datos, la base de la paridad cubierta del QYN es de −30 pb. ¿Qué significa para una estrategia de carry que use los puntos forward como señal?
- AQue el carry del QYN es 30 pb mejor de lo que dicen los depósitos
- BQue el diferencial implícito en el forward se separa del de los depósitos en 30 pb; ordena las divisas casi igual y, en los datos, la base no predice nada (t = −0,39 con el diferencial en la regresión)
- CQue hay un arbitraje sin riesgo de 30 pb al año
- DQue el forward está mal calculado
Ver respuesta
Respuesta B. Que el diferencial implícito en el forward se separa del de los depósitos en 30 pb; ordena las divisas casi igual y, en los datos, la base no predice nada (t = −0,39 con el diferencial en la regresión)
La base es la diferencia entre el tipo en USD implícito en el forward y el del depósito. Existe desde 2008 en las divisas reales (Du, Tepper y Verdelhan, 2018) porque arbitrarla consume balance de los bancos. Para el carry casi no cambia el orden de las divisas, y en estos datos es un control: no entra en ninguna ecuación de rentabilidad.
La cartera de carry de tres contra tres tiene un Sharpe de 0,52 desde 2006; escalada por volatilidad y neta de costes, de 0,82 desde agosto de 2011. ¿Qué explica sobre todo la diferencia?
- AQue la estrategia mejoró con el tiempo
- BQue el periodo corto no incluye el crash de octubre de 2008, el peor mes (−27,6 %): el Sharpe del carry depende de si la muestra tiene o no un crash grande
- CQue los costes bajaron
- DUn error de cálculo
Ver respuesta
Respuesta B. Que el periodo corto no incluye el crash de octubre de 2008, el peor mes (−27,6 %): el Sharpe del carry depende de si la muestra tiene o no un crash grande
El carry gana de forma regular y lo devuelve en pocos meses. Una muestra sin su peor episodio da una asimetría positiva (+0,28) y un Sharpe mucho mayor. Es la razón por la que el carry se presenta siempre con su peor episodio, no solo con su Sharpe.
La pendiente medida del tramo de estrés de 2018 es +2,62, con k = 0,6 plantado (las divisas de tipo alto deberían caer). ¿Qué ocurre?
- AEl generador tiene un error
- BEl tramo dura 105 sesiones y la caída plantada, unos 0,6 × 5 puntos ≈ 3 %, queda enterrada bajo movimientos del 20–30 % de las divisas de tipo alto por otras fuentes; los tramos cortos y grandes (2008, 2020) se recuperan bien
- CQue en 2018 el carry no tenía riesgo
- DQue hay que quitar 2018 de la muestra
Ver respuesta
Respuesta B. El tramo dura 105 sesiones y la caída plantada, unos 0,6 × 5 puntos ≈ 3 %, queda enterrada bajo movimientos del 20–30 % de las divisas de tipo alto por otras fuentes; los tramos cortos y grandes (2008, 2020) se recuperan bien
Un efecto pequeño repartido en cinco meses tiene una relación señal-ruido muy baja. En 2008 (k = 3,0) se mide −3,14 con error 0,17, y en 2020 (k = 2,5), −2,38. Lo que no se ve en un tramo no deja de existir: se ve agregando muchos.
Escalar la cartera de carry por su volatilidad de los 63 días anteriores reduce la caída máxima del 51,8 % al 45,3 %, pero el peor mes solo pasa de −27,6 % a −23,7 %. ¿Por qué no protege más?
- APorque el escalado usa la volatilidad futura
- BPorque los crashes del carry llegan después de periodos tranquilos: la volatilidad pasada era baja y la posición estaba en su tamaño máximo cuando empezó la caída
- CPorque el escalado aumenta los costes
- DPorque la volatilidad no cambia en las crisis
Ver respuesta
Respuesta B. Porque los crashes del carry llegan después de periodos tranquilos: la volatilidad pasada era baja y la posición estaba en su tamaño máximo cuando empezó la caída
El escalado reacciona a la volatilidad que ya ha ocurrido. Ayuda en crisis largas, en las que la volatilidad sube y la posición se reduce a tiempo para el resto, pero no en el primer golpe. El seguro contra el primer golpe es otro instrumento (opciones, módulos 3.4 y 3.5) o un tamaño que lo sobreviva.
La señal de value con una ventana de 36 meses da un Sharpe de 0,05; con 48, 0,34; con 60, 0,18; con 72, 0,60. ¿Qué haces?
- AUsar 72 meses
- BPromediar o fijar la ventana por razones previas (la vida media plantada es de 3 años) y tratar la dispersión como la incertidumbre del método, no como un parámetro que optimizar
- CDescartar el value
- DUsar la ventana del último año
Ver respuesta
Respuesta B. Promediar o fijar la ventana por razones previas (la vida media plantada es de 3 años) y tratar la dispersión como la incertidumbre del método, no como un parámetro que optimizar
Con 12 divisas y unos 15 años de señal, el Sharpe de una ventana concreta tiene un error típico de alrededor de 0,25. Elegir la mejor de cuatro es sobreajuste (lecciones 2.5.4 y 2.5.6). La dispersión entre ventanas es la información útil: el value de estos datos es real pero débil.
Ideas clave
- El carry se puede leer en los forwards: el diferencial implícito coincide con el de los depósitos salvo la base de la paridad cubierta, que en los datos se recupera exacta (−30 pb en el QYN, −15 en el QEV) y no predice rentabilidades.
- Ordenadas por diferencial, las carteras ganan casi todo por el interés: la de tipo más alto cobra un 6,25 % anual y pierde un 0,31 % en el tipo de cambio. Su diferencia con la de tipo más bajo es el HML de divisas: el carry 3 contra 3.
- El carry pierde de golpe en los tramos de estrés: −28,4 % en octubre de 2008, con una pendiente medida de −3,14 frente a k = 3,0 plantado. Los tramos pequeños y largos no se distinguen del ruido.
- Momentum y value son primas más débiles y frágiles: el momentum transversal (Sharpe 0,20 bruto) se queda casi sin nada tras costes (0,08) por su giro de 20 veces al año, y el value cambia de 0,05 a 0,60 según la ventana.
- La cesta de los tres con pesos por volatilidad pasada y costes tiene un Sharpe de 0,91 y una caída máxima del 12 % desde 2011, frente al 0,82 y el 25 % del carry solo, escalado igual, en el mismo periodo: su ventaja está en las caídas y en los subperiodos malos de cada factor, no en el Sharpe.
- Los datos son sintéticos y los efectos están plantados: las cifras enseñan los métodos y sus trampas, no prueban que estas estrategias funcionen en mercados reales; para eso está la literatura citada.
Recursos
Research
Documentación
- Lustig, Roussanov y Verdelhan (2011): Common Risk Factors in Currency Markets, Review of Financial Studies 24(11)
- Menkhoff, Sarno, Schmeling y Schrimpf (2012): Carry Trades and Global Foreign Exchange Volatility, Journal of Finance 67(2)
- Menkhoff, Sarno, Schmeling y Schrimpf (2012): Currency momentum strategies, Journal of Financial Economics 106(3)
- Asness, Moskowitz y Pedersen (2013): Value and Momentum Everywhere, Journal of Finance 68(3)
- Du, Tepper y Verdelhan (2018): Deviations from Covered Interest Rate Parity, Journal of Finance 73(3)