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.

Fig. — Estadístico t del IC de rangos de seis características del universo: tres primas plantadas y tres controles sin prima
Lecciones del nivel 2
  1. 2.1 · Estadística y econometría para trading

    1. 2.1.1 Contrastes de hipótesis, p-valores, bootstrap y regresión con errores robustos
    2. 2.1.2 Estacionariedad: ADF, KPSS, diferenciación y diferenciación fraccional
    3. 2.1.3 Modelos ARMA y ARIMA: autocorrelación simple (ACF) y parcial (PACF)
    4. 2.1.4 Modelos de volatilidad: ARCH, GARCH, EGARCH y GJR con la librería arch
    5. 2.1.5 Cointegración: Engle-Granger, Johansen y vida media de la reversión
    6. 2.1.6 Filtro de Kalman para hedge ratios dinámicos
    7. 2.1.7 Detección de regímenes: modelos de Markov ocultos y cambios estructurales
    8. 2.1.8 El problema de las pruebas múltiples: Bonferroni, FDR y el «factor zoo»
  2. 2.2 · Anatomía de un sistema de trading

    1. 2.2.1 Hipótesis de ventaja: riesgo, comportamiento, estructura e información
    2. 2.2.2 Universo, señal, filtro, entrada, salida, sizing y gestión del riesgo
    3. 2.2.3 Frecuencias: intradía, swing y posicional, y la capacidad de una estrategia
    4. 2.2.4 Diario de investigación y registro de experimentos contra el data snooping
  3. 2.3 · Familias de estrategias

    1. 2.3.1 Seguimiento de tendencia y momentum
    2. 2.3.2 Reversión a la media: RSI y Bollinger, reversión intradía y huecos
    3. 2.3.3 Pairs trading y arbitraje estadístico: distancia, cointegración y Kalman
    4. 2.3.4 Carry en divisas, futuros y bonos; estacionalidad y efectos calendario
    5. 2.3.5 Factores (value, calidad, baja volatilidad, tamaño) y estrategias de eventos
    6. 2.3.6 Introducción al market making: inventario, spread y selección adversa (Avellaneda-Stoikov)
    7. 2.3.7 Estrategias específicas de cripto: momentum, arbitraje de funding y basis
  4. 2.4 · Backtesting: motores y paradigmas

    1. 2.4.1 Backtest vectorizado desde cero: matrices, desfase, costes y límites
    2. 2.4.2 Backtest por eventos desde cero: bucle, cartera, órdenes y fills
    3. 2.4.3 vectorbt: barridos masivos de parámetros y simulación de carteras
    4. 2.4.4 backtesting.py y backtrader: dos motores didácticos y su estado
    5. 2.4.5 NautilusTrader: arquitectura, datos, estrategias y el mismo código en backtest y en real
    6. 2.4.6 LEAN/QuantConnect y Zipline Reloaded; bt para asignación de activos
    7. 2.4.7 Modelado de costes: comisiones, horquilla, impacto, financiación y préstamo de títulos
    8. 2.4.8 Modelado de fills: órdenes limitadas, cola, fills parciales, huecos y stops
  5. 2.5 · Sesgos y validación

    1. 2.5.1 Sesgos de backtest: anticipación, supervivencia, selección y narrativa
    2. 2.5.2 In-sample y out-of-sample: walk-forward anclado y móvil
    3. 2.5.3 Validación cruzada combinatoria purgada para estrategias
    4. 2.5.4 Deflated Sharpe Ratio y Probability of Backtest Overfitting (PBO)
    5. 2.5.5 Monte Carlo: remuestreo de trades, bootstrap de bloques y permutaciones de señales
    6. 2.5.6 Sensibilidad de parámetros: mesetas frente a picos, robustez entre activos y periodos
    7. 2.5.7 Datos sintéticos para pruebas de estrés: GBM, GARCH y bootstrapping
    8. 2.5.8 Checklist de «backtest honesto» antes de pasar a paper trading
  6. 2.6 · Métricas de rendimiento

    1. 2.6.1 CAGR, Sharpe, Sortino, Calmar, Omega, drawdown y Ulcer Index
    2. 2.6.2 Win rate, payoff, expectativa, alpha, beta, information ratio, turnover y capacidad
    3. 2.6.3 Informes con QuantStats o pyfolio-reloaded y tearsheets propios

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:

Datos PITSeñal fin de mesPuntuación rangoOrden y quintilesCartera long-shortRent. mes sig.IC y Fama-MacBeth
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.

IN [01]python
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)
OUT [01]Rentabilidades diarias y mensuales con las de deslistado
(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.210

QXQT 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 t−12t-12 al de t−1t-1: 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.

IN [02]python
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()
OUT [02]Señales a fin de mes
25890 acciones-mes, 239 meses
momentum    24450
value       25890
baja_vol    25170
calidad     25890
tamano      25890
esg         25890
dtype: int64

Quedan 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.

IN [03]python
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)
OUT [03]IC de rangos mensual por característica
          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      0

Los 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.

IN [04]python
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)
OUT [04]Rentabilidad anual por quintil (equiponderada)
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.44

Las 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.

Fig. — Rentabilidad anual del mes siguiente por quintil: value y baja volatilidad (equiponderada, 2006-2025)

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:

IN [05]python
S[["value", "calidad", "momentum", "baja_vol", "beta",
   "tamano"]].corr(method="spearman").round(2)
OUT [05]Correlación de rangos entre características
          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.00

Las 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,

ri,t+1=γ0,t+∑kγk,t zk,i,t+εi,t+1,r_{i,t+1} = \gamma_{0,t} + \sum_k \gamma_{k,t}\, z_{k,i,t} + \varepsilon_{i,t+1},

y después se promedian los γk,t\gamma_{k,t} 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.

IN [06]python
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)
OUT [06]Fama-MacBeth con todas las señales y la beta
          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.00

Con 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.

IN [07]python
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)
OUT [07]Coeficiente de la primera señal según con qué se controle
                            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.69

Cada 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):

rLSβ=rLβL−rSβSr^{\beta}_{LS} = \frac{r_L}{\beta_L} - \frac{r_S}{\beta_S}

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.

IN [08]python
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)
OUT [08]Long-short por quintiles, con costes de horquilla
                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.01

La 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.

Fig. — Long-short de baja volatilidad (quintil tranquilo contra volátil), neutral en dólares y neutral en beta, neto de horquilla

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.

AnuncioReacciónEntrada día +1Salida día +60
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.

IN [09]python
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()
OUT [09]SUE, sesión de reacción y deciles por trimestre
8066 anuncios con SUE
array([-1.93, -1.13, -0.69, -0.4 ,  0.  ,  0.  ,  0.36,  0.68,  1.08,
        1.9 ])
IN [10]python
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)
OUT [10]Rentabilidad anormal en tiempo de evento
   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.37

El 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).

Fig. — Deriva tras resultados: rentabilidad anormal acumulada desde la sesión siguiente a la reacción, por decil de sorpresa (SUE)

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.

IN [11]python
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)
OUT [11]CAR alrededor de los rebalanceos del índice
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.57

Del 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.

Fig. — Altas y bajas del índice por revisión trimestral: rentabilidad anormal acumulada desde el anuncio

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.

IN [12]python
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)
OUT [12]Sesión ex: precio negociado frente a rentabilidad total
7029 fechas ex
      precio  anormal_total
mean  -0.732          0.005
std    1.991          1.509

En 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.csv tiene 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

  1. 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
IN [13]python
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))
OUT [13]Momentum sin las quiebras
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.0219

Sin 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.

  1. 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
IN [14]python
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)")
OUT [14]PEAD neto de costes
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.

  1. 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

  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. ↩

Respuesta plegada en cada pregunta

Autoevaluación

  1. 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.

  2. 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.

  3. ¿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.

  4. 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 %.

  5. 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