Lección 4.1.6 · 75 min
Modelos factoriales: CAPM, Fama-French, PCA y atribución
Un modelo factorial separa lo que una cartera gana por exponerse a primas conocidas de lo que gana por sí misma. Esta lección regresa 20 fondos sintéticos con alfas y cargas publicadas sobre el CAPM y sobre seis factores con el formato de la biblioteca de Kenneth French, y comprueba qué se recupera, qué falla (el falso positivo, el market timing, la venta de puts, el cambio de estilo) y qué ven los componentes principales de un universo cuyos factores verdaderos se conocen.
- Módulo
- 4.1 · Teoría y optimización de carteras
- Duración
- 75 min
- Requisitos
- 2.1.1 · Contrastes de hipótesis, p-valores, bootstrap y regresión con errores robustos, 2.1.8 · El problema de las pruebas múltiples: Bonferroni, FDR y el «factor zoo», 2.3.5 · Factores (value, calidad, baja volatilidad, tamaño) y estrategias de eventos, 2.6.2 · Win rate, payoff, expectativa, alpha, beta, information ratio, turnover y capacidad
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
Del CAPM a seis factores
El CAPM (Sharpe, 1964) dice que, en equilibrio, el exceso de rendimiento esperado de cualquier activo es su beta por la prima del mercado. Aplicado a una cartera en serie temporal, la regresión
separa lo que la cartera gana por exponerse al mercado () de lo que gana por sí misma (). La lección 2.6.2 la usó para medir una estrategia frente a un índice. El problema es que el mercado no es la única prima por la que se cobra. Fama y French (1993) añadieron dos factores construidos como carteras largas y cortas de acciones: el tamaño (SMB, pequeñas menos grandes) y el valor (HML, cotización baja frente a su valor contable menos alta). Carhart (1997) añadió el momentum (los ganadores de los últimos doce meses, sin el último, menos los perdedores) y Fama y French (2015), la rentabilidad (RMW, empresas rentables menos poco rentables) y la inversión (CMA, empresas que invierten poco menos las que invierten mucho). Con los seis:
Fuente Mermaid
flowchart TB C[CAPM: mercado] --> F3[FF3: + SMB, HML] F3 --> C4[Carhart: + MOM] F3 --> F5[FF5: + RMW, CMA] C4 --> F6[Seis factores] F5 --> F6
Un alfa en un modelo es un rendimiento que ese modelo no explica. Cada factor que se añade convierte en exposición algo que antes era alfa. Si las primas de los factores se pueden comprar barato (fondos indexados de estilos, futuros), un gestor que solo cobra primas de factores no aporta nada que no se pueda replicar. Cómo se construyen los factores con datos de acciones (ordenamientos por quintiles, Fama-MacBeth, IC) es la lección 2.3.5; aquí se usan ya construidos, para evaluar carteras.
Los datos son sintéticos y tienen la respuesta conocida. ff4_factores_mensuales.csv trae seis factores
mensuales de 2006 a 2025 con el formato de la biblioteca de Kenneth French (en tanto por uno), coherentes con el
universo de la lección 4.1.4. ff4_fondos_mensuales.csv trae 20 fondos ficticios generados con alfas y cargas
publicadas en ff4_fondos_verdad.csv, entre ellos un fondo con alfa real, uno con alfa pequeño, varios sin alfa, dos
con alfa negativo por comisiones, dos no lineales y uno que cambia de estilo. Ningún resultado de esta lección dice
nada de fondos reales.
import numpy as np
import pandas as pd
import statsmodels.api as sm
fac = pd.read_csv("ff4_factores_mensuales.csv", index_col="mes")
fondos = pd.read_csv("ff4_fondos_mensuales.csv").pivot(
index="mes", columns="fondo", values="rendimiento")
verdad = pd.read_csv("ff4_fondos_verdad.csv", index_col="fondo")
exc = fondos.sub(fac["rf"], axis=0) # exceso del tipo libre
F6 = ["mkt_rf", "smb", "hml", "rmw", "cma", "mom"]
print(fac.shape, exc.shape, fac.index[0], "a", fac.index[-1])
resumen = pd.DataFrame({
"media_%": 1200 * fac[F6].mean(),
"vol_%": 100 * np.sqrt(12) * fac[F6].std(),
"peor_mes_%": 100 * fac[F6].min(),
})
resumen["sharpe"] = resumen["media_%"] / resumen["vol_%"]
resumen.round(2)(240, 7) (240, 20) 2006-01 a 2025-12
media_% vol_% peor_mes_% sharpe
mkt_rf 9.27 22.22 -28.94 0.42
smb 0.18 8.01 -8.75 0.02
hml -2.45 8.35 -7.77 -0.29
rmw 3.52 7.67 -6.15 0.46
cma 1.69 6.94 -7.41 0.24
mom 4.05 17.64 -33.13 0.23El mercado sintético tiene una prima media del 9,27 % anual con un 22,2 % de volatilidad y un peor mes del −28,9 %. Valor (HML) tiene prima negativa en la muestra, rentabilidad (RMW) la más alta en Sharpe (0,46) y momentum un peor mes del −33,1 %, el crash de 2009 que imitan los datos.
La fuente primaria: la biblioteca de Kenneth French
Los factores reales se publican gratis en la biblioteca de datos de Kenneth French (Dartmouth), que es la fuente
primaria: los artículos de Fama y French usan esas series y casi todo el sector las toma como referencia. Cada
fichero es un ZIP con un CSV de formato propio. Consultada el 22 de octubre de 2026,
F-F_Research_Data_5_Factors_2x3_CSV.zip tiene tres líneas de texto («This file was created using the 202608 CRSP
database», de dónde sale el tipo libre: Ibbotson hasta mayo de 2024 y el índice ICE BofA de letras a un mes desde
junio), una línea en blanco, la cabecera ,Mkt-RF,SMB,HML,RMW,CMA,RF, los meses en formato AAAAMM con los valores
en porcentaje desde julio de 1963 hasta agosto de 2026, otra línea en blanco y una segunda tabla, Annual Factors: January-December, con los años. El momentum va en otro fichero, F-F_Momentum_Factor_CSV.zip, con una cabecera de
texto más larga que avisa de que los datos que faltan se marcan con −99,99 o −999. Las líneas terminan en CRLF.
Un lector robusto no cuenta líneas de cabecera, porque cambian de un fichero a otro: busca la primera línea que empieza por coma y lee hasta la primera que no empieza por un año. La celda lo prueba con un texto que copia esa disposición con los valores sintéticos del curso:
import io
# Un fichero con la misma disposición que F-F_Research_Data_5_Factors
# _2x3.csv: cabecera de texto, meses AAAAMM, valores en %, y después
# los factores anuales. Los valores son los sintéticos del curso.
cab = ",Mkt-RF,SMB,HML,RMW,CMA,RF"
filas = [f"{m.replace('-', '')},"
+ ",".join(f"{100 * fac.loc[m, c]:8.2f}"
for c in ["mkt_rf", "smb", "hml", "rmw", "cma",
"rf"])
for m in fac.index[:3]]
texto = "\r\n".join(
["Fichero de ejemplo (formato de la biblioteca de K. French).",
"", cab, *filas, "", " Annual Factors: January-December ",
cab, " 2006, 10.00, 1.00, 2.00, 3.00, 4.00,"
" 5.00"])
def leer_french(texto):
"""Primera tabla (mensual) de un CSV de la biblioteca de French."""
lineas = texto.splitlines()
ini = next(i for i, l in enumerate(lineas) if l.startswith(","))
fin = next(i for i in range(ini + 1, len(lineas))
if not lineas[i].strip()[:1].isdigit())
df = pd.read_csv(io.StringIO("\n".join(lineas[ini:fin])),
index_col=0)
df.index = pd.PeriodIndex(df.index.astype(str), freq="M")
df.columns = (df.columns.str.strip().str.lower()
.str.replace("-", "_"))
df = df.replace([-99.99, -999], np.nan) # faltantes
return df / 100 # de % a tanto por uno
leer_french(texto) mkt_rf smb hml rmw cma rf
2006-01 0.0080 -0.0235 0.0296 0.0151 0.0107 0.0034
2006-02 -0.0064 -0.0207 0.0091 0.0099 0.0265 0.0033
2006-03 0.0236 -0.0360 -0.0225 0.0560 0.0189 0.0041Con red, el mismo lector sirve para los ficheros reales. El código descarga el ZIP con urllib (de la biblioteca
estándar), lee el CSV de dentro y une los cinco factores con el momentum por mes; no se ejecuta en el curso:
import io
import urllib.request
import zipfile
BASE = ("https://mba.tuck.dartmouth.edu/pages/faculty/ken.french/"
"ftp/")
def descargar_french(nombre):
"""Descarga un ZIP de la biblioteca y devuelve el CSV como texto."""
with urllib.request.urlopen(BASE + nombre, timeout=30) as r:
z = zipfile.ZipFile(io.BytesIO(r.read()))
with z.open(z.namelist()[0]) as f:
return f.read().decode("latin-1")
ff5 = leer_french(descargar_french(
"F-F_Research_Data_5_Factors_2x3_CSV.zip"))
mom = leer_french(descargar_french("F-F_Momentum_Factor_CSV.zip"))
ff6 = ff5.join(mom, how="inner")Veinte fondos con la respuesta conocida
La regresión es la misma para todos los fondos: mínimos cuadrados con constante, en datos mensuales, y errores de Newey-West con tres retardos (lección 2.1.1), que corrigen la heterocedasticidad y algo de autocorrelación de los residuos. El alfa mensual se multiplica por 12 para anualizarlo.
def regresion(y, X, hac=True):
"""MCO con constante; errores de Newey-West (3 meses) si hac."""
m = sm.OLS(y, sm.add_constant(X))
if hac:
return m.fit(cov_type="HAC", cov_kwds={"maxlags": 3})
return m.fit()
filas = {}
for f in exc:
capm = regresion(exc[f], fac[["mkt_rf"]])
ff = regresion(exc[f], fac[F6])
filas[f] = {
"alfa_verdad_%": 100 * verdad.loc[f, "alfa_anual"],
"alfa_capm_%": 1200 * capm.params.iloc[0],
"beta_capm": capm.params.iloc[1],
"alfa_ff6_%": 1200 * ff.params.iloc[0],
"t_ff6": ff.tvalues.iloc[0],
"r2_ff6": ff.rsquared,
}
tab = pd.DataFrame(filas).T
tab.round(2) alfa_verdad_% alfa_capm_% beta_capm alfa_ff6_% t_ff6 r2_ff6
F01 -0.1 0.04 1.00 0.05 0.55 1.00
F02 -1.0 -0.24 1.02 -0.24 -0.56 0.99
F03 0.0 -1.60 1.01 -0.72 -0.74 0.97
F04 0.0 -1.18 1.07 -1.05 -1.02 0.97
F05 0.0 3.31 0.85 0.09 0.12 0.97
F06 0.0 1.72 0.95 -1.67 -1.52 0.97
F07 0.0 -0.65 1.28 0.64 0.46 0.96
F08 3.0 3.52 0.98 3.29 2.86 0.96
F09 1.5 3.04 1.00 2.90 2.57 0.95
F10 0.0 1.45 0.73 -0.09 -0.13 0.98
F11 2.0 3.21 0.03 2.24 2.05 0.38
F12 0.0 2.88 0.67 3.13 4.15 0.96
F13 6.0 -1.19 0.54 -1.21 -1.82 0.96
F14 0.0 -3.70 1.01 -2.79 -1.98 0.94
F15 1.0 2.24 0.57 0.25 0.32 0.94
F16 -2.5 -1.99 1.00 -2.18 -3.15 0.99
F17 0.0 0.42 0.78 -0.39 -0.55 0.97
F18 0.0 -1.17 1.08 2.06 2.55 0.98
F19 -1.5 -0.09 2.00 -0.27 -0.53 1.00
F20 0.0 0.99 0.99 1.12 0.76 0.94La figura de la cabecera pone el alfa verdadero (cuadrado hueco) junto al medido con seis factores (cuadrado lleno) y su intervalo del 95 %. En la mayoría de fondos el intervalo contiene la verdad. Cuatro lecturas de la tabla:
- El CAPM se equivoca con los fondos de estilo. F05 (calidad) mide un 3,31 % de alfa del CAPM y un 0,09 % con seis factores; F06 (momentum), un 1,72 % y un −1,67 %; F03 (valor profundo), un −1,60 % y un −0,72 %. El alfa del CAPM de un fondo de estilo es, sobre todo, la prima de su estilo en la muestra.
- El R² separa fondos activos de indexados. F01 (indexado amplio) y F19 (apalancado 2x) tienen R² de 1,00; F11, un largo-corto neutral al mercado, 0,38: casi todo su riesgo es propio.
- El alfa medido tiene un error grande. Con 20 años, el error típico del alfa anual es la volatilidad propia dividida por √20: para un fondo con un 5 % de volatilidad propia, 1,1 puntos. F09, con un 1,5 % verdadero, mide un 2,90 %; F19, con un −1,5 % verdadero, mide un −0,27 %.
- Tres fondos salen mal por razones distintas. F12 y F13 son no lineales y F14 cambia de estilo; se ven más abajo.
Las cargas se recuperan mucho mejor que los alfas, porque los factores se mueven mucho más que su media:
cargas = pd.DataFrame(
{f: regresion(exc[f], fac[F6]).params[1:] for f in exc},
index=F6).T
err = cargas - verdad[[f"b_{c}" for c in F6]].values
lineales = [f for f in exc if f not in ("F12", "F13", "F14")]
print("error absoluto medio de las cargas (17 fondos lineales):")
err.loc[lineales].abs().mean().round(3)error absoluto medio de las cargas (17 fondos lineales):
mkt_rf 0.007
smb 0.023
hml 0.040
rmw 0.028
cma 0.042
mom 0.008
dtype: float64En los 17 fondos lineales, el error absoluto medio de la carga de mercado es de 0,007 y el de las demás, entre 0,008 y 0,042. Una regresión factorial es un buen instrumento para medir exposiciones y uno malo para medir alfas.
Atribución: de dónde sale el rendimiento
Con las cargas estimadas, el exceso medio de un fondo se descompone exactamente en alfa más carga por media de cada factor (la regresión con constante pasa por las medias):
F10 es el caso de libro: un 1,45 % de alfa del CAPM.
def atribucion(f, factores):
"""Rendimiento medio anual: alfa + carga × media de cada factor."""
m = regresion(exc[f], fac[factores], hac=False)
partes = 1200 * m.params[1:] * fac[factores].mean()
partes["alfa"] = 1200 * m.params.iloc[0]
partes["total"] = 1200 * exc[f].mean()
return partes
pd.DataFrame({"capm": atribucion("F10", ["mkt_rf"]),
"ff6": atribucion("F10", F6)}).round(2) capm ff6
alfa 1.45 -0.09
cma NaN 0.44
hml NaN -1.24
mkt_rf 6.79 7.51
mom NaN 0.34
rmw NaN 1.21
smb NaN 0.07
total 8.25 8.25El exceso medio del 8,25 % anual es el mismo en las dos columnas; cambia el reparto. Con el CAPM, 6,79 puntos son mercado y 1,45 alfa. Con seis factores, el mercado sube a 7,51 (la beta de mercado de F10 con los demás factores es mayor que su beta del CAPM), rentabilidad aporta 1,21, inversión 0,44, momentum 0,34, valor resta 1,24 y el alfa queda en −0,09. El fondo tenía exposiciones a primas que el CAPM no mide; no tenía alfa.
Muchos fondos, algunos con suerte
Probar el alfa de 20 fondos es hacer 20 contrastes. Con un nivel del 5 %, aunque ninguno tuviera alfa, se esperaría uno significativo por azar. La lección 2.1.8 explica las correcciones; aquí se aplican con la verdad a la vista:
from statsmodels.stats.multitest import multipletests
p = pd.Series({f: regresion(exc[f], fac[F6]).pvalues.iloc[0]
for f in exc})
holm = multipletests(p, alpha=0.05, method="holm")
bh = multipletests(p, alpha=0.05, method="fdr_bh")
sig = pd.DataFrame({"alfa_verdad_%": tab["alfa_verdad_%"],
"t": tab["t_ff6"], "p": p,
"holm": holm[0], "fdr_bh": bh[0]})
sig[sig["p"] < 0.05].round(4) alfa_verdad_% t p holm fdr_bh
F08 3.0 2.8593 0.0042 False True
F09 1.5 2.5705 0.0102 False True
F11 2.0 2.0466 0.0407 False False
F12 0.0 4.1459 0.0000 True True
F14 0.0 -1.9828 0.0474 False False
F16 -2.5 -3.1471 0.0016 True True
F18 0.0 2.5541 0.0106 False TrueSiete fondos tienen p < 0,05. Con la verdad conocida:
| Fondo | Alfa verdadero | p | ¿Qué es? | Holm | BH |
|---|---|---|---|---|---|
| F12 | 0 | 0,0000 | Market timing: alfa espurio de un modelo mal especificado | sí | sí |
| F16 | −2,5 % | 0,0016 | Comisión alta: alfa negativo real | sí | sí |
| F08 | 3 % | 0,0042 | Alfa real | no | sí |
| F09 | 1,5 % | 0,0102 | Alfa real pequeño, medido con suerte | no | sí |
| F18 | 0 | 0,0106 | Falso positivo | no | sí |
| F11 | 2 % | 0,0407 | Alfa real | no | no |
| F14 | 0 | 0,0474 | Cambio de estilo: alfa espurio | no | no |
Holm controla la probabilidad de cometer algún falso positivo entre los 20 y se queda con F12 y F16: elimina el falso positivo F18, pero también los alfas reales de F08, F09 y F11. Benjamini-Hochberg controla la proporción de falsos descubrimientos y acepta cinco: tres con alfa real o negativo real, el falso positivo F18 y el espurio F12. Ninguna corrección arregla F12: su t de 4,15 es real, pero mide un modelo mal planteado, no una habilidad.
F18 es un fondo «contrario» con una carga de −0,4 en momentum y cero alfa. En esta muestra su residuo tuvo una media positiva del orden de dos errores típicos. Con 20 fondos, que a uno le pase es lo esperable.
Fuente Mermaid
flowchart TB
A["Alfa con t > 2"] --> M{¿Muchos fondos?}
M -- sí --> C[Holm o BH]
M -- no --> N{¿No lineal?}
C --> N
N -- sí --> T[Añadir TM o HM]
N -- no --> E{¿Carga estable?}
T --> E
E -- no --> V[Ventanas móviles]
E -- sí --> R[Alfa creíble]Market timing y venta de opciones
Dos fondos tienen rendimientos que no son lineales en el mercado:
- F12 hace market timing. Sube su exposición antes de las subidas y la baja antes de las caídas. El resultado es una relación convexa con el mercado; Treynor y Mazuy (1966) la modelaron con un término cuadrático: . Verdad: , .
- F13 vende puts. Cobra una prima fija y, cuando el mercado cae, pierde más que su beta normal. Henriksson y Merton (1981) lo modelaron con un término : una beta distinta en las caídas. Verdad: anual (la prima), beta normal 0,4 y beta extra de 0,25 en las caídas.
X = fac[F6].copy()
X["mkt2"] = fac["mkt_rf"] ** 2 # Treynor-Mazuy
X["mkt_neg"] = np.minimum(fac["mkt_rf"], 0) # Henriksson-Merton
tm = regresion(exc["F12"], X[F6 + ["mkt2"]])
hm = regresion(exc["F13"], X[F6 + ["mkt_neg"]])
pd.DataFrame({
"F12_lineal": [tab.loc["F12", "alfa_ff6_%"], np.nan],
"F12_TM": [1200 * tm.params.iloc[0], tm.params.iloc[-1]],
"F13_lineal": [tab.loc["F13", "alfa_ff6_%"], np.nan],
"F13_HM": [1200 * hm.params.iloc[0], hm.params.iloc[-1]],
}, index=["alfa_%", "coef_no_lineal"]).round(2) F12_lineal F12_TM F13_lineal F13_HM
alfa_% 3.13 0.68 -1.21 5.83
coef_no_lineal NaN 0.44 NaN 0.24El modelo lineal se equivoca con los dos y en sentidos opuestos. A F12 le atribuye un 3,13 % de alfa: la media de es positiva y, sin el término cuadrático, acaba en la constante. Con Treynor-Mazuy el alfa baja al 0,68 % (no significativo) y sale 0,44, cerca de 0,5. A F13 le atribuye un −1,21 %, y con Henriksson-Merton el alfa sale 5,83 % y la beta extra 0,24: la verdad.
¿Qué es entonces el 6 % de F13? Lo que cobra por asegurar a otros contra las caídas. Lo que le cuesta:
b_mkt = hm.params.iloc[1]
extra = hm.params.iloc[-1]
print(f"beta normal {b_mkt:.2f}, beta extra en caídas {extra:.2f}")
coste_put = 1200 * extra * X["mkt_neg"].mean()
alfa_hm = 1200 * hm.params.iloc[0]
print(f"alfa (prima cobrada): {alfa_hm:6.2f} %")
print(f"media de extra·min(mkt, 0): {coste_put:6.2f} %")
peores = fac["mkt_rf"].nsmallest(3).index
(100 * pd.DataFrame({"mkt_rf": fac.loc[peores, "mkt_rf"],
"F13": exc.loc[peores, "F13"]})).round(2)beta normal 0.40, beta extra en caídas 0.24
alfa (prima cobrada): 5.83 %
media de extra·min(mkt, 0): -5.68 %
mkt_rf F13
mes
2018-10 -28.94 -18.54
2011-09 -21.28 -12.79
2025-03 -20.74 -13.54La beta extra de 0,24 en las caídas tiene un coste medio del 5,68 % anual, casi todo el alfa. En los tres peores meses del mercado sintético (−28,9 %, −21,3 % y −20,7 %), F13 pierde un 18,5 %, un 12,8 % y un 13,5 %. La figura muestra la forma: medias por tramos del mercado sobre una línea quebrada con pendiente 0,40 en las subidas y 0,64 en las caídas. Un «alfa» que se cobra a cambio de un riesgo de cola no es habilidad; es una prima de riesgo con otro nombre (lección 3.6.1). La regresión lineal, además, lo esconde.
Cambio de estilo
F14 era un fondo de valor (carga de 0,8 en HML) hasta diciembre de 2015 y desde enero de 2016 es de crecimiento (−0,3). Una sola regresión de toda la muestra promedia los dos:
movil = {}
for i in range(36, len(exc) + 1):
v = slice(i - 36, i)
m = regresion(exc["F14"].iloc[v], fac[F6].iloc[v], hac=False)
movil[exc.index[i - 1]] = m.params["hml"]
movil = pd.Series(movil)
mitades = {t: regresion(exc["F14"].loc[a:b], fac[F6].loc[a:b],
hac=False).params["hml"]
for t, (a, b) in {"2006-2015": ("2006-01", "2015-12"),
"2016-2025": ("2016-01", "2025-12")
}.items()}
print("toda la muestra:", round(cargas.loc["F14", "hml"], 2),
"· por mitades:", {k: round(float(v), 2)
for k, v in mitades.items()})
movil.iloc[::24].round(2)toda la muestra: 0.22 · por mitades: {'2006-2015': 0.85, '2016-2025': -0.35}
2008-12 0.94
2010-12 0.84
2012-12 0.86
2014-12 0.79
2016-12 0.51
2018-12 -0.39
2020-12 -0.35
2022-12 -0.36
2024-12 -0.35
dtype: float64Con toda la muestra, la carga en valor sale 0,22: un fondo «ligeramente de valor» que nunca existió. Por mitades sale 0,85 y −0,35, cerca de la verdad. La regresión móvil de 36 meses ve el cambio, pero tarde: cruza el cero en septiembre de 2018, casi tres años después. Además, el promedio mal especificado deja un alfa de −2,79 % con t −1,98, que no está en el fondo: es la consecuencia de imponer una carga fija a un fondo que no la tiene. Ventanas móviles, regresiones por tramos o modelos de cargas variables (el filtro de Kalman de la lección 2.1.6) son las herramientas para esto.
Atribución del riesgo de una cartera
La atribución también sirve para el riesgo. Con un modelo factorial , la varianza de una cartera es con , la exposición de la cartera a cada factor. Por Euler, como en la lección 4.1.4, cada factor aporta y el resto es idiosincrático.
El universo de la lección 4.1.4 se generó con un modelo de diez factores publicado: cargas verdaderas en
universo4_verdad.csv, volatilidades y correlaciones de los factores por régimen en universo4_factores_verdad.csv y
los rendimientos diarios realizados de los factores en universo4_factores_diarios.parquet. Se puede calcular la
atribución verdadera del régimen de calma y la estimada con regresiones diarias en las sesiones de calma, y
compararlas:
precios = pd.read_parquet("universo4_precios.parquet")
precios = precios.set_index("fecha")
diarios = pd.read_parquet("universo4_factores_diarios.parquet")
diarios = diarios.set_index("fecha")
r = precios.pct_change()
r.iloc[0] = precios.iloc[0] / 100 - 1
cols = [c for c in precios if c not in ("MONET", "RF_T13")]
ex_d = r[cols].sub(diarios["rf"], axis=0)
nombres = [c[2:] for c in diarios if c.startswith("f_")]
f_d = diarios[[f"f_{k}" for k in nombres]].set_axis(nombres, axis=1)
# verdad: B, covarianza de los factores y D del régimen de calma
uv = pd.read_csv("universo4_verdad.csv", index_col="activo").loc[cols]
B = uv[[f"b_{k}" for k in nombres]].values
fv = pd.read_csv("universo4_factores_verdad.csv")
fv = fv[fv["regimen"] == "calma"].set_index("factor").loc[nombres]
C = fv[[f"corr_{k}" for k in nombres]].values
Sf = np.outer(fv["vol_anual"], fv["vol_anual"]) * C
D = np.diag(uv["vol_idio"] ** 2)
cvr = pd.read_csv("universo4_cov_verdad.csv")
cvr = cvr[cvr["regimen"] == "calma"].set_index("activo")
dif = B @ Sf @ B.T + D - cvr.loc[cols, cols].values
print(f"máx. |BΣfB' + D − Σ_calma|: {np.abs(dif).max():.1e}")
# estimación: regresiones diarias en las sesiones de calma
calma = diarios["regimen"] == "calma"
Xf = sm.add_constant(f_d[calma])
coef = np.linalg.lstsq(Xf.values, ex_d[calma].values, rcond=None)[0]
B_e = coef[1:].T
res = ex_d[calma].values - Xf.values @ coef
Sf_e = 252 * np.cov(f_d[calma].values.T)
D_e = np.diag(252 * res.var(axis=0, ddof=Xf.shape[1]))
def riesgo_factores(w, B, Sf, D):
x = B.T @ w # exposición a cada factor
var = x @ Sf @ x + w @ D @ w
rc = x * (Sf @ x) / var # contribución de cada factor
return pd.Series(np.r_[rc, w @ D @ w / var],
index=nombres + ["idiosincrático"])
w = pd.Series(0.0, index=cols)
w[["RV_EEUU", "RF_T710"]] = [0.6, 0.4]
w_n = np.full(len(cols), 1 / len(cols))
pd.DataFrame({
"60/40_verdad": riesgo_factores(w.values, B, Sf, D),
"60/40_estim": riesgo_factores(w.values, B_e, Sf_e, D_e),
"1/N_verdad": riesgo_factores(w_n, B, Sf, D),
"1/N_estim": riesgo_factores(w_n, B_e, Sf_e, D_e),
}).round(3)máx. |BΣfB' + D − Σ_calma|: 5.0e-09
60/40_verdad 60/40_estim 1/N_verdad 1/N_estim
mercado 0.976 0.974 0.786 0.777
tamano -0.008 -0.006 0.007 0.006
valor 0.002 0.003 0.004 0.002
tipos -0.000 0.002 -0.011 -0.010
credito 0.000 -0.000 0.021 0.021
materias 0.000 0.000 0.107 0.114
dolar -0.000 -0.001 0.052 0.055
emergentes 0.000 -0.001 0.019 0.021
europa 0.000 0.000 0.001 0.000
japon 0.000 -0.000 0.003 0.002
idiosincrático 0.030 0.029 0.011 0.011Primero, la comprobación: con los ficheros publicados reproduce la covarianza verdadera de calma con un error de , es decir, el redondeo de los ficheros. Después, la atribución: en la 60/40, el factor de mercado explica el 97,6 % del riesgo (verdad) y el 97,4 % (estimado); los tipos, nada: la cartera tiene exposición a ellos, pero su covarianza con la cartera es casi nula, porque la varianza propia del factor se compensa con su correlación negativa con el mercado. En la 1/N de los 36 activos, el mercado explica el 78,6 %, las materias primas el 10,7 % y el dólar el 5,2 %; las estimaciones están a menos de un punto de la verdad en todos los factores. Con un modelo bien especificado y 20 años de datos diarios, la atribución del riesgo es fiable. La del rendimiento, como se vio con los alfas, no tanto.
PCA: factores sin nombre
Los factores de Fama-French son observados: carteras construidas con una regla económica. Los componentes principales son factores estadísticos: las direcciones de máxima varianza de los propios rendimientos, ortogonales entre sí, sin nombre ni signo fijado. Se calculan con la descomposición en autovalores de la matriz de correlación (lección 0.7.3); su uso para estimar covarianzas con pocos datos está en la lección 4.1.2. Aquí la pregunta es otra: ¿los componentes de un universo son sus factores?
Z = (ex_d - ex_d.mean()) / ex_d.std()
autoval, autovec = np.linalg.eigh(np.corrcoef(Z.values.T))
autoval, autovec = autoval[::-1], autovec[:, ::-1]
pcs = Z.values @ autovec[:, :10]
print("autovalores:", autoval[:6].round(2))
print("varianza explicada por 1, 3 y 10:",
(autoval.cumsum()[[0, 2, 9]] / autoval.sum()).round(3))
def r2(y, X):
X = np.column_stack([np.ones(len(X)), X])
e = y - X @ np.linalg.lstsq(X, y, rcond=None)[0]
return 1 - e.var() / y.var()
pd.DataFrame({k: [r2(f_d[f].values, pcs[:, :k]) for f in nombres]
for k in (1, 3, 5, 10)}, index=nombres).round(2)autovalores: [20.15 6.11 2.68 1.06 0.95 0.74]
varianza explicada por 1, 3 y 10: [0.56 0.804 0.931]
1 3 5 10
mercado 0.97 0.98 0.99 0.99
tamano 0.05 0.05 0.06 0.09
valor 0.00 0.06 0.08 0.78
tipos 0.05 0.96 0.98 0.98
credito 0.28 0.33 0.93 0.95
materias 0.21 0.85 0.89 0.93
dolar 0.16 0.35 0.65 0.91
emergentes 0.19 0.23 0.24 0.41
europa 0.00 0.00 0.01 0.03
japon 0.00 0.00 0.00 0.95El primer componente explica el 56 % de la varianza de los 36 activos; los tres primeros, el 80 %, y los diez primeros, el 93 %. Que haya diez factores verdaderos no quiere decir que los diez primeros componentes sean esos diez factores. La tabla da el R² de la regresión de cada factor verdadero sobre los primeros componentes:
- Un componente ya es el mercado (R² 0,97). Con tres, aparecen los tipos (0,96) y las materias primas (0,85).
- Con cinco, el crédito (0,93); con diez, el dólar (0,91), Japón (0,95) y el valor (0,78).
- Tamaño, emergentes y Europa no aparecen ni con diez componentes (0,09, 0,41 y 0,03). El factor europeo, con un 6 % de volatilidad, solo carga en dos activos (la bolsa europea con y sin cobertura); el de Japón, con un 9 %, también en dos, y sí aparece. PCA ordena por varianza explicada: un factor estrecho y poco volátil queda por debajo de combinaciones de ruido idiosincrático de otros activos.
cargas_pc = pd.DataFrame(autovec[:, :3], index=cols,
columns=["pc1", "pc2", "pc3"])
for c in cargas_pc:
s = cargas_pc[c].sort_values()
print(c, "extremos:", ", ".join(
f"{k} {v:+.2f}" for k, v in pd.concat([s.head(2),
s.tail(2)]).items()))pc1 extremos: RF_T20 -0.05, RF_T710 -0.05, SC_IND +0.21, RV_EEUU +0.22
pc2 extremos: RF_T710 -0.38, RF_T37 -0.38, ES_VAL +0.05, SC_FIN +0.06
pc3 extremos: MP_AMP -0.48, MP_ENE -0.47, RF_CIG +0.10, RF_CHY +0.14Las cargas de los tres primeros componentes se pueden interpretar porque el universo es sencillo: el primero carga positivo en todas las bolsas (mercado), el segundo carga casi solo en los bonos del Tesoro con el mismo signo (tipos; el signo negativo es arbitrario: un componente y su opuesto son el mismo) y el tercero, en las materias primas. En un universo real la interpretación es más difícil y cambia de una ventana a otra; a partir del cuarto componente casi nunca hay una lectura económica limpia.
Para qué sirve cada tipo de factor:
| Factores observados (Fama-French) | Componentes principales | |
|---|---|---|
| Qué son | Carteras con una regla económica | Direcciones de máxima varianza |
| Interpretación | Directa (valor, tamaño…) | Solo los primeros, y no siempre |
| Se pueden comprar | Sí, aproximadamente | No como tales |
| Para qué | Evaluar gestores, atribuir rendimiento | Reducir dimensión, estimar covarianzas (4.1.2), detectar cambios de régimen |
| Riesgo | Olvidar un factor (el alfa lo absorbe) | Sobreinterpretar los componentes |
Errores típicos
- Quedarse con el CAPM. Un alfa del CAPM en un fondo de estilo es la prima del estilo.
- Factores de otra región o moneda. Factores de EE. UU. para un fondo europeo meten en el alfa el tipo de cambio y la diferencia entre mercados.
- Leer el t de un fondo entre muchos. Entre 20 fondos, uno sin alfa sale con t > 2 (F18).
- Modelos lineales para estrategias con opciones o con timing. La regresión lineal fabrica alfa (F12) o lo esconde (F13). Para estrategias de venta de volatilidad, el alfa es la prima de un seguro.
- Cargas fijas para fondos que cambian. F14 sale con un estilo que nunca tuvo y un alfa negativo espurio.
- Interpretar todos los componentes principales. El primero suele ser el mercado; a partir de ahí, cuidado.
- Rendimientos logarítmicos. La regresión factorial de una cartera usa rendimientos simples en exceso, como los factores de French.
Ejercicios
- Repite la regresión de seis factores de F08 solo con los diez primeros años y solo con los diez últimos. ¿Cuántas veces sale significativo su alfa (verdadero: 3 %)? ¿Qué dice eso de evaluar a un gestor con diez años de datos?
Solución
for a, b in [("2006-01", "2015-12"), ("2016-01", "2025-12")]:
m = regresion(exc["F08"].loc[a:b], fac[F6].loc[a:b])
print(f"{a[:4]}-{b[:4]}: alfa {1200 * m.params.iloc[0]:5.2f} %"
f" t {m.tvalues.iloc[0]:5.2f}")2006-2015: alfa 3.69 % t 2.42
2016-2025: alfa 2.47 % t 1.43Con diez años, el error típico del alfa crece en un factor respecto a veinte, y un alfa real del 3 % con un 4,5 % de volatilidad propia tiene un t esperado de solo . Aproximadamente la mitad de las veces no saldrá significativo. En estos datos pasa exactamente eso: en 2006-2015 el alfa sale un 3,69 % con t = 2,42 y en 2016-2025 un 2,47 % con t = 1,43, que no es significativo. Diez años de historia no bastan para separar a un buen gestor de uno con suerte, y casi ningún gestor tiene veinte con la misma estrategia.
- Estima F13 con el CAPM y con el modelo de Henriksson-Merton solo con mercado (sin los otros cinco factores) y compara el alfa. ¿Cambia la conclusión respecto al modelo de seis factores?
Solución
capm13 = regresion(exc["F13"], fac[["mkt_rf"]])
hm13 = regresion(exc["F13"], X[["mkt_rf", "mkt_neg"]])
print(f"CAPM: alfa {1200 * capm13.params.iloc[0]:.2f} %, "
f"beta {capm13.params.iloc[1]:.2f}")
print(f"HM: alfa {1200 * hm13.params.iloc[0]:.2f} %, "
f"beta {hm13.params.iloc[1]:.2f}, "
f"extra {hm13.params.iloc[2]:.2f}")CAPM: alfa -1.19 %, beta 0.54
HM: alfa 5.79 %, beta 0.40, extra 0.24No cambia: F13 no tiene exposición a los otros factores, así que el mercado basta. Lo que decide es el término no lineal. Con el CAPM lineal, el alfa es del −1,19 % y la beta, 0,54, queda entre la normal y la de las caídas; con Henriksson-Merton, el alfa vuelve al 5,79 %, la beta normal a 0,40 y la extra a 0,24, cerca de la verdad. Elegir los factores importa; elegir la forma del modelo, también.
- Sin código: un gestor de una estrategia de venta de puts sobre un índice presenta un alfa de seis factores del −0,5 % y argumenta que «su estrategia no tiene alfa, pero tampoco cuesta nada, porque su beta es solo 0,5». Usa F13 para explicar qué está ocultando la regresión lineal y qué pedirías para evaluarlo.
Solución
La regresión lineal mezcla dos cosas que en F13 son de signo contrario: una prima que se cobra todos los meses (el alfa de Henriksson-Merton, 5,83 % anual) y una pérdida extra en las caídas (beta adicional de 0,24, que cuesta de media un 5,68 % al año y concentra las pérdidas en los peores meses: −18,5 % cuando el mercado cae un 28,9 %). La beta de 0,5 es un promedio de 0,40 en las subidas y 0,64 en las caídas: la exposición en el peor momento es mayor que la que se declara. Pediría la regresión con el término , los peores meses de la estrategia junto a los del mercado, su caída máxima y un análisis de estrés (lección 4.2.4) con caídas de mercado que no estén en su historial. La pregunta no es si tiene alfa, sino cuánto pierde cuando el mercado cae y si alguien le está pagando lo suficiente por asumirlo.
Respuesta plegada en cada pregunta
Autoevaluación
F10 tiene un alfa del CAPM del 1,45 % anual y un alfa de seis factores del −0,09 %. ¿De dónde salía el alfa del CAPM?
- ADe la habilidad del gestor, que el modelo de seis factores no sabe medir
- BDe un error de los datos
- CDe exposiciones a primas que el CAPM no incluye: rentabilidad (RMW, +1,21 puntos), inversión (CMA, +0,44) y momentum (+0,34), con HML restando 1,24
- DDel tipo libre de riesgo
Ver respuesta
Respuesta C. De exposiciones a primas que el CAPM no incluye: rentabilidad (RMW, +1,21 puntos), inversión (CMA, +0,44) y momentum (+0,34), con HML restando 1,24
La atribución con seis factores reparte el exceso medio del 8,25 % en mercado (7,51), los otros cinco factores y un alfa de −0,09. El fondo cobra primas que se pueden comprar con un índice de factores: no es alfa.
Entre los 20 fondos, siete tienen p < 0,05 en el alfa de seis factores. Con la corrección de Holm, solo quedan F12 y F16. ¿Qué pasa con F08, que tiene un alfa verdadero del 3 %?
- AQue Holm está mal aplicado
- BQue con 20 años y su volatilidad propia, un 3 % de alfa da un t de 2,86 y p = 0,0042: no supera el umbral de Holm (0,05/20 = 0,0025 para el primero). Controlar los falsos positivos cuesta potencia
- CQue F08 no tiene alfa
- DQue hay que usar errores ordinarios en lugar de Newey-West
Ver respuesta
Respuesta B. Que con 20 años y su volatilidad propia, un 3 % de alfa da un t de 2,86 y p = 0,0042: no supera el umbral de Holm (0,05/20 = 0,0025 para el primero). Controlar los falsos positivos cuesta potencia
Holm controla la probabilidad de algún falso positivo entre los 20; con eso pierde fondos con alfa real. Benjamini-Hochberg (FDR) sí acepta F08 y F09, pero también el falso positivo F18. No hay corrección sin coste.
F12 tiene un alfa lineal del 3,13 % con t = 4,15, el más significativo de todos, y un alfa verdadero de cero. ¿Por qué?
- APorque hace market timing: su rendimiento lleva un término 0,5·mkt_rf², siempre positivo, que la regresión lineal mete en el alfa. Con el término cuadrático de Treynor-Mazuy el alfa baja al 0,68 % y el coeficiente sale 0,44
- BPor azar
- CPorque su beta es baja
- DPorque los errores de Newey-West inflan el t
Ver respuesta
Respuesta A. Porque hace market timing: su rendimiento lleva un término 0,5·mkt_rf², siempre positivo, que la regresión lineal mete en el alfa. Con el término cuadrático de Treynor-Mazuy el alfa baja al 0,68 % y el coeficiente sale 0,44
La media de mkt_rf² es positiva; en una regresión lineal se convierte en constante. El modelo correcto es no lineal, y la significación del alfa lineal no dice nada de la habilidad.
F13 vende puts: alfa verdadero del 6 % y beta extra de 0,25 cuando el mercado cae. ¿Qué alfa da la regresión lineal y por qué?
- AUn 6 %, porque la regresión es insesgada
- BCero
- CUn 12 %
- DUn −1,21 %: el término 0,25·min(mkt_rf, 0) tiene media negativa y la regresión lineal lo absorbe entre el alfa y la beta. Con el término de Henriksson-Merton el alfa sale 5,83 %
Ver respuesta
Respuesta D. Un −1,21 %: el término 0,25·min(mkt_rf, 0) tiene media negativa y la regresión lineal lo absorbe entre el alfa y la beta. Con el término de Henriksson-Merton el alfa sale 5,83 %
El 6 % es la prima que cobra por vender seguro; la media de 0,24·min(mkt_rf, 0) le cuesta un 5,68 % al año. Lo que gana en los meses tranquilos lo devuelve en los de caída (−18,5 % en el peor mes). Separar los dos términos exige el modelo no lineal.
PCA sobre los 36 activos del universo recupera el factor de mercado con un solo componente (R² 0,97) y no recupera el factor europeo ni con diez (R² 0,03). ¿Por qué?
- APorque el factor europeo no existe
- BPorque PCA necesita datos mensuales
- CPorque PCA ordena por varianza: el mercado mueve casi todos los activos y domina, mientras que el factor europeo, de un 6 % de volatilidad, solo carga en dos activos y su efecto se confunde con otras combinaciones
- DPorque los componentes principales son siempre los factores de Fama-French
Ver respuesta
Respuesta C. Porque PCA ordena por varianza: el mercado mueve casi todos los activos y domina, mientras que el factor europeo, de un 6 % de volatilidad, solo carga en dos activos y su efecto se confunde con otras combinaciones
Los componentes son las direcciones de máxima varianza, ortogonales entre sí, no los factores económicos. Recuperan bien los factores grandes y anchos (mercado, tipos, materias primas) y mal los estrechos (Europa, tamaño, emergentes).
Ideas clave
- Una regresión de series temporales de los excesos de una cartera sobre los factores da su alfa (la constante), sus cargas y su R². Con 240 meses, las cargas de los 17 fondos lineales se recuperan con un error medio de 0,01 a 0,04.
- Un alfa del CAPM puede ser una exposición a otros factores: el de F10 (1,45 %) es rentabilidad, inversión y momentum, y desaparece con seis factores.
- Entre 20 fondos sin corregir, un fondo sin alfa sale significativo (F18, t = 2,55). Holm lo elimina pero también elimina alfas reales; Benjamini-Hochberg conserva los reales y el falso.
- Las estrategias no lineales engañan a la regresión lineal: el market timing parece alfa (F12) y la venta de puts parece no tenerlo (F13). Hay que añadir mkt_rf² o min(mkt_rf, 0).
- Una carga estable en toda la muestra puede ser la media de dos estilos (F14: 0,22 frente a 0,85 y −0,35 por mitades); una ventana móvil de 36 meses tarda casi tres años en verlo.
- PCA recupera los factores grandes y anchos (mercado, tipos, materias primas) y no los estrechos; sus componentes no son factores económicos, sino direcciones de varianza.
Recursos
Documentación
Libros
- Sharpe, W. F. (1964): Capital Asset Prices: A Theory of Market Equilibrium under Conditions of Risk. The Journal of Finance 19(3), 425-442
- Fama, E. F. y French, K. R. (1993): Common risk factors in the returns on stocks and bonds. Journal of Financial Economics 33(1), 3-56
- Fama, E. F. y French, K. R. (2015): A five-factor asset pricing model. Journal of Financial Economics 116(1), 1-22
- Carhart, M. M. (1997): On Persistence in Mutual Fund Performance. The Journal of Finance 52(1), 57-82
- Henriksson, R. D. y Merton, R. C. (1981): On Market Timing and Investment Performance. II. Statistical Procedures for Evaluating Forecasting Skills. The Journal of Business 54(4)