Lección 1.4.4 · 55 min
Universos sin sesgo de supervivencia: deslistados, fusiones y cambios de símbolo
Descargar los miembros actuales de un índice y probar una estrategia con toda su historia parece razonable y duplica la rentabilidad. Esta lección reconstruye, día a día, la composición de un índice ficticio de 40 valores durante once años y mide el sesgo, sus causas y cómo tratar cada salida.
- Módulo
- 1.4 · Ingeniería de datos financieros
- Duración
- 55 min
Lecciones del nivel 1
1.1 · Cómo funcionan los mercados
1.2 · Clases de activos
- 1.2.1 Acciones: splits, dividendos y eventos corporativos
- 1.2.2 ETFs: réplica, tracking error, apalancados y UCITS
- 1.2.3 Futuros: especificaciones, margen, roll y contango
- 1.2.4 Forex: pares, pips, lotes, rollover y CFD
- 1.2.5 Opciones: call, put, valor intrínseco y valor temporal
- 1.2.6 Cripto: spot, perpetuos, funding y custodia
- 1.2.7 Renta fija y tipos de interés; CFDs y productos apalancados
1.3 · Fuentes de datos de mercado
- 1.3.1 Tipos de datos de mercado: EOD, barras, ticks, libro y referencia
- 1.3.2 Fuentes de datos gratuitas
- 1.3.3 APIs de datos retail y prosumer
- 1.3.4 Proveedores de datos profesionales
- 1.3.5 Datos de cripto y on-chain
- 1.3.6 Brókeres como fuente de datos
- 1.3.7 Licencias y términos de uso de datos
- 1.3.8 Cómo elegir proveedor de datos
1.4 · Ingeniería de datos financieros
- 1.4.1 Descarga por REST y websockets: paginación, límites y reintentos
- 1.4.2 Limpieza de datos: faltantes, ticks erróneos, huecos y zonas horarias
- 1.4.3 Ajustes por splits y dividendos: precios ajustados y sin ajustar
- 1.4.4 Universos sin sesgo de supervivencia: deslistados, fusiones y cambios de símbolo
- 1.4.5 Construcción de barras: tiempo, ticks, volumen, dólar y desequilibrio
- 1.4.6 Series continuas de futuros: reglas de roll, ajuste por diferencia y ajuste por ratio
- 1.4.7 Almacenamiento: CSV, Parquet, particionado, DuckDB, TimescaleDB y ArcticDB
- 1.4.8 Pipeline reproducible: descarga, validación, actualización incremental y tests de calidad
1.5 · Datos fundamentales y macro
1.6 · Análisis exploratorio financiero
1.7 · Análisis técnico programado
Lo que la lista de hoy no cuenta
La manera más rápida de construir un universo para un backtest de acciones es pedir a cualquier fuente gratuita los miembros actuales de un índice y descargar la historia de cada uno. El resultado parece completo: 40 valores, once años de precios. Lo que falta no se ve, porque son las empresas que ya no están: las que quebraron, las que fueron compradas y las que salieron del índice por encogerse. Un backtest sobre ese universo responde a la pregunta «¿cuánto habría ganado eligiendo en 2015 las empresas que en 2025 seguirían en el índice?», que nadie pudo hacer en 2015.
Es el sesgo de supervivencia, y la lección 0.1.1 lo citó entre los errores clásicos. Aquí se mide. Los datos son un
índice ficticio de 40 valores entre 2015 y 2025 (tickers inventados ZX..) con tres archivos: altas y bajas de cada
miembro, los eventos que causaron cada baja y los precios de los 57 valores que pasaron por el índice, también de los
que ya no existen. Ajustados por dividendos y splits, como en la lección 1.4.3.
import numpy as np
import pandas as pd
miembros = pd.read_csv("universo_miembros.csv",
parse_dates=["alta", "baja"])
eventos = pd.read_csv("universo_eventos.csv", parse_dates=["fecha"])
precios = pd.read_parquet("universo_precios.parquet")
print(len(miembros), "periodos de pertenencia de",
miembros["ticker"].nunique(), "valores")
print(len(precios), "precios de", precios["ticker"].nunique(),
"valores,", precios["fecha"].min().date(), "a",
precios["fecha"].max().date())
miembros.tail(3)57 periodos de pertenencia de 57 valores
137177 precios de 57 valores, 2015-01-02 a 2025-12-31
ticker alta baja
54 ZXXK 2023-05-01 NaT
55 ZXQK 2024-03-18 NaT
56 ZXAI 2025-03-24 NaTCada fila de miembros es un periodo [alta, baja): el valor pertenece al índice desde el día de su alta hasta el día
anterior a su baja. Una baja vacía significa que sigue dentro. Con eso, los miembros de cualquier fecha salen de un
filtro:
def miembros_en(fecha):
fecha = pd.Timestamp(fecha)
dentro = (miembros["alta"] <= fecha) & (
miembros["baja"].isna() | (fecha < miembros["baja"]))
return set(miembros.loc[dentro, "ticker"])
inicio, fin = miembros_en("2015-01-02"), miembros_en("2025-12-31")
print("Miembros el 2015-01-02:", len(inicio))
print("Miembros el 2025-12-31:", len(fin))
print("Están en las dos listas:", len(inicio & fin))
print("Salieron:", sorted(inicio - fin))Miembros el 2015-01-02: 40
Miembros el 2025-12-31: 40
Están en las dos listas: 23
Salieron: ['ZXAO', 'ZXBA', 'ZXBY', 'ZXCD', 'ZXFA', 'ZXHX', 'ZXIR', 'ZXKB', 'ZXLU', 'ZXOJ', 'ZXOX', 'ZXRQ', 'ZXSW', 'ZXTW', 'ZXUE', 'ZXUR', 'ZXUV']De los 40 de 2015 solo 23 siguen en 2025. Los otros 17 son exactamente los que la lista de hoy no ve. Sus salidas están en el archivo de eventos, con su motivo:
print(eventos["evento"].value_counts().to_dict())
cols = ["fecha", "ticker", "evento", "ticker_nuevo"]
eventos[cols].iloc[[1, 3, 5, 6]]{'exclusion': 7, 'deslistado_quiebra': 4, 'fusion': 4, 'cambio_simbolo': 2}
fecha ticker evento ticker_nuevo
1 2016-05-16 ZXIR deslistado_quiebra NaN
3 2017-06-19 ZXRQ fusion NaN
5 2018-06-04 ZXUV cambio_simbolo ZXMA
6 2018-10-15 ZXUR fusion ZXZNDiecisiete eventos en once años, repartidos sin un patrón: siete exclusiones en revisiones trimestrales, cuatro quiebras (dos en 2020), cuatro fusiones y dos cambios de símbolo. Cada tipo pide un tratamiento distinto en los datos:
| Evento | Qué le pasa a la acción | Qué pasa en el índice | Último día para la cartera |
|---|---|---|---|
| Exclusión | Sigue cotizando, más pequeña | Sale en la revisión trimestral y entra otra | Se vende en la revisión, a precio de mercado |
| Quiebra | Deja de cotizar (o pasa a mercados sin liquidez) | Sale el día del evento | Último precio, o cero si no hay recuperación |
| Fusión en efectivo | Deja de cotizar al cerrarse la operación | Sale el día del evento | Cobra el precio de la oferta en efectivo |
| Fusión por acciones | Deja de cotizar; sus acciones se canjean | Sale el día del evento | Recibe acciones del absorbente según el canje |
| Cambio de símbolo | Sigue cotizando con otro ticker | Misma empresa, ticker nuevo | Nada: la posición sigue |
La vida de un valor en el universo, de su salida a bolsa a su final, tiene esta forma. Cualquier backtest serio tiene que poder seguirla en todos sus caminos, no solo en el de la flecha que llega al final de los datos:
Fuente Mermaid
stateDiagram-v2 [*] --> Cotiza: OPV Cotiza --> Miembro: alta Miembro --> Cotiza: exclusión Miembro --> Miembro: cambio de símbolo Miembro --> Quebrada: quiebra Miembro --> Absorbida: fusión Quebrada --> [*] Absorbida --> [*]
Un backtest point-in-time
La estrategia de prueba es la más sencilla posible, para que lo único que cambie sea el universo: cartera equiponderada entre los miembros, reequilibrada cada día. Primero, una tabla de pertenencia con una fila por sesión y una columna por valor:
cierres = precios.pivot(index="fecha", columns="ticker",
values="cierre")
dias = cierres.index
es_miembro = pd.DataFrame(False, index=dias, columns=cierres.columns)
for t, alta, baja in miembros[["ticker", "alta", "baja"]].itertuples(
index=False):
hasta = baja if pd.notna(baja) else dias[-1] + pd.Timedelta("1D")
es_miembro.loc[(dias >= alta) & (dias < hasta), t] = True
print("Miembros por sesión:", es_miembro.sum(axis=1).unique())
es_miembro.loc["2016-05-12":"2016-05-17", ["ZXIR", "ZXQZ"]]Miembros por sesión: [40]
ticker ZXIR ZXQZ
fecha
2016-05-12 True False
2016-05-13 True False
2016-05-16 False True
2016-05-17 False TrueSiempre 40. El ejemplo de la tabla es una sustitución: ZXIR quiebra y sale el 16 de mayo de 2016, y ese mismo día entra
ZXQZ. Para no usar el futuro, la cartera de un día se decide con la pertenencia conocida al cierre del día anterior
(shift(1)): se compra al cierre del día D lo que es miembro el día D y se cobra la rentabilidad del día D+1. La
versión con supervivientes usa siempre los 40 miembros de 2025.
r = cierres.pct_change(fill_method=None)
cartera = es_miembro.shift(1, fill_value=False)
anos = (len(dias) - 1) / 252 # años de 252 sesiones
def tasa_anual(rent):
return (1 + rent.fillna(0)).prod() ** (1 / anos) - 1
r_pit = r.where(cartera).mean(axis=1)
supervivientes = sorted(fin)
r_superv = r[supervivientes].mean(axis=1)
for nombre, x in [("point-in-time", r_pit),
("supervivientes", r_superv)]:
print(f"{nombre:>15}: {tasa_anual(x):.1%} anual, "
f"1 USD → {(1 + x.fillna(0)).prod():.2f} USD") point-in-time: 5.2% anual, 1 USD → 1.74 USD
supervivientes: 11.0% anual, 1 USD → 3.13 USDLa misma estrategia, el mismo periodo y los mismos precios: un 5,2 % anual con el universo que existía y un 11,0 % con el de hoy. Once años después, el dólar invertido vale 1,74 en un caso y 3,13 en el otro (las tasas anuales usan años de 252 sesiones). La figura de la cabecera dibuja las dos curvas. Ninguna regla de trading ha cambiado; solo la lista de valores, y la del segundo backtest se escribió en 2025.
De dónde sale la diferencia
El índice es por capitalización: en cada revisión trimestral entran los valores que han crecido y salen los que se han encogido. Eso deja una huella clara en los precios del año anterior a cada movimiento:
def rent_previa(t, fecha, sesiones=250):
s = cierres[t].dropna()
i = s.index.searchsorted(fecha)
return s.iloc[i - 1] / s.iloc[max(0, i - 1 - sesiones)] - 1
entran = miembros[miembros["alta"] > "2015-01-02"]
primer_precio = entran["ticker"].map(
lambda t: cierres[t].first_valid_index())
entran = entran[primer_precio < entran["alta"]] # con historia previa
ex = eventos[eventos["evento"] == "exclusion"]
previa = pd.DataFrame({
"entran": pd.Series([rent_previa(t, a) for t, a in zip(
entran["ticker"], entran["alta"])]),
"excluidos": pd.Series([rent_previa(t, f) for t, f in zip(
ex["ticker"], ex["fecha"])]),
})
print("Valores:", previa.count().to_dict())
(100 * previa).agg(["mean", "median"]).round(1)Valores: {'entran': 15, 'excluidos': 7}
entran excluidos
mean 27.1 -19.9
median 16.3 -12.7Los que entraron venían de ganar de media un 27 % en el año anterior (mediana, 16 %); los excluidos, de perder un 20 % (mediana, 13 %). (Los dos valores que entran por un cambio de símbolo no tienen historia previa con su ticker nuevo y no cuentan; por eso salen 15 entradas y no 17.) El backtest con supervivientes se queda con la subida previa a la entrada de cada valor, que el índice real no tenía porque aún no eran miembros, y se ahorra la caída de los que salieron, que el índice sí sufrió. Las quiebras son el caso extremo:
quiebras = eventos[eventos["evento"] == "deslistado_quiebra"]
filas = []
for t in quiebras["ticker"]:
s = cierres[t].dropna()
filas.append({"ticker": t, "ultima_fecha": s.index[-1].date(),
"ultimo": s.iloc[-1],
"ultimo_ano_pct":
100 * (s.iloc[-1] / s.iloc[-251] - 1)})
pd.DataFrame(filas).round(2) ticker ultima_fecha ultimo ultimo_ano_pct
0 ZXIR 2016-05-13 0.90 -96.53
1 ZXTW 2020-05-01 0.37 -97.87
2 ZXFA 2020-08-14 0.37 -95.05
3 ZXSW 2023-04-28 0.63 -97.18En su último año, las cuatro pierden entre el 95 % y el 98 %. Para el universo point-in-time son parte de la cartera hasta el último día; para el de supervivientes no existieron.
El último día: rentabilidades de deslistado
Cuando un valor deja de cotizar, el backtest tiene que decidir qué rentabilidad tiene la posición en el paso de la
última cotización a la nada. Con pct_change sobre una tabla de precios, la respuesta por defecto es «ninguna»: el día
siguiente la rentabilidad es NaN, mean la ignora y la posición desaparece con su último precio, como si se hubiera
vendido a ese precio. Para cada tipo de salida, eso es:
- Fusión en efectivo. Correcto: el último precio es el de la oferta (153,50, 400,00 y 265,00 USD en el universo) y el accionista cobra eso.
- Exclusión. Correcto: el valor sigue cotizando y se vende en la revisión.
- Fusión por acciones. Casi: el accionista no cobra, recibe acciones del absorbente. La rentabilidad del día del canje es la del absorbente aplicada a las acciones recibidas.
- Quiebra. Optimista: tras la quiebra, la acción suele dejar de negociarse en la bolsa principal y puede acabar valiendo cero. Las bases de datos de investigación (CRSP es la referencia) incluyen una rentabilidad de deslistado para no tener que suponerla; Tyler Shumway mostró en 1997 que, en los deslistados por quiebra y otras causas negativas, ese dato faltaba casi siempre y que las rentabilidades omitidas eran grandes.
La fusión por acciones del universo, ZXUR absorbida por ZXZN con un canje de 7,45 acciones, se puede comprobar:
canje = 7.45
ultimo_zxur = cierres.loc["2018-10-12", "ZXUR"]
print(f"ZXUR el 2018-10-12: {ultimo_zxur:.2f}")
print(f"7,45 × ZXZN el 2018-10-12: "
f"{canje * cierres.loc['2018-10-12', 'ZXZN']:.2f}")
r_canje = canje * cierres.loc["2018-10-15", "ZXZN"] / ultimo_zxur - 1
print(f"Rentabilidad de ZXUR el 2018-10-15, vía ZXZN: {r_canje:+.2%}")ZXUR el 2018-10-12: 171.57
7,45 × ZXZN el 2018-10-12: 171.57
Rentabilidad de ZXUR el 2018-10-15, vía ZXZN: +0.18%El último precio de ZXUR es justo 7,45 veces el de ZXZN: el mercado ya cotizaba el canje. La rentabilidad del día que falta es la de ZXZN, un +0,18 %. Dejarla fuera apenas cambia nada aquí; con un canje mal valorado o un absorbente que se mueve mucho, sí. Las quiebras importan más. La prueba más pesimista es suponer que cada quiebra acaba en cero el día de su deslistado:
r_pit_cero = r.where(cartera)
for t, f in zip(quiebras["ticker"], quiebras["fecha"]):
r_pit_cero.loc[f, t] = -1.0 # −100 % el día de la baja
print(f"Último precio: {tasa_anual(r_pit):.1%} anual")
r_cero = r_pit_cero.mean(axis=1)
print(f"Quiebras a 0: {tasa_anual(r_cero):.1%} anual")Último precio: 5.2% anual
Quiebras a 0: 4.2% anualUn punto anual menos. Cuatro quiebras en once años pesan tanto porque la cartera es equiponderada y se reequilibra a diario: hasta el último día mantiene un 2,5 % del capital en cada valor, también en el que cotiza a 0,37 USD, y perderlo entero cuesta un 2,5 % de la cartera cada vez. El universo point-in-time con quiebras a cero es la cota baja; con el último precio, la alta. El sesgo de supervivencia, en cambio, ni siquiera entra en el intervalo.
Un símbolo no es una empresa
El cuarto tipo de evento no saca a nadie del índice, pero rompe los datos unidos por ticker. ZXUV pasa a llamarse ZXMA el 4 de junio de 2018, y ZXLU pasa a ser ZXBI el 10 de enero de 2022. En la tabla de precios son cuatro columnas:
cierres.loc["2018-05-30":"2018-06-06", ["ZXUV", "ZXMA"]]ticker ZXUV ZXMA
fecha
2018-05-30 37.50 NaN
2018-05-31 37.00 NaN
2018-06-01 37.55 NaN
2018-06-04 NaN 37.11
2018-06-05 NaN 36.75
2018-06-06 NaN 37.97Por símbolo, ZXUV «deja de cotizar» un viernes y ZXMA «sale a bolsa» el lunes siguiente, a casi el mismo precio. La rentabilidad de ese lunes no existe en ninguna de las dos columnas, una media móvil de ZXMA empieza de cero y una búsqueda de «la historia de ZXMA» da cuatro años en lugar de once. Los símbolos se cambian por fusiones, cambios de nombre o de bolsa, y se reutilizan: un ticker que dejó una empresa quebrada puede aparecer años después en otra. Por eso las bases de datos serias identifican cada valor con un identificador permanente que no cambia nunca (en CRSP, el PERMNO) y guardan aparte qué símbolo tuvo en cada intervalo.
cambios = eventos[eventos["evento"] == "cambio_simbolo"]
id_de = dict(zip(cambios["ticker_nuevo"], cambios["ticker"]))
def id_permanente(ticker):
while ticker in id_de: # sigue la cadena hacia atrás
ticker = id_de[ticker]
return ticker
precios["id"] = precios["ticker"].map(id_permanente)
por_id = precios.pivot(index="fecha", columns="id", values="cierre")
print("Columnas por ticker:", cierres.shape[1],
"· por identificador:", por_id.shape[1])
s = por_id["ZXUV"]
print(f"Rentabilidad del 2018-06-04: "
f"{s['2018-06-04'] / s['2018-06-01'] - 1:+.2%}")
print("Historia de ZXMA:", s.first_valid_index().date(), "a",
s.last_valid_index().date())Columnas por ticker: 57 · por identificador: 55
Rentabilidad del 2018-06-04: -1.17%
Historia de ZXMA: 2015-01-02 a 2025-12-31Con el identificador, ZXUV y ZXMA son una sola serie de once años y el lunes del cambio tiene su rentabilidad. Aquí el identificador es el primer ticker de la cadena, que basta para dos cambios; en un sistema real se usa un número o un código que no se parezca a ningún símbolo, para que nadie lo confunda con uno.
Fuente Mermaid
flowchart LR M[Miembros por fecha] --> U[Universo en t] I[Id permanente] --> P[Precios por id] U --> B[Backtest] P --> B D[Deslistados] --> B
Cómo conseguir un universo así
Este universo es sintético y viene completo. Con datos reales, conseguirlo es la parte difícil:
- Fuentes gratuitas. Casi todas dan solo los valores que cotizan hoy; los deslistados desaparecen de la búsqueda. Sirven para aprender, no para medir una estrategia de selección de acciones (lección 1.3.2).
- Proveedores de pago. Algunos venden expresamente históricos con deslistados y composiciones históricas de índices; es una de las preguntas que hay que hacer antes de pagar (lección 1.3.8).
- Bases de investigación. CRSP, la referencia académica para EE. UU., tiene identificadores permanentes, rentabilidades de deslistado y la historia de cada símbolo.
- Construcción propia. Si solo hay datos de hoy, al menos hay que guardar desde hoy: una foto de los miembros en cada revisión y los precios de todos los valores, también de los que salen. Dentro de unos años, el universo será point-in-time desde la primera foto.
Cuando no se tiene un universo point-in-time, lo honrado es decirlo y limitar las conclusiones: una estrategia de timing sobre un índice o un ETF no sufre este sesgo; una que elige acciones dentro de una lista de hoy, sí.
Ejercicios
- Repite el backtest point-in-time reequilibrando una vez al mes (el primer día de cada mes) en lugar de cada día. ¿Cambia mucho la rentabilidad? ¿Y la diferencia con los supervivientes?
Solución
def cartera_mensual(r, pertenencia):
pesos, mes, serie = None, None, []
for dia, rd in r.fillna(0).iterrows():
if dia.month != mes: # primer día del mes
mes = dia.month
dentro = pertenencia.loc[dia] & cierres.loc[dia].notna()
pesos = dentro / max(dentro.sum(), 1) # pesos iguales
total = pesos.sum()
serie.append((pesos * rd).sum() / total if total else 0.0)
pesos = pesos * (1 + rd) # los pesos corren
return pd.Series(serie, index=r.index)
mensual_pit = cartera_mensual(r, cartera)
fijo = pd.DataFrame(False, index=dias, columns=cierres.columns)
fijo[supervivientes] = True
mensual_sup = cartera_mensual(r, fijo)
print(f"Point-in-time mensual: {tasa_anual(mensual_pit):.1%} anual")
print(f"Supervivientes mensual: {tasa_anual(mensual_sup):.1%} anual")Point-in-time mensual: 5.6% anual
Supervivientes mensual: 11.1% anualLa cartera mensual deja correr los pesos durante el mes: los valores que suben ganan peso y los que caen lo pierden, y un valor que deja de cotizar sale de la cartera con su último precio. La rentabilidad cambia algo respecto al reequilibrio diario (5,6 % frente a 5,2 % en el universo point-in-time; 11,1 % frente a 11,0 % con supervivientes), pero la distancia entre el universo real y el de supervivientes sigue siendo de varios puntos al año: el sesgo no depende de la frecuencia de reequilibrio, sino de la lista.
- Toma la lista de miembros del 2 de enero de 2015 y compra los 40 a partes iguales sin tocar nunca la cartera (comprar y mantener). ¿Qué rentabilidad anual tiene? Compárala con las dos anteriores y explica por qué esta cartera no tiene sesgo de supervivencia aunque no siga al índice.
Solución
lista_2015 = sorted(inicio)
p0 = cierres.loc["2015-01-02", lista_2015]
n = (1 / 40) / p0 # 1 USD repartido
valor_final = 0.0
for t in lista_2015:
s = por_id[id_permanente(t)].dropna()
if t == "ZXUR": # canje por ZXZN
valor_final += n[t] * canje * cierres["ZXZN"].iloc[-1]
else: # último precio conocido
valor_final += n[t] * s.iloc[-1]
print(f"1 USD → {valor_final:.2f} USD, "
f"{valor_final ** (1 / anos) - 1:.1%} anual")1 USD → 2.11 USD, 7.1% anualLa lista de 2015 es conocida en 2015: no usa el futuro. Las empresas que quiebran se quedan en la cartera con su último precio (casi cero), las compradas se convierten en su precio de compra (aquí se dejan en efectivo, sin reinvertir), ZXUR se canjea por ZXZN y ZXUV sigue como ZXMA gracias al identificador permanente. No sigue al índice, porque no incorpora a los que entran, pero es una cartera que alguien pudo tener de verdad. Que su resultado, un 7,1 % anual, quede más cerca del point-in-time (5,2 %) que del de supervivientes (11,0 %) confirma que la diferencia de este último no es una cuestión de método, sino de información del futuro.
- Un compañero hace un backtest de «comprar las 10 acciones del S&P 500 con mayor momentum cada mes» con la lista de miembros que publica hoy una web gratuita. Explica qué sesgos tiene y qué datos necesitaría para quitarlos.
Solución
Tiene sesgo de supervivencia (solo ve las empresas que siguen en el índice hoy, no las que quebraron, fueron compradas o salieron) y un sesgo de anticipación en la selección: las empresas que entraron en el índice durante el periodo aparecen como elegibles antes de su entrada, justo cuando venían de subir, que es lo que busca una estrategia de momentum. Necesitaría la composición histórica del índice (altas y bajas con fecha), precios de todos los valores que pasaron por él, incluidos los deslistados, con su rentabilidad de deslistado, y un identificador permanente para seguir los cambios de símbolo. Sin ellos, el resultado no dice nada sobre la estrategia.
Respuesta plegada en cada pregunta
Autoevaluación
Un backtest usa los 40 miembros actuales de un índice con toda su historia desde 2015. ¿Qué error comete?
- ANinguno, si los precios están ajustados
- BUsa información del futuro: elige en 2015 las empresas que se sabe que en 2025 seguirán en el índice, y deja fuera las que quebraron o salieron
- CSolo uno de rendimiento: tarda más en calcularse
- DUsa demasiados valores
Ver respuesta
Respuesta B. Usa información del futuro: elige en 2015 las empresas que se sabe que en 2025 seguirán en el índice, y deja fuera las que quebraron o salieron
La lista de hoy es el resultado de once años de selección: entraron las que subieron y salieron las que cayeron o desaparecieron. Usarla en 2015 es saber el futuro. En el índice del curso, la rentabilidad pasa de 5,2 % a 11,0 % anual.
En el índice del curso, ¿qué hicieron de media los valores que entraron, en el año anterior a entrar?
- ACayeron con fuerza
- BLo mismo que el índice
- CSubieron con fuerza: por eso entraron (más capitalización)
- DNo se puede saber
Ver respuesta
Respuesta C. Subieron con fuerza: por eso entraron (más capitalización)
Un índice por capitalización incorpora a las empresas que han crecido y expulsa a las que se han encogido. Un backtest con supervivientes se queda la subida previa a la entrada, que el índice real no tuvo, y se ahorra la caída previa a la salida, que sí tuvo.
Una acción del universo quiebra y su última cotización es 0,37 USD. El proveedor deja de dar precios. ¿Qué supone un backtest que ignora el día siguiente?
- AQue la posición se vendió a 0,37
- BQue la posición vale cero
- CQue la acción sigue cotizando
- DNada
Ver respuesta
Respuesta A. Que la posición se vendió a 0,37
Al no haber rentabilidad el día siguiente, la posición desaparece de la cartera con el último precio, como si se hubiera vendido a 0,37. Una quiebra puede acabar en cero para el accionista; con −100 % el día del deslistado, la cartera point-in-time del curso pasa de 5,2 % a 4,2 % anual.
ZXUV pasa a llamarse ZXMA el 2018-06-04. ¿Qué ocurre si el backtest une los datos por símbolo?
- ANada: el proveedor ya lo resuelve
- BZXUV parece deslistada y ZXMA una empresa nueva sin historia: se pierde la rentabilidad del día del cambio y cualquier indicador de ZXMA empieza de cero
- CSe duplica la empresa
- DEl backtest falla con un error
Ver respuesta
Respuesta B. ZXUV parece deslistada y ZXMA una empresa nueva sin historia: se pierde la rentabilidad del día del cambio y cualquier indicador de ZXMA empieza de cero
El símbolo no identifica a la empresa: se reutiliza, cambia con fusiones y cambios de nombre. Con un identificador permanente, ZXUV y ZXMA son una sola serie continua; por símbolo, son dos trozos sin relación.
Se repite el backtest con supervivientes reequilibrando una vez al mes en lugar de cada día. ¿Desaparece el sesgo?
- ASí: el sesgo viene del reequilibrio diario
- BNo: la rentabilidad apenas cambia (11,1 % frente a 11,0 %); el sesgo viene de la lista de valores, no de la frecuencia
- CSe invierte: los supervivientes rinden menos
- DDepende de los costes
Ver respuesta
Respuesta B. No: la rentabilidad apenas cambia (11,1 % frente a 11,0 %); el sesgo viene de la lista de valores, no de la frecuencia
La frecuencia de reequilibrio cambia algo el resultado de las dos carteras, pero la distancia entre ellas sigue siendo de más de cinco puntos al año. Lo que sesga el resultado es haber elegido los valores con información de 2025.
Ideas clave
- Un universo point-in-time es el conjunto de valores que se conocían en cada fecha: miembros con su alta y su baja, y precios de todos ellos, también de los que ya no existen.
- Probar con los supervivientes de hoy usa el futuro. En el índice del curso: 5,2 % anual point-in-time frente a 11,0 % con supervivientes.
- El sesgo sale de la selección: los que entraron venían de subir (+27 % de media en el año previo) y los excluidos de caer (−20 %).
- Cada salida necesita su rentabilidad final: quiebra (posiblemente −100 %), fusión en efectivo (el precio de la oferta), fusión por acciones (las acciones del absorbente).
- Los símbolos cambian y se reutilizan: une los datos por un identificador permanente, nunca por el ticker.