Quant500 Academy

6. Z-score: cuándo un precio está muy alto o muy bajo

Nivel 5: Estrategias de Momentum, Mean-Reversion y Pairs Trading

Para convertir "el precio está anormalmente alto o bajo" en una regla programable, necesitas una medida concreta de qué tan lejos está el precio de su media, expresada en unidades comparables entre distintos activos. Esa medida es el z-score: cuántas desviaciones estándar separan un valor de la media de su serie.

z = (x - media) / desviacion_estandar

Un z-score de 1 significa que el precio está una desviación estándar por encima de la media; un z-score de -2, dos desviaciones estándar por debajo. La ventaja de trabajar en desviaciones estándar en vez de en precio absoluto es que el mismo umbral (por ejemplo, z > 1) significa lo mismo para una acción de 20€ que para una de 2.000€.

Aplicado a un solo activo, la estrategia calcula primero la media móvil acumulada del precio — el promedio de todos los días hasta la fecha actual, no una ventana fija — y a partir de ahí, el z-score de cada día:

datos = yf.download("AAPL", start="2022-02-02", progress=False)
datos["media_acumulada"] = [datos["Close"][:i+1].mean() for i in range(len(datos))]

zscores = []
for i in range(len(datos)):
    desviacion = np.std(datos["Close"][:i+1])
    if desviacion == 0:
        zscores.append(0)
    else:
        z = (datos["Close"].iloc[i] - datos["media_acumulada"].iloc[i]) / desviacion
        zscores.append(z)

Usar la media y la desviación acumuladas hasta cada día, en vez de calculadas sobre todo el histórico de golpe, es importante por la misma razón que el shift(1) del módulo anterior: en el momento real de operar el día i, todavía no conoces los precios de los días posteriores. Calcular la media con datos futuros sería otra forma de look-ahead bias, aunque más sutil que la anterior.

Con el z-score calculado, la regla de la estrategia es simple: vender en corto cuando el z-score supera 1 (el precio está anormalmente caro), comprar en largo cuando cae por debajo de -1 (anormalmente barato), y cerrar toda posición cuando el z-score vuelve a estar cerca de cero — dentro de medio punto — porque en ese momento el precio ya ha revertido a su media y la ventaja de la operación se ha agotado.

dinero = 0
posicion = 0

for i in range(len(datos)):
    if zscores[i] > 1:
        dinero += datos["Open"].iloc[i]   # la condicion debe evaluarse con zscores[i-1] (el z-score que ya existia al cierre de ayer) y ejecutarse a la apertura de hoy
        posicion -= 1
    elif zscores[i] < -1:
        dinero -= datos["Close"].iloc[i]
        posicion += 1
    elif abs(zscores[i]) < 0.5:
        dinero += posicion * datos["Close"].iloc[i]
        posicion = 0

Este fragmento es, de nuevo, una simulación simplificada, no un backtest completo: dinero acumula el flujo de caja de comprar y vender, sin comisiones ni gestión de tamaño de posición, y posicion puede acumularse en varias operaciones seguidas sin ningún límite. Sirve para entender el mecanismo del z-score aplicado a una estrategia; no está listo para decidir cuánto capital real arriesgar.

Para ampliar esta lección

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.