Date: 2025-12-03
Conclusion: YES, we can systematically screen for value stocks using available APIs.
| 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.
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:
NOT available on free tier. The /index/constituents endpoint returns 403 Forbidden.
However, Finnhub excels at deep-dive analysis on individual tickers:
/stock/metric - 132 fundamental metrics per stock/stock/profile2 - Company details/stock/financials-reported - SEC filings data| 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 |
| 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 |
| Field | Description |
|---|---|
currentratio.lasttwelvemonths |
Current ratio |
quickratio.lasttwelvemonths |
Quick ratio |
totaldebtequity.lasttwelvemonths |
Debt/Equity |
altmanzscoreusingtheaveragestockinformationforaperiod.lasttwelvemonths |
Altman Z-Score |
| Field | Description |
|---|---|
epsgrowth.lasttwelvemonths |
EPS growth |
totalrevenues1yrgrowth.lasttwelvemonths |
Revenue growth |
quarterlyrevenuegrowth.quarterly |
Quarterly rev growth |
| Field | Description |
|---|---|
intradaymarketcap |
Market cap |
avgdailyvol3m |
3-month avg volume |
forward_dividend_yield |
Forward yield |
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
Download S&P 500 list from Wikipedia, then query Finnhub for each:
time.sleep(1.1) between callsyfinance includes predefined screens:
undervalued_growth_stocksundervalued_large_capsmost_shorted_stocksday_gainers / day_losersgrowth_technology_stocksresult = yf.screen("undervalued_large_caps")
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
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
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
EquityQuery('and', [
EquityQuery('lt', ['pegratio_5y', 1]), # PEG < 1
EquityQuery('btwn', ['peratio.lasttwelvemonths', 0, 20]),
EquityQuery('gt', ['epsgrowth.lasttwelvemonths', 10]), # Growing
])
Run multiple screens to generate candidate list (~50-200 tickers)
For each candidate, fetch:
/stock/metric - Full fundamental metrics (132 fields)/stock/financials-reported - SEC filings/stock/recommendation - Analyst ratings/company-news - Recent newsticker = yf.Ticker("AAPL")
hist = ticker.history(period="5y") # 5 years of price data
time.sleep(1.1) between calls/home/pengacau/pasar-malam/scripts/value_screener.pyReady-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
Yes, we can systematically screen for Graham-style value stocks using the combination of:
The workflow is: