---
title: "1.4.4 Sesgo de supervivencia y universos point-in-time"
description: "Miembros de un índice en cada fecha, quiebras, fusiones y cambios de símbolo: un backtest rinde 5,2 % anual point-in-time y 11,0 % con supervivientes."
url: "https://pythonparatrading.com/curso-python-trading/nivel-1/sesgo-de-supervivencia-y-point-in-time/"
lang: "es"
image: "https://pythonparatrading.com/og/curso-python-trading/nivel-1/sesgo-de-supervivencia-y-point-in-time.png"
---

# 1.4.4 · 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.

- Curso: [Nivel 1: Datos financieros, mercados y análisis](https://pythonparatrading.com/curso-python-trading/nivel-1/)
- Módulo: 1.4 · Ingeniería de datos financieros
- Duración: 55 min
- Requisitos: [1.4.3 · Ajustes por splits y dividendos: precios ajustados y sin ajustar](https://pythonparatrading.com/curso-python-trading/nivel-1/ajustes-por-splits-y-dividendos/), [0.5.3 · Selección, filtrado, groupby, merge, pivot, resample y ventanas móviles](https://pythonparatrading.com/curso-python-trading/nivel-0/pandas-groupby-merge-resample-y-rolling/)

## Al terminar esta lección

- Reconstruir los miembros de un índice en cualquier fecha a partir de sus altas y bajas, y hacer un backtest que use solo los conocidos en cada momento.
- Medir el sesgo de supervivencia comparando el universo point-in-time con los supervivientes de hoy, y explicar de dónde sale la diferencia.
- Tratar quiebras, fusiones en efectivo y por acciones: qué rentabilidad tiene el último día y qué supone dejarla fuera.
- Seguir a una empresa a través de un cambio de símbolo con un identificador permanente.

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

```python
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)
```
```output
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  NaT
```

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

```python
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))
```
```output
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:

```python
print(eventos["evento"].value_counts().to_dict())
cols = ["fecha", "ticker", "evento", "ticker_nuevo"]
eventos[cols].iloc[[1, 3, 5, 6]]
```
```output
{'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         ZXZN
```

*Fig. — Los 17 eventos del universo del curso entre 2015 y 2025, por tipo.* Línea de tiempo con cuatro filas. Siete exclusiones repartidas entre 2015 y 2025 (ZXUE, ZXOX, ZXBA, ZXAO, ZXHX, ZXCD, ZXKB); cuatro quiebras, destacadas (ZXIR en 2016, ZXTW y ZXFA en 2020, ZXSW en 2023); cuatro fusiones (ZXRQ, ZXUR, ZXOJ, ZXBY); y dos cambios de símbolo (ZXUV en 2018, ZXLU en 2022).

Diecisiete 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:

```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:

```python
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"]]
```
```output
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   True
```

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

```python
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")
```
```output
  point-in-time: 5.2% anual, 1 USD → 1.74 USD
 supervivientes: 11.0% anual, 1 USD → 3.13 USD
```

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

```python
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)
```
```output
Valores: {'entran': 15, 'excluidos': 7}
        entran  excluidos
mean      27.1      -19.9
median    16.3      -12.7
```

*Fig. — Rentabilidad en las 250 sesiones anteriores a entrar en el índice, a ser excluido y a quebrar.* Tres filas de puntos. Los 15 valores que entran venían de ganar de media un 27,1 % (destacado), con uno por encima del 120 %. Los 7 excluidos venían de perder de media un 19,9 %. Las 4 quiebras perdieron entre el 95 % y el 98 % en su último año.

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

```python
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)
```
```output
  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.18
```

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

<aside class="callout" data-kind="nota">

El sesgo no viene solo de los perdedores. Las tres empresas compradas en efectivo (ZXRQ, ZXOJ, ZXBY) también faltan en
la lista de hoy, y una compra suele llegar con prima: son salidas buenas. En este universo pesan menos que las quiebras
y las exclusiones, pero en un índice real el signo del sesgo no se puede suponer: se mide.

</aside>

## 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:

```python
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%}")
```
```output
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:

```python
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")
```
```output
Último precio:  5.2% anual
Quiebras a 0:   4.2% anual
```

Un 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:

```python
cierres.loc["2018-05-30":"2018-06-06", ["ZXUV", "ZXMA"]]
```
```output
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.97
```

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

```python
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())
```
```output
Columnas por ticker: 57 · por identificador: 55
Rentabilidad del 2018-06-04: -1.17%
Historia de ZXMA: 2015-01-02 a 2025-12-31
```

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

```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

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

```python
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")
```
```output
Point-in-time mensual:  5.6% anual
Supervivientes mensual: 11.1% anual
```

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

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

```python
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")
```
```output
1 USD → 2.11 USD, 7.1% anual
```

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

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

## Autoevaluación

1. Un backtest usa los 40 miembros actuales de un índice con toda su historia desde 2015. ¿Qué error comete?

   - A. Ninguno, si los precios están ajustados
   - 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
   - C. Solo uno de rendimiento: tarda más en calcularse
   - D. Usa demasiados valores

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

2. En el índice del curso, ¿qué hicieron de media los valores que entraron, en el año anterior a entrar?

   - A. Cayeron con fuerza
   - B. Lo mismo que el índice
   - C. Subieron con fuerza: por eso entraron (más capitalización)
   - D. No se puede saber

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

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

   - A. Que la posición se vendió a 0,37
   - B. Que la posición vale cero
   - C. Que la acción sigue cotizando
   - D. Nada

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

4. ZXUV pasa a llamarse ZXMA el 2018-06-04. ¿Qué ocurre si el backtest une los datos por símbolo?

   - A. Nada: el proveedor ya lo resuelve
   - 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
   - C. Se duplica la empresa
   - D. El backtest falla con un error

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

5. Se repite el backtest con supervivientes reequilibrando una vez al mes en lugar de cada día. ¿Desaparece el sesgo?

   - A. Sí: el sesgo viene del reequilibrio diario
   - B. No: la rentabilidad apenas cambia (11,1 % frente a 11,0 %); el sesgo viene de la lista de valores, no de la frecuencia
   - C. Se invierte: los supervivientes rinden menos
   - D. Depende de los costes

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

## Recursos

### Research

- [Introduccion al Quant Research](https://pythonparatrading.com/quant-research/) · Research-0023 · 8 min
- [Big Data Management 4 - Utilizando el datalake en Zipline](https://pythonparatrading.com/backtesting-con-zipline-y-articdb/) · Research-0022 · 5 min

### Documentación

- [CRSP US Stock Databases (identificadores permanentes PERMNO y PERMCO)](https://www.crsp.org/research/crsp-us-stock-databases/) · crsp.org
- [Tyler Shumway, «The Delisting Bias in CRSP Data», Journal of Finance 52(1), 1997](https://ideas.repec.org/a/bla/jfinan/v52y1997i1p327-40.html) · ideas.repec.org

### Datos

- [Datos del nivel 1: miembros del índice (CSV)](https://pythonparatrading.com/curso/datos/nivel-1/universo_miembros.csv) · Descarga · universo_miembros.csv
- [Datos del nivel 1: eventos del universo (CSV)](https://pythonparatrading.com/curso/datos/nivel-1/universo_eventos.csv) · Descarga · universo_eventos.csv
- [Datos del nivel 1: precios del universo (Parquet)](https://pythonparatrading.com/curso/datos/nivel-1/universo_precios.parquet) · Descarga · universo_precios.parquet

Lección anterior: [1.4.3 · Ajustes por splits y dividendos: precios ajustados y sin ajustar](https://pythonparatrading.com/curso-python-trading/nivel-1/ajustes-por-splits-y-dividendos/)

Lección siguiente: [1.4.5 · Construcción de barras: tiempo, ticks, volumen, dólar y desequilibrio](https://pythonparatrading.com/curso-python-trading/nivel-1/barras-de-tiempo-ticks-volumen-y-dolar/)
