Algo Trading
Build a Comprehensive Mark Minervini Screener using Python

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 0Where 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
Algo Trading Cost in India: How I Built a Reliable Setup for ₹150/Month
Wondering how much algo trading costs in India? In this article, I break down the real expenses involved in running an algorithmic...
When Your Job Feels Shaky: Can Trading Become an Alternate Income Stream?
Can trading become a stable source of income in India? While many consider it during times of job uncertainty, the reality is...
How Much Capital Do You Really Need for Sustainable Trading Income in India?
How much capital is needed for sustainable trading income in India? This in-depth guide explores the realistic returns traders can expect, the...
