Lección 2.3.5 · 75 min
Factores (value, calidad, baja volatilidad, tamaño) y estrategias de eventos
Una prima factorial es una relación entre una característica conocida hoy y la rentabilidad del mes que viene. Esta lección la mide de tres formas (quintiles, IC de rangos y regresiones de Fama-MacBeth) en un universo sintético con tres primas plantadas y tres controles sin ninguna, construye las carteras long-short con y sin neutralidad de beta y estudia tres tipos de evento: resultados, cambios en un índice y dividendos.
- Módulo
- 2.3 · Familias de estrategias
- Duración
- 75 min
- Requisitos
- 1.2.1 · Acciones: splits, dividendos y eventos corporativos, 1.4.4 · Universos sin sesgo de supervivencia: deslistados, fusiones y cambios de símbolo, 1.5.1 · Estados financieros, ratios, XBRL y SEC EDGAR, 2.1.1 · Contrastes de hipótesis, p-valores, bootstrap y regresión con errores robustos
Lecciones del nivel 2
2.1 · Estadística y econometría para trading
- 2.1.1 Contrastes de hipótesis, p-valores, bootstrap y regresión con errores robustos
- 2.1.2 Estacionariedad: ADF, KPSS, diferenciación y diferenciación fraccional
- 2.1.3 Modelos ARMA y ARIMA: autocorrelación simple (ACF) y parcial (PACF)
- 2.1.4 Modelos de volatilidad: ARCH, GARCH, EGARCH y GJR con la librería arch
- 2.1.5 Cointegración: Engle-Granger, Johansen y vida media de la reversión
- 2.1.6 Filtro de Kalman para hedge ratios dinámicos
- 2.1.7 Detección de regímenes: modelos de Markov ocultos y cambios estructurales
- 2.1.8 El problema de las pruebas múltiples: Bonferroni, FDR y el «factor zoo»
2.2 · Anatomía de un sistema de trading
- 2.2.1 Hipótesis de ventaja: riesgo, comportamiento, estructura e información
- 2.2.2 Universo, señal, filtro, entrada, salida, sizing y gestión del riesgo
- 2.2.3 Frecuencias: intradía, swing y posicional, y la capacidad de una estrategia
- 2.2.4 Diario de investigación y registro de experimentos contra el data snooping
2.3 · Familias de estrategias
- 2.3.1 Seguimiento de tendencia y momentum
- 2.3.2 Reversión a la media: RSI y Bollinger, reversión intradía y huecos
- 2.3.3 Pairs trading y arbitraje estadístico: distancia, cointegración y Kalman
- 2.3.4 Carry en divisas, futuros y bonos; estacionalidad y efectos calendario
- 2.3.5 Factores (value, calidad, baja volatilidad, tamaño) y estrategias de eventos
- 2.3.6 Introducción al market making: inventario, spread y selección adversa (Avellaneda-Stoikov)
- 2.3.7 Estrategias específicas de cripto: momentum, arbitraje de funding y basis
2.4 · Backtesting: motores y paradigmas
- 2.4.1 Backtest vectorizado desde cero: matrices, desfase, costes y límites
- 2.4.2 Backtest por eventos desde cero: bucle, cartera, órdenes y fills
- 2.4.3 vectorbt: barridos masivos de parámetros y simulación de carteras
- 2.4.4 backtesting.py y backtrader: dos motores didácticos y su estado
- 2.4.5 NautilusTrader: arquitectura, datos, estrategias y el mismo código en backtest y en real
- 2.4.6 LEAN/QuantConnect y Zipline Reloaded; bt para asignación de activos
- 2.4.7 Modelado de costes: comisiones, horquilla, impacto, financiación y préstamo de títulos
- 2.4.8 Modelado de fills: órdenes limitadas, cola, fills parciales, huecos y stops
2.5 · Sesgos y validación
- 2.5.1 Sesgos de backtest: anticipación, supervivencia, selección y narrativa
- 2.5.2 In-sample y out-of-sample: walk-forward anclado y móvil
- 2.5.3 Validación cruzada combinatoria purgada para estrategias
- 2.5.4 Deflated Sharpe Ratio y Probability of Backtest Overfitting (PBO)
- 2.5.5 Monte Carlo: remuestreo de trades, bootstrap de bloques y permutaciones de señales
- 2.5.6 Sensibilidad de parámetros: mesetas frente a picos, robustez entre activos y periodos
- 2.5.7 Datos sintéticos para pruebas de estrés: GBM, GARCH y bootstrapping
- 2.5.8 Checklist de «backtest honesto» antes de pasar a paper trading
2.6 · Métricas de rendimiento
Una prima por característica
Un factor es una característica observable de cada acción (lo barata que es, lo que ha subido, lo volátil que es) que se relaciona con su rentabilidad futura. La estrategia factorial más simple ordena el universo por la característica, compra un extremo y vende el otro, y rehace la cartera cada mes. La misma estructura sirve para el value, el momentum, la baja volatilidad, la calidad o el tamaño:
Fuente Mermaid
flowchart TB D[Datos PIT] --> S[Señal fin de mes] S --> P[Puntuación rango] P --> O[Orden y quintiles] O --> C[Cartera long-short] C --> R[Rent. mes sig.] R --> M[IC y Fama-MacBeth]
La literatura de factores es enorme y está llena de falsos positivos: Harvey, Liu y Zhu (2016) cuentan cientos de factores publicados y proponen exigir a uno nuevo un estadístico t bastante mayor que 2 (lección 2.1.8). Por eso esta lección trabaja con datos donde se conoce la respuesta.
Señales sin anticipación y con las quiebras dentro
Dos errores invalidan un estudio de factores antes de empezar: usar un dato que no se conocía en la fecha de la señal
y perder las acciones que dejan de cotizar (lecciones 1.4.4 y 1.5.1). El primero lo resuelve
universo2_caracteristicas.parquet, que trae una fila por acción y fin de mes con lo que se sabía ese día (el
patrimonio y el beneficio del último informe publicado). El segundo hay que resolverlo a mano: el mes en que una
empresa quiebra, su rentabilidad es −100 %, y esa fila no está en ninguna tabla mensual porque la acción ya no cotiza
al final del mes.
import numpy as np
import pandas as pd
aj = pd.read_parquet("universo2_ajustados.parquet")
P = aj.pivot(index="fecha", columns="ticker", values="cierre_ajustado")
rd = P.pct_change(fill_method=None)
ev = pd.read_csv("universo2_eventos.csv", parse_dates=["fecha"])
for _, e in ev.dropna(subset=["rentabilidad_deslistado"]).iterrows():
ultima = P[e["ticker"]].last_valid_index()
rd.loc[ultima, e["ticker"]] = ((1 + rd.loc[ultima, e["ticker"]])
* (1 + e["rentabilidad_deslistado"])
- 1)
def a_fin_de_mes(df):
df.index = df.index.to_timestamp("M")
return df
mes = rd.index.to_period("M")
r_mes = a_fin_de_mes((1 + rd).groupby(mes).prod(min_count=1) - 1)
print(r_mes.shape)
r_mes.loc["2008-09":"2008-11", ["QXQT", "QXAC", "QXAI"]].round(3)(240, 120)
ticker QXQT QXAC QXAI
fecha
2008-09-30 -0.357 NaN -0.154
2008-10-31 -1.000 NaN -0.087
2008-11-30 NaN NaN 0.210QXQT quiebra en octubre de 2008: su última sesión es el 17 y la rentabilidad de ese mes es −100 %, porque la de
deslistado (las acciones se cancelan) se suma a la de su última sesión. QXAC, comprada en efectivo en 2007, ya no
aparece. Sin este paso, la cartera que tuviera QXQT en octubre de 2008 contaría su −35,7 % de septiembre y después
nada, como si se hubiera vendido a tiempo.
Las señales se calculan al cierre del último día de cada mes y se comparan con la rentabilidad del mes siguiente. El momentum 12-1 es la rentabilidad del fin de mes al de : se salta el último mes, que tiene una pequeña reversión. La volatilidad usa las últimas 252 sesiones (mínimo 126) y la beta, la covarianza con el índice equiponderado del universo en la misma ventana. Value, calidad, tamaño y ESG salen de la tabla point-in-time.
P_mes = a_fin_de_mes(P.groupby(mes).last())
momentum = P_mes.shift(1) / P_mes.shift(12) - 1
mercado = rd.mean(axis=1)
vol_d = rd.rolling(252, min_periods=126).std()
beta_d = (rd.rolling(252, min_periods=126).cov(mercado)
.div(mercado.rolling(252, min_periods=126).var(), axis=0))
vol = a_fin_de_mes(vol_d.groupby(mes).last())
beta = a_fin_de_mes(beta_d.groupby(mes).last())
ch = pd.read_parquet("universo2_caracteristicas.parquet")
ch["mes"] = ch["fecha"].dt.to_period("M").dt.to_timestamp("M")
ch = ch.set_index(["mes", "ticker"])
S = pd.DataFrame({"momentum": momentum.stack(),
"baja_vol": -vol.stack(), "beta": beta.stack()})
S.index.names = ["mes", "ticker"]
S = S.join(ch[["capitalizacion", "patrimonio_neto", "roe",
"puntuacion_esg", "horquilla_pb"]], how="inner")
pn = S["patrimonio_neto"].where(S["patrimonio_neto"] > 0)
S["value"] = np.log(pn / S["capitalizacion"])
S["calidad"] = S["roe"]
S["tamano"] = np.log(S["capitalizacion"])
S["esg"] = S["puntuacion_esg"].astype(float)
siguiente = r_mes.shift(-1).stack()
siguiente.index.names = ["mes", "ticker"]
S["r_sig"] = siguiente
S = S.dropna(subset=["r_sig"])
SENALES = ["momentum", "value", "baja_vol",
"calidad", "tamano", "esg"]
print(len(S), "acciones-mes,",
S.index.get_level_values("mes").nunique(), "meses")
S[SENALES].count()25890 acciones-mes, 239 meses
momentum 24450
value 25890
baja_vol 25170
calidad 25890
tamano 25890
esg 25890
dtype: int64Quedan 25.890 acciones-mes en 239 meses, unas 108 acciones por mes. El momentum necesita doce meses de historia y la volatilidad seis, así que tienen menos observaciones. Ninguna señal usa un dato posterior al fin de mes: los precios son de ese día, y el patrimonio y el ROE, del último informe publicado antes.
Tres formas de medir una prima
IC de rangos
El IC de rangos (information coefficient) es la correlación de Spearman, cada mes, entre la señal y la rentabilidad del mes siguiente. Su media dice cuánto ordena la señal; su estadístico t, con un dato por mes, si esa media se distingue de cero. Al usar rangos, unas pocas rentabilidades extremas (una quiebra, una OPA) no la dominan.
def ic_mensual(s):
return S.groupby(level="mes").apply(
lambda g: g[s].corr(g["r_sig"], method="spearman")).dropna()
ic = pd.DataFrame({s: ic_mensual(s) for s in SENALES})
tabla_ic = pd.DataFrame({
"ic_medio": ic.mean(),
"t": ic.mean() / ic.std() * np.sqrt(ic.count()),
"meses_pos_pct": 100 * (ic > 0).mean(),
"verdad": ["prima", "prima", "prima", "0", "0", "0"],
})
tabla_ic.round(3) ic_medio t meses_pos_pct verdad
momentum 0.028 2.928 55.649 prima
value 0.025 3.077 56.485 prima
baja_vol 0.013 1.049 53.138 prima
calidad -0.033 -4.610 38.912 0
tamano -0.006 -0.834 46.862 0
esg 0.010 1.558 56.485 0Los tres factores plantados tienen IC positivo: el momentum (0,028, t = 2,9) y el value (0,025, t = 3,1), cerca del +0,03 y el +0,02-0,03 que anuncia el generador; la baja volatilidad, 0,013 con t = 1,0, casi invisible. Un IC de 0,03 es pequeño: es positivo en el 56 % de los meses y negativo en el resto. Entre los controles, el tamaño y el ESG no se distinguen de cero, pero la calidad tiene el IC más significativo de la tabla: −0,033 con t = −4,6, y negativo en el 61 % de los meses. Un análisis que se quedara aquí concluiría que comprar empresas de ROE bajo es la mejor estrategia del universo. La verdad del generador es que la calidad no tiene ninguna prima.
Quintiles
El ordenamiento por quintiles responde a otra pregunta: ¿la relación es monótona, o la prima está toda en un extremo? Cada mes se reparten las acciones en cinco grupos por la señal y se mide la rentabilidad media equiponderada de cada grupo el mes siguiente.
def quintil(g, s):
return pd.qcut(g[s].rank(method="first"), 5, labels=False)
filas = {}
for s in SENALES:
d = S.dropna(subset=[s]).copy()
d["q"] = d.groupby(level="mes", group_keys=False).apply(
lambda g: quintil(g, s))
q = d.groupby(["mes", "q"])["r_sig"].mean().unstack()
fila = 100 * 12 * q.mean()
fila["Q5-Q1"] = fila[4] - fila[0]
dif_q = q[4] - q[0]
fila["t"] = dif_q.mean() / dif_q.std() * np.sqrt(len(dif_q))
filas[s] = fila
pd.DataFrame(filas).T.round(2)q 0 1 2 3 4 Q5-Q1 t
momentum 0.63 9.88 10.06 10.48 10.27 9.65 3.48
value 4.09 6.88 10.07 10.21 10.79 6.71 2.92
baja_vol 3.32 10.64 9.88 9.10 7.71 4.39 1.25
calidad 11.19 12.41 7.59 6.03 4.71 -6.48 -3.26
tamano 8.46 9.76 7.41 8.22 8.07 -0.38 -0.18
esg 7.43 9.25 6.42 8.31 10.47 3.04 1.44Las columnas 0 a 4 son los quintiles, del valor más bajo de la señal al más alto, en rentabilidad anual. El value
es casi monótono (del 4,1 % al 10,8 %). El momentum no: el quintil perdedor rinde un 0,6 % y los otros cuatro, todos
alrededor del 10 %. Esa diferencia de 9,6 puntos es mucho mayor de lo que da la prima plantada; el ejercicio 1
explica de dónde sale. La baja volatilidad tiene forma de joroba: el quintil más volátil rinde un 3,3 % y el más
tranquilo, un 7,7 %, por debajo de los intermedios. La calidad repite la señal del IC con −6,5 puntos entre extremos.
La figura compara las dos formas. En el value, la rentabilidad sube con la señal de forma ordenada. En la baja volatilidad, el quintil más volátil, destacado, se queda muy por debajo, pero el más tranquilo no gana al resto: la prima de baja volatilidad está, en rentabilidad bruta, tapada por la beta. Las acciones tranquilas tienen menos beta y en un mercado que sube un 8 % al año ganan menos por esa vía.
Fama-MacBeth
Las dos medidas anteriores miran una señal cada vez. Pero las características están correladas entre sí, y una puede parecer rentable solo porque se parece a otra que lo es:
S[["value", "calidad", "momentum", "baja_vol", "beta",
"tamano"]].corr(method="spearman").round(2) value calidad momentum baja_vol beta tamano
value 1.00 -0.79 -0.23 -0.23 0.12 -0.37
calidad -0.79 1.00 -0.06 0.15 -0.13 0.34
momentum -0.23 -0.06 1.00 0.31 0.02 0.14
baja_vol -0.23 0.15 0.31 1.00 -0.56 0.26
beta 0.12 -0.13 0.02 -0.56 1.00 -0.28
tamano -0.37 0.34 0.14 0.26 -0.28 1.00Las características no son independientes. La calidad y el value tienen una correlación de rangos de −0,79: el ROE es el beneficio entre el patrimonio, y una empresa barata por patrimonio (value alto) suele tener ROE bajo. La baja volatilidad y la beta, −0,56: las acciones tranquilas tienen menos beta. El value y el momentum, −0,23: las acciones que han caído son más baratas. Cada una de estas parejas puede hacer que una señal parezca lo que no es.
La regresión de Fama-MacBeth (1973) separa lo que aporta cada una. Cada mes se estima una regresión transversal de la rentabilidad del mes siguiente sobre todas las señales a la vez,
y después se promedian los de los 239 meses. El error típico sale de cómo varían los coeficientes de un mes a otro, que ya incluye la correlación entre acciones de un mismo mes; con Newey-West se añade su autocorrelación (lección 2.1.1). Las señales entran como puntuaciones normales por rangos, la misma transformación del generador, así que los coeficientes son directamente comparables con la respuesta conocida: 0,25 % al mes para el momentum, 0,40 % para el value, 0,25 % para la baja volatilidad y 0 para el resto.
import statsmodels.api as sm
from scipy.stats import norm
def puntuacion(v):
return norm.ppf((v.rank() - 0.5) / v.count())
def fama_macbeth(cols, lags=3):
coefs = []
for m, g in S.dropna(subset=cols).groupby(level="mes"):
if len(g) < 30:
continue
X = np.column_stack([np.ones(len(g))]
+ [puntuacion(g[c]) for c in cols])
b = np.linalg.lstsq(X, g["r_sig"].to_numpy(), rcond=None)[0]
coefs.append(b[1:])
coefs = pd.DataFrame(coefs, columns=cols)
filas = {}
for c in cols:
nw = sm.OLS(coefs[c], np.ones(len(coefs))).fit(
cov_type="HAC", cov_kwds={"maxlags": lags})
filas[c] = {"coef_pct_mes": 100 * nw.params.iloc[0],
"t_nw": nw.tvalues.iloc[0]}
return pd.DataFrame(filas).T
fm = fama_macbeth(SENALES + ["beta"])
fm["verdad"] = [0.25, 0.40, 0.25, 0, 0, 0, 0]
fm.round(2) coef_pct_mes t_nw verdad
momentum 0.46 5.09 0.25
value 0.29 2.14 0.40
baja_vol 0.30 2.47 0.25
calidad -0.02 -0.13 0.00
tamano 0.06 0.98 0.00
esg 0.09 1.66 0.00
beta 0.29 1.94 0.00Con todas las señales a la vez, la calidad baja a −0,02 % al mes con t = −0,13: nada, la respuesta conocida. El value sale 0,29 (verdad 0,40) y la baja volatilidad 0,30 (verdad 0,25), las dos con |t| > 2. El tamaño, el ESG y la beta no se distinguen de cero con un umbral de 2, aunque el ESG se acerca (t = 1,66). El momentum sale 0,46, casi el doble de su 0,25: el coeficiente es lineal en la rentabilidad, y las cuatro quiebras guiadas (acciones con momentum muy bajo que acaban en −100 %) lo inflan. El IC de rangos, que no pesa las rentabilidades extremas, lo mide mejor.
pares = {"value solo": ["value"],
"value + momentum": ["value", "momentum"],
"calidad sola": ["calidad"],
"calidad + value": ["calidad", "value"],
"calidad + value + momentum": ["calidad", "value", "momentum"],
"baja_vol sola": ["baja_vol"],
"baja_vol + beta": ["baja_vol", "beta"]}
pd.DataFrame({k: fama_macbeth(v).iloc[0]
for k, v in pares.items()}).T.round(2) coef_pct_mes t_nw
value solo 0.16 2.09
value + momentum 0.27 3.38
calidad sola -0.20 -2.85
calidad + value -0.21 -1.75
calidad + value + momentum 0.02 0.19
baja_vol sola 0.10 0.92
baja_vol + beta 0.31 2.69Cada fila es el coeficiente de la primera señal con las demás de control. Sola, la calidad tiene −0,20 % al mes con t = −2,85. Con el value, sigue en −0,21: el value solo está a su vez atenuado (0,16) por su correlación negativa con el momentum. Con value y momentum, la calidad cae a 0,02 con t = 0,19. El value, al añadir el momentum, sube de 0,16 a 0,27. La baja volatilidad pasa de 0,10 (t = 0,92) a 0,31 (t = 2,69) al controlar por la beta.
Es la confusión típica de los estudios de factores: el efecto de una característica depende de con qué otras se compare, y la omisión de una sola (aquí, el momentum) cambia las conclusiones sobre las demás. La regresión múltiple no garantiza la verdad, pero el ordenamiento univariante ni siquiera lo intenta.
Carteras long-short: neutral en dólares o en beta
Una cartera long-short neutral en dólares compra el quintil alto y vende el bajo por el mismo importe. Si los dos quintiles tienen betas distintas, la cartera queda expuesta al mercado. La alternativa, neutral en beta, escala cada pata por la inversa de su beta media de antes de formar la cartera, como en Frazzini y Pedersen (2014):
Los costes se aproximan con la media horquilla cotizada de cada acción (horquilla_pb), pagada sobre el giro de cada
pata. La cartera se forma al cierre de fin de mes con las señales de ese día y gana la rentabilidad del mes siguiente:
la ejecución es al cierre de la señal, una simplificación optimista que la lección 2.4.8 discute.
mercado_mes = r_mes.mean(axis=1)
def long_short(s, neutral_beta=False):
d = S.dropna(subset=[s, "beta"]).copy()
d["q"] = d.groupby(level="mes", group_keys=False).apply(
lambda g: quintil(g, s))
r, coste, prev = {}, {}, None
for m, g in d.groupby(level="mes"):
L, C = g[g["q"] == 4], g[g["q"] == 0]
kL = 1 / L["beta"].mean() if neutral_beta else 1.0
kC = 1 / C["beta"].mean() if neutral_beta else 1.0
tL = L.index.get_level_values("ticker")
tC = C.index.get_level_values("ticker")
w = pd.concat([pd.Series(kL / len(L), tL),
pd.Series(-kC / len(C), tC)])
giro = w.sub(prev, fill_value=0).abs() if prev is not None \
else w.abs()
media_h = g["horquilla_pb"].droplevel(0) / 2 / 1e4
coste[m] = (giro * media_h.reindex(giro.index)).sum()
r[m] = kL * L["r_sig"].mean() - kC * C["r_sig"].mean()
prev = w
r, coste = pd.Series(r), pd.Series(coste).fillna(0)
neto = r - coste
b = np.polyfit(mercado_mes.shift(-1).reindex(r.index), r, 1)[0]
return pd.Series({"rent_anual_pct": 1200 * neto.mean(),
"coste_anual_pct": 1200 * coste.mean(),
"sharpe": neto.mean() / neto.std() * np.sqrt(12),
"beta": b}), neto
ls_tabla, ls_series = {}, {}
for s in ["baja_vol", "value", "momentum", "calidad", "tamano"]:
for nb in (False, True):
clave = f"{s} {'beta' if nb else 'dólar'}"
ls_tabla[clave], ls_series[clave] = long_short(s, nb)
pd.DataFrame(ls_tabla).T.round(2) rent_anual_pct coste_anual_pct sharpe beta
baja_vol dólar 4.27 0.13 0.28 -0.45
baja_vol beta 8.36 0.15 0.65 0.08
value dólar 6.98 0.12 0.68 0.07
value beta 5.63 0.13 0.59 -0.01
momentum dólar 9.28 0.37 0.77 0.04
momentum beta 11.04 0.38 1.01 -0.01
calidad dólar -6.52 0.06 -0.74 -0.08
calidad beta -5.45 0.07 -0.65 0.03
tamano dólar -0.20 0.05 -0.02 -0.17
tamano beta 1.81 0.06 0.21 0.01La baja volatilidad neutral en dólares gana un 4,27 % anual neto con un Sharpe de 0,28 y una beta de −0,45: está corta de mercado. Neutral en beta, un 8,36 % con Sharpe 0,65 y beta 0,08. En el value y el momentum los quintiles extremos tienen betas parecidas y la neutralidad cambia poco. La calidad pierde con las dos construcciones (Sharpe −0,74 y −0,65): el long-short de una característica confundida hereda la confusión, y nadie lo notaría sin la regresión. El tamaño no da nada. Los costes de horquilla son pequeños (0,05 % a 0,38 % al año) porque los quintiles cambian poco de un mes a otro y las acciones del universo son líquidas; el momentum, que rota más, es el más caro.
La figura enseña las dos curvas de la baja volatilidad. La neutral en dólares gana en los años de caídas (un 23 % en 2008, un 31 % en 2022) y pierde en los de subidas fuertes (de 2017 a 2019 y de 2023 a 2025), porque está corta de mercado; la neutral en beta, destacada, termina en 430 frente a 182. La neutralidad de beta tiene un coste que la tabla no ve: exige apalancar la pata tranquila (con una beta media de 0,71, unos 1,4 USD comprados por cada dólar) y financiar ese apalancamiento.
Estrategias de eventos
Un estudio de eventos alinea cada observación por su fecha de evento, no por calendario, y mide la rentabilidad anormal: la de la acción menos la del mercado (aquí, el índice equiponderado del universo). Sumada en una ventana es la CAR (cumulative abnormal return). La regla de oro es la misma que en todo el nivel: la ventana de la estrategia empieza después de que la información sea pública.
Fuente Mermaid
flowchart LR A[Anuncio] --> R[Reacción] R --> E[Entrada día +1] E --> S[Salida día +60]
Deriva tras resultados (PEAD)
eventos2_resultados.csv tiene 8.656 presentaciones trimestrales con el BPA estimado y el real. La sorpresa
tipificada (SUE) divide la sorpresa por su desviación típica en los ocho trimestres anteriores (mínimo cuatro):
solo usa sorpresas ya publicadas. La sesión de reacción es la del anuncio si se publica antes de la apertura y la
siguiente si se publica tras el cierre.
res = pd.read_csv("eventos2_resultados.csv",
parse_dates=["fecha_anuncio", "periodo_fin"])
res = res.sort_values(["ticker", "periodo_fin"])
res["sd8"] = res.groupby("ticker")["sorpresa"].transform(
lambda s: s.shift(1).rolling(8, min_periods=4).std())
res["sue"] = res["sorpresa"] / res["sd8"]
sesiones = P.index
res["d0"] = (sesiones.searchsorted(res["fecha_anuncio"])
+ (res["momento"] == "tras_cierre").astype(int))
res = res.dropna(subset=["sue"])
res = res[res["d0"] + 60 < len(sesiones)].copy()
trimestre = res["fecha_anuncio"].dt.to_period("Q")
res["decil"] = res.groupby(trimestre)["sue"].transform(
lambda s: pd.qcut(s.rank(method="first"), 10, labels=False))
print(len(res), "anuncios con SUE")
res.groupby("decil")["sue"].median().round(2).to_numpy()8066 anuncios con SUE
array([-1.93, -1.13, -0.69, -0.4 , 0. , 0. , 0.36, 0.68, 1.08,
1.9 ])anormal = rd.sub(rd.mean(axis=1), axis=0).to_numpy()
col = {t: j for j, t in enumerate(rd.columns)}
def ventana(d0, tickers, desde, hasta):
dias = np.arange(desde, hasta + 1)
M = np.full((len(d0), len(dias)), np.nan)
for k, (d, t) in enumerate(zip(d0, tickers)):
idx = d + dias
ok = (idx >= 0) & (idx < len(sesiones))
M[k, ok] = anormal[idx[ok], col[t]]
return pd.DataFrame(np.nan_to_num(M), columns=dias)
ar = ventana(res["d0"].to_numpy(), res["ticker"], -20, 60)
ar_dec = ar.groupby(res["decil"].to_numpy()).mean()
car_dec = 100 * ar_dec.loc[:, 1:].cumsum(axis=1)
resumen_pead = pd.DataFrame({
"antes_-20_-1": 100 * ar_dec.loc[:, -20:-1].sum(axis=1),
"reaccion_0": 100 * ar_dec[0],
"deriva_1_60": car_dec[60],
})
resumen_pead.loc[[0, 1, 4, 5, 8, 9]].round(2) antes_-20_-1 reaccion_0 deriva_1_60
0 0.10 -4.32 -0.98
1 -0.02 -3.13 -0.19
4 -0.04 -0.36 -0.68
5 0.07 0.38 0.08
8 0.56 2.82 0.91
9 -0.17 4.33 1.37El decil 10 (las mayores sorpresas positivas) salta un 4,33 % el día de reacción y sigue subiendo un 1,37 % en las 60
sesiones siguientes; el decil 1 cae un 4,32 % y deriva un −0,98 % más. La diferencia de deriva entre extremos es de un
2,35 %, justo lo que planta el generador (de un 2 % a un 3 %). En los deciles centrales no hay ni salto ni deriva
apreciables. La columna antes_-20_-1 es el control: en las 20 sesiones anteriores al anuncio no hay deriva plantada,
y los valores medidos son ruido (de −0,17 % a +0,56 %, sin orden por deciles).
La figura dibuja la CAR desde la sesión siguiente a la reacción. Las curvas de los deciles extremos se separan poco a poco durante las 60 sesiones: una deriva lenta, que no se agota en unos días. Es la regularidad que Bernard y Thomas (1989) documentaron en datos reales: los precios tardan en incorporar del todo la sorpresa de los resultados. En los datos del curso es una deriva de +0,01 % por sesión y unidad de sorpresa, plantada.
Altas y bajas de un índice
El índice ficticio del universo se revisa cada trimestre: entra la empresa que ha crecido y sale la que ha encogido. El cambio se anuncia cinco sesiones antes de aplicarse, y los fondos que replican el índice tienen que comprar o vender al cierre de la fecha efectiva. Ese flujo obligado mueve el precio (Shleifer, 1986; Harris y Gurel, 1986)1Nota 1 · Andrei Shleifer (1986), «Do Demand Curves for Stocks Slope Down?», The Journal of Finance 41(3), doi:10.1111/j.1540-6261.1986.tb04518.x; Lawrence Harris y Eitan Gurel (1986), «Price and Volume Effects Associated with Changes in the S&P 500 List», The Journal of Finance 41(4), doi:10.1111/j.1540-6261.1986.tb04550.x.: sube entre el anuncio y la fecha efectiva y revierte después. La estrategia clásica compra al día siguiente del anuncio y vende al cierre de la fecha efectiva, a los fondos indexados.
rb = pd.read_csv("eventos2_rebalanceos.csv",
parse_dates=["fecha_anuncio", "fecha_efectiva"])
rb["d0"] = sesiones.searchsorted(rb["fecha_anuncio"])
rb["de"] = sesiones.searchsorted(rb["fecha_efectiva"]) - rb["d0"]
print("sesiones del anuncio a la fecha efectiva:",
sorted(rb["de"].unique()))
filas = {}
car_rb = {}
for (tipo, motivo), g in rb.groupby(["tipo", "motivo"]):
car = 100 * ventana(g["d0"].to_numpy(), g["ticker"],
-5, 25).mean().cumsum()
car_rb[(tipo, motivo)] = car
de = int(g["de"].max())
filas[(tipo, motivo)] = {
"n": len(g),
"anuncio_a_efectiva": car[de] - car[0],
"efectiva_a_+20": car[de + 20] - car[de]}
pd.DataFrame(filas).T.round(2)sesiones del anuncio a la fecha efectiva: [np.int64(4), np.int64(5)]
n anuncio_a_efectiva efectiva_a_+20
alta revision_trimestral 20.0 2.20 -1.70
sustitucion 7.0 2.39 -4.21
baja deslistado 7.0 -13.42 0.00
revision_trimestral 20.0 -3.97 -0.57Del anuncio a la fecha efectiva hay 4 o 5 sesiones. Las altas por revisión suben un 2,20 % en esa ventana y revierten un −1,70 % en las 20 sesiones siguientes: casi lo plantado (+3,0 % y −1,5 %), con 20 eventos. Las bajas por revisión caen un 3,97 % y apenas revierten (−0,57 % en lugar de +1,5 %): con 20 eventos, el error típico de una CAR de 20 sesiones es de varios puntos y no se distingue una reversión de 1,5 de una de 0. Las altas por sustitución (7 casos) se comportan como las otras altas. Las bajas por deslistado no tienen efecto plantado: su −13,4 % es la caída de las empresas que quiebran, que ya se conocía antes del anuncio. Mezclarlas con las demás bajas falsearía el estudio.
La figura enseña la CAR media de las altas y las bajas por revisión, con el anuncio y la fecha efectiva marcados. El movimiento se concentra entre las dos líneas: es la ventana en la que los fondos indexados todavía no han comprado y los demás se adelantan.
Dividendos: el control
En la fecha ex de un dividendo el precio negociado cae aproximadamente lo que se paga, porque quien compra ese día ya no lo cobra (lección 1.2.1). Con precios sin ajustar parece un «efecto fecha ex» negativo; con la rentabilidad total no hay nada, y en estos datos no hay ningún efecto plantado: es el control de esta sección.
dv = pd.read_csv("universo2_dividendos.csv", parse_dates=["fecha_ex"])
pr = pd.read_parquet("universo2_precios.parquet").pivot(
index="fecha", columns="ticker", values="cierre")
r_precio = pr.pct_change(fill_method=None).to_numpy()
i = sesiones.searchsorted(dv["fecha_ex"])
j = dv["ticker"].map(col).to_numpy()
ok = i < len(sesiones)
rex = pd.DataFrame({"precio": r_precio[i[ok], j[ok]],
"anormal_total": anormal[i[ok], j[ok]]})
print(len(rex), "fechas ex")
(100 * rex.agg(["mean", "std"])).round(3)7029 fechas ex
precio anormal_total
mean -0.732 0.005
std 1.991 1.509En las 7.029 fechas ex, el precio negociado cae de media un 0,73 %, lo que pagan los dividendos trimestrales del universo. La rentabilidad total anormal es de +0,005 %: nada. Una regla que «venda antes de la fecha ex» con precios sin ajustar parecería evitar una caída segura; con rentabilidades totales se ve que la caída la compensa el dividendo (y en real, los impuestos sobre el dividendo cambian la cuenta).
Lo que cambia en real
- Vender en corto cuesta. Un long-short necesita tomar prestadas las acciones del quintil vendido. Las más
baratas de vender son las grandes y líquidas; las que más pesan en una cartera de momentum o de baja calidad suelen
ser las difíciles de tomar prestadas.
costes2_prestamo.csvtiene tasas del 0,25 % al 0,50 % para la mayoría y del 25 % al 60 % en las quiebras en sus últimos meses (lección 2.4.7). - Las primas publicadas se reducen. Muchas anomalías rinden menos después de publicarse; parte de la prima era sobreajuste y parte se arbitra.
- Capacidad. Una cartera de quintiles equiponderada en 108 acciones pone el mismo dinero en la más pequeña que en la más grande. Ponderar por capitalización o limitar por volumen (lección 2.2.3) cambia el resultado.
- Eventos y ejecución. La deriva tras resultados se reparte en 60 sesiones, así que tolera bien una ejecución al día siguiente; la de los rebalanceos se concentra en cinco sesiones y la compiten muchos participantes, y vender al cierre de la fecha efectiva exige operar en la subasta (lección 1.1.2).
Ejercicios
- El long-short de momentum por quintiles gana un 9,6 % anual, mucho más de lo que predice su prima plantada. Repite el cálculo de quintiles quitando las cuatro empresas que quiebran y explica la diferencia. ¿Qué medida de la lección no tiene este problema?
Solución
quiebras = ev.loc[ev["evento"] == "deslistado_quiebra", "ticker"]
sin_q = S[~S.index.get_level_values("ticker").isin(quiebras)]
sin_q = sin_q.dropna(subset=["momentum"]).copy()
sin_q["q"] = sin_q.groupby(level="mes", group_keys=False).apply(
lambda g: quintil(g, "momentum"))
q_sin = sin_q.groupby(["mes", "q"])["r_sig"].mean().unstack()
print("Q1..Q5 sin quiebras (% anual):",
(1200 * q_sin.mean()).round(2).tolist())
print("Q5-Q1:", round(1200 * (q_sin[4] - q_sin[0]).mean(), 2))
print("IC medio con y sin quiebras:",
round(ic["momentum"].mean(), 4),
round(sin_q.groupby(level="mes").apply(
lambda g: g["momentum"].corr(g["r_sig"], "spearman")
).mean(), 4))Q1..Q5 sin quiebras (% anual): [3.45, 10.03, 10.54, 10.4, 10.29]
Q5-Q1: 6.84
IC medio con y sin quiebras: 0.0279 0.0219Sin las cuatro quiebras, el quintil perdedor pasa de rendir un 0,63 % a un 3,45 % anual y la diferencia entre extremos baja de 9,65 a 6,84 puntos. Las quiebras guiadas del generador pasan meses cayendo (momentum muy bajo) y terminan en −100 %: una cartera equiponderada de veinte acciones que tiene una de ellas pierde cinco puntos ese mes. El IC de rangos casi no cambia (de 0,028 a 0,022): al usar rangos, una rentabilidad de −100 % pesa lo mismo que una de −20 %. Por eso el IC de rangos es la medida robusta, y por eso nunca hay que quitar las quiebras de un backtest: en real también ocurren, y son parte de lo que gana (o pierde) una estrategia.
- Calcula una cartera de PEAD: compra el decil 10 de SUE y vende el decil 1 en la sesión siguiente a la reacción, mantén 60 sesiones y paga 10 puntos básicos por lado en cada pata. ¿Cuánto queda de la deriva?
Solución
coste_ida_vuelta = 2 * 10 / 1e4 # entrar y salir, por pata
bruto = car_dec[60].loc[9] - car_dec[60].loc[0]
neto = bruto - 100 * 2 * coste_ida_vuelta
print(f"deriva D10 - D1: {bruto:.2f} % | neta: {neto:.2f} %"
" (en 60 sesiones)")deriva D10 - D1: 2.35 % | neta: 1.95 % (en 60 sesiones)La deriva entre extremos es de un 2,35 % en 60 sesiones; con 10 puntos básicos por lado en las dos patas (40 puntos básicos por operación completa) quedan un 1,95 %. Con cuatro anuncios al año por empresa y una ventana de 60 sesiones, una cartera de PEAD está casi siempre invertida, así que ese 1,95 % se repite unas cuatro veces al año sobre la parte del capital asignada a cada evento. Faltaría el coste de vender en corto el decil bajo.
- Un proveedor de datos vende una puntuación ESG y enseña un backtest con un Sharpe de 0,4 y un IC con t = 1,6 en veinte años. Con lo visto en la lección, ¿qué le preguntarías antes de comprarla?
Solución
Cuántas señales probó antes de enseñar esa (con seis características, un t de 1,6 en una de ellas es lo esperable por azar: en este universo la ESG es ruido puro y da justo eso). Si la puntuación es point-in-time o se revisó hacia atrás con información posterior. Si el resultado sobrevive a una regresión de Fama-MacBeth con value, tamaño, momentum y beta, porque una puntuación ESG suele estar correlada con el tamaño y el sector. Si la cartera es neutral en beta y en sector, cuánto cuesta vender en corto el quintil bajo y qué pasa fuera de la muestra en que se diseñó. Con t = 1,6 y sin hipótesis previa, ningún umbral razonable para un factor nuevo (lección 2.1.8) la acepta.
Footnotes
-
Andrei Shleifer (1986), «Do Demand Curves for Stocks Slope Down?», The Journal of Finance 41(3), doi:10.1111/j.1540-6261.1986.tb04518.x; Lawrence Harris y Eitan Gurel (1986), «Price and Volume Effects Associated with Changes in the S&P 500 List», The Journal of Finance 41(4), doi:10.1111/j.1540-6261.1986.tb04550.x. ↩
Respuesta plegada en cada pregunta
Autoevaluación
La calidad (ROE) tiene un IC de rangos de −0,033 con t = −4,6, pero en una regresión de Fama-MacBeth con las demás señales su coeficiente es casi cero. ¿Qué pasa?
- AQue la calidad está muy correlada (−0,79) con el value y, sola, hereda su prima con el signo cambiado; controlando por value y momentum no explica nada
- BQue la calidad tiene una prima negativa
- CQue Fama-MacBeth está mal calculado
- DQue el IC de rangos no sirve
Ver respuesta
Respuesta A. Que la calidad está muy correlada (−0,79) con el value y, sola, hereda su prima con el signo cambiado; controlando por value y momentum no explica nada
El ROE es (E/P)·(P/VC): una empresa barata por P/VC suele tener ROE bajo. El ordenamiento univariante mide la relación total, que incluye lo que la calidad comparte con el value. La regresión múltiple separa lo propio de cada característica: con value y momentum, el coeficiente de la calidad baja a 0,02 % al mes con t = 0,19 (con todas las señales, −0,02 y t = −0,13). Solo con el value no basta, porque el value está a su vez atenuado por su correlación con el momentum. La respuesta conocida es 0.
La cartera que compra el quintil de menor volatilidad y vende el de mayor gana un 4,3 % anual neto con un Sharpe de 0,28 y una beta de −0,45. Neutral en beta, un 8,4 % con Sharpe 0,65. ¿Por qué cambia tanto?
- APor los costes
- BPorque la cartera neutral en beta tiene menos acciones
- CPor azar
- DPorque las acciones volátiles tienen más beta: la cartera neutral en dólares está corta de mercado y pierde la prima del mercado; escalando cada pata por su beta aparece la anomalía
Ver respuesta
Respuesta D. Porque las acciones volátiles tienen más beta: la cartera neutral en dólares está corta de mercado y pierde la prima del mercado; escalando cada pata por su beta aparece la anomalía
Con betas medias de 0,71 en el quintil tranquilo y de 1,24 en el volátil, comprar uno y vender otro a partes iguales deja una beta muy negativa. En un mercado que sube, eso cuesta la prima del mercado y tapa la anomalía. Es la construcción de Frazzini y Pedersen (2014): apalancar la pata de beta baja y reducir la de beta alta.
¿Qué distingue a Fama-MacBeth de una regresión agrupada de todos los meses y acciones a la vez?
- ANada
- BEstima una regresión transversal por mes y promedia los coeficientes: el error típico sale de la variación de los coeficientes entre meses, que recoge la correlación transversal de un mismo mes
- CUsa solo las acciones grandes
- DNo necesita datos point-in-time
Ver respuesta
Respuesta B. Estima una regresión transversal por mes y promedia los coeficientes: el error típico sale de la variación de los coeficientes entre meses, que recoge la correlación transversal de un mismo mes
En un mismo mes todas las acciones comparten el shock de mercado y de sector, y los residuos están correlados. Una regresión agrupada con errores ordinarios trataría 26.000 observaciones como independientes. Fama-MacBeth reduce el problema a una serie de 239 coeficientes mensuales y, con Newey-West, también tiene en cuenta su autocorrelación.
En la deriva tras resultados (PEAD), ¿por qué la ventana de medida empieza en la sesión siguiente a la de reacción?
- APorque el día de reacción no hay datos
- BPara que el resultado sea mayor
- CPorque el salto del día de reacción ya está en el precio cuando la sorpresa se conoce: una estrategia solo puede comprar después, y lo que mide la deriva es lo que queda
- DPor convención contable
Ver respuesta
Respuesta C. Porque el salto del día de reacción ya está en el precio cuando la sorpresa se conoce: una estrategia solo puede comprar después, y lo que mide la deriva es lo que queda
Con un anuncio antes de la apertura, la sesión de reacción es la misma; tras el cierre, la siguiente. En los dos casos, la sorpresa se conoce y el precio salta a la vez. Contar ese salto en la estrategia sería anticipación. En los datos, el salto medio es de ±4,3 % y la deriva posterior entre el decil alto y el bajo, de un 2,4 %.
La puntuación ESG del universo da un IC con t = 1,6 y un coeficiente de Fama-MacBeth con t = 1,7. ¿Qué es?
- AUn placebo: el generador la hace independiente de todo; su t es el tipo de resultado que el azar da a menudo cuando se prueban varias señales
- BUna prima ESG real
- CUn error de datos
- DUn efecto del tamaño
Ver respuesta
Respuesta A. Un placebo: el generador la hace independiente de todo; su t es el tipo de resultado que el azar da a menudo cuando se prueban varias señales
La puntuación ESG del universo es un proceso AR(1) mensual independiente de las rentabilidades. Que salga con t = 1,6 y 1,7 es un recordatorio de cómo de fácil es encontrar una señal «prometedora» en ruido: con seis características probadas, uno o dos t por encima de 1,5 son lo normal.
Ideas clave
- Una prima factorial se mide en tres capas: quintiles (¿es monótona?), IC de rangos (¿ordena?) y Fama-MacBeth (¿qué aporta controlando por las demás?).
- Las señales se calculan con datos de fin de mes, point-in-time, y las rentabilidades incluyen las de deslistado: sin ellas, las quiebras desaparecen y la cartera parece mejor.
- La calidad del universo tiene IC −0,033 (t −4,6) solo por su correlación con el value; con value y momentum en la regresión, su coeficiente es ≈ 0, la respuesta conocida.
- La baja volatilidad casi no se ve en un long-short neutral en dólares (Sharpe 0,28, beta −0,45); neutral en beta sube a 0,65.
- En eventos se mide en tiempo de evento y desde la sesión siguiente a la información: PEAD de un 2,4 % entre deciles en 60 sesiones, +2,2 % del anuncio a la entrada en el índice y nada en las fechas ex.
- Tamaño y ESG son controles sin prima; el ESG da t = 1,6 por azar: el umbral de un factor nuevo tiene que ser mucho más exigente que t = 2.
Recursos
Documentación
- Fama y MacBeth (1973): Risk, Return, and Equilibrium: Empirical Tests, Journal of Political Economy 81(3)
- Frazzini y Pedersen (2014): Betting against beta, Journal of Financial Economics 111(1)
- Bernard y Thomas (1989): Post-Earnings-Announcement Drift, Journal of Accounting Research 27
- Harvey, Liu y Zhu (2016): … and the Cross-Section of Expected Returns, Review of Financial Studies 29(1)