4. Reconstruir una serie ajustada y comprobar que está bien
Nivel 3: Ajustes y operaciones societarias
Sabes qué eventos rompen una serie y qué factor corrige cada uno. Falta la parte que nadie enseña y que separa un pipeline de datos de un montón de scripts: construir la serie ajustada desde cero y después demostrar que está bien. No suponerlo. Demostrarlo.
El factor acumulado
La idea es una sola: cada evento aporta un factor menor o mayor que uno que se aplica a todo lo anterior. Con varios eventos, los factores se multiplican, y la serie ajustada se ancla en el último precio publicado para que el extremo derecho siga siendo un precio negociable de verdad.
import numpy as np
import pandas as pd
def factor_acumulado(indice, eventos):
"""Devuelve el factor a aplicar a cada fecha. Vale 1 en el ultimo tramo."""
factor = pd.Series(1.0, index=indice, dtype="float64")
for fecha, f in zip(eventos["fecha"], eventos["factor"]):
factor[indice < fecha] *= f
return factor
def ajustar(precios, volumen, eventos):
f = factor_acumulado(precios.index, eventos)
# El precio se multiplica por el factor; el volumen, por su inverso.
return precios * f, volumen / f
eventos = pd.DataFrame({
"fecha": pd.to_datetime(["2024-06-10", "2024-09-19"]),
"tipo": ["split", "dividendo"],
"factor": [0.5, 0.9880952],
})Fíjate en dos decisiones. Primera: el bucle recorre los eventos en cualquier orden y el resultado es el mismo, porque la multiplicación conmuta; si tu código depende del orden, hay un bug. Segunda: el volumen se divide entre el mismo factor, no se multiplica. Con un split dos por uno el factor es 0,5, los precios antiguos se dividen entre dos y los volúmenes antiguos se multiplican por dos.
Comprobación 1: la rentabilidad ajustada es la rentabilidad del accionista
Esta es la única que de verdad importa. Reconstruyes, evento a evento y a partir de los datos crudos, lo que ganó o perdió el accionista cada día, y exiges que coincida con lo que devuelve la serie ajustada.
def retorno_economico(precios, dividendos, splits):
"""Rentabilidad diaria reconstruida desde los datos crudos."""
d = dividendos.reindex(precios.index).fillna(0.0)
r = splits.reindex(precios.index).fillna(1.0) # razon del desdoblamiento de ese dia
return (precios * r + d) / precios.shift(1) - 1
def auditar(ajustada, precios, dividendos, splits, tolerancia=2e-4):
r_ajustada = ajustada.pct_change()
r_real = retorno_economico(precios, dividendos, splits)
dif = (r_ajustada - r_real).abs().dropna()
return dif[dif > tolerancia].sort_values(ascending=False)Un matiz que conviene conocer: el factor habitual del dividendo (uno menos el dividendo entre el cierre previo) y el retorno total aditivo no dan exactamente lo mismo. Coinciden de forma exacta solo si el precio cae justo el importe repartido; en cualquier otro caso difieren en un término de segundo orden que, para un dividendo normal, se queda por debajo de un punto básico. Por eso la tolerancia del ejemplo es 2e-4 y no cero. Lo que no puede aparecer nunca son diferencias de puntos porcentuales: eso ya no es convención, es un factor mal aplicado.
Comprobación 2: ningún salto sin dueño
def saltos_sin_explicacion(ajustada, eventos, umbral=0.25):
r = ajustada.pct_change()
fechas_evento = set(pd.to_datetime(eventos["fecha"]))
grandes = r[r.abs() > umbral]
return grandes[[d not in fechas_evento for d in grandes.index]]Todo movimiento diario grande tiene que tener detrás un evento de tu tabla o una noticia real. Los candidatos habituales de esta lista son un split que tu proveedor no reportó, un dividendo extraordinario, una escisión sin ajustar y un símbolo reasignado a otra compañía. Un dato adicional ayuda a discriminar: un salto de escala no viene acompañado de volumen anómalo, mientras que un desplome auténtico casi siempre multiplica el volumen.
Comprobación 3: coherencia entre precio y volumen
Si el ajuste está bien hecho, el importe negociado (precio por volumen) tiene que ser invariante: los factores se cancelan, porque uno multiplica y el otro divide.
def coherencia_importe(px_aj, vol_aj, px_crudo, vol_crudo, tolerancia=1e-6):
importe_aj = px_aj * vol_aj
importe_crudo = px_crudo * vol_crudo
error = ((importe_aj - importe_crudo) / importe_crudo).abs().max()
assert error < tolerancia, "El volumen no se ajusto con el inverso del factor"
return errorSi el importe negociado del periodo anterior a un split cinco por uno vale la quinta parte del original, el diagnóstico es inmediato: ajustaste el precio y dejaste el volumen intacto. Es una comprobación barata y sorprendentemente eficaz, porque atrapa el olvido más frecuente de todo el proceso y lo hace sin depender de ninguna fuente externa: la propia serie cruda es el testigo.
Comprobación 4: las invariantes baratas
- El último valor de la serie ajustada coincide con el último cierre publicado, y el factor de ese tramo es exactamente 1.
- El factor no contiene ceros, ni valores negativos, ni valores no definidos en ninguna fecha del índice.
- Ajustar dos veces con la misma tabla de eventos da el mismo resultado que ajustar una: el proceso tiene que ser idempotente.
- El número de filas no cambia. Un ajuste que borra o añade sesiones no es un ajuste.
Caso real: NVIDIA, 10 de junio de 2024
NVIDIA ejecutó un desdoblamiento diez por uno con efecto el 10 de junio de 2024. El cierre del 7 de junio superaba los 1.200 dólares; el del 10 de junio quedó en torno a los 122. Sobre la serie cruda, eso es una caída del orden del 90% en una sesión.
Corre la auditoría sobre ese día. Sin ajustar, el retorno diario da aproximadamente menos 0,90 y el retorno económico reconstruido da alrededor de más 0,007: una diferencia de más de 90 puntos porcentuales, imposible de confundir con una cuestión de convención. Con el factor 0,1 aplicado a todo el histórico anterior, ambas cifras convergen y la comprobación del importe negociado sale plana. Ese contraste (más de noventa puntos frente a menos de un punto básico) es exactamente lo que una auditoría automática debe distinguir sin que nadie mire el gráfico.