Quant500 Academy

3. Your first complete strategy: the moving average crossover

Level 5: Momentum, Mean-Reversion and Pairs Trading Strategies

It is time to program your first complete strategy from start to finish: the simplest one there is, and for that very reason the compulsory starting point for anyone beginning in algorithmic trading. The logic is minimal: go long when the price closes above its simple moving average; exit (or go short) when it closes below.

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)

The Posicion column is the trick that turns a continuous signal (1 or -1 every day) into a one-off event: by subtracting yesterday's signal from today's, a non-zero value appears only on the exact day the signal changes. A jump from -1 to 1 gives 2 (buy signal); a jump from 1 to -1 gives -2 (sell signal). On every other day, with no change of signal, the result is 0.

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

With the signals identified, the natural next step is to ask yourself: would this have worked in the past? This is where a first backtest comes in — deliberately naive, and the source material itself insists on calling it that — which sweeps through different moving average periods (from 2 to 100 days) and computes the return each one would have delivered:

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) is a detail you cannot skip: today's return can only be captured if you already held yesterday's signal, never today's own — otherwise you would be using information that did not yet exist at the real moment of trading. It is an early version of the look-ahead bias you will see formalized later in the course.

And here comes the warning that the original material underlines explicitly, and that is worth taking seriously: this backtest assumes you can buy and sell exactly at the closing price, with no commissions, no slippage, no liquidity constraints and no taxes. None of those conditions exists in the real market. The result of this exercise is there to explore the idea, not to decide which moving average you are going to trade tomorrow with real money — that demands a far more rigorous backtest, which you will see in later levels.

To go deeper into this lesson

The rest of Quant500

What you are learning, applied to the index every day:

The model portfolio, weekly in your inbox

Every Saturday, its week in one minute: how it went, the best and the worst, and which insiders bought with their own money. Plus, the day the model is rebuilt, which stocks come in and which go out. A simulation, not advice. At most one email a week, no ads, no account; one-click unsubscribe. Made by one person: Pol Gracia.

A model portfolio with simulated results, not a personalized recommendation. Your email is used for this only: privacy.

Behind the Academy and the rest of Quant500 there is one person: Pol Gracia Serrahima.