2. Valores atípicos: una errata o un movimiento real
Nivel 4: Limpieza y validación
Una acción marca un menos 40% en un solo día. Puede ser un decimal que el proveedor colocó donde no debía, y entonces es basura que contamina todo lo que calcules encima. O puede ser una opa fallida, un ensayo clínico que sale mal, una suspensión de pagos, y entonces es exactamente el tipo de día que tu estrategia tiene que saber atravesar. Tratar los dos casos igual arruina la serie en las dos direcciones.
El coste de equivocarse va en los dos sentidos
Si te tragas la errata, la volatilidad estimada se dispara, cualquier modelo de reversión ve una oportunidad enorme donde no hay nada, y el backtest compra un desplome que nunca ocurrió, cobrando un rebote igual de ficticio.
Si borras el movimiento real, es peor, porque el daño no se nota. La serie queda limpia y suave, el ratio de Sharpe mejora, y has eliminado precisamente las sesiones donde una estrategia se juega la supervivencia. El 19 de octubre de 1987 el S&P 500 cayó en torno a un 20% en una sola sesión; el 6 de mayo de 2010, el llamado desplome relámpago, los índices estadounidenses se hundieron alrededor de un 9% intradía y recuperaron casi todo en minutos. Un filtro que elimine sin preguntar todo lo que supere el 15% diario borra el primero, y uno que limpie por rango intradía se come el segundo. Ninguno de los dos era un error de datos.
Cuatro filtros baratos que cazan casi todo
Ninguno decide nada por sí solo: cada uno levanta una bandera que un humano o una regla explícita resolverá después.
- Umbral sobre la rentabilidad. No un número absoluto para todo el universo, sino relativo a la volatilidad histórica del propio valor. Un 8% es rutina en una biotecnológica y es una anomalía en una eléctrica regulada.
- Coherencia interna de la vela. Máximo mayor o igual que mínimo, apertura y cierre dentro de ese rango. Son identidades contables: si fallan, no hay interpretación posible, hay una errata.
- Volumen y precio en desacuerdo. Volumen cero con cierre distinto al del día anterior significa que alguien inventó un precio, o que el volumen viene mal. Las dos posibilidades merecen una bandera.
- Comparación entre fuentes. Es el filtro más caro y el más concluyente. Dos proveedores independientes rara vez cometen la misma errata; una opa la recogen los dos.
import numpy as np
df = df.sort_index()
df["ret"] = df["cierre"].pct_change()
# Umbral relativo: cuantas desviaciones tipicas del propio valor, no del mercado.
vol = df["ret"].rolling(60, min_periods=30).std().shift(1)
df["z"] = df["ret"] / vol
df["f_retorno"] = df["z"].abs() > 8
df["f_imposible"] = (df["cierre"] <= 0) | (df["ret"] <= -1)
df["f_ohlc"] = (
(df["maximo"] < df["minimo"])
| (df["cierre"] > df["maximo"]) | (df["cierre"] < df["minimo"])
| (df["apertura"] > df["maximo"]) | (df["apertura"] < df["minimo"])
)
df["f_volumen"] = (df["volumen"] == 0) & (df["cierre"] != df["cierre"].shift(1))
banderas = [c for c in df.columns if c.startswith("f_")]
df["sospechosa"] = df[banderas].any(axis=1)
print(df[banderas].sum())Fíjate en dos detalles del código. El primero, que la volatilidad de referencia lleva un desplazamiento de una posición: se calcula con datos anteriores a la sesión que se está juzgando, para que el propio movimiento sospechoso no infle la vara con la que se le mide. El segundo, que la comprobación de la vela incluye la apertura. Es el olvido más frecuente de todos: se escriben las tres comparaciones obvias sobre el cierre, el filtro parece completo, y el fichero pasa el control con la errata intacta en la columna que nadie miró.
Confirmar contra una segunda fuente
# Dos fuentes alineadas por fecha. Si ambas ven el mismo salto, es real.
comp = principal[["cierre"]].join(secundaria[["cierre"]],
how="inner", lsuffix="_a", rsuffix="_b")
comp["dif_rel"] = (comp["cierre_a"] - comp["cierre_b"]).abs() / comp["cierre_b"]
# Discrepan mas de un 2%: una de las dos se equivoca, y hay que mirarla.
comp["discrepan"] = comp["dif_rel"] > 0.02
# Un salto grande que las DOS fuentes recogen deja de ser sospechoso.
df.loc[df.index.isin(comp.index[~comp["discrepan"]]), "f_retorno"] = FalseEsta confirmación cruzada convierte una sospecha en un diagnóstico, pero tiene un límite: cuando las dos fuentes discrepan sabes que una se equivoca, no cuál. El desempate suele ser barato: si el precio nuevo persiste al día siguiente en ambas, el salto fue real y la discrepancia era un retraso de actualización.
Marcar y registrar, jamás borrar en silencio
La regla no admite excepciones: una fila sospechosa se marca, se registra y se decide con criterio explícito, nunca desaparece sin dejar rastro. El motivo es reproducibilidad, no delicadeza. Si borras en silencio, dentro de seis meses nadie, tú incluido, podrá explicar por qué la serie tiene 2.487 filas en lugar de 2.503.
incidencias = df.loc[df["sospechosa"], ["cierre", "ret", "z", "volumen"] + banderas]
incidencias = incidencias.assign(ticker=ticker, revisado_el=pd.Timestamp.utcnow())
incidencias.to_csv("incidencias.csv", mode="a", header=False)
# La serie limpia se DERIVA, no sustituye. El original se conserva siempre.
df["cierre_limpio"] = df["cierre"].where(~df["f_imposible"] & ~df["f_ohlc"])Ejemplo trabajado: el umbral que mejora el Sharpe sin mejorar nada
Imagina un panel de 500 valores y 2.500 sesiones: 1,25 millones de observaciones. Un filtro que elimine toda sesión con rentabilidad por encima del 15% en valor absoluto quizá toque unas 1.200 filas, menos del 0,1% del panel. Suena inofensivo, y ese es exactamente el argumento con el que se cuela: nadie discute un cambio que afecta a una milésima parte de los datos.
Pero el porcentaje de filas es la métrica equivocada. Si de esas mil doscientas, mil eran movimientos reales, has borrado mil desplomes y mil saltos verdaderos, y esas son las sesiones que concentran casi toda la pérdida potencial de la estrategia. El ratio de Sharpe subirá porque el denominador ha encogido, no porque el sistema haya mejorado. La forma barata de comprobarlo es ejecutar el backtest dos veces, con la serie original y con la limpia, y mirar la diferencia en el peor día y en la caída máxima acumulada, no en la rentabilidad media. Si la caída máxima mejora mucho más que la rentabilidad, no has limpiado datos: has quitado el riesgo del papel sin quitarlo del mercado. Regla práctica: cuando una limpieza mejora mucho el resultado, sospecha de la limpieza antes que del mercado.