Lección 4.1.3 · 70 min
Black-Litterman: del equilibrio del mercado a las visiones
Black-Litterman sustituye la media muestral, la entrada más frágil de Markowitz, por los rendimientos que hacen óptima la cartera de mercado, y solo se separa de ellos donde el gestor tiene una opinión y en la medida de su confianza. Esta lección lo construye desde cero, lo contrasta con PyPortfolioOpt y lo prueba con seis visiones cuya verdad está publicada: tres correctas y tres equivocadas, una de ellas de un modelo de machine learning.
- 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
El problema que resuelve
La lección 4.1.1 dejó dos resultados. El primero, que la media es la entrada más frágil de Markowitz: con 20 años, su error típico ronda los 5 puntos en un índice de acciones. El segundo, que el optimizador amplifica ese error: busca las diferencias de rendimiento estimadas y apuesta por ellas. Basta ver qué hace con la media muestral de 20 años de los 38 activos del universo, sin restricciones:
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")
r = precios.pct_change()
r.iloc[0] = precios.iloc[0] / 100 - 1
exceso = r.sub(fac.set_index("fecha")["rf"], axis=0)
activos = list(exceso.columns)
info = pd.read_csv("universo4_activos.csv", index_col="activo")
verdad = pd.read_csv("universo4_verdad.csv", index_col="activo")
cov_v = pd.read_csv("universo4_cov_verdad.csv")
S = cov_v[cov_v["regimen"] == "total"].set_index("activo")
S = S.loc[activos, activos].to_numpy()
w_mkt = info.loc[activos, "peso_mercado"].to_numpy()
mu_v = verdad.loc[activos, "mu_verdadero"].to_numpy()
mu_m = exceso.mean().to_numpy() * 252
delta = 2.5
w_m = np.linalg.solve(delta * S, mu_m)
print(f"apalancamiento bruto: {np.abs(w_m).sum():.0f} veces")
print((100 * pd.Series(w_m, index=activos)).round(0)
.sort_values().iloc[[0, 1, -2, -1]])apalancamiento bruto: 138 veces
MONET -2077.0
RF_T710 -1991.0
RF_CIG 1214.0
RF_T37 2395.0
dtype: float64Para aislar el problema de la media, la lección usa la covarianza verdadera total del universo (la 4.1.2 trató la covarianza; el ejercicio 1 repite el cálculo con la muestral). Aun con la covarianza exacta, la cartera óptima para una aversión al riesgo de 2,5 tiene un apalancamiento bruto de 138 veces: vende un 2.077 % del monetario y un 1.991 % del bono de 7-10 años para comprar un 2.395 % del de 3-7 años, activos casi idénticos cuyas medias muestrales difieren en décimas. Ningún gestor lo haría. Black y Litterman (1992), en Goldman Sachs, propusieron otro punto de partida.
El equilibrio: optimización inversa
La condición de primer orden de un inversor de media-varianza con aversión al riesgo es . Si se conocen los pesos y no la media, se despeja al revés:
son los rendimientos esperados en exceso que harían óptima la cartera de mercado. Si todos los inversores fueran de media-varianza y tuvieran la misma información, la cartera de mercado sería la tangente (el CAPM) y sería la verdad. La aversión al riesgo se puede calibrar con el Sharpe del mercado: .
Fuente Mermaid
flowchart TB W[Pesos de mercado] --> PI["π = δΣw"] S[Covarianza Σ] --> PI V["Visiones P, Q"] --> BL[Black-Litterman] O["Confianza Ω"] --> BL PI --> BL BL --> M[Media posterior] M --> OP[Optimizador]
El universo publica los pesos de la cartera de mercado (peso_mercado en universo4_activos.csv: los 18 bloques
sin solapamiento, por capitalización a 2025) y su propio con :
pi = delta * S @ w_mkt
pub = verdad.loc[activos, "pi_equilibrio"].to_numpy()
print("igual que el publicado:", np.allclose(pi, pub, atol=1e-4))
mu_mkt, vol_mkt = w_mkt @ mu_v, np.sqrt(w_mkt @ S @ w_mkt)
print(f"mercado: exceso {mu_mkt:.2%}, vol {vol_mkt:.2%}, "
f"Sharpe {mu_mkt / vol_mkt:.2f}")
print(f"δ implícito con la verdad: {mu_mkt / vol_mkt**2:.2f}")
r_mkt = exceso.to_numpy() @ w_mkt
print(f"δ implícito con la muestra: "
f"{r_mkt.mean() / r_mkt.var():.2f}")
w_tan = np.linalg.solve(S, mu_v)
print("Sharpe de la tangente verdadera:",
(w_tan @ mu_v / np.sqrt(w_tan @ S @ w_tan)).round(2))igual que el publicado: True
mercado: exceso 5.77%, vol 12.95%, Sharpe 0.45
δ implícito con la verdad: 3.44
δ implícito con la muestra: 2.94
Sharpe de la tangente verdadera: 1.35El que implican los datos (2,94 con la muestra, 3,44 con la verdad) es algo mayor que el 2,5 con el que se publica en los datos. Cambiar escala todo por igual; lo que interesa es su forma. Y la forma tiene dos rasgos de este universo que conviene ver antes de seguir:
ventana = exceso.loc["2021":"2025"].mean().to_numpy() * 252
def ecm(x):
return np.sqrt(np.mean((x - mu_v) ** 2))
print(f"error cuadrático medio frente a la verdad: "
f"equilibrio {ecm(pi):.2%}, muestra 20 años {ecm(mu_m):.2%}, "
f"muestra 2021-2025 {ecm(ventana):.2%}")
sel = ["RV_EEUU", "RV_EMG", "RF_T20", "RF_CIG", "MP_ORO", "MP_ENE"]
tabla = pd.DataFrame({"equilibrio": pi, "verdad": mu_v,
"muestra_20": mu_m, "muestra_5": ventana},
index=activos).loc[sel]
print((100 * tabla).round(2))error cuadrático medio frente a la verdad: equilibrio 3.45%, muestra 20 años 1.71%, muestra 2021-2025 6.32%
equilibrio verdad muestra_20 muestra_5
RV_EEUU 6.46 9.62 8.79 10.51
RV_EMG 7.96 3.80 5.46 7.22
RF_T20 -0.52 3.68 1.54 -11.60
RF_CIG 0.41 2.76 2.13 -2.06
MP_ORO 0.77 9.98 10.03 13.72
MP_ENE 3.32 -4.62 -5.46 10.38El equilibrio da primas casi nulas o negativas a los bonos del Tesoro (−0,52 % al de más de 20 años, frente a un 3,68 % verdadero) porque en la calma, el régimen dominante, los bonos tienen correlación negativa con las acciones: en una cartera de mercado dominada por acciones, cubren, y lo que cubre «merece» una prima baja. Y da primas positivas a las materias primas (3,32 % a la energía), que en el guion de estos 20 años pierden (−4,62 %). El oro es el caso extremo: equilibrio 0,77 %, verdad 9,98 %. En este universo la cartera de mercado no es eficiente: su Sharpe verdadero es 0,45, y el de la cartera tangente verdadera, sin restricciones, 1,35.
En error cuadrático medio, el equilibrio (3,45 puntos) queda peor que la media muestral de 20 años (1,71 puntos) y mejor que la de cinco años (6,32). La muestra de 20 años es inusualmente buena: las semillas del generador se eligieron para que las medias realizadas quedaran cerca de las verdaderas. Lo normal, y lo que tiene un gestor que mira cinco años, se parece más a la última columna. La ventaja del equilibrio no es acertar más que una buena muestra, sino no producir carteras absurdas: un coherente con la cartera de mercado lleva a la cartera de mercado.
Visiones: P, Q y Ω
Una visión es una afirmación sobre el rendimiento esperado de una cartera. Las absolutas hablan de un activo («emergentes rinde un 5 % anual en exceso»); las relativas, de una diferencia («momentum bate a valor en un 3 %»). Con visiones se escriben como
con una matriz (una fila por visión, con los pesos de su cartera), los rendimientos que afirman y
su incertidumbre, normalmente diagonal. El fichero universo4_visiones.csv trae seis, con la respuesta
conocida: verdad es con la media verdadera y correcta marca las que están a menos de un punto de ella.
vis = pd.read_csv("universo4_visiones.csv")
P = np.zeros((len(vis), len(activos)))
for i, fila in enumerate(vis["activos"]):
for parte in fila.split(";"):
activo, peso = parte.split(":")
P[i, activos.index(activo)] = float(peso)
Q = vis["q"].to_numpy()
conf = vis["confianza"].to_numpy()
print(vis[["vision", "q", "confianza", "pi_equilibrio", "verdad",
"correcta"]].round(4).to_string(index=False))
print("P·π y P·μ coinciden con el fichero:",
np.allclose(P @ pi, vis["pi_equilibrio"], atol=1e-4),
np.allclose(P @ mu_v, vis["verdad"], atol=1e-4))vision q confianza pi_equilibrio verdad correcta
V1 0.05 0.50 0.0796 0.0380 False
V2 0.02 0.40 -0.0055 -0.0330 False
V3 0.03 0.60 0.0027 0.0328 True
V4 0.02 0.30 0.0206 0.0220 True
V5 -0.01 0.50 -0.0200 -0.0173 True
V6 0.08 0.25 0.0077 0.0998 False
P·π y P·μ coinciden con el fichero: True TrueLas visiones V3, V4 y V5 son correctas; V1, V2 y V6 no. La V6 viene «de un modelo de ML» y afirma que el oro rinde un 8 % anual en exceso: es incorrecta por el criterio del punto (la verdad es 9,98 %), pero está mucho más cerca de la verdad que el equilibrio. Esa diferencia va a importar.
La posterior de Black-Litterman
Black-Litterman trata como un prior sobre la media verdadera, , con un escalar pequeño (la incertidumbre de la media es mucho menor que la de los rendimientos), y lo combina con las visiones como en una regresión bayesiana. La media posterior es
El último factor es la «sorpresa» de cada visión frente al equilibrio, . Se reparte entre todos los activos según sus covarianzas con las carteras de las visiones, así que una visión sobre emergentes mueve también la media de las bolsas y de la deuda emergente.
Falta . Idzorek propuso fijarla con una confianza entre 0 y 1, más intuitiva: con confianza del 100 % la visión se cumple exactamente; con el 0 %, se ignora; con , los pesos de la cartera recorren una fracción del camino entre la de mercado y la de confianza total. Su método original busca cada numéricamente; Walters, en su revisión del modelo, mostró que sin restricciones la solución es cerrada:
tau = 0.05
def omega_idzorek(P, conf):
return np.diag([tau * (1 - c) / c * p @ S @ p
for p, c in zip(P, conf)])
def black_litterman(P, Q, Om):
tS = tau * S
sorpresa = np.linalg.solve(P @ tS @ P.T + Om, Q - P @ pi)
return pi + tS @ P.T @ sorpresa
Om = omega_idzorek(P, conf)
mu_bl = black_litterman(P, Q, Om)
from pypfopt.black_litterman import BlackLittermanModel
blm = BlackLittermanModel(
pd.DataFrame(S, index=activos, columns=activos),
pi=pd.Series(pi, index=activos), P=P, Q=Q, tau=tau,
omega="idzorek", view_confidences=conf)
print("igual que PyPortfolioOpt:",
np.allclose(blm.bl_returns().to_numpy(), mu_bl, atol=1e-12))
post = pd.DataFrame({"q": Q, "equilibrio": P @ pi,
"posterior": P @ mu_bl, "verdad": P @ mu_v},
index=vis["vision"])
print((100 * post).round(2))igual que PyPortfolioOpt: True
q equilibrio posterior verdad
vision
V1 5.0 7.96 6.21 3.80
V2 2.0 -0.55 0.43 -3.30
V3 3.0 0.27 1.76 3.28
V4 2.0 2.06 1.46 2.20
V5 -1.0 -2.00 -1.36 -1.72
V6 8.0 0.77 2.46 9.98La figura de la cabecera dibuja esta tabla. La posterior de cada visión queda entre el equilibrio y lo que afirma la visión, más cerca de lo segundo cuanto mayor es la confianza y menor la varianza de su cartera. La V6, con confianza del 25 %, mueve el oro del 0,77 % al 2,46 %: menos de lo que dice el modelo y muy lejos de la verdad. La V4 se mueve del 2,06 % al 1,46 %, en dirección contraria a su propia : las visiones interactúan, porque el crédito de alto rendimiento está correlado con las acciones y otras visiones (V1, V2) mueven las acciones.
Que la forma cerrada reproduce el método original de Idzorek se comprueba buscando cada como él: la que deja los pesos sin restricciones a una fracción del camino.
from scipy.optimize import minimize_scalar
def pesos(m):
return np.linalg.solve(delta * S, m)
for k in range(len(Q)):
p, q = P[[k]], Q[[k]]
w_100 = pesos(black_litterman(p, q, np.array([[1e-12]])))
objetivo = w_mkt + conf[k] * (w_100 - w_mkt)
def distancia(log_om):
m = black_litterman(p, q, np.array([[np.exp(log_om)]]))
return np.sum((pesos(m) - objetivo) ** 2)
res = minimize_scalar(distancia, bounds=(-20, 5),
method="bounded",
options={"xatol": 1e-10})
print(vis["vision"][k], f"{np.exp(res.x) / Om[k, k]:.6f}")V1 1.000000
V2 1.000000
V3 1.000000
V4 1.000000
V5 1.000000
V6 1.000000El cociente entre el numérico y el cerrado es 1 en las seis visiones. Con restricciones (solo largos, por ejemplo) ya no hay forma cerrada y el método numérico es el único correcto.
¿Ayuda cada visión?
La pregunta práctica no es si una visión es correcta, sino si la cartera mejora con ella. Para cada visión sola, se calcula la media posterior, la cartera de máximo Sharpe de solo largos (la misma función que en la lección 4.1.1) y su Sharpe con la media y la covarianza verdaderas:
import cvxpy as cp
def max_sharpe(m):
y = cp.Variable(len(m))
pr = cp.Problem(cp.Minimize(cp.quad_form(y, cp.psd_wrap(S))),
[m @ y == 1, y >= 0])
pr.solve(solver="CLARABEL")
return y.value / y.value.sum()
def sharpe(w):
return w @ mu_v / np.sqrt(w @ S @ w)
filas = []
for k in range(len(Q)):
m = black_litterman(P[[k]], Q[[k]], Om[[k]][:, [k]])
filas.append({
"vision": vis["vision"][k],
"correcta": vis["correcta"][k],
"error_equilibrio": abs(P[k] @ pi - P[k] @ mu_v),
"error_q": abs(Q[k] - P[k] @ mu_v),
"error_posterior": abs(P[k] @ m - P[k] @ mu_v),
"sharpe": sharpe(max_sharpe(m))})
cada = pd.DataFrame(filas).set_index("vision")
print(cada.round(3))
print("Sharpe de la cartera de mercado:", sharpe(w_mkt).round(3)) correcta error_equilibrio error_q error_posterior sharpe
vision
V1 False 0.042 0.012 0.027 0.492
V2 False 0.028 0.053 0.038 0.357
V3 True 0.030 0.003 0.014 0.583
V4 True 0.001 0.002 0.002 0.446
V5 True 0.003 0.007 0.002 0.473
V6 False 0.092 0.020 0.074 0.627
Sharpe de la cartera de mercado: 0.446La tabla separa dos ideas que se suelen confundir:
- V1 y V6, «equivocadas», mejoran la cartera. Sus no aciertan por un punto, pero el equilibrio estaba mucho más lejos: 4,2 puntos en emergentes y 9,2 en el oro. La visión del oro, con solo un 25 % de confianza, sube el Sharpe de 0,45 a 0,63.
- V4 y V5, «correctas», casi no cambian nada. El equilibrio ya estaba a 0,1 y 0,3 puntos de la verdad; la visión no añade información y el Sharpe queda en 0,45 y 0,47.
- V2 es la única dañina. Afirma que Europa cubierta bate a EE. UU. en un 2 %, cuando la verdad es −3,30 % y el equilibrio decía −0,55 %: aleja la media de la verdad y el Sharpe cae a 0,36, por debajo del mercado.
- V3, correcta y lejos del equilibrio, es el caso de manual: 3,0 puntos de error en el equilibrio, 0,3 en la visión, y el Sharpe sube a 0,58.
Una visión vale lo que su distancia a la verdad tiene de menos que la del equilibrio. Por eso no basta con contar aciertos de un analista o de un modelo: hay que medir si acierta más que el mercado.
La cartera de Black-Litterman
He y Litterman (1999) mostraron una propiedad que hace al modelo fácil de explicar a un comité: sin restricciones, la cartera óptima con la media posterior es la cartera de mercado más una posición en la cartera de cada visión. Los activos que no aparecen en ninguna visión conservan su peso de mercado, aunque su media haya cambiado:
w_bl = pesos(mu_bl)
dif = pd.Series(w_bl - w_mkt, index=activos)
en_vistas = np.abs(P).sum(axis=0) > 0
print("activos con cambio > 0,01 %:", list(dif[dif.abs() > 1e-4].index))
cambian = set(dif[dif.abs() > 1e-4].index)
print("todos en alguna visión:",
cambian <= set(np.array(activos)[en_vistas]))
print((100 * dif[dif.abs() > 1e-4]).round(1))activos con cambio > 0,01 %: ['RV_EEUU', 'RV_EUR_C', 'RV_EMG', 'ES_VAL', 'ES_MOM', 'ES_MIN', 'RF_CIG', 'RF_CHY', 'MP_ORO']
todos en alguna visión: True
RV_EEUU -96.4
RV_EUR_C 71.8
RV_EMG -6.9
ES_VAL -55.7
ES_MOM 55.7
ES_MIN 24.7
RF_CIG -10.3
RF_CHY 10.3
MP_ORO 34.7
dtype: float64La cartera sin restricciones lleva posiciones cortas en EE. UU. (su peso baja 96 puntos, por V2 y V5), en valor (V3) y en grado de inversión (V4): son las carteras de las visiones relativas, que son largas-cortas por definición. Un fondo de solo largos aplica el optimizador con la media posterior y sus restricciones. La comparación final, con el Sharpe evaluado con la verdad:
grupos = {"mercado": None, "solo correctas": [2, 3, 4],
"solo incorrectas": [0, 1, 5], "las seis": list(range(6))}
res = {}
for nombre, idx in grupos.items():
if idx is None:
res[nombre] = sharpe(w_mkt)
continue
m = black_litterman(P[idx], Q[idx], Om[np.ix_(idx, idx)])
res[nombre] = sharpe(max_sharpe(m))
res["media 2021-2025"] = sharpe(max_sharpe(ventana))
res["media 20 años"] = sharpe(max_sharpe(mu_m))
print(pd.Series(res).round(3))mercado 0.446
solo correctas 0.595
solo incorrectas 0.597
las seis 0.700
media 2021-2025 0.668
media 20 años 0.901
dtype: float64Con las seis visiones, el Sharpe verdadero pasa de 0,45 (mercado) a 0,70. Las tres correctas solas dan 0,60; las tres incorrectas, otro 0,60, por V1 y V6. La media de los últimos cinco años da 0,67 con una cartera elegida por el azar de la ventana (lección 4.1.1), y la de 20 años, 0,90: con datos tan buenos como los de este generador, la media muestral con restricciones de solo largos gana. Black-Litterman no promete la mejor cartera; promete partir de una razonable y separarse de ella solo donde se sabe algo.
Visiones de modelos de machine learning
Un modelo de ML (los del nivel 5 del curso) produce predicciones, y una predicción se puede convertir en visión: es la predicción y , la incertidumbre de esa predicción. El error típico es tomar la confianza del ajuste dentro de la muestra de entrenamiento. Lo correcto es medirla fuera de muestra y frente al prior: si el modelo no predice mejor que el equilibrio, su visión no debe mover nada.
Fuente Mermaid
flowchart TB D[Datos hasta t] --> M[Modelo] M --> Q["Predicción Q"] M --> E[Errores fuera] E --> O["Ω"] Q --> BL[Black-Litterman] O --> BL
Un ejemplo sencillo con el oro: un «modelo» que predice el exceso del año siguiente con la media de los tres anteriores, evaluado año a año de 2009 a 2025 (cada predicción usa solo datos pasados). Su error cuadrático se compara con el del equilibrio, y lo que el modelo gana sobre el ruido del propio rendimiento anual es lo que justifica :
oro = exceso["MP_ORO"]
anual = (1 + oro).groupby(oro.index.year).prod() - 1
pred = {a: oro.loc[str(a - 3):str(a - 1)].mean() * 252
for a in range(2009, 2026)}
pred = pd.Series(pred)
real = anual.loc[pred.index]
i_oro = activos.index("MP_ORO")
ecm_mod = np.mean((pred - real) ** 2)
ecm_eq = np.mean((pi[i_oro] - real) ** 2)
print(f"error cuadrático: modelo {np.sqrt(ecm_mod):.1%}, "
f"equilibrio {np.sqrt(ecm_eq):.1%}, "
f"R² fuera de muestra {1 - ecm_mod / ecm_eq:.3f}")
ruido = S[i_oro, i_oro] # varianza de un año de oro
omega_ml = max(ecm_mod - ruido, 1e-6) # error del modelo en la media
p6 = P[5]
c_ml = 1 / (1 + omega_ml / (tau * p6 @ S @ p6))
print(f"ω del modelo {omega_ml:.4f} frente al publicado {Om[5, 5]:.4f}")
print(f"confianza implícita: {c_ml:.1%} (publicada {conf[5]:.0%})")error cuadrático: modelo 20.8%, equilibrio 21.5%, R² fuera de muestra 0.068
ω del modelo 0.0218 frente al publicado 0.0032
confianza implícita: 4.7% (publicada 25%)El modelo bate al equilibrio por muy poco (un R² fuera de muestra de 0,07, con 17 años, indistinguible de cero). Su error cuadrático es el doble de la varianza de un año de oro: la mitad del error es ruido del propio rendimiento anual, que ningún modelo puede quitar, y la otra mitad, , es error del modelo sobre la media, casi siete veces la publicada. La confianza que eso justifica es de alrededor del 5 %, no del 25 %. En este universo, la visión V6 está cerca de la verdad, pero un gestor no podría saberlo con este historial: su confianza tiene que salir de los datos y no del resultado que se espera.
Cuatro precauciones con visiones de ML:
- Fuera de muestra de verdad: predicciones hechas con datos anteriores a cada fecha, con la purga de la lección 2.5.3 si las etiquetas se solapan.
- Contra el prior, no contra cero: un modelo que predice «las acciones suben» acierta más que cero, pero no más que el equilibrio.
- No contar dos veces: si el modelo usa los mismos datos que estiman o la media, la visión repite información que el prior ya tiene.
- Horizonte coherente: y en exceso anual, como ; una predicción a un mes se anualiza, y su error también.
Errores típicos
- Mezclar rendimientos totales y en exceso entre (en exceso) y .
- Visiones relativas mal escritas: la fila de de «A bate a B» es en A y en B; con varios activos por lado, los pesos de cada lado suman 1 y −1.
- Confundir τ con la confianza: con la forma cerrada de Idzorek, τ no cambia la media posterior (ejercicio 3); sí cambia la covarianza posterior.
- Pesos de mercado que no son de mercado: hereda lo que se ponga en . Con un universo que incluye sectores y estilos que se solapan con el índice, hay que usar solo los bloques sin solapamiento, como hace el fichero.
- Creer que la posterior es la verdad: es un compromiso entre dos fuentes con error, y en este universo el equilibrio tiene errores grandes en bonos y materias primas.
Ejercicios
- Repite el equilibrio y la posterior con la covarianza muestral de los 20 años en lugar de la verdadera. ¿Cuánto cambian y la posterior de cada visión?
Solución
S_m = exceso.cov().to_numpy() * 252
pi_m = delta * S_m @ w_mkt
print(f"diferencia máxima de π: {np.abs(pi_m - pi).max():.2%}")
tS_m = tau * S_m
Om_m = np.diag([tau * (1 - c) / c * p @ S_m @ p
for p, c in zip(P, conf)])
mu_bl_m = pi_m + tS_m @ P.T @ np.linalg.solve(
P @ tS_m @ P.T + Om_m, Q - P @ pi_m)
print((100 * pd.DataFrame({"verdadera": P @ mu_bl,
"muestral": P @ mu_bl_m},
index=vis["vision"])).round(2))diferencia máxima de π: 0.33%
verdadera muestral
vision
V1 6.21 6.44
V2 0.43 0.43
V3 1.76 1.83
V4 1.46 1.43
V5 -1.36 -1.37
V6 2.46 2.45Con 20 años, la covarianza muestral está muy cerca de la verdadera (lección 4.1.2), y cambia como mucho un tercio de punto. Las posteriores se mueven unas décimas. Black-Litterman es poco sensible al error de la covarianza y muy sensible a las visiones: el riesgo del modelo está en y en la confianza, no en .
- Quita la visión V2 de las seis. ¿Cuánto mejora el Sharpe verdadero de la cartera de solo largos? ¿Podría un gestor haber sabido de antemano que era la visión que había que quitar?
Solución
idx = [0, 2, 3, 4, 5]
m = black_litterman(P[idx], Q[idx], Om[np.ix_(idx, idx)])
print("sin V2:", sharpe(max_sharpe(m)).round(3),
"| las seis:", res["las seis"].round(3))sin V2: 0.798 | las seis: 0.7Sin la V2, el Sharpe sube. Pero solo se sabe porque la verdad está publicada. Con datos reales, la V2 era tan defendible como las demás. Lo que sí se puede hacer es llevar un registro de las visiones y de su resultado fuera de muestra frente al equilibrio, y ajustar la confianza de cada fuente (analista o modelo) con ese historial.
- Comprueba que, con la Ω de Idzorek en forma cerrada, la media posterior no cambia si τ pasa de 0,05 a 0,5. Explica por qué.
Solución
def bl_tau(t):
Om_t = np.diag([t * (1 - c) / c * p @ S @ p
for p, c in zip(P, conf)])
tS = t * S
return pi + tS @ P.T @ np.linalg.solve(P @ tS @ P.T + Om_t,
Q - P @ pi)
print("misma media:", np.allclose(bl_tau(0.05), bl_tau(0.5)))misma media: TrueEn la fórmula, la media posterior depende de (el peso del prior) frente a (el de las visiones). Si es proporcional a , como en la forma cerrada de Idzorek, los dos crecen juntos y se cancela. Importa cuando se fija por fuera, como en la sección de ML, donde sale del error del modelo: ahí un mayor da más peso a la visión.
Respuesta plegada en cada pregunta
Autoevaluación
¿Qué rendimientos esperados devuelve la optimización inversa de Black-Litterman?
- ALas medias históricas de cada activo
- BLos que harían óptima la cartera de mercado para un inversor de media-varianza con aversión al riesgo δ: π = δΣw_mercado
- CLos del CAPM con la beta de cada activo frente al S&P 500
- DRendimientos nulos para todos los activos
Ver respuesta
Respuesta B. Los que harían óptima la cartera de mercado para un inversor de media-varianza con aversión al riesgo δ: π = δΣw_mercado
Se parte de la condición de primer orden de Markowitz (los pesos óptimos son la inversa de δΣ por μ) y se despeja μ con los pesos de mercado. Si el mercado fuera eficiente en media-varianza, π serían los rendimientos esperados verdaderos; es un supuesto, no un hecho.
La visión V4 (alto rendimiento bate a grado de inversión en un 2 %) es correcta según el criterio de los datos y la V1 (emergentes rinde un 5 %) no. Sin embargo, sola, la V1 mejora la cartera y la V4 no. ¿Por qué?
- APor azar
- BPorque la V1 tiene más confianza
- CPorque las visiones absolutas pesan más que las relativas
- DPorque lo que importa es si la visión está más cerca de la verdad que el equilibrio: en V4 el equilibrio ya decía 2,06 % (verdad 2,21 %), y en V1 decía 7,96 % frente a una verdad de 3,81 %, así que un 5 % acerca
Ver respuesta
Respuesta D. Porque lo que importa es si la visión está más cerca de la verdad que el equilibrio: en V4 el equilibrio ya decía 2,06 % (verdad 2,21 %), y en V1 decía 7,96 % frente a una verdad de 3,81 %, así que un 5 % acerca
Black-Litterman mueve la posterior desde π hacia Q. Si π ya estaba bien, una visión correcta no aporta; si π estaba lejos, una visión imperfecta en la dirección buena ayuda. La V2 es la única de las seis que empeora: afirma un +2 % donde la verdad es −3,30 % y el equilibrio, −0,55 %.
Con el método de Idzorek en su forma cerrada, Ω de una visión es τ·(1 − c)/c·pΣp′. ¿Qué ocurre con la media posterior si se cambia τ de 0,05 a 0,5?
- ANada: τ aparece en el peso del prior y en Ω, y se cancela
- BSe acerca a las visiones
- CSe acerca al equilibrio
- DSe vuelve inestable
Ver respuesta
Respuesta A. Nada: τ aparece en el peso del prior y en Ω, y se cancela
La posterior depende del cociente entre la incertidumbre del prior (τΣ) y la de las visiones (Ω). Si Ω se define proporcional a τ, el cociente no cambia. τ sí importa si Ω se fija por otro camino, por ejemplo con el error de un modelo.
Una visión de un modelo de ML predice el rendimiento del oro. ¿De dónde debe salir su Ω?
- ADel R² del modelo en la muestra de entrenamiento
- BDe la confianza que le tenga el equipo que lo desarrolló
- CDel error de sus predicciones fuera de muestra, comparado con el del equilibrio: si no lo bate, la visión no debe mover la cartera
- DDe la volatilidad del oro
Ver respuesta
Respuesta C. Del error de sus predicciones fuera de muestra, comparado con el del equilibrio: si no lo bate, la visión no debe mover la cartera
El ajuste en la muestra de entrenamiento siempre es optimista. En la lección, un modelo sencillo que bate al equilibrio por muy poco fuera de muestra justifica una confianza de alrededor del 5 %, muy por debajo del 25 % publicado.
La cartera de Black-Litterman sin restricciones difiere de la de mercado...
- AEn todos los activos, porque cambia la media de todos
- BSolo en los activos que aparecen en alguna visión: es la cartera de mercado más una posición en cada cartera de visión
- CSolo en los activos con más peso de mercado
- DEn nada, si las visiones son correctas
Ver respuesta
Respuesta B. Solo en los activos que aparecen en alguna visión: es la cartera de mercado más una posición en cada cartera de visión
Con los pesos óptimos sin restricciones (la inversa de δΣ por μ_BL), la media posterior cambia en todos los activos correlacionados, pero la inversa de Σ deshace esa propagación y los pesos solo se mueven en las filas de P (He y Litterman, 1999). Es una de las propiedades que hacen a Black-Litterman intuitivo.
Ideas clave
- El equilibrio π = δΣw_mercado da unos rendimientos esperados coherentes con la cartera de mercado y estables. Es un supuesto: en el universo del curso, la cartera de mercado no es eficiente y π se equivoca con los bonos y las materias primas.
- Black-Litterman combina el prior N(π, τΣ) con visiones P·μ = Q + ruido de varianza Ω. La implementación propia coincide con PyPortfolioOpt, y la confianza de Idzorek en forma cerrada coincide con su método original.
- Lo que decide si una visión ayuda no es si es correcta, sino si está más cerca de la verdad que el equilibrio: V1 y V6, «equivocadas», mejoran la cartera; V4 y V5, «correctas», casi no cambian nada; solo V2 empeora.
- Con las seis visiones, la cartera de solo largos pasa de un Sharpe verdadero de 0,45 (mercado) a 0,70, con pesos razonables. Con la media muestral sin restricciones, la cartera necesitaría un apalancamiento bruto de 138 veces.
- Una visión de ML se trata como cualquier otra: Q de su predicción y Ω de su error fuera de muestra frente al equilibrio. Un modelo que apenas lo bate merece una confianza cercana a cero.