Stock Screening Investigation: Finding "Good Businesses at Fair Prices"

Date: 2025-12-03
Conclusion: YES, we can systematically screen for value stocks using available APIs.


Executive Summary

Capability yfinance Finnhub (Free Tier)
Stock Screener YES - Excellent NO - Premium only
Index Constituents NO (but workaround exists) NO - Premium only
Fundamental Metrics YES - via screener YES - per ticker
Batch Queries YES - screener returns 250 max NO - one ticker at a time
Rate Limits Aggressive but manageable 60 calls/minute

Best approach: Use yfinance screener for discovery, then Finnhub for deep-dive on candidates.


1. Screening Capabilities

yfinance Screener (RECOMMENDED)

yfinance has a powerful built-in screener that queries Yahoo Finance's backend:

from yfinance import EquityQuery
import yfinance as yf

# Example: Graham-style value screen
query = EquityQuery('and', [
    EquityQuery('btwn', ['peratio.lasttwelvemonths', 0, 15]),  # P/E 0-15
    EquityQuery('lt', ['pricebookratio.quarterly', 1.5]),      # P/B < 1.5
    EquityQuery('gt', ['currentratio.lasttwelvemonths', 2.0]), # Current Ratio > 2
    EquityQuery('gt', ['intradaymarketcap', 1000000000]),      # Market Cap > $1B
    EquityQuery('is-in', ['exchange', 'NMS', 'NYQ']),          # NYSE/NASDAQ
])

result = yf.screen(query, sortField='intradaymarketcap', sortAsc=False, size=250)
# Returns up to 250 stocks with 85+ data fields per stock

Capabilities:

Finnhub Screener

NOT available on free tier. The /index/constituents endpoint returns 403 Forbidden.

However, Finnhub excels at deep-dive analysis on individual tickers:


2. Available Screening Metrics (yfinance)

Valuation

Field Description
peratio.lasttwelvemonths P/E ratio (TTM)
pricebookratio.quarterly P/B ratio
pegratio_5y PEG ratio (5-year)
lastclosetevtotalrevenue.lasttwelvemonths EV/Revenue
lastclosemarketcaptotalrevenue.lasttwelvemonths Market Cap/Revenue

Profitability & Quality

Field Description
returnonequity.lasttwelvemonths ROE (TTM)
returnonassets.lasttwelvemonths ROA (TTM)
grossprofitmargin.lasttwelvemonths Gross margin
netincomemargin.lasttwelvemonths Net margin
consecutive_years_of_dividend_growth_count Dividend streak

Financial Health

Field Description
currentratio.lasttwelvemonths Current ratio
quickratio.lasttwelvemonths Quick ratio
totaldebtequity.lasttwelvemonths Debt/Equity
altmanzscoreusingtheaveragestockinformationforaperiod.lasttwelvemonths Altman Z-Score

Growth

Field Description
epsgrowth.lasttwelvemonths EPS growth
totalrevenues1yrgrowth.lasttwelvemonths Revenue growth
quarterlyrevenuegrowth.quarterly Quarterly rev growth

Trading/Size

Field Description
intradaymarketcap Market cap
avgdailyvol3m 3-month avg volume
forward_dividend_yield Forward yield

3. Getting a Stock Universe

Option A: Use yfinance Screener (Recommended)

Query for large/mid cap US stocks, then screen:

# Get ~950 US stocks with market cap > $10B
query = EquityQuery('and', [
    EquityQuery('gt', ['intradaymarketcap', 10_000_000_000]),
    EquityQuery('is-in', ['exchange', 'NMS', 'NYQ']),
])
result = yf.screen(query, size=250)  # Returns top 250 by criteria

Option B: External List + Batch Query

Download S&P 500 list from Wikipedia, then query Finnhub for each:

Option C: Predefined Screens

yfinance includes predefined screens:

result = yf.screen("undervalued_large_caps")

4. Graham-Style Value Screening Methodology

Screen 1: Strict Graham

Classic Benjamin Graham criteria:

EquityQuery('and', [
    EquityQuery('btwn', ['peratio.lasttwelvemonths', 0, 15]),      # P/E < 15
    EquityQuery('lt', ['pricebookratio.quarterly', 1.5]),          # P/B < 1.5  
    EquityQuery('gt', ['currentratio.lasttwelvemonths', 2.0]),     # Liquidity
    EquityQuery('gt', ['intradaymarketcap', 1_000_000_000]),       # Size filter
])

Results: ~90 stocks match

Screen 2: Quality at Reasonable Price (QARP)

EquityQuery('and', [
    EquityQuery('btwn', ['peratio.lasttwelvemonths', 5, 20]),      # Reasonable P/E
    EquityQuery('gt', ['returnonequity.lasttwelvemonths', 15]),    # High ROE
    EquityQuery('lt', ['totaldebtequity.lasttwelvemonths', 100]),  # Low debt
    EquityQuery('gt', ['intradaymarketcap', 2_000_000_000]),
])

Results: ~180 stocks match

Screen 3: Dividend Value

EquityQuery('and', [
    EquityQuery('gt', ['forward_dividend_yield', 2]),              # Yield > 2%
    EquityQuery('btwn', ['peratio.lasttwelvemonths', 0, 20]),
    EquityQuery('gt', ['consecutive_years_of_dividend_growth_count', 5]),
])

Results: ~260 stocks match

Screen 4: PEG-Based Value

EquityQuery('and', [
    EquityQuery('lt', ['pegratio_5y', 1]),                         # PEG < 1
    EquityQuery('btwn', ['peratio.lasttwelvemonths', 0, 20]),
    EquityQuery('gt', ['epsgrowth.lasttwelvemonths', 10]),         # Growing
])

5. Practical Workflow

Step 1: Screen with yfinance

Run multiple screens to generate candidate list (~50-200 tickers)

Step 2: Deep Dive with Finnhub

For each candidate, fetch:

Step 3: Historical Analysis with yfinance

ticker = yf.Ticker("AAPL")
hist = ticker.history(period="5y")  # 5 years of price data

Step 4: Apply Second-Level Filters


6. Rate Limits & Best Practices

yfinance

Finnhub (Free Tier)

Recommended Approach

  1. Use yfinance screener for bulk filtering (free, fast)
  2. Export top 50-100 candidates
  3. Deep-dive on candidates using Finnhub (richer data)

7. Available Tools

/home/pengacau/pasar-malam/scripts/value_screener.py

Ready-to-use screening script:

uv run python scripts/value_screener.py --strict    # Graham screen
uv run python scripts/value_screener.py --quality   # QARP screen
uv run python scripts/value_screener.py --dividend  # Dividend value
uv run python scripts/value_screener.py --all       # All screens

8. Limitations

  1. yfinance screener max 250 results - must use multiple targeted queries
  2. Finnhub index constituents premium only - can't get S&P 500 list directly
  3. Data freshness - screener data may lag by hours/days
  4. Field availability - some metrics missing for smaller companies
  5. Financial sector exclusions - banks have different metrics

Conclusion

Yes, we can systematically screen for Graham-style value stocks using the combination of:

The workflow is:

  1. Define criteria (P/E, P/B, current ratio, ROE, etc.)
  2. Run yfinance screen (~1 second for 250 results)
  3. Export candidate tickers
  4. Deep-dive with Finnhub (or yfinance Ticker for additional data)
  5. Manual review of top candidates