Everyone uses stock screeners. Almost nobody profits from them. The difference between a Finviz filter and an institutional scanner is four pillars: Filter, Score, Rank, Validate. This is where we start building yours.
Here is a stat that should bother you: over 90% of retail traders who use stock screeners lose money. Not because screening is a bad idea — it is, in fact, the foundation of every systematic trading approach — but because the way most people screen is fundamentally broken.
The typical retail workflow looks like this:
The problem is not the screener. The problem is that filtering is only 25% of the job. The other 75% — scoring, ranking, and validation — is where the edge lives.
Think about what a basic screener does: it applies binary pass/fail filters to a universe of stocks. RSI below 30? Pass. Volume above 1 million? Pass. That is it. No ranking, no scoring, no context about whether the market is in a bull run or a crash. A stock that barely passes every filter gets treated the same as one that crushes every metric.
The result is a flat list of "candidates" with zero signal about which ones are actually worth trading. You are left making the final decision based on gut feel, chart pattern recognition (which is notoriously unreliable), or whatever the latest Reddit thread says.
A screener that only filters is like a search engine that only matches keywords without ranking results. Google became Google because of PageRank — the scoring algorithm. Your scanner needs the same thing.
| Flaw | What Happens | The Fix |
|---|---|---|
| No Scoring | All results treated equally. A stock at RSI 29 is "passed" just like RSI 15 (which is much more oversold) | Composite scoring that quantifies HOW MUCH a stock passes each criterion |
| No Regime Awareness | Momentum filters in a bear market = buying the dip into a crash. Oversold filters in a bull market = missing the trend | Detect the market regime FIRST, then select which strategy to weight heavily |
| No Validation | No way to know if your filters historically produce profitable trades. You are flying blind | Backtest every scanner output. Track hit rates by strategy. Kill what does not work |
A scanner that actually generates alpha is built on four pillars. Each one is necessary. Skip any one and your scanner is a random number generator with a nice UI.
Apply technical and fundamental criteria to reduce 10,000 stocks to 50-100 candidates. Use a flexible DSL, not hardcoded rules.
Assign a composite score (0-100) to each candidate based on multiple weighted factors. Boost for catalysts, penalize for risks.
Sort by score. Take only the top 10. Enforce diversification: no more than 2 from the same sector, mix geographies.
Backtest historically. Track live results with weekly retrospectives. Kill strategies with hit rates below 40%.
Notice how each stage dramatically reduces the number of stocks. The funnel is doing the heavy lifting for you — transforming thousands of names into a handful of high-conviction setups that you can actually research and trade.
Filters are the first pass. They exist to eliminate, not to select. A good filter removes 95% of the universe in milliseconds. The key insight: filters should be strategy-specific. A momentum strategy and a mean-reversion strategy need completely different filters — they are looking for opposite things.
This is why you need a DSL (Domain-Specific Language) for your filters instead of hardcoded rules. In Part 2, we will build one that lets you swap filter sets instantly based on what the market is doing.
Once you have 50-100 candidates that passed the filter, you need to answer: which ones are the best? A composite scoring system assigns points for technical strength, momentum, risk profile, and catalyst proximity. It also penalizes for red flags: insider selling, dilution risk, high short interest with no catalyst.
The scoring system is the brain of your scanner. We build it in Part 3.
Ranking is not just sorting by score. It includes diversification constraints: you do not want 10 tech stocks, even if they all score 95/100. Real ranking enforces sector limits, geographic diversity, and beta balance. It also applies regime-weighted strategy preferences — if the market is risk-off, you want more defensive setups at the top.
A scanner without validation is faith-based trading. You must track every output: did the entry trigger? Did it hit TP1? Did it stop out? Over time, you build a statistical picture of your scanner's edge (or lack thereof). In Part 5, we build a backtesting engine. In Part 6, we build a live retrospective system.
Hedge funds and prop desks do not use Finviz. They build custom systems that incorporate layers of intelligence retail traders never see. Here is what separates a $50B fund's scanner from a retail screener:
| Feature | Retail Screener | Institutional Scanner |
|---|---|---|
| Filters | Static (RSI < 30, P/E < 20) | Dynamic DSL with regime-dependent parameters |
| Scoring | None — binary pass/fail | Multi-factor composite (technical + catalyst + risk) |
| Regime | None — same filters in bull and bear | Auto-detects regime, shifts strategy weights daily |
| Data | EOD prices, maybe 1 year | 200+ days OHLCV, splits-adjusted, with volume profile |
| Catalysts | None | Insider buys, SEC filings, analyst upgrades, earnings dates |
| Risk Layer | None | Cost-to-borrow, dilution detection, sector correlation |
| Validation | None — "trust the process" | Backtested, live-tracked with weekly retrospectives |
| Output | Flat list of tickers | Scored, ranked, with entry/stop/target and catalyst calendar |
The good news: you can build most of this yourself. The data is available (often free or cheap). The computation is trivial for modern hardware. The only barrier is knowledge — which is exactly what this series provides.
The institutional edge is not proprietary data or faster computers. It is process. A systematic, multi-layered approach that removes emotion and replaces it with statistics. That is exactly what we are building.
The first decision you make when building a scanner is also the most consequential: what is your universe?
There are over 10,000 stocks listed on US exchanges alone. Add international markets and you are looking at 40,000+. Scanning all of them is technically possible but practically terrible. Here is why:
Instead of scanning everything, institutional scanners work with a curated universe of 300-800 quality names that meet minimum criteria:
# Universe selection criteria def build_universe(all_stocks): """Filter to quality names only.""" universe = [] for stock in all_stocks: if ( stock.market_cap > 500_000_000 # $500M+ market cap and stock.avg_volume_20d > 500_000 # 500K avg daily volume and stock.price > 5.0 # No penny stocks and stock.listed_years >= 2 # At least 2 years listed and stock.has_options # Options available (liquidity proxy) ): universe.append(stock) return universe # Typically 400-800 stocks
This is not just about speed — though scanning 500 stocks is 20x faster than scanning 10,000. It is about signal quality. Every stock in your universe should be one you could actually trade: liquid enough to enter and exit, large enough that technical analysis works, and with clean enough data to compute reliable indicators.
| Universe Size | Typical Pass Count | Signal Quality | Processing Time |
|---|---|---|---|
| 10,000+ (full US) | 150-300 | Low — many false positives from illiquid/micro-cap stocks | 30-60 seconds |
| 2,000 (mid/large cap) | 40-80 | Medium — still some noise from edge cases | 5-10 seconds |
| 500 (curated quality) | 15-30 | High — every candidate is tradeable | 1-2 seconds |
Once your scanner works for US stocks, you can expand geographically. Add 50-100 European names (DAX, CAC 40, FTSE 100 components), 30-50 Asian names (Nikkei, HSI components), and ETFs (sector, commodity, bond). Your total universe grows to 600-800, but every name is quality. This is what we cover in Part 6.
Your scanner is only as good as its data. This sounds obvious, but the number of scanners built on bad data is staggering. Here are the data problems you must solve before writing a single filter:
When a stock splits 4:1, the historical price drops by 75% overnight — but that is not a real price move. If your scanner sees AAPL at $40 in 2019 and $175 in 2024, it looks like a 337% gain. But AAPL had a 4:1 split in August 2020, so the actual gain is different. You must use split-adjusted data.
# Always use adjusted close prices import yfinance as yf # WRONG: unadjusted prices include split artifacts # data = yf.download("AAPL", period="2y")["Close"] # RIGHT: adjusted prices account for splits and dividends data = yf.download("AAPL", period="2y", auto_adjust=True) close = data["Close"] volume = data["Volume"] # Verify: no single-day drops > 30% (split artifact check) daily_returns = close.pct_change() assert (daily_returns.abs() < 0.50).all(), "Possible unadjusted split detected"
Your indicators need history. An EMA(200) needs at least 200 bars to be meaningful. A Bollinger Band Width comparison needs at least 20 bars of BBW history, which itself needs 20 bars of price — so 40 bars minimum. A volume spike detection comparing current volume to 20-day average needs 20 bars.
Rule of thumb: require 252 trading days (1 year) minimum, prefer 504 days (2 years). Anything less and your indicators are unreliable in the early bars.
If you only scan currently listed stocks and backtest against the same list, you have survivorship bias. The stocks that went bankrupt, got delisted, or merged out of existence are invisible in your data. Your backtest will overestimate returns because it never saw the losers.
In 2008, a momentum scanner on today's S&P 500 would have found many "survivors" that recovered beautifully from the crash. But back in 2008, the S&P 500 had different members — Lehman Brothers, Washington Mutual, Bear Stearns were all in it. A scanner that never saw these blow-ups would massively overestimate its performance.
Volume is critical for confirmation signals, but volume data has its own problems:
# Data quality validation pipeline def validate_bar_data(df, ticker): """Check data quality before feeding to scanner.""" issues = [] # 1. Minimum history if len(df) < 252: issues.append(f"Only {len(df)} bars (need 252+)") # 2. Missing data missing_pct = df.isnull().sum().max() / len(df) * 100 if missing_pct > 2: issues.append(f"{missing_pct:.1f}% missing data") # 3. Suspicious returns (unadjusted splits?) max_daily = df["close"].pct_change().abs().max() if max_daily > 0.50: issues.append(f"Suspicious {max_daily:.0%} daily move") # 4. Volume sanity zero_vol_days = (df["volume"] == 0).sum() if zero_vol_days > 5: issues.append(f"{zero_vol_days} zero-volume days") if issues: print(f"[WARN] {ticker}: {', '.join(issues)}") return False return True
Now let us put it all together. Here is the complete pipeline that we will build over the next five parts of this series:
A production scanner runs on a daily cycle. Every morning (or evening, depending on your market), the following happens:
Regime Detection. Check SPY, VIX, DXY, TLT, HYG/LQD, BTC. Determine if the market is Risk-On, Neutral, or Risk-Off. Adjust strategy weights accordingly.
Data Refresh. Pull latest OHLCV for your 500-stock universe. Validate data quality. Compute all indicators (EMAs, RSI, MACD, ATR, BBW).
Filter. Run 4 strategy-specific DSL filters against the universe. Each filter produces 15-30 candidates. Deduplicate and merge.
Score & Rank. Apply composite scoring to all candidates. Add catalyst boosts (insider buys, earnings). Penalize risks (dilution, high CTB). Sort by regime-weighted score. Select top 10.
Enrich. For each of the 10 finalists: compute ATR-based entry/stop/target levels. Pull analyst targets, insider activity, earnings dates. Build the trade card.
Publish. Generate the morning report. Send alerts via Telegram/Discord. Log everything for the retrospective.
The entire process takes under 30 minutes, most of which is data fetching. The computation itself is measured in seconds. This is achievable on a $5/month VPS — you do not need a Bloomberg terminal.
A scanner finds candidates. You make the trade decision. The scanner says "here are 10 stocks that meet my criteria, ranked by score, with entry/stop/target levels." But the final call — whether to actually enter the trade — is yours.
Why? Because the scanner does not know about:
The scanner narrows 500 stocks to 10 high-probability candidates. That is a 98% reduction in your decision space. But the last 2% is human judgment, and it should stay that way.
| Part | What You Build | Key Concept |
|---|---|---|
| 1. Philosophy (this article) | Mental model for scanner architecture | Filter + Score + Rank + Validate |
| 2. DSL & Filters | A flexible filter language with 4 strategies | Expression evaluation, strategy-specific filters |
| 3. Scoring | Multi-factor scoring with boosts and penalties | Composite scoring, catalyst integration, risk detection |
| 4. Regime Detection | Auto-adaptive regime detector with 6 indicators | Market regime, dynamic strategy weighting |
| 5. Backtesting | Historical validation engine with walk-forward testing | Win rate, profit factor, overfitting avoidance |
| 6. Production | Live daily pipeline with alerts and retrospectives | ATR-based levels, multi-market, cost optimization |