Stock Screening
Purpose
Build a screen that narrows a universe to a reviewable shortlist. A screen is a filter, not a decision — its output is a list of things to look at, and treating it as a list of things to buy is the fastest way to lose money with a spreadsheet.
When to Use
- Building a systematic screen for candidates.
- A screen that returns hundreds of results, or none.
- Evaluating whether a screen's criteria actually discriminate.
- Reviewing a screen that historically performed well and now does not.
Capabilities
- Screen design: fundamental, technical, and combined criteria.
- Criteria selection and threshold setting.
- Overfitting detection.
- Survivorship and look-ahead bias avoidance.
- Ranking and shortlisting.
Inputs
- The universe, and how it is defined.
- The characteristics being sought, and why they should matter.
- Historical data, if the screen is to be validated.
Outputs
- A screen that returns a reviewable number of names — typically 10 to 40.
- Each criterion justified by a mechanism, not by backtest fit.
- A ranked shortlist for manual review.
Workflow
- Define the universe first — Liquidity and market-capitalization floors. A screen that returns illiquid microcaps you cannot actually trade is returning noise.
- Choose criteria with a mechanism — Each filter should have a reason it should work, stated before you test it. "It scored well in the backtest" is not a mechanism; it is a warning sign.
- Use few criteria — Three to six. Each additional filter cuts the result set and increases the chance you have fitted the screen to the past rather than to a real effect.
- Set thresholds at round, defensible numbers — A revenue growth threshold of 23.7% is fitted. 20% is a judgment. The former will not survive out of sample.
- Check the biases — Does the historical universe include companies that no longer exist? If not, the backtest is measuring the performance of survivors, which is not a strategy you could have run.
- Rank and review manually — The screen produces candidates. A human — or a careful, separate analysis — decides. The screen does not know why a company is cheap.
Best Practices
- A screen that returns 300 names has not screened. A screen that returns zero has been over-fitted. Aim for a number you will actually review.
- Every criterion should have a mechanism you can articulate before testing. Criteria discovered by searching the space of possible criteria are discovered by fitting noise, and there is a great deal of noise.
- Survivorship bias makes almost every naive backtest look good. A universe of "today's S&P 500 constituents, historically" excludes every company that failed, and that is precisely the population the screen needed to avoid.
- Look-ahead bias is subtler: using a fiscal-year figure on the day the year ended, rather than the day it was reported, gives the screen information nobody had. Use point-in-time data.
- Value screens find cheap stocks, and stocks are usually cheap for a reason. The screen finds the candidates; the work of determining whether the reason is fatal is not done by the screen.
- Do not re-optimize a screen after a period of poor performance. That fits it to the most recent regime, which is exactly the regime least likely to persist.
Examples
A screen with few criteria, each with a mechanism:
def momentum_quality_screen(universe: pd.DataFrame) -> pd.DataFrame:
"""Each filter has a stated mechanism. None was found by searching.
Mechanisms:
- Liquidity: we must be able to enter and exit without moving the price.
- Relative strength: momentum is one of the most durable documented
anomalies. Persistence over 6-12 months is the effect.
- Trend structure: we do not want falling knives. Above a rising 200-day.
- Profitability: quality screens out the low-priced momentum that is
momentum toward zero.
- Earnings growth: confirms the price move has a fundamental driver.
"""
return (
universe
# Liquidity: tradable, not theoretical.
.query("market_cap_usd > 2e9 and avg_dollar_volume_50d > 20e6")
# Relative strength: top quintile over 6 months, versus the benchmark.
.query("rs_126d > rs_126d.quantile(0.80)")
# Trend: above a rising 200-day. No falling knives.
.query("close > ma200 and ma200_slope_20d > 0")
# Quality: profitable. Filters momentum-toward-zero.
.query("roe > 0.15 and net_margin > 0.05")
# Fundamental confirmation of the price move.
.query("eps_growth_yoy > 0.20 and revenue_growth_yoy > 0.10")
.sort_values("rs_126d", ascending=False)
.head(25) # a reviewable shortlist, not a portfolio
)
# Five criteria. Round thresholds. Each defensible without reference to a
# backtest. It will not be the best-performing screen in a backtest, and it is
# far more likely to work out of sample than one that is.
The bias that makes a backtest lie:
# WRONG: today's index constituents, tested historically. Every company that
# was delisted, acquired, or went to zero is absent. The screen appears to
# have avoided them; in reality it was never offered them.
universe = current_sp500_members()
backtest(screen, universe, start="2010-01-01") # results are fiction
# RIGHT: point-in-time membership, including everything that later failed.
universe = sp500_members_as_of(date) # survivorship-free
fundamentals = fundamentals_as_reported(date) # not as later restated,
# and not before the filing date
backtest(screen, universe, fundamentals, start="2010-01-01")
# The difference is routinely several percentage points of annual return —
# frequently the entire apparent edge.
Notes
- Survivorship bias alone typically accounts for 1-4 percentage points of annualized return in a naive backtest. An apparent edge of that size is often nothing but the bias.
- The number of criteria is a proxy for overfitting risk. Every additional filter is another chance to have fitted the past. Three good criteria beat eight fitted ones.
- This is educational material about screening methodology, not financial advice, and no screen output is a recommendation to buy anything.
1---2name: stock-screening3description: Use when building a screen to find candidates. Covers screen design, criteria that actually discriminate, avoiding overfitting, survivorship bias, and turning a screen's output into a shortlist rather than a shopping list.4---56# Stock Screening78## Purpose910Build a screen that narrows a universe to a reviewable shortlist. A screen is a filter, not a decision — its output is a list of things to look at, and treating it as a list of things to buy is the fastest way to lose money with a spreadsheet.1112## When to Use1314- Building a systematic screen for candidates.15- A screen that returns hundreds of results, or none.16- Evaluating whether a screen's criteria actually discriminate.17- Reviewing a screen that historically performed well and now does not.1819## Capabilities2021- Screen design: fundamental, technical, and combined criteria.22- Criteria selection and threshold setting.23- Overfitting detection.24- Survivorship and look-ahead bias avoidance.25- Ranking and shortlisting.2627## Inputs2829- The universe, and how it is defined.30- The characteristics being sought, and why they should matter.31- Historical data, if the screen is to be validated.3233## Outputs3435- A screen that returns a reviewable number of names — typically 10 to 40.36- Each criterion justified by a mechanism, not by backtest fit.37- A ranked shortlist for manual review.3839## Workflow40411. **Define the universe first** — Liquidity and market-capitalization floors. A screen that returns illiquid microcaps you cannot actually trade is returning noise.422. **Choose criteria with a mechanism** — Each filter should have a reason it should work, stated before you test it. "It scored well in the backtest" is not a mechanism; it is a warning sign.433. **Use few criteria** — Three to six. Each additional filter cuts the result set and increases the chance you have fitted the screen to the past rather than to a real effect.444. **Set thresholds at round, defensible numbers** — A revenue growth threshold of 23.7% is fitted. 20% is a judgment. The former will not survive out of sample.455. **Check the biases** — Does the historical universe include companies that no longer exist? If not, the backtest is measuring the performance of survivors, which is not a strategy you could have run.466. **Rank and review manually** — The screen produces candidates. A human — or a careful, separate analysis — decides. The screen does not know why a company is cheap.4748## Best Practices4950- A screen that returns 300 names has not screened. A screen that returns zero has been over-fitted. Aim for a number you will actually review.51- Every criterion should have a mechanism you can articulate before testing. Criteria discovered by searching the space of possible criteria are discovered by fitting noise, and there is a great deal of noise.52- Survivorship bias makes almost every naive backtest look good. A universe of "today's S&P 500 constituents, historically" excludes every company that failed, and that is precisely the population the screen needed to avoid.53- Look-ahead bias is subtler: using a fiscal-year figure on the day the year ended, rather than the day it was reported, gives the screen information nobody had. Use point-in-time data.54- Value screens find cheap stocks, and stocks are usually cheap for a reason. The screen finds the candidates; the work of determining whether the reason is fatal is not done by the screen.55- Do not re-optimize a screen after a period of poor performance. That fits it to the most recent regime, which is exactly the regime least likely to persist.5657## Examples5859**A screen with few criteria, each with a mechanism:**6061```python62def momentum_quality_screen(universe: pd.DataFrame) -> pd.DataFrame:63 """Each filter has a stated mechanism. None was found by searching.6465 Mechanisms:66 - Liquidity: we must be able to enter and exit without moving the price.67 - Relative strength: momentum is one of the most durable documented68 anomalies. Persistence over 6-12 months is the effect.69 - Trend structure: we do not want falling knives. Above a rising 200-day.70 - Profitability: quality screens out the low-priced momentum that is71 momentum toward zero.72 - Earnings growth: confirms the price move has a fundamental driver.73 """74 return (75 universe76 # Liquidity: tradable, not theoretical.77 .query("market_cap_usd > 2e9 and avg_dollar_volume_50d > 20e6")7879 # Relative strength: top quintile over 6 months, versus the benchmark.80 .query("rs_126d > rs_126d.quantile(0.80)")8182 # Trend: above a rising 200-day. No falling knives.83 .query("close > ma200 and ma200_slope_20d > 0")8485 # Quality: profitable. Filters momentum-toward-zero.86 .query("roe > 0.15 and net_margin > 0.05")8788 # Fundamental confirmation of the price move.89 .query("eps_growth_yoy > 0.20 and revenue_growth_yoy > 0.10")9091 .sort_values("rs_126d", ascending=False)92 .head(25) # a reviewable shortlist, not a portfolio93 )9495# Five criteria. Round thresholds. Each defensible without reference to a96# backtest. It will not be the best-performing screen in a backtest, and it is97# far more likely to work out of sample than one that is.98```99100**The bias that makes a backtest lie:**101102```python103# WRONG: today's index constituents, tested historically. Every company that104# was delisted, acquired, or went to zero is absent. The screen appears to105# have avoided them; in reality it was never offered them.106universe = current_sp500_members()107backtest(screen, universe, start="2010-01-01") # results are fiction108109# RIGHT: point-in-time membership, including everything that later failed.110universe = sp500_members_as_of(date) # survivorship-free111fundamentals = fundamentals_as_reported(date) # not as later restated,112 # and not before the filing date113backtest(screen, universe, fundamentals, start="2010-01-01")114115# The difference is routinely several percentage points of annual return —116# frequently the entire apparent edge.117```118119## Notes120121- Survivorship bias alone typically accounts for 1-4 percentage points of annualized return in a naive backtest. An apparent edge of that size is often nothing but the bias.122- The number of criteria is a proxy for overfitting risk. Every additional filter is another chance to have fitted the past. Three good criteria beat eight fitted ones.123- This is educational material about screening methodology, not financial advice, and no screen output is a recommendation to buy anything.