Lección 4.1.2 · 70 min
Estimación de covarianzas: shrinkage, modelos factoriales y denoising
Con 37 activos, una covarianza tiene 703 parámetros, y seis meses de datos dan 128 observaciones por activo. Esta lección compara cuatro familias de estimadores (muestral, Ledoit-Wolf, modelos factoriales y denoising por teoría de matrices aleatorias) contra la covarianza verdadera del universo sintético, y mide lo que importa a una cartera: cuánto riesgo tiene de verdad y cuánto cree tener.
- Módulo
- 4.1 · Teoría y optimización de carteras
- Duración
- 70 min
Lecciones del nivel 4
4.1 · Teoría y optimización de carteras
- 4.1.1 Markowitz: frontera eficiente y restricciones reales
- 4.1.2 Estimación de covarianzas: shrinkage, modelos factoriales y denoising
- 4.1.3 Black-Litterman: del equilibrio del mercado a las visiones
- 4.1.4 Risk parity, mínima varianza, HRP y HERC
- 4.1.5 Optimización con CVaR, drawdown, rotación y costes
- 4.1.6 Modelos factoriales: CAPM, Fama-French, PCA y atribución
- 4.1.7 Rebalanceo y carteras de estrategias
- 4.1.8 Herramientas de carteras: Riskfolio-Lib, PyPortfolioOpt, skfolio y bt
4.2 · Gestión del riesgo y tamaño de posición
- 4.2.1 Riesgo por operación, stops y riesgo de ruina
- 4.2.2 Criterio de Kelly y volatility targeting: cuánto arriesgar
- 4.2.3 VaR, Expected Shortfall y backtesting del VaR
- 4.2.4 Stress testing y riesgos de correlación, liquidez, gap y contraparte
- 4.2.5 Control del drawdown y kill switch
- 4.2.6 Riesgo de derivados: griegas de cartera, margen y liquidaciones
4.3 · Microestructura y ejecución
4.4 · Brokers y APIs
- 4.4.1 Elegir bróker desde España o la UE: regulación, activos, costes y API
- 4.4.2 Interactive Brokers: TWS e IB Gateway, ib_async, contratos, datos y órdenes
- 4.4.3 Alpaca, Tradier, MetaTrader 5 con Python y OANDA v20
- 4.4.4 APIs de exchanges cripto: ccxt, Binance, Bybit, Kraken y Coinbase
- 4.4.5 Credenciales seguras: variables de entorno, gestores de secretos y permisos mínimos
4.5 · Sistema de trading en vivo
- 4.5.1 Arquitectura de un sistema en vivo: de los datos a la reconciliación
- 4.5.2 Gestión de órdenes: estados, fills parciales, cancelaciones y reconexiones
- 4.5.3 Paper trading, shadow trading y seguimiento del slippage
- 4.5.4 Programación de tareas y alertas: cron, APScheduler, Prefect, Telegram, Slack y email
- 4.5.5 Tolerancia a fallos: idempotencia, estado persistente y reconciliación al arrancar
- 4.5.6 NautilusTrader o LEAN en real: del backtest a producción con el mismo código
Demasiados parámetros para tan pocos datos
La lección 4.1.1 terminó con una regla: la varianza se estima con días; la media, con años. Esta lección se ocupa de la parte que sí se puede estimar con días, la covarianza, y de por qué ni siquiera esa sale gratis. Con activos, la matriz de covarianzas tiene parámetros distintos. Con los 37 activos arriesgados del universo (todos menos el monetario) son 703. Seis meses de datos diarios aportan 128 observaciones de cada activo: el número de datos crece con , pero el de parámetros, con .
El estimador de partida es la covarianza muestral,
insesgada elemento a elemento. El problema no está en cada elemento, sino en la matriz como un todo: cuando la razón no es pequeña, sus autovalores se dispersan, los grandes salen mayores y los pequeños, menores de lo que son. Un optimizador que busca la varianza mínima va directo a esas direcciones de varianza infravalorada.
Hay cuatro familias de respuestas, y esta lección las compara todas contra la verdad:
Fuente Mermaid
flowchart TB M[Muestral] --> L[Shrinkage] M --> F[Factores] M --> D[Denoising RMT] L --> L1[Ledoit-Wolf] F --> F1[Observados] F --> F2[PCA] D --> D1[Marchenko-Pastur]
La ventana y la verdad
Los datos son los del universo sintético del nivel 4 (lección 4.1.1): 38 índices de 2006 a 2025 generados con un
modelo de diez factores publicado. La covarianza verdadera está en universo4_cov_verdad.csv, una por régimen. La
ventana de trabajo es el segundo semestre de 2024, entera en régimen de calma, así que la verdad con la que se compara
es la covarianza de la calma. El monetario queda fuera: es liquidez, y con una volatilidad del 0,2 % se comería
cualquier cartera de mínima varianza.
import numpy as np
import pandas as pd
precios = pd.read_parquet("universo4_precios.parquet")
precios = precios.set_index("fecha")
fac = pd.read_parquet("universo4_factores_diarios.parquet")
fac = fac.set_index("fecha")
r = precios.pct_change()
r.iloc[0] = precios.iloc[0] / 100 - 1
exceso = r.sub(fac["rf"], axis=0).drop(columns="MONET")
activos = list(exceso.columns)
cov_v = pd.read_csv("universo4_cov_verdad.csv")
calma = cov_v[cov_v["regimen"] == "calma"].set_index("activo")
S_v = calma.loc[activos, activos].to_numpy()
ventana = exceso.loc["2024-07-01":"2024-12-31"]
print("regímenes en la ventana:",
list(fac.loc[ventana.index, "regimen"].unique()))
X = ventana.to_numpy() * np.sqrt(252) # escala anual
T, N = X.shape
print(f"N = {N} activos, T = {T} sesiones, N/T = {N / T:.2f}")
print("parámetros de la covarianza:", N * (N + 1) // 2)regímenes en la ventana: ['calma']
N = 37 activos, T = 128 sesiones, N/T = 0.29
parámetros de la covarianza: 703Multiplicar los rendimientos diarios por deja las covarianzas directamente en escala anual, la misma que la verdad publicada. Los autovalores de la matriz de correlación enseñan dónde está el problema:
def a_corr(S):
d = np.sqrt(np.diag(S))
return S / np.outer(d, d)
S_m = np.cov(X, rowvar=False)
l_v = np.linalg.eigvalsh(a_corr(S_v))[::-1]
l_m = np.linalg.eigvalsh(a_corr(S_m))[::-1]
print("mayores, verdad:", l_v[:4].round(2),
" muestra:", l_m[:4].round(2))
print("menores, verdad:", l_v[-3:].round(4), " muestra:",
l_m[-3:].round(4))
print(f"número de condición: verdad {np.linalg.cond(S_v):,.0f}, "
f"muestra {np.linalg.cond(S_m):,.0f}")mayores, verdad: [18.21 7.27 2.78 1.13] muestra: [17.08 7.6 2.9 1.16]
menores, verdad: [0.0202 0.013 0.0117] muestra: [0.0139 0.0129 0.0064]
número de condición: verdad 32,579, muestra 31,703El primer autovalor, el del mercado, absorbe la mitad de la traza (37 en una correlación de 37 activos). Los autovalores pequeños son muy pequeños también en la verdad: el universo tiene versiones cubiertas y sin cubrir de las mismas bolsas, cuatro tramos de bonos del Tesoro que cargan sobre el mismo factor de tipos, y sectores que son casi el índice. Esas combinaciones tienen muy poco riesgo de verdad. En la muestra, el menor autovalor sale a la mitad del verdadero (0,0064 frente a 0,0117): la muestra cree que existe una cartera con la mitad del riesgo que de verdad tiene.
Cómo medir un estimador
La distancia entre matrices (por ejemplo, la norma de Frobenius de la diferencia de correlaciones) es la medida más citada y la menos útil: la dominan los elementos grandes y no dice nada de lo que le pasa a una cartera. Esta lección usa dos medidas de cartera, con la cartera de mínima varianza, que solo depende de la covarianza:
- Volatilidad real frente a la óptima: los pesos se calculan con la estimación y su volatilidad se evalúa con la verdad, dividida por la de la cartera de mínima varianza verdadera. 1 es lo mejor posible.
- Volatilidad prometida frente a la real: la volatilidad que la estimación atribuye a sus propios pesos, dividida por la verdadera. Por debajo de 1, la cartera tiene más riesgo del que cree.
uno = np.ones(N)
def min_var(S):
x = np.linalg.solve(S, uno)
return x / x.sum()
w_opt = min_var(S_v)
vol_opt = np.sqrt(w_opt @ S_v @ w_opt)
def evalua(S):
w = min_var(S)
real = np.sqrt(w @ S_v @ w)
return {"err_corr": np.linalg.norm(a_corr(S) - a_corr(S_v))
/ np.linalg.norm(a_corr(S_v)),
"vol_real/optima": real / vol_opt,
"prometida/real": np.sqrt(w @ S @ w) / real}
print(f"vol. de la mínima varianza verdadera: {vol_opt:.2%}")
print({k: round(float(v), 3) for k, v in evalua(S_m).items()})vol. de la mínima varianza verdadera: 0.47%
{'err_corr': 0.173, 'vol_real/optima': 1.28, 'prometida/real': 0.657}La cartera de mínima varianza verdadera tiene una volatilidad del 0,47 % (bonos cortos y combinaciones cubiertas). La construida con la muestral tiene un 28 % más de volatilidad, y promete un 66 % de la que tiene: cree tener un tercio menos de riesgo. Parte de esa diferencia es de escala (el semestre fue más tranquilo que la media de la calma); el Monte Carlo de más abajo, sin ese efecto, da un 72 % con 126 sesiones. Es el sesgo de la lección 4.1.1 aplicado a la covarianza: el optimizador elige las direcciones con el error más favorable.
Shrinkage: Ledoit-Wolf
La idea del shrinkage es combinar la muestral, insesgada pero ruidosa, con un objetivo estructurado, sesgado pero estable, :
Ledoit y Wolf (2004) eligieron como objetivo la identidad escalada, con , y derivaron la intensidad que minimiza el error cuadrático esperado. Su estimador de la intensidad compara la dispersión de la muestral alrededor del objetivo con la varianza de la propia muestral:
Si la muestral es muy ruidosa ( grande frente a ), se encoge mucho; con muchos datos, poco. Es corto de implementar y se puede contrastar con scikit-learn:
from sklearn.covariance import LedoitWolf
def ledoit_wolf(X):
Xc = X - X.mean(axis=0)
T, N = Xc.shape
S = Xc.T @ Xc / T
m = np.trace(S) / N
d2 = np.sum((S - m * np.eye(N)) ** 2) / N
b2 = sum(np.sum((np.outer(x, x) - S) ** 2) for x in Xc)
b2 = min(b2 / N / T**2, d2)
delta = b2 / d2
return delta * m * np.eye(N) + (1 - delta) * S, delta
S_lw, delta = ledoit_wolf(X)
skl = LedoitWolf().fit(X)
print(f"intensidad: propia {delta:.4f}, sklearn {skl.shrinkage_:.4f}")
print("misma matriz:", np.allclose(S_lw, skl.covariance_, atol=1e-12))intensidad: propia 0.0490, sklearn 0.0490
misma matriz: TrueLa implementación propia y la de scikit-learn coinciden (las dos usan la covarianza con divisor , no ). La intensidad es pequeña, un 4,9 %, porque 128 sesiones no son tan pocas para 37 activos. Y aun así el resultado es desastroso, por el objetivo:
def lw_correlacion(X):
sd = X.std(axis=0)
C, delta = ledoit_wolf((X - X.mean(axis=0)) / sd)
return a_corr(C) * np.outer(sd, sd), delta
S_lwc, delta_c = lw_correlacion(X)
from pypfopt import risk_models
S_cc = risk_models.CovarianceShrinkage(
pd.DataFrame(X), returns_data=True, frequency=1
).ledoit_wolf(shrinkage_target="constant_correlation").to_numpy()
tabla = pd.DataFrame({"muestral": evalua(S_m),
"LW covarianza": evalua(S_lw),
"LW correlación": evalua(S_lwc),
"LW corr. constante": evalua(S_cc)}).T
print(tabla.round(3))
vols = np.sqrt(np.diag(S_m))
print("vol. mínima, media y máxima:", vols.min().round(3),
vols.mean().round(3), vols.max().round(3)) err_corr vol_real/optima prometida/real
muestral 0.173 1.280 0.657
LW covarianza 0.226 2.758 1.333
LW correlación 0.190 1.238 1.077
LW corr. constante 0.205 1.222 1.150
vol. mínima, media y máxima: 0.014 0.112 0.224Las volatilidades de los activos van del 1,4 % (bono de 1-3 años) al 22 % (energía). La identidad escalada empuja todas las varianzas hacia su media: el bono corto, el activo clave de la cartera de mínima varianza, parece más arriesgado de lo que es, y la cartera resultante tiene 2,76 veces la volatilidad óptima. El shrinkage no es un estimador «mejor» en abstracto: es tan bueno como su objetivo.
Hay dos formas de arreglarlo. La primera, encoger la correlación: se tipifican los rendimientos, se aplica
Ledoit-Wolf a esa matriz (cuyo objetivo natural sí es la identidad) y se vuelven a poner las volatilidades de la
muestra. La segunda, el objetivo de correlación constante de Ledoit y Wolf (2004, «Honey, I Shrunk...»): todas las
correlaciones iguales a la media, cada varianza la suya. PyPortfolioOpt lo trae hecho; el argumento frequency=1
evita que multiplique por 252, porque los datos ya están en escala anual. Las dos dan alrededor de 1,23 veces la
volatilidad óptima, algo mejor que la muestral (1,28), y prometen un poco más de riesgo del que tienen (1,08 y 1,15):
se equivocan del lado prudente.
Modelos factoriales
Un modelo factorial supone que los rendimientos dependen de pocos factores comunes y de un residuo propio de cada activo, independiente del de los demás:
con diagonal. En lugar de 703 parámetros hay cargas, covarianzas de los factores y varianzas residuales. Hay dos maneras de obtener los factores:
- Observados: series que se conocen (el mercado, los tipos, el dólar, los factores de Fama-French de la lección 4.1.6). Las cargas salen de regresiones activo a activo.
- Estadísticos: los primeros componentes principales de la propia muestra. No hace falta saber qué son, pero hay que elegir .
El universo trae en universo4_factores_diarios.parquet los rendimientos realizados de sus diez factores:
F = fac.loc[ventana.index,
[c for c in fac.columns if c.startswith("f_")]]
F = F.to_numpy() * np.sqrt(252)
def factorial(X, F):
Fc = np.column_stack([np.ones(len(F)), F])
coef, *_ = np.linalg.lstsq(Fc, X, rcond=None)
B = coef[1:]
resid = X - Fc @ coef
D = np.diag(resid.var(axis=0, ddof=Fc.shape[1]))
return B.T @ np.cov(F, rowvar=False) @ B + D
def pca(X, k):
S = np.cov(X, rowvar=False)
l, V = np.linalg.eigh(S)
l, V = l[::-1][:k], V[:, ::-1][:, :k]
Sk = V @ np.diag(l) @ V.T
return Sk + np.diag(np.diag(S - Sk))
S_f = factorial(X, F)
tabla.loc["factores observados"] = evalua(S_f)
tabla.loc["PCA, 5 componentes"] = evalua(pca(X, 5))
print(tabla.round(3).iloc[[0, 2, 4, 5]]) err_corr vol_real/optima prometida/real
muestral 0.173 1.280 0.657
LW correlación 0.190 1.238 1.077
factores observados 0.173 1.080 0.982
PCA, 5 componentes 0.184 1.267 1.113El modelo de factores observados es el mejor de la lección: 1,08 veces la volatilidad óptima y un riesgo prometido casi exacto (0,98). Hay que leerlo con cuidado: los datos se generaron con exactamente estos diez factores y residuos independientes, así que el modelo está bien especificado por construcción. En mercados reales los factores son aproximaciones y los residuos tienen correlaciones (sectoriales, de estilo) que el modelo ignora. El modelo con cinco componentes principales lo ilustra: deja fuera factores débiles, como el dólar (que separa las versiones cubiertas de las no cubiertas), su residuo deja de ser diagonal y la cartera queda en 1,27, como la muestral.
Denoising con Marchenko-Pastur
La teoría de matrices aleatorias da una referencia para separar señal de ruido. Si los activos fueran independientes y de varianza , los autovalores de su correlación muestral con observaciones no saldrían todos iguales a 1: se repartirían, cuando y son grandes, según la distribución de Marchenko-Pastur, en el intervalo
Laloux, Cizeau, Bouchaud y Potters (1999) observaron que casi todos los autovalores de la correlación de las acciones del S&P 500 caían dentro de ese intervalo: indistinguibles del ruido. El denoising que populariza López de Prado (2020) actúa en consecuencia: deja los autovalores por encima de , sustituye los demás por su media (para conservar la traza) y reconstruye la correlación.
Fuente Mermaid
flowchart TB
S[Covarianza] --> C[Correlación]
C --> A[Autovalores]
A --> R{"¿Mayor que λ+?"}
R -- sí --> K[Se conserva]
R -- no --> P[Se promedia]
K --> N[Reconstruir]
P --> N
N --> V[Volatilidades]q = N / T
lam_mas, lam_menos = (1 + np.sqrt(q)) ** 2, (1 - np.sqrt(q)) ** 2
print(f"Marchenko-Pastur con σ² = 1: [{lam_menos:.3f}, {lam_mas:.3f}]")
print("autovalores por encima de λ+: muestra", (l_m > lam_mas).sum(),
"| verdad", (l_v > lam_mas).sum())
print("autovalores verdaderos por debajo de λ-:",
(l_v < lam_menos).sum(), "de", N)Marchenko-Pastur con σ² = 1: [0.214, 2.364]
autovalores por encima de λ+: muestra 3 | verdad 3
autovalores verdaderos por debajo de λ-: 21 de 37Con seis meses, el borde superior es 2,36 y solo tres autovalores de la muestra lo superan; en la verdad, también tres. Hasta aquí, la receta funciona. Pero la verdad no es «tres factores y ruido blanco»: 21 de los 37 autovalores verdaderos están por debajo del borde inferior. No son ruido: son las combinaciones de muy poco riesgo que tiene el universo (cubierto frente a sin cubrir, tramos de bonos). Promediarlos con los demás les sube la varianza, que es lo mismo que borrar esas coberturas:
def denoise(X, ajusta=True):
S = np.cov(X, rowvar=False)
sd = np.sqrt(np.diag(S))
l, V = np.linalg.eigh(a_corr(S))
q = X.shape[1] / X.shape[0]
s2 = 1.0
for _ in range(50 if ajusta else 0): # punto fijo para σ²
s2 = l[l <= s2 * (1 + np.sqrt(q)) ** 2].mean()
ruido = l <= s2 * (1 + np.sqrt(q)) ** 2
l = l.copy()
l[ruido] = l[ruido].mean()
C = a_corr(V @ np.diag(l) @ V.T)
return C * np.outer(sd, sd), int((~ruido).sum()), s2
S_dn, k1, _ = denoise(X, ajusta=False)
S_dn2, k2, s2 = denoise(X)
print(f"σ² = 1: conserva {k1}; σ² ajustada = {s2:.3f}: conserva {k2}")
tabla.loc["denoising σ² = 1"] = evalua(S_dn)
tabla.loc["denoising σ² ajustada"] = evalua(S_dn2)
print(tabla.round(3).iloc[[0, 2, 6, 7]])σ² = 1: conserva 3; σ² ajustada = 0.108: conserva 14
err_corr vol_real/optima prometida/real
muestral 0.173 1.280 0.657
LW correlación 0.190 1.238 1.077
denoising σ² = 1 0.209 1.504 1.239
denoising σ² ajustada 0.172 1.291 1.010Con , el denoising conserva tres autovalores, aplana los otros 34 y la cartera de mínima varianza empeora hasta 1,50 veces la óptima, peor que la muestral. Ajustando como hace López de Prado (aquí con un punto fijo sencillo: la varianza del ruido es la media de los autovalores que quedan dentro del borde), el borde baja, se conservan 14 autovalores y el resultado iguala a la muestral (1,29), con un riesgo prometido mucho más honesto (1,01 frente a 0,66). Es una herramienta útil cuando la correlación verdadera se parece al supuesto (pocos factores fuertes y un residuo parecido al ruido); cuando hay estructura en los autovalores pequeños, hay que comprobar antes de aplicar.
Muchas ventanas: Monte Carlo desde la verdad
Una sola ventana es una sola muestra. Para comparar con más datos, se simulan rendimientos con el modelo verdadero
(cargas de universo4_verdad.csv, covarianza de los factores en calma de universo4_factores_verdad.csv y
residuos independientes), con normales y sin GARCH para aislar el error de estimación, y se repite la comparación con
30 muestras por longitud de ventana:
verdad = pd.read_csv("universo4_verdad.csv", index_col="activo")
fv = pd.read_csv("universo4_factores_verdad.csv")
fv = fv[fv["regimen"] == "calma"].set_index("factor")
nombres = list(fv.index)
B = verdad.loc[activos, [f"b_{k}" for k in nombres]].to_numpy()
vol_f = fv["vol_anual"].to_numpy()
S_fv = fv[[f"corr_{k}" for k in nombres]].to_numpy()
S_fv = S_fv * np.outer(vol_f, vol_f)
idio = verdad.loc[activos, "vol_idio"].to_numpy()
print("B·Σf·B' + D = calma:",
np.allclose(B @ S_fv @ B.T + np.diag(idio**2), S_v))B·Σf·B' + D = calma: Truerng = np.random.default_rng(7)
Lf = np.linalg.cholesky(S_fv)
estimadores = {
"muestral": lambda X, F: np.cov(X, rowvar=False),
"LW correlación": lambda X, F: lw_correlacion(X)[0],
"factores": factorial,
"denoising σ²=1": lambda X, F: denoise(X, ajusta=False)[0],
"denoising ajustado": lambda X, F: denoise(X)[0],
}
filas = []
for T_ in (63, 126, 252, 504):
for _ in range(30):
Fs = rng.standard_normal((T_, len(nombres))) @ Lf.T
Xs = Fs @ B.T + rng.standard_normal((T_, N)) * idio
for nombre, est in estimadores.items():
m = evalua(est(Xs, Fs))
filas.append({"T": T_, "estimador": nombre, **m})
mc = pd.DataFrame(filas).groupby(["T", "estimador"]).mean()
print(mc["vol_real/optima"].unstack().round(2))
print(mc["prometida/real"].unstack().round(2))estimador LW correlación denoising ajustado denoising σ²=1 factores muestral
T
63 1.26 1.27 1.96 1.09 1.55
126 1.13 1.19 1.57 1.04 1.17
252 1.08 1.16 1.46 1.02 1.08
504 1.04 1.05 1.44 1.01 1.04
estimador LW correlación denoising ajustado denoising σ²=1 factores muestral
T
63 1.10 0.90 1.02 0.91 0.43
126 1.06 0.84 1.17 0.96 0.72
252 1.05 0.76 1.27 0.98 0.87
504 1.02 0.93 1.30 0.99 0.92La figura de la cabecera resume la primera tabla. Con tres meses de datos (63 sesiones, ), la cartera de mínima varianza muestral tiene un 55 % más de volatilidad que la óptima y promete el 43 % de la que tiene. Con dos años, la diferencia baja al 4 %. Ledoit-Wolf sobre la correlación gana sobre todo con ventanas cortas (1,26 frente a 1,55 a 63 sesiones) y converge con la muestral cuando hay datos: su intensidad cae sola. El modelo factorial correctamente especificado es el mejor en todas las longitudes. Y el denoising con es el peor con ventanas largas: con 504 sesiones el borde se estrecha, pero sigue borrando las direcciones pequeñas que son reales, así que su error no desaparece (1,44). El denoising ajustado, en cambio, promete sistemáticamente menos riesgo del real (de 0,76 a 0,93).
Lo que ningún estimador ve
Todos los estimadores de esta lección estiman la covarianza del régimen que tienen delante. La ventana de 2024 es de calma; la verdad publicada incluye también la covarianza de estrés. Basta evaluar las mismas carteras con ella:
estres = cov_v[cov_v["regimen"] == "estres"].set_index("activo")
S_e = estres.loc[activos, activos].to_numpy()
for nombre, S in [("muestral", S_m), ("LW correlación", S_lwc),
("factores", S_f)]:
w = min_var(S)
print(f"{nombre:15s} prometida {np.sqrt(w @ S @ w):.2%} "
f"calma {np.sqrt(w @ S_v @ w):.2%} "
f"estrés {np.sqrt(w @ S_e @ w):.2%}")muestral prometida 0.39% calma 0.60% estrés 0.81%
LW correlación prometida 0.62% calma 0.58% estrés 0.78%
factores prometida 0.50% calma 0.50% estrés 0.70%En estrés, la volatilidad de las tres carteras sube entre un 34 % y un 40 % sobre la de la calma, y en la muestral es más del doble de lo prometido. Es un problema distinto del error de estimación: no se resuelve con más datos de la calma, sino con escenarios de estrés y límites de riesgo (lecciones 4.2.4 y 4.2.5).
Qué estimador usar
La figura reúne la ventana de la lección: la volatilidad real de la cartera de mínima varianza construida con cada estimador, en veces la óptima.
| Estimador | Cuándo funciona | Riesgo principal |
|---|---|---|
| Muestral | mucho mayor que (más de 10 veces) | Promete menos riesgo del real cuando crece |
| Ledoit-Wolf sobre la correlación | Por defecto, sobre todo con ventanas cortas | Poco, si el objetivo es razonable |
| Ledoit-Wolf sobre la covarianza | Activos con volatilidades parecidas | Destruye la estructura si no lo son |
| Factores observados | Cuando los factores explican la estructura | Mala especificación (factores que faltan) |
| PCA | Pocos factores fuertes | Elegir ; deja fuera los débiles |
| Denoising (Marchenko-Pastur) | Pocos autovalores fuertes y un resto parecido al ruido | Borra coberturas reales si hay autovalores muy pequeños |
Errores típicos:
- Comparar estimadores con la norma de Frobenius. En la ventana de la lección, el error de correlación va de 0,17 a 0,23 entre los ocho estimadores y no ordena bien los resultados de cartera.
- Datos no sincrónicos: cierres de Tokio, Fráncfort y Nueva York el mismo día infravaloran las correlaciones entre regiones. Con datos diarios de mercados de distintas zonas horarias se usan rendimientos semanales o se corrigen los retardos.
- Mezclar escalas: diarios y anuales, simples y logarítmicos, o un activo en divisa local y otro en USD.
- Ventanas sin pensar el régimen: una ventana que mezcla calma y crisis estima una covarianza que no es ni la de una ni la de la otra.
Ejercicios
- Repite la tabla de estimadores con dos años de datos en calma (2023-01-01 a 2024-12-31). ¿Qué estimador gana ahora y cuánto se reduce la diferencia con la muestral?
Solución
v2 = exceso.loc["2023-01-01":"2024-12-31"]
X2 = v2.to_numpy() * np.sqrt(252)
F2 = fac.loc[v2.index,
[c for c in fac.columns if c.startswith("f_")]]
F2 = F2.to_numpy() * np.sqrt(252)
print("T =", len(X2), "| regímenes:",
list(fac.loc[v2.index, "regimen"].unique()))
t2 = pd.DataFrame({
"muestral": evalua(np.cov(X2, rowvar=False)),
"LW correlación": evalua(lw_correlacion(X2)[0]),
"factores": evalua(factorial(X2, F2)),
"denoising ajustado": evalua(denoise(X2)[0])}).T
print(t2.round(3))T = 502 | regímenes: ['calma']
err_corr vol_real/optima prometida/real
muestral 0.113 1.053 0.847
LW correlación 0.120 1.068 0.984
factores 0.113 1.025 0.913
denoising ajustado 0.113 1.058 0.861Con 502 sesiones, la muestral queda a un 5 % de la óptima y los estimadores se juntan: Ledoit-Wolf apenas encoge y el modelo factorial sigue siendo el mejor por estar bien especificado. El riesgo prometido sale por debajo del real en la muestral, en el modelo factorial y en el denoising: en esos dos años la volatilidad conjunta fue un 14 % más baja que la media de la calma, un error de escala que ningún estimador corrige. Ledoit-Wolf, que en el Monte Carlo promete algo más riesgo del real (encoge las correlaciones y, con ellas, las coberturas), lo compensa aquí por casualidad (0,98).
- Un gestor rebalancea cada seis meses a la cartera de mínima varianza. Calcula la rotación (la mitad de la suma de los cambios absolutos de peso) entre los semestres primero y segundo de 2024 con la muestral y con Ledoit-Wolf sobre la correlación.
Solución
def pesos_mv(desde, hasta, est):
Xv = exceso.loc[desde:hasta].to_numpy() * np.sqrt(252)
return min_var(est(Xv))
muestral = lambda Xv: np.cov(Xv, rowvar=False)
lwc = lambda Xv: lw_correlacion(Xv)[0]
for nombre, est in [("muestral", muestral), ("LW correlación", lwc)]:
w1 = pesos_mv("2024-01-01", "2024-06-30", est)
w2 = pesos_mv("2024-07-01", "2024-12-31", est)
print(f"{nombre:15s} rotación {0.5 * np.abs(w2 - w1).sum():.0%} "
f"bruto {np.abs(w2).sum():.0%}")muestral rotación 60% bruto 196%
LW correlación rotación 28% bruto 160%La cartera de mínima varianza sin restricciones es una cartera larga-corta apalancada, y sus pesos se mueven mucho de un semestre a otro con cualquiera de los dos estimadores. Ledoit-Wolf reduce la rotación a menos de la mitad (del 60 % al 28 %) y el apalancamiento bruto (del 196 % al 160 %), pero la cartera sigue siendo larga-corta y apalancada: el shrinkage estabiliza la covarianza, no las decisiones de la cartera. Para eso hacen falta restricciones (lección 4.1.1) y penalizar la rotación (lección 4.1.5).
- Explica con tus palabras por qué un autovalor muy pequeño de la correlación verdadera, que corresponde a «versión cubierta menos versión sin cubrir de Europa», no es ruido, y qué tendría que hacer un procedimiento de denoising para respetarlo.
Solución
Las dos versiones de Europa tienen la misma exposición a la bolsa europea y solo se diferencian en el dólar: su diferencia es, casi exactamente, una posición en divisa, con mucha menos volatilidad que cada una por separado. Esa combinación de poco riesgo existe por construcción del producto, no por azar de la muestra, y produce un autovalor pequeño de la correlación. Marchenko-Pastur describe matrices de ruido puro, sin direcciones así; cuando la verdad las tiene, el borde inferior no separa ruido de señal. Un procedimiento que las respete tiene que ajustar el nivel de ruido (σ²) en lugar de suponer 1, o actuar solo sobre los autovalores intermedios, o partir de un modelo factorial que incluya el factor que las genera (el dólar).
Respuesta plegada en cada pregunta
Autoevaluación
Con seis meses de datos, la cartera de mínima varianza construida con la covarianza muestral promete una volatilidad que es el 66 % de la real. ¿Por qué se equivoca siempre en esa dirección?
- APor un error de redondeo
- BPorque los datos tienen colas gruesas
- CPorque el optimizador busca las direcciones de menor varianza estimada, y esas son justamente las que la muestra infravalora: los autovalores pequeños de la muestral salen más pequeños que los verdaderos
- DPorque la covarianza muestral está sesgada hacia arriba
Ver respuesta
Respuesta C. Porque el optimizador busca las direcciones de menor varianza estimada, y esas son justamente las que la muestra infravalora: los autovalores pequeños de la muestral salen más pequeños que los verdaderos
La muestral es insesgada elemento a elemento, pero sus autovalores no: los extremos se dispersan (los grandes salen mayores y los pequeños, menores). Minimizar la varianza estimada selecciona las direcciones con el error más favorable, y el riesgo prometido queda por debajo del real.
Ledoit-Wolf con el objetivo de la identidad escalada, aplicado a la covarianza de 37 activos con volatilidades del 1,4 % al 22 %, da una cartera de mínima varianza con 2,76 veces la volatilidad óptima. ¿Qué falla?
- AEl objetivo: la identidad escalada empuja todas las varianzas hacia la varianza media, así que el bono corto parece mucho más arriesgado de lo que es y la cartera deja de usarlo
- BLa fórmula de la intensidad
- Cscikit-learn tiene un error
- DQue hacen falta más datos
Ver respuesta
Respuesta A. El objetivo: la identidad escalada empuja todas las varianzas hacia la varianza media, así que el bono corto parece mucho más arriesgado de lo que es y la cartera deja de usarlo
El objetivo tiene que ser razonable para la estructura de los datos. Con volatilidades tan distintas, se encoge la correlación (con las varianzas de la muestra) o se usa el objetivo de correlación constante; los dos dan alrededor de 1,23.
En el universo del curso, 21 de los 37 autovalores de la correlación verdadera están por debajo del borde inferior de Marchenko-Pastur con seis meses de datos. ¿Qué implica para el denoising?
- AQue el denoising funciona mejor que nunca
- BQue hay que usar más componentes principales
- CQue la correlación verdadera está mal calculada
- DQue la estructura real no es «unos pocos factores más ruido blanco»: hay direcciones de muy poco riesgo que son reales (versiones cubiertas y sin cubrir, bonos de distinto plazo), y sustituir todos los autovalores pequeños por su media las borra
Ver respuesta
Respuesta D. Que la estructura real no es «unos pocos factores más ruido blanco»: hay direcciones de muy poco riesgo que son reales (versiones cubiertas y sin cubrir, bonos de distinto plazo), y sustituir todos los autovalores pequeños por su media las borra
Marchenko-Pastur describe los autovalores de una matriz de ruido puro. Si la verdad tiene autovalores muy pequeños, como aquí, recortar todo lo que cae dentro del borde aplasta direcciones reales. Con σ² = 1 la cartera de mínima varianza empeora hasta 1,50 veces la óptima; ajustando σ², queda como la muestral.
El modelo de diez factores observados es el mejor estimador de la lección (1,08 veces la volatilidad óptima con seis meses). ¿Por qué hay que ser prudente al generalizarlo?
- APorque los modelos factoriales siempre sobreajustan
- BPorque aquí los datos se generaron con exactamente esos diez factores: el modelo está bien especificado por construcción, cosa que en mercados reales no ocurre
- CPorque tiene más parámetros que la muestral
- DPorque no se puede invertir
Ver respuesta
Respuesta B. Porque aquí los datos se generaron con exactamente esos diez factores: el modelo está bien especificado por construcción, cosa que en mercados reales no ocurre
Un modelo factorial gana cuando sus factores capturan la estructura de verdad. Con factores equivocados o incompletos (como PCA con cinco componentes, que deja fuera factores débiles), el residuo no es diagonal y el estimador pierde la ventaja: 1,27.
¿Qué estimador anticipa la covarianza de una crisis a partir de seis meses en calma?
- ALedoit-Wolf
- BEl modelo factorial
- CNinguno: todos estiman la covarianza de la calma; en estrés, la volatilidad real de las mismas carteras sube entre un 34 % y un 40 %
- DEl denoising
Ver respuesta
Respuesta C. Ninguno: todos estiman la covarianza de la calma; en estrés, la volatilidad real de las mismas carteras sube entre un 34 % y un 40 %
Los estimadores reducen el error de estimación dentro de un régimen. El cambio de régimen es otro problema: hay que tratarlo con escenarios de estrés y límites (lecciones 4.2.4 y 4.2.5).
Ideas clave
- La covarianza muestral tiene N(N+1)/2 parámetros. Cuando N/T no es pequeño, sus autovalores extremos se dispersan y la cartera de mínima varianza promete menos riesgo del que tiene: el 66 % del real con 37 activos y 128 sesiones.
- Ledoit-Wolf combina la muestral con un objetivo estructurado con la intensidad que minimiza el error esperado. La implementación propia coincide con scikit-learn; el objetivo importa: con volatilidades heterogéneas, se encoge la correlación, no la covarianza.
- Un modelo factorial bien especificado es el mejor estimador (1,08 veces la volatilidad óptima con seis meses); con factores incompletos, como cinco componentes principales, pierde la ventaja.
- El denoising de Marchenko-Pastur supone que la estructura está en pocos autovalores grandes. Si la verdad tiene direcciones de muy poco riesgo, las borra: con σ² = 1 empeora la cartera; con σ² ajustada, iguala a la muestral.
- Los estimadores se juzgan por la cartera, no por la distancia entre matrices: el error de Frobenius apenas los distingue. Y ninguno anticipa la covarianza de una crisis.
Recursos
Libros
- Ledoit, O. y Wolf, M. (2004): A well-conditioned estimator for large-dimensional covariance matrices. Journal of Multivariate Analysis 88(2), 365-411
- Ledoit, O. y Wolf, M. (2004): Honey, I Shrunk the Sample Covariance Matrix. The Journal of Portfolio Management 30(4), 110-119
- Laloux, L., Cizeau, P., Bouchaud, J.-P. y Potters, M. (1999): Noise Dressing of Financial Correlation Matrices. Physical Review Letters 83(7), 1467-1470
- López de Prado, M. (2020): Machine Learning for Asset Managers. Cambridge University Press (capítulo 2, denoising)