Quant500 Academy

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 error

Si 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

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.

El resto de Quant500

Lo que estás aprendiendo, aplicado al índice todos los días:

La cartera del modelo, cada semana en tu correo

Cada sábado, su semana en un minuto: cómo fue, lo mejor y lo peor, y qué directivos compraron con su dinero. Y el día que el modelo se rehace, qué entra y qué sale. Es una simulación, no un consejo. Como mucho un correo a la semana, sin publicidad y sin cuenta; te das de baja con un clic. Lo hace una persona: Pol Gracia.

Cartera modelo con resultados simulados, no una recomendación personalizada. Tu correo se usa solo para esto: privacidad.

Detrás de la Academia y del resto de Quant500 hay una sola persona: Pol Gracia Serrahima.