Skip to content

Algo Trading

Build a Comprehensive Mark Minervini Screener using Python

16 March 20269 min readAlgo Trading
FabTrader author portrait

FabTrader

Article overview

Most stock screeners only capture fragments of what makes a true market leader. A moving average filter here, a momentum check there — but rarely the full picture. In this article, we build a complete, professional-grade Minervini screener in Python that combines trend structure, relative strength, volume confirmation, and breakout characteristics into a single powerful scan. The result is a fast, fully vectorized tool capable of scanning hundreds of stocks in seconds and surfacing the few that truly behave like potential leaders.

A Practical Guide to Building a Professional Growth Stock Scanner in Python. Every serious growth investor eventually encounters the work of Mark Minervini.

His approach to stock selection is deceptively simple on the surface: find companies in powerful uptrends, near their highs, showing strong relative strength, and breaking out of tight bases with expanding volume.

But anyone who has tried to systematically scan the market using these principles quickly discovers something: replicating the full methodology is not trivial. Most public screeners capture only fragments of the process. A few moving averages here, a relative strength filter there. What they miss is the combination of signals that defines a true Minervini candidate.

So I decided to build something better. This article walks through the process of creating what I like to call the Mother of All Minervini Screeners — a Python-based, fully vectorized scanner capable of scanning hundreds or even thousands of stocks in seconds.

Why Build Your Own Screener?

Professional traders rarely rely on generic scans. The reason is simple: edge lives in customization. When you control the scanner, you can:

  • Implement precise trading rules
  • Add filters that most platforms don't offer
  • Scan large universes quickly
  • Integrate the results directly into your workflow

For algorithmic traders and technically inclined investors, a custom screener becomes an incredibly powerful research tool.

What This Screener Does

This implementation combines several core elements of the Minervini framework. It screens for stocks that satisfy:

Trend Strength

  • Price above 50, 150, and 200 day moving averages
  • Proper moving average alignment
  • Rising long-term trend

Momentum

  • A custom Relative Strength Rating based on multi-period momentum

Position Within the Range

  • Near 52-week highs
  • Well above 52-week lows

Volume Confirmation

  • Relative volume expansion

Structure

  • Stage-2 trend detection
  • Pivot breakout signals
  • Pocket pivots
  • Volatility contraction patterns

Taken together, these filters dramatically narrow the universe to stocks that are behaving like true leaders.

The Python Code

import pandas as pd
from datetime import timedelta, date

def get_historical_data(symbol, start_date, end_date, interval="1d"):
    """
    Must return DataFrame with:
    Date index
    Columns:
    Open, High, Low, Close, Volume
    """
    raise NotImplementedError("Implement your data source")

def load_universe(symbols, start_date, end_date):

    close = {}
    high = {}
    low = {}
    volume = {}

    for s in symbols:

        df = get_historical_data(s, start_date, end_date)

        if df.empty:
            continue

        if len(df) < 256:
            continue

        close[s] = df["Close"]
        high[s] = df["High"]
        low[s] = df["Low"]
        volume[s] = df["Volume"]

    close = pd.DataFrame(close)
    high = pd.DataFrame(high)
    low = pd.DataFrame(low)
    volume = pd.DataFrame(volume)

    return close, high, low, volume

def compute_rs_rating(close):

    r12 = close.pct_change(252, fill_method=None)
    r6 = close.pct_change(126, fill_method=None)
    r3 = close.pct_change(63, fill_method=None)
    r1 = close.pct_change(21, fill_method=None)

    rs = (0.4*r12) + (0.2*r6) + (0.2*r3) + (0.2*r1)
    latest_rs = rs.iloc[-1]
    rs_rating = latest_rs.rank(pct=True) * 100

    return rs_rating

def compute_trend_template(close, high, low):

    sma50 = close.rolling(50).mean()
    sma150 = close.rolling(150).mean()
    sma200 = close.rolling(200).mean()

    high_52 = high.rolling(252).max()
    low_52 = low.rolling(252).min()

    sma200_shift = sma200.shift(30)

    score = (
        (close > sma50).astype(int) +
        (close > sma150).astype(int) +
        (close > sma200).astype(int) +
        (sma50 > sma150).astype(int) +
        (sma50 > sma200).astype(int) +
        (sma150 > sma200).astype(int) +
        (close >= low_52 * 1.30).astype(int) +
        (close >= high_52 * 0.75).astype(int) +
        (sma200 > sma200_shift).astype(int)
    )

    return score.iloc[-1]

def compute_relative_volume(volume):

    avg_vol = volume.rolling(50).mean()
    rvol = volume / avg_vol

    return rvol.iloc[-1]


def compute_high_proximity(close, high):

    high_52 = high.rolling(252).max()
    latest_close = close.iloc[-1]
    latest_high = high_52.iloc[-1]
    proximity = latest_close / latest_high

    return proximity

def compute_stage2(close):

    sma150 = close.rolling(150).mean()
    sma200 = close.rolling(200).mean()
    cond1 = close.iloc[-1] > sma150.iloc[-1]
    cond2 = sma150.iloc[-1] > sma200.iloc[-1]
    sma50 = close.rolling(50).mean()
    cond3 = sma50.iloc[-1] > sma50.iloc[-10]

    return cond1 & cond2 & cond3

def compute_pivot_breakout(close, high):

    base_high = high.rolling(60).max()
    pivot = base_high.iloc[-1]
    latest_close = close.iloc[-1]
    breakout = latest_close > pivot

    return pivot, breakout

def compute_pocket_pivot(close, volume):

    down_days = close.diff() < 0
    down_volume = volume.where(down_days)
    max_down_volume = down_volume.rolling(10).max()
    pocket = volume.iloc[-1] > max_down_volume.iloc[-1]

    return pocket

def compute_vcp(close):

    ranges = close.pct_change(fill_method=None).abs()
    vol20 = ranges.rolling(20).mean()
    contraction = vol20.iloc[-1] < vol20.iloc[-40]

    return contraction

def run_minervini_screener(symbols):

    end = date.today()
    start = end - timedelta(days=400)

    close, high, low, volume = load_universe(symbols, start, end)
    rs_rating = compute_rs_rating(close)
    template_score = compute_trend_template(close, high, low)
    rvol = compute_relative_volume(volume)
    proximity = compute_high_proximity(close, high)
    stage2 = compute_stage2(close)
    vcp = compute_vcp(close)
    pivot, breakout = compute_pivot_breakout(close, high)
    pocket = compute_pocket_pivot(close, volume)

    df = pd.DataFrame({

        "RS_Rating": rs_rating,
        "Template_Score": template_score,
        "RVOL": rvol,
        "High_Proximity": proximity,
        "Stage2": stage2,
        "VCP": vcp,
        "Pivot": pivot,
        "Breakout": breakout,
        "PocketPivot": pocket

    })

    return df

def filter_candidates(df):

    candidates = df[

        (df["RS_Rating"] >= 70) &
        (df["Template_Score"] >= 8) &
        (df["Stage2"]) &
        (df["High_Proximity"] >= 0.85) &
        (df["RVOL"] >= 1.2)

    ]

    return candidates.sort_values("RS_Rating", ascending=False)

if __name__ == "__main__":

    pd.set_option("display.max_rows", None)
    pd.set_option("display.max_columns", None)

    symbols = ['360ONE', '3MINDIA', 'AADHARHFC', 'AARTIIND', 'AAVAS', 'ABB', 'ABBOTINDIA', 'ABCAPITAL', 'ABFRL',
               'ABLBL', 'ABREL', 'ABSLAMC', 'ACC', 'ACE', 'ACMESOLAR', 'ADANIENSOL', 'ADANIENT', 'ADANIGREEN',
               'ADANIPORTS', 'ADANIPOWER', 'AEGISLOG', 'AEGISVOPAK', 'AFCONS', 'AFFLE', 'AGARWALEYE', 'AIAENG', 'AIIL',
               'AJANTPHARM', 'AKUMS', 'AKZOINDIA', 'ALKEM', 'ALKYLAMINE', 'ALOKINDS', 'AMBER', 'AMBUJACEM',
               'ANANDRATHI', 'ANANTRAJ', 'ANGELONE', 'APARINDS', 'APLAPOLLO', 'APLLTD', 'APOLLOHOSP', 'APOLLOTYRE',
               'APTUS', 'ARE&M', 'ASAHIINDIA', 'ASHOKLEY', 'ASIANPAINT', 'ASTERDM', 'ASTRAL', 'ASTRAZEN', 'ATGL',
               'ATHERENERG', 'ATUL', 'AUBANK', 'AUROPHARMA', 'AWL', 'AXISBANK', 'BAJAJ-AUTO', 'BAJAJFINSV', 'BAJAJHFL',
               'BAJAJHLDNG', 'BAJFINANCE', 'BALKRISIND', 'BALRAMCHIN', 'BANDHANBNK', 'BANKBARODA', 'BANKINDIA', 'BASF',
               'BATAINDIA', 'BAYERCROP', 'BBTC', 'BDL', 'BEL', 'BEML', 'BERGEPAINT', 'BHARATFORG', 'BHARTIARTL',
               'BHARTIHEXA', 'BHEL', 'BIKAJI', 'BIOCON', 'BLS', 'BLUEDART', 'BLUEJET', 'BLUESTARCO', 'BOSCHLTD', 'BPCL',
               'BRIGADE', 'BRITANNIA', 'BSE', 'BSOFT', 'CAMPUS', 'CAMS', 'CANBK', 'CANFINHOME', 'CAPLIPOINT',
               'CARBORUNIV', 'CASTROLIND', 'CCL', 'CDSL', 'CEATLTD', 'CENTRALBK', 'CENTURYPLY', 'CERA', 'CESC', 'CGCL',
               'CGPOWER', 'CHALET', 'CHAMBLFERT', 'CHENNPETRO', 'CHOICEIN', 'CHOLAFIN', 'CHOLAHLDNG', 'CIPLA', 'CLEAN',
               'COALINDIA', 'COCHINSHIP', 'COFORGE', 'COHANCE', 'COLPAL', 'CONCOR', 'CONCORDBIO', 'COROMANDEL',
               'CRAFTSMAN', 'CREDITACC', 'CRISIL', 'CROMPTON', 'CUB', 'CUMMINSIND', 'CYIENT', 'DABUR', 'DALBHARAT',
               'DATAPATTNS', 'DBREALTY', 'DCMSHRIRAM', 'DEEPAKFERT', 'DEEPAKNTR', 'DELHIVERY', 'DEVYANI', 'DIVISLAB',
               'DIXON', 'DLF', 'DMART', 'DOMS', 'DRREDDY', 'ECLERX', 'EICHERMOT', 'EIDPARRY', 'EIHOTEL', 'ELECON',
               'ELGIEQUIP', 'EMAMILTD', 'EMCURE', 'ENDURANCE', 'ENGINERSIN', 'ENRIN', 'ERIS', 'ESCORTS', 'ETERNAL',
               'EXIDEIND', 'FACT', 'FEDERALBNK', 'FINCABLES', 'FINPIPE', 'FIRSTCRY', 'FIVESTAR', 'FLUOROCHEM',
               'FORCEMOT', 'FORTIS', 'FSL', 'GAIL', 'GESHIP', 'GICRE', 'GILLETTE', 'GLAND', 'GLAXO', 'GLENMARK',
               'GMDCLTD', 'GMRAIRPORT', 'GODFRYPHLP', 'GODIGIT', 'GODREJAGRO', 'GODREJCP', 'GODREJIND', 'GODREJPROP',
               'GPIL', 'GRANULES', 'GRAPHITE', 'GRASIM', 'GRAVITA', 'GRSE', 'GSPL', 'GUJGASLTD', 'GVT&D', 'HAL',
               'HAPPSTMNDS', 'HAVELLS', 'HBLENGINE', 'HCLTECH', 'HDFCAMC', 'HDFCBANK', 'HDFCLIFE', 'HEG', 'HEROMOTOCO',
               'HEXT', 'HFCL', 'HINDALCO', 'HINDCOPPER', 'HINDPETRO', 'HINDUNILVR', 'HINDZINC', 'HOMEFIRST', 'HONASA',
               'HONAUT', 'HSCL', 'HUDCO', 'HYUNDAI', 'ICICIBANK', 'ICICIGI', 'ICICIPRULI', 'IDBI', 'IDEA', 'IDFCFIRSTB',
               'IEX', 'IFCI', 'IGIL', 'IGL', 'IIFL', 'IKS', 'INDGN', 'INDHOTEL', 'INDIACEM', 'INDIAMART', 'INDIANB',
               'INDIGO', 'INDUSINDBK', 'INDUSTOWER', 'INFY', 'INOXINDIA', 'INOXWIND', 'INTELLECT', 'IOB', 'IOC',
               'IPCALAB', 'IRB', 'IRCON', 'IRCTC', 'IREDA', 'IRFC', 'ITC', 'ITCHOTELS', 'ITI', 'J&KBANK', 'JBCHEPHARM',
               'JBMA', 'JINDALSAW', 'JINDALSTEL', 'JIOFIN', 'JKCEMENT', 'JKTYRE', 'JMFINANCIL', 'JPPOWER', 'JSL',
               'JSWCEMENT', 'JSWENERGY', 'JSWINFRA', 'JSWSTEEL', 'JUBLFOOD', 'JUBLINGREA', 'JUBLPHARMA', 'JWL',
               'JYOTHYLAB', 'JYOTICNC', 'KAJARIACER', 'KALYANKJIL', 'KARURVYSYA', 'KAYNES', 'KEC', 'KEI', 'KFINTECH',
               'KIMS', 'KIRLOSBROS', 'KIRLOSENG', 'KOTAKBANK', 'KPIL', 'KPITTECH', 'KPRMILL', 'KSB', 'LALPATHLAB',
               'LATENTVIEW', 'LAURUSLABS', 'LEMONTREE', 'LICHSGFIN', 'LICI', 'LINDEINDIA', 'LLOYDSME', 'LODHA', 'LT',
               'LTF', 'LTFOODS', 'LTM', 'LTTS', 'LUPIN', 'M&M', 'M&MFIN', 'MAHABANK', 'MAHSCOOTER', 'MAHSEAMLES',
               'MANAPPURAM', 'MANKIND', 'MANYAVAR', 'MAPMYINDIA', 'MARICO', 'MARUTI', 'MAXHEALTH', 'MAZDOCK', 'MCX',
               'MEDANTA', 'METROPOLIS', 'MFSL', 'MGL', 'MINDACORP', 'MMTC', 'MOTHERSON', 'MOTILALOFS', 'MPHASIS', 'MRF',
               'MRPL', 'MSUMI', 'MUTHOOTFIN', 'NAM-INDIA', 'NATCOPHARM', 'NATIONALUM', 'NAUKRI', 'NAVA', 'NAVINFLUOR',
               'NBCC', 'NCC', 'NESTLEIND', 'NETWEB', 'NEULANDLAB', 'NEWGEN', 'NH', 'NHPC', 'NIACL', 'NIVABUPA',
               'NLCINDIA', 'NMDC', 'NSLNISP', 'NTPC', 'NTPCGREEN', 'NUVAMA', 'NUVOCO', 'NYKAA', 'OBEROIRLTY', 'OFSS',
               'OIL', 'OLAELEC', 'OLECTRA', 'ONESOURCE', 'ONGC', 'PAGEIND', 'PATANJALI', 'PAYTM', 'PCBL', 'PERSISTENT',
               'PETRONET', 'PFC', 'PFIZER', 'PGEL', 'PGHH', 'PHOENIXLTD', 'PIDILITIND', 'PIIND', 'PNB', 'PNBHOUSING',
               'POLICYBZR', 'POLYCAB', 'POLYMED', 'POONAWALLA', 'POWERGRID', 'POWERINDIA', 'PPLPHARMA', 'PRAJIND',
               'PREMIERENE', 'PRESTIGE', 'PTCIL', 'PVRINOX', 'RADICO', 'RAILTEL', 'RAINBOW', 'RAMCOCEM', 'RBLBANK',
               'RCF', 'RECLTD', 'REDINGTON', 'RELIANCE', 'RELINFRA', 'RHIM', 'RITES', 'RKFORGE', 'RPOWER', 'RRKABEL',
               'RVNL', 'SAGILITY', 'SAIL', 'SAILIFE', 'SAMMAANCAP', 'SAPPHIRE', 'SARDAEN', 'SAREGAMA', 'SBFC',
               'SBICARD', 'SBILIFE', 'SBIN', 'SCHAEFFLER', 'SCHNEIDER', 'SCI', 'SHREECEM', 'SHRIRAMFIN', 'SHYAMMETL',
               'SIEMENS', 'SIGNATURE', 'SJVN', 'SOBHA', 'SOLARINDS', 'SONACOMS', 'SONATSOFTW', 'SRF', 'STARHEALTH',
               'SUMICHEM', 'SUNDARMFIN', 'SUNDRMFAST', 'SUNPHARMA', 'SUNTV', 'SUPREMEIND', 'SUZLON', 'SWANCORP',
               'SWIGGY', 'SYNGENE', 'SYRMA', 'TARIL', 'TATACHEM', 'TATACOMM', 'TATACONSUM', 'TATAELXSI', 'TATAINVEST',
               'TATAPOWER', 'TATASTEEL', 'TATATECH', 'TBOTEK', 'TCS', 'TECHM', 'TECHNOE', 'TEJASNET', 'THELEELA',
               'THERMAX', 'TIINDIA', 'TIMKEN', 'TITAGARH', 'TITAN', 'TMPV', 'TORNTPHARM', 'TORNTPOWER', 'TRENT',
               'TRIDENT', 'TRITURBINE', 'TRIVENI', 'TTML', 'TVSMOTOR', 'UBL', 'UCOBANK', 'ULTRACEMCO', 'UNIONBANK',
               'UNITDSPR', 'UNOMINDA', 'UPL', 'USHAMART', 'UTIAMC', 'VBL', 'VEDL', 'VENTIVE', 'VGUARD', 'VIJAYA', 'VMM',
               'VOLTAS', 'VTL', 'WAAREEENER', 'WELCORP', 'WELSPUNLIV', 'WHIRLPOOL', 'WIPRO', 'WOCKPHARMA', 'YESBANK',
               'ZEEL', 'ZENSARTECH', 'ZENTEC', 'ZFCVINDIA', 'ZYDUSLIFE']

    results = run_minervini_screener(symbols)

    candidates = filter_candidates(results)

    print("\n----- Mark Minervini's Trend Template Screener -------")
    print("---------- FabTrader Algo (Fabtrader.in) -------------\n")

    if candidates.empty:
        print("No stocks met the Trend Template criteria")
    else:
        print(candidates)
----- Mark Minervini's Trend Template Screener -------

            RS_Rating  Template_Score      RVOL  High_Proximity  Stage2  \
NATIONALUM  98.983740               9  1.593551        0.897683    True   
GESHIP      98.577236               9  2.266864        0.967462    True   
CUMMINSIND  93.495935               9  1.960415        0.932364    True   
TORNTPHARM  93.292683               9  1.651834        0.979277    True   
JBCHEPHARM  91.463415               9  2.665705        0.991032    True   
COALINDIA   90.243902               9  1.919210        0.981092    True   
HINDALCO    88.821138               8  1.546976        0.883715    True   
GPIL        86.585366               8  1.691215        0.867414    True   
NTPC        83.333333               8  2.216245        0.974525    True   
JINDALSTEL  83.130081               9  1.237899        0.898750    True   
NLCINDIA    76.219512               8  2.272906        0.888090    True   

              VCP   Pivot  Breakout  PocketPivot  
NATIONALUM   True   431.5     False        False  
GESHIP      False  1509.0     False        False  
CUMMINSIND  False  4987.0     False        False  
TORNTPHARM  False  4482.9     False        False  
JBCHEPHARM  False  2141.0     False        False  
COALINDIA   False   476.0     False        False  
HINDALCO    False  1029.8     False        False  
GPIL        False   280.4     False        False  
NTPC        False   394.5     False        False  
JINDALSTEL  False  1272.1     False        False  
NLCINDIA     True   276.9     False        False  

Process finished with exit code 0

Where to Go From Here

Once you have a working screener, the possibilities expand quickly:

  • Add Volatility Contraction Pattern detection
  • Build a pivot breakout monitor
  • Add sector rotation filters
  • Create a Streamlit dashboard
  • Run the screener automatically each day

Over time, a simple script evolves into a complete market intelligence system.

Final Thoughts

Building your own tools fundamentally changes the way you interact with the market. Instead of reacting to tips, news, or random ideas, you develop a systematic process for finding strength.

That process — repeated consistently — is what ultimately produces results. And once you have a scanner like this running, you'll notice something interesting:

You start seeing leadership before everyone else does. That is where the real edge lies.

More from Algo Trading