Quant500 Academy

3. Tu primera estrategia completa: el cruce de una media móvil

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

Es hora de programar tu primera estrategia completa de principio a fin: la más simple que existe, y por eso mismo el punto de partida obligado de cualquiera que empiece en trading algorítmico. La lógica es minimalista: entra largo cuando el precio cierra por encima de su media móvil simple; sal (o ponte corto) cuando cierra por debajo.

import yfinance as yf

datos = yf.download("GS", start="2022-01-01", progress=False)
datos["SMA100"] = datos["Close"].rolling(window=100).mean()

datos["Señal"] = 0
datos.loc[datos["Close"] > datos["SMA100"], "Señal"] = 1
datos.loc[datos["Close"] < datos["SMA100"], "Señal"] = -1

datos["Posicion"] = datos["Señal"] - datos["Señal"].shift(1)

La columna Posicion es el truco que convierte una señal continua (1 o -1 cada día) en un evento puntual: al restar la señal de ayer a la de hoy, solo aparece un valor distinto de cero el día exacto en que la señal cambia. Un salto de -1 a 1 da como resultado 2 (señal de compra); un salto de 1 a -1 da -2 (señal de venta). El resto de los días, sin cambio de señal, el resultado es 0.

señales_compra = datos[datos["Posicion"] > 0]
señales_venta = datos[datos["Posicion"] == -2]

Con las señales identificadas, el siguiente paso natural es preguntarte: ¿habría funcionado esto en el pasado? Aquí es donde entra un primer backtest — deliberadamente ingenuo, y el propio material fuente insiste en llamarlo así — que recorre distintos periodos de media móvil (de 2 a 100 días) y calcula qué retorno habría dado cada uno:

resultados = []
for periodo in range(2, 101):
    datos["SMA"] = datos["Close"].rolling(window=periodo).mean()
    datos["Señal"] = 0
    datos.loc[datos["Close"] > datos["SMA"], "Señal"] = 1
    datos.loc[datos["Close"] < datos["SMA"], "Señal"] = -1

    datos["Retorno_diario"] = datos["Close"].pct_change() * datos["Señal"].shift(1)
    retorno_acumulado = (datos["Retorno_diario"] + 1).prod() - 1

    resultados.append((periodo, retorno_acumulado * 100))

datos['Señal'].shift(1) es un detalle que no puedes saltarte: el retorno de hoy solo se puede capturar si ya tenías la señal de ayer, nunca la de hoy mismo — de lo contrario estarías usando información que en el momento real de operar todavía no existía. Es una versión temprana del look-ahead bias que verás formalizado más adelante en el curso.

Y aquí llega la advertencia que el propio material original subraya de forma explícita, y que conviene tomarse en serio: este backtest asume que puedes comprar y vender exactamente al precio de cierre, sin comisiones, sin slippage, sin restricciones de liquidez y sin impuestos. Ninguna de esas condiciones existe en el mercado real. El resultado de este ejercicio sirve para explorar la idea, no para decidir con qué media móvil vas a operar mañana con dinero de verdad — eso exige un backtest mucho más riguroso, que verás en niveles posteriores.

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.