Quant500 Academy

4. Programming the full Minervini filter: from 500 shares to a short list

Level 4: Stock Screening

With the RS Rating already computed for the top 30% of shares, the next step is to check, one by one, whether each of those shares also meets the price and moving average conditions of the Trend Template. First, we compute the three moving averages we need:

for periodo in [50, 150, 200]:
    df[f"SMA_{periodo}"] = df["Adj Close"].rolling(window=periodo).mean()

With the averages computed, we pull out the most recent value of each one, along with the 52-week low and high (roughly 260 trading days):

precio_actual = df["Adj Close"].iloc[-1]
media_50 = df["SMA_50"].iloc[-1]
media_150 = df["SMA_150"].iloc[-1]
media_200 = df["SMA_200"].iloc[-1]
minimo_52sem = round(df["Low"].iloc[-260:].min(), 2)
maximo_52sem = round(df["High"].iloc[-260:].max(), 2)
media_200_hace_un_mes = df["SMA_200"].iloc[-20]

And now the seven Trend Template conditions translate literally, one by one, into boolean comparisons:

condicion_1 = precio_actual > media_150 > media_200
condicion_2 = media_150 > media_200
condicion_3 = media_200 > media_200_hace_un_mes
condicion_4 = media_50 > media_150 > media_200
condicion_5 = precio_actual > media_50
condicion_6 = precio_actual >= 1.3 * minimo_52sem
condicion_7 = precio_actual >= 0.75 * maximo_52sem

cumple_todas = all([condicion_1, condicion_2, condicion_3, condicion_4,
                     condicion_5, condicion_6, condicion_7])

One Python detail shows up here and deserves an explanation: precio_actual > media_150 > media_200 is a chained comparison, equivalent to writing (precio_actual > media_150) and (media_150 > media_200), but easier to read. It is only True if both comparisons hold at once, in the exact order in which they are written.

all() checks that every element of the list is True; a single failed condition is enough for the share to be dropped from the screener — it is a strict filter by design: Minervini's method does not average conditions, it demands all of them at once.

A practical detail that is not in the theory but is very much in real code: when you loop over hundreds of tickers, some will fail — missing data, a delisted ticker, a temporary error at the source. Wrapping each iteration in a try/except (seen in Level 2) stops a single problem ticker from interrupting the screening of the remaining 500 companies.

resultados = []
for ticker in top_30["Ticker"]:
    try:
        # ... compute the conditions for this ticker ...
        if cumple_todas:
            resultados.append(ticker)
    except Exception as error:
        print(f"No se pudo procesar {ticker}: {error}")
        continue

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.