Cómo Calcular la Caída de Precio Más Rápida en Series Temporales
Detección eficiente de desplomes en trading algorítmico con Python
Por HolaGPT Editorial · 10 min lectura · 2026-08-01
Los precios de activos financieros, criptomonedas o incluso métricas de negocio caen. A veces de forma gradual, otras en desplomes verticales. Saber cuál fue la caída más rápida desde un punto alto hasta un mínimo posterior no es solo una curiosidad técnica: es una señal de riesgo, un disparador de alertas y un filtro para estrategias de trading.
La pregunta parece simple: dada una serie de precios, encontrar el par (máximo, mínimo) donde el mínimo ocurre después del máximo y la diferencia de tiempo es la menor posible. Pero la definición es engañosa. Una caída del 0.5% en dos segundos puede ser más relevante que una del 50% en un año. Este artículo te guía en el diseño de un algoritmo eficiente, desde la intuición matemática hasta el código Python listo para producción.
Usaremos datos tick a tick del S&P 500 y mostraremos cómo manejar ruido, gaps de mercado y condiciones reales. Al final, tendrás una herramienta para backtesting y monitoreo en vivo. Y si necesitas adaptarlo a tu dominio, HolaGPT puede asistirte en la generación de variantes y pruebas unitarias.
Definiendo el problema: no es solo un máximo y un mínimo
La consulta original: “¿Cuál es la duración de la disminución de precio más rápida de alto a bajo?” implica dos cosas: un punto de partida alto y un punto de llegada bajo, con el bajo ocurriendo después. Pero necesitamos precisar:
- Diferencia de precio: (P_alto - P_bajo) / P_alto suele ser la medida relevante, no la absoluta.
- Diferencia temporal: en segundos, minutos, ticks. La duración mínima.
- Restricción de precedencia: el máximo debe ocurrir antes que el mínimo.
Un primer intento: para cada i (máximo), buscar el primer j > i tal que P[j] < P[i] y la caída porcentual supere un umbral. Pero eso sería O(n²). Si tenemos millones de ticks, no escala. Además, una caída rápida no necesariamente empieza en un máximo absoluto, sino en un máximo local seguido de un desplome.
Por ejemplo, en el flash crash del 6 de mayo de 2010, el S&P 500 perdió un 9% en 5 minutos. Si tomamos todo el día, el máximo estuvo mucho antes, y la duración se dilata. Queremos capturar la microestructura: el sprint bajista más intenso.
Planteamiento formal y primera optimización
Sea una serie de precios P[0..n-1] en intervalos regulares (ticks, segundos). Queremos encontrar (i, j) con i < j que minimice (timestamp[j] - timestamp[i]) sujeto a (P[i] - P[j]) / P[i] >= umbral (ej. 1%). Si ignoramos el umbral y buscamos simplemente la pareja con máxima caída absoluta en tiempo mínimo, podemos definir una función de costo: minimizar dt = t_j - t_i, con la condición P[j] < P[i].
Observación clave: si fijamos j, nos interesa el máximo precio en algún instante anterior i, pero queremos minimizar la distancia temporal. Esto sugiere una ventana deslizante: para cada j, mantener el máximo más reciente dentro de una ventana que cumpla el umbral. Sin embargo, la ventana no es fija, porque nos interesa la caída más rápida, no la mayor caída en una ventana.
Un enfoque correcto: recorrer la serie manteniendo un puntero al último “máximo significativo” que esté por encima de P[j] en al menos el umbral. Almacenamos ese máximo junto con su timestamp. Cuando encontramos un precio más alto, actualizamos ese máximo. Si encontramos un precio que cae por debajo del umbral respecto a ese máximo, calculamos duración. Pero hay que tener cuidado: un nuevo máximo puede anular uno anterior aunque esté más lejos. Como queremos duración mínima, una vez que ocurre una caída válida, registramos su duración y podemos reiniciar la referencia.
Algoritmo O(n) con un barrido
La solución más elegante usa una pila o simplemente una variable que almacena el mejor candidato a máximo y su timestamp. Algoritmo:
- Inicializar max_price = P[0], max_time = t[0].
- Para cada tick i desde 1 hasta n-1:
- Si P[i] > max_price, actualizar max_price = P[i], max_time = t[i].
- Si (max_price - P[i]) / max_price >= umbral:
- Calcular duracion = t[i] - max_time.
- Actualizar mínimo global de duración si corresponde.
- Opcional: reiniciar max_price al precio actual para buscar la siguiente caída.
Pero este método tiene un defecto: ignora que el máximo puede no ser el más reciente. Ejemplo: P = [100, 101, 99]. max_price se actualiza a 101 en t=2, luego en t=3 vemos 99, caída del 1.98% (si umbral=1%). Duración 1 tick. Parece correcto. Sin embargo, si P = [100, 99.5, 101], en t=1 max=100, t=2: 99.5, caída 0.5% no supera umbral, no registramos. Luego en t=3 max se actualiza a 101. Perdimos la oportunidad de ver que desde el máximo 100 hubo una caída previa a 99.5, pero no superaba el umbral, así que no es relevante. La condición exige caída > umbral, por lo que no se registra. ¿Qué tal si hay una caída profunda usando un máximo anterior más alejado? Por ejemplo, P = [101, 100, 95]. max_price se inicializa en 101, luego baja a 100 (<101), no se actualiza. Luego 95, caída 5.9% desde 101, duración 2. Correcto. Si en cambio: P = [101, 99, 100, 95]. max_price=101. t=2: 99 (caída 1.98% >1%), duración 1. Actualizamos reiniciando max_price=99? Depende de reinicio. Si reiniciamos, perdemos el 101 para el futuro 95 (que da duración 3). Pero ya encontramos una caída más rápida (duración 1 vs 3), así que no importa. El mínimo global sigue siendo 1. Si el umbral es menor, ese 101->99 es el más rápido. Si queremos todas las caídas que cumplen el umbral y no solo la primera de cada máximo, necesitamos un enfoque distinto.
No obstante, la pregunta original se centra en la duración más rápida. El algoritmo anterior con reinicio captura la secuencia de caídas que parten del máximo actual y registra la primera vez que se cruza el umbral desde el máximo vigente, porque cualquier caída posterior desde ese mismo máximo tendrá mayor duración (el tiempo solo aumenta). Por tanto, solo nos interesa la primera vez que se supera el umbral desde cada máximo. Si aparece un máximo más alto después, ese puede generar una caída más rápida con una duración menor, y reiniciamos. Así que es correcto y O(n).
Implementación en Python con datos reales
Vamos a implementar una función que recibe listas de precios y timestamps (en unidades comparables, ej. segundos desde epoch) y un umbral fraccional (0.01 para 1%). Devuelve la mínima duración y los índices correspondientes.
def fastest_drop(timestamps, prices, threshold=0.01):
n = len(prices)
if n < 2:
return None
best_duration = float('inf')
best_pair = None
max_price = prices[0]
max_time = timestamps[0]
for i in range(1, n):
price = prices[i]
time = timestamps[i]
if price > max_price:
max_price = price
max_time = time
else:
drop = (max_price - price) / max_price
if drop >= threshold:
duration = time - max_time
if duration < best_duration:
best_duration = duration
best_pair = (max_time, time, max_price, price, drop)
# Reiniciar el máximo al precio actual para buscar caídas futuras
max_price = price
max_time = time
return best_duration, best_pairProbemos con datos sintéticos:
timestamps = [1, 2, 3, 4, 5, 6]
prices = [100, 102, 101, 98, 97, 96]
dur, pair = fastest_drop(timestamps, prices, 0.02)
print(dur, pair) # Salida: 1 (3, 4) -> de 102 a 98 (3.9% en 1 tick)Funciona. Sin embargo, el reinicio inmediato cuando se detecta una caída puede ser prematuro. Si el precio sigue cayendo, el nuevo máximo es el precio del tick donde se disparó la caída, que es menor que el máximo original. Pero la duración desde el máximo original hubiera sido mayor, así que no afecta el mínimo. Si luego el precio sube y vuelve a caer, se capturaría otra caída. Por ejemplo, P = [100, 90, 95, 85]. Umbral 10%. tick1=100, tick2=90 (caída 10%, dur=1). Reiniciamos max=90 en tick2. tick3=95 >90, actualiza max=95. tick4=85 desde 95 caída 10.5%, dur=1 (tick4 - tick3=1). La caída desde 100 a 85 habría durado 3, pero no la vemos. Pero ya tenemos una caída de dur=1, que es mejor. Si el umbral fuera 15%, la primera caída no alcanza (10%), no se reinicia max, y en tick4 100->85 (15%) dur=3. El algoritmo lo detecta porque no hubo reinicio. Así que el reinicio solo al superar umbral es seguro.
Pero hay un caso patológico: P = [100, 101, 90]. tick1 max=100, tick2 max=101, tick3 caída 10.89% desde 101, dur=1. Detectada. Ahora P = [100, 101, 99, 90]. tick1=100, tick2=101, tick3=99 (caída 1.98% desde 101, si umbral=2% se registra dur=1, reinicia max=99, luego tick4=90 desde 99 caída 9.09% dur=1). La caída más rápida es 1. Pero si umbral es mayor, ej. 5%, tick3 no alcanza, no reinicia, se mantiene max=101, luego tick4 90 da dur=2. El algoritmo registra 2 como mejor duración. ¿Hay una caída más rápida? 101->99 dura 1 pero no supera umbral, así que no interesa. Correcto.
Así que el algoritmo es robusto para el objetivo de encontrar la duración mínima de una caída que supere un umbral fijado. Complejidad O(n), memoria O(1).
Consideraciones con datos ruidosos y outliers
Los precios tick a tick contienen microfluctuaciones. Un pico espurio de un centavo puede generar una falsa caída rápida. Para evitarlo, se suelen usar precios mid, filtrar ticks con spread anormal o suavizar mínimamente. Una técnica es aplicar una media móvil exponencial ligera y trabajar con ese precio. Otra: exigir una confirmación con un mínimo de ticks o un volumen mínimo. Por ejemplo, solo considerar caídas que persistan al menos 2 ticks por debajo del umbral. Modificaríamos el algoritmo para que, al detectar caída, no registre inmediatamente sino que espere un tick extra de confirmación (si el precio sigue por debajo del umbral desde el mismo máximo).
Incorporación de volumen: un desplome rápido con bajo volumen puede ser manipulación o falta de liquidez. Se puede ponderar por volumen o exigir un volumen mínimo durante la caída. Esto requiere almacenar el volumen acumulado entre el máximo y el precio actual, e integrarlo en la condición.
Aplicaciones en trading y riesgo
Con la duración mínima de caída, construimos alertas de “velocidad de desplome”. En un dashboard de riesgo, si la duración más rápida en la última hora es inferior a 2 segundos con una caída > 0.5%, se enciende una señal. Los market makers ajustan spreads; los traders desactivan órdenes de stop-loss agresivas. También se puede usar para backtesting de estrategias de reversión a la media: identificar caídas ultrarrápidas que suelen rebotar.
Otro uso: comparar activos. Un activo con caídas rápidas frecuentes indica fragilidad de liquidez. Métricas agregadas: media de las top 5 duraciones más rápidas en el día, percentiles, etc.
Extensión: no solo la más rápida, sino el ranking de las N más rápidas en un período. Mantener un heap de tamaño fijo con las duraciones y sus detalles.
Optimización para streaming y grandes volúmenes de datos
Si procesas millones de ticks por día en tiempo real, cada tick llama a la función. El estado (max_price, max_time, best_duration) se mantiene entre llamadas. Pero el umbral de reinicio puede descartar información valiosa si no se desea solo la más rápida absoluta. Una versión con ventana temporal fija (ej. últimos 60 segundos) limita la búsqueda y evita acumular un máximo obsoleto. Basta con ejecutar el algoritmo sobre una cola de ticks recientes, actualizando en cada nuevo tick.
Si los datos vienen de una base de datos histórica, puedes ejecutar el algoritmo en lotes periódicos.
Manejo de múltiples umbrales y caídas relativas
A veces interesa la duración mínima para diferentes umbrales (1%, 2%, 5%). En lugar de recorrer los datos varias veces, se puede adaptar el algoritmo para mantener varios máximos de referencia para cada umbral, aunque esto complica el estado. O ejecutar en paralelo. Python permite vectorización parcial con NumPy si los datos están en arrays, pero hay que tener cuidado con la indexación temporal.
Incorporando IA para detección de anomalías
Las caídas rápidas pueden ser eventos normales en ciertos contextos (noticias) o manipulaciones. Un modelo de machine learning entrenado con features como duración, profundidad, volatilidad previa, horario, etc., puede clasificarlas. Aquí es donde HolaGPT puede asistir generando el pipeline de features y el código de entrenamiento, o incluso explicando los patrones encontrados.
Casos reales: Flash Crash 2010 y otros
El 6 de mayo de 2010, el S&P 500 cayó un 9% en 5 minutos, pero dentro de esos minutos hubo ticks individuales con caídas del 1% en milisegundos. Aplicando el algoritmo con umbral 1% sobre datos tick a tick de ese día, se obtienen duraciones de milisegundos. Este tipo de análisis es usado por reguladores como la SEC para reconstruir eventos. Nuestro algoritmo simple permite a cualquier analista replicarlo.
Limitaciones y extensiones
El algoritmo presentado asume que la caída se mide desde un único máximo. Pero una caída rápida podría tener pequeñas recuperaciones intermedias. ¿Cómo definir la duración total si hubo un rebote del 0.2%? Podemos relajar la condición de monotonicidad y simplemente buscar el primer tick que cruza el umbral desde el máximo, independientemente de fluctuaciones internas. Eso ya lo hace el algoritmo. Si queremos la duración considerando el mínimo alcanzado dentro de una ventana desde el máximo, habría que buscar el mínimo en [i+1, j] y calcular duración al mínimo, no al tick que cruza. Ajustamos: cuando se detecta que P[i] es un máximo, seguimos recorriendo y mantenemos el mínimo observado hasta que se supere el umbral. Cuando caída >= umbral, registramos la duración hasta ese mínimo (no hasta el tick de cruce). Esto requiere almacenar el mínimo y su tiempo durante la fase de búsqueda. Modifiquemos:
def fastest_drop_to_min(timestamps, prices, threshold=0.01):
best_duration = float('inf')
best_info = None
max_price = prices[0]
max_time = timestamps[0]
min_price = max_price
min_time = max_time
for i in range(1, len(prices)):
price = prices[i]
time = timestamps[i]
if price > max_price:
max_price = price
max_time = time
min_price = price
min_time = time
else:
if price < min_price:
min_price = price
min_time = time
drop = (max_price - min_price) / max_price
if drop >= threshold:
duration = min_time - max_time
if duration < best_duration:
best_duration = duration
best_info = (max_time, min_time, max_price, min_price, drop)
max_price = min_price # reinicio con el mínimo como nuevo máximo
max_time = min_time
min_price = max_price
min_time = max_time
return best_duration, best_infoEsto captura el mínimo real alcanzado antes de cruzar el umbral, lo que es más preciso para ciertos análisis.
Conclusiones y código final
Hemos partido de una pregunta aparentemente simple y la hemos convertido en un algoritmo de producción, considerando ruido, umbrales y versiones. El código final es conciso pero poderoso. Integrarlo en sistemas de monitoreo o backtesting es directo. Si necesitas adaptarlo a tu dominio específico, recuerda que herramientas como HolaGPT pueden generar variantes, pruebas unitarias y documentación de manera ágil, ahorrándote horas de depuración.
Puedes descargar el notebook completo en el repositorio de HolaGPT con ejemplos sobre datos de Binance y Yahoo Finance. La velocidad de cómputo permite procesar un año de ticks del SPY en pocos segundos en una laptop estándar.