7.9 KiB
Market Screener
A Node.js stock screener and personal finance assistant. Screens stocks, ETFs, and bonds using live Yahoo Finance data and scores each asset under two lenses — Market-Adjusted (what's acceptable in today's market) and Fundamental (strict Graham value-investing) — then compares both to give you an honest signal.
Comes with a Fastify API server and a companion SvelteKit dashboard (../market-screener-ui) for an interactive UI, or use it as a CLI to generate HTML reports.
Quick Start
# API + Dashboard (recommended)
npm install
cd ../market-screener-ui && npm install && cd ../market_screener
npm run dev # starts API on :3000 + UI on :5173
# open http://localhost:5173
# CLI only
npm start # screen today's news catalyst tickers → screener-report.html
Commands
| Command | What it does |
|---|---|
npm run dev |
Start API server (port 3000) + SvelteKit UI (port 5173) together |
npm run server |
Start API server only |
npm start |
CLI: fetch today's market news, extract tickers, screen them |
npm start -- watch |
CLI: screen the default watchlist |
npm start -- AAPL MSFT VOO |
CLI: screen specific tickers |
npm run finance |
CLI: portfolio advice + SimpleFIN → finance-report.html |
npm run import-portfolio -- file.csv |
Import Robinhood/Vanguard/Fidelity CSV into portfolio.json |
npm test |
Run all 61 unit tests |
npm run test:watch |
Re-run tests on file changes |
npm run format |
Format all source files with Prettier |
How the Screener Works
Every asset is scored twice under different rule sets:
Market-Adjusted mode
Gates derived from live Yahoo Finance benchmarks — reflects what is acceptable in today's market:
| Gate | Formula |
|---|---|
| Stock P/E | S&P 500 P/E (via SPY) × 1.5× (or × 1.2× in HIGH rate regime) |
| Tech P/E | XLK sector P/E × 1.3× |
| REIT min yield | XLRE dividend yield × 0.85× |
| Bond min spread | LQD − TNX spread × 0.80× |
Fundamental mode
Strict Graham/value-investing gates — reflects genuine value regardless of market conditions:
| Gate | Value | Rationale |
|---|---|---|
| Stock P/E | < 15× | Graham's actual rule (trailing earnings) |
| Stock PEG | < 1.0 | Lynch standard: PEG > 1.0 = paying full price |
| D/E ratio | < 1.5× | Distress typically starts above 2× |
| Quick ratio | > 0.8 | Below 0.8 = real liquidity stress |
| Bond spread | > 1.5% | Must clear risk-free rate meaningfully |
| Bond duration | < 7 years | Rate sensitivity management |
Signals
| Signal | Meaning |
|---|---|
| ✅ Strong Buy | Passes both lenses — genuinely good value |
| ⚡ Momentum | Passes market-adjusted, holds fundamentally |
| ⚠️ Speculation | Passes market-adjusted, fails fundamental — priced for perfection |
| 🔄 Neutral | Hold territory in one or both lenses |
| ❌ Avoid | Fails both |
Sector Overrides
Sector-specific rules apply in both modes (not just inflated):
| Sector | Key adjustments |
|---|---|
| Technology | P/E up to 35×, D/E up to 2.0 (buybacks), FCF weight raised |
| REIT | P/E/PEG disabled, scored on dividend yield + P/FFO proxy |
| Financial | D/E disabled, scored on ROE (≥12%) + P/B (< 1.5×) |
| Energy | FCF primary signal (weight 4), dividend yield scored |
| Healthcare | Revenue growth primary, P/E up to 25× (R&D burn) |
| Communication | FCF primary (META/GOOGL platforms), P/E up to 25× |
| Consumer Staples | Margin/ROE focus, low revenue growth expectations (2–5%) |
| Consumer Discretionary | Revenue growth primary, P/E up to 25× |
API Server
GET /health → { status: "ok" }
POST /api/screen → screen tickers
body: { tickers: string[] }
GET /api/screen/catalysts → Yahoo news → { tickers, stories }
GET /api/finance/portfolio → portfolio advice + net worth
GET /api/finance/market-context → live benchmark data
Set CLIENT_ORIGIN env var to allow a different CORS origin (default: http://localhost:5173).
Personal Finance
Edit portfolio.json with your holdings (or import from a broker CSV):
{
"holdings": [
{ "ticker": "AAPL", "shares": 10, "costBasis": 150.00, "source": "Robinhood", "type": "stock" },
{ "ticker": "VOO", "shares": 8, "costBasis": 380.00, "source": "Vanguard", "type": "etf" },
{ "ticker": "BTC-USD", "shares": 0.25, "costBasis": 45000, "source": "Coinbase", "type": "crypto" }
]
}
npm run finance (CLI) or GET /api/finance/portfolio (API) screens your holdings and cross-references the screener signal with your gain/loss position to give hold/sell/add advice.
SimpleFIN (optional — live bank/brokerage balances)
- Get your setup token from beta-bridge.simplefin.org
- Add to
.env:SIMPLEFIN_SETUP_TOKEN=aHR0cHM6Ly... - On first run the Access URL is claimed and saved to
.envautomatically
Importing broker holdings
npm run import-portfolio -- ~/Downloads/robinhood_holdings.csv
npm run import-portfolio -- ~/Downloads/vanguard_holdings.csv
Broker is auto-detected from CSV headers. Multiple imports merge into portfolio.json.
Project Structure
bin/
screen.js CLI screener entry point
finance.js CLI personal finance entry point
import-portfolio.js Broker CSV importer
server.js Fastify API server entry point
scripts/
summary-reporter.js Custom node:test reporter (silent pass, shows failures + summary)
src/
config/
ScoringConfig.js All gates, weights, thresholds (single source of truth)
constants.js Shared enums: SIGNAL, ASSET_TYPE, SECTOR, SCORE_MODE, REGIME
market/
YahooClient.js Wraps yahoo-finance2 v3 with retry + backoff
BenchmarkProvider.js Fetches live benchmarks → marketContext (1-hour cache)
MarketRegime.js Derives inflated gate overrides from live data + rate regime
screener/
ScreenerEngine.js Orchestrates fetch → score × 2.
screenTickers() → pure data (server/CLI)
screenWithProgress() → with stdout progress (CLI only)
DataMapper.js Normalises Yahoo payload → flat asset objects
Uses trailingPE (not forwardPE). Preserves negative FCF.
RuleMerger.js Merges base rules + sector overrides + MarketRegime
assets/ Stock, Etf, Bond data containers
scorers/ StockScorer, EtfScorer, BondScorer (stateless)
analyst/
CatalystAnalyst.js Extracts tickers from Yahoo Finance news headlines
finance/
clients/
SimpleFINClient.js Auth + account fetching via Basic Auth header
PersonalFinanceAnalyzer.js Net worth, cash vs investments, spending
PortfolioAdvisor.js Hold/sell/add advice per holding
PortfolioImporter.js Parses Robinhood/Vanguard/Fidelity CSV
reporters/
HtmlReporter.js render() → string | generate() → file (CLI)
FinanceReporter.js render() → string | generate() → file (CLI)
server/
app.js Fastify app factory (buildApp)
routes/
screener.js POST /api/screen, GET /api/screen/catalysts
finance.js GET /api/finance/portfolio, GET /api/finance/market-context
Environment Variables
# .env
SIMPLEFIN_ACCESS_URL=https://user:pass@beta-bridge.simplefin.org/simplefin
# or on first run:
SIMPLEFIN_SETUP_TOKEN=aHR0cHM6Ly...
# Optional server config
PORT=3000
HOST=0.0.0.0
CLIENT_ORIGIN=http://localhost:5173 # CORS allowed origin for SvelteKit UI
Testing
61 unit tests, no external test framework:
npm test # summary output: "✅ 61 tests: 61 passed (0.02s)"
npm run test:watch # verbose spec output for development
Pre-commit: Prettier (auto-format staged files) + full test suite. Pre-push: full test suite.