Files
market_screener/CLAUDE.md
T
Kazuma cd74497de6 refactor: restructure to clean architecture
fix: restore ScoringConfig improvements lost in refactor commit

docs: rewrite README and CLAUDE.md to reflect current architecture

code-format

code fixes
2026-06-03 01:36:21 -04:00

232 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# CLAUDE.md
Guidance for working in this repository.
## Overview
`market-screener` is a Node.js CLI tool that screens stocks, ETFs, and bonds by fetching live data from Yahoo Finance and scoring each asset under two lenses:
- **Market-Adjusted** — gates derived from live Yahoo benchmarks (SPY P/E, XLK P/E, XLRE yield, LQD spread). Reflects what is acceptable in today's inflated market.
- **Fundamental** — strict Graham/value-investing style gates from `ScoringConfig`. Reflects genuine value regardless of market conditions.
The comparison produces a **Signal** (Strong Buy / Momentum / Speculation / Neutral / Avoid).
ES module project (`"type": "module"`); use `import`/`export`, not `require`.
## Commands
```bash
npm install # install dependencies (yahoo-finance2, dotenv, prettier)
npm start # Yahoo news → catalyst tickers → screener-report.html
npm start -- watch # default watchlist
npm start -- AAPL MSFT VOO # specific tickers
npm run finance # portfolio advice + SimpleFIN → finance-report.html
npm run import-portfolio -- holdings.csv # import Robinhood/Vanguard CSV into portfolio.json
npm test # run all unit tests (node:test, zero deps)
npm run test:watch # watch mode during development
npm run format # format all src/bin/tests with Prettier
npm run format:check # check formatting without writing (CI)
```
## Project Structure
```
bin/
screen.js ← market screener entry point
finance.js ← personal finance entry point
import-portfolio.js ← broker CSV importer
prompts/
catalyst-analysis.md ← daily catalyst analysis playbook (LLM prompt + workflow)
src/
config/
ScoringConfig.js ← CREDIT_RATING_SCALE + ScoringRules (single source of truth)
market/ ← Yahoo Finance data layer
YahooClient.js ← wraps yahoo-finance2 v3, retry + backoff
BenchmarkProvider.js ← fetches ^GSPC, ^TNX, ^VIX, SPY, XLK, XLRE, LQD → marketContext
MarketRegime.js ← derives INFLATED gate overrides from live benchmarks
screener/ ← core screening domain
ScreenerEngine.js ← orchestrates: fetch → score × 2 → HTML report
DataMapper.js ← normalises Yahoo payload → flat asset data object
RuleMerger.js ← merges base rules + sector overrides + MarketRegime (INFLATED mode)
Chunker.js ← splits ticker list into batches
assets/
Asset.js ← abstract base: ticker, currentPrice, type, formatting helpers
Stock.js ← metrics: peRatio, pegRatio, priceToBook, ROE, opMargin, etc.
Etf.js ← metrics: expenseRatio, yield, totalAssets
Bond.js ← metrics: ytm, duration, creditRating, creditRatingNumeric
scorers/
StockScorer.js ← gate checks + weighted registry (ROE, opMargin, margin, peg, rev, fcf)
EtfScorer.js ← expense gate + registry (cost, yield, volume)
BondScorer.js ← credit gate + spread/duration scoring
analyst/
CatalystAnalyst.js ← fetches Yahoo Finance news, extracts relatedTickers
finance/
clients/
SimpleFINClient.js ← claims setup token → access URL, fetches /accounts
PersonalFinanceAnalyzer.js ← net worth, cash vs investments, spending by category
PortfolioAdvisor.js ← cross-references holdings with screener signals → hold/sell/add advice
PortfolioImporter.js ← parses Robinhood/Vanguard/Fidelity CSV → merges into portfolio.json
reporters/
HtmlReporter.js ← generates screener-report.html (tabbed inflated/fundamental views)
FinanceReporter.js ← generates finance-report.html (net worth, portfolio, spending)
portfolio.json ← user's holdings: ticker, shares, costBasis, source, type
```
## Data Flow
```
Yahoo Finance API
BenchmarkProvider — fetches ^GSPC, ^TNX, ^VIX, SPY, XLK, XLRE, LQD
builds marketContext { sp500Price, riskFreeRate, vixLevel,
rateRegime, volatilityRegime, benchmarks { marketPE, techPE, reitYield, igSpread } }
DataMapper — normalises raw Yahoo payload → flat data object with type (STOCK/ETF/BOND)
computes: pFFO proxy, FCF yield, computed PEG, 52-week position
Asset subclass — Stock / Etf / Bond holds metrics, formats display
RuleMerger × 2 — FUNDAMENTAL mode: ScoringConfig as-is (Graham-style)
INFLATED mode: sector override + MarketRegime live gate overrides
Scorer × 2 — StockScorer / EtfScorer / BondScorer, fully stateless
ScreenerEngine — derives Signal from comparing both verdicts
HtmlReporter — screener-report.html: signal summary + two tabbed tables per asset class
```
## Scoring Modes
| Mode | P/E Gate (general) | Source |
|---|---|---|
| FUNDAMENTAL | 20x | ScoringConfig (Graham) |
| INFLATED | S&P 500 P/E × 1.5 | Live SPY data |
| Signal | Meaning |
|---|---|
| ✅ Strong Buy | Passes both fundamental AND inflated gates |
| ⚡ Momentum | Passes inflated, holds fundamentally |
| ⚠️ Speculation | Passes inflated, fails fundamental |
| 🔄 Neutral | Hold territory in one or both lenses |
| ❌ Avoid | Fails both |
## ScoringConfig Structure
`src/config/ScoringConfig.js` exports two things:
- `CREDIT_RATING_SCALE``{ AAA: 10, AA: 9, ..., D: 1 }`. Used by Bond.js and BondScorer.
- `ScoringRules` — per-type `{ gates, weights, thresholds }` + `SECTOR_OVERRIDE` map for STOCK.
Sector overrides are structural (apply in both modes). MarketRegime overrides valuation gates in INFLATED mode only.
## Sector Notes
- **TECHNOLOGY** — `maxDebtToEquity: 2.0` (mega-cap tech borrows for buybacks), `minQuickRatio: 0.8` (AAPL runs ~0.9). P/E and PEG gates inflated from XLK live data.
- **REIT** — P/E and PEG disabled (9999). Scored on dividend yield and P/FFO proxy. All base weights (margin, peg, revenue) explicitly zeroed out.
- **FINANCIAL** — P/E, PEG, D/E disabled. Scored on ROE + Price-to-Book. Quick ratio gate lowered (0.1).
## MarketRegime (INFLATED overrides)
`src/market/MarketRegime.js` derives gate overrides from live benchmarks:
| Gate | Formula |
|---|---|
| Stock maxPERatio | SPY trailing P/E × 1.5 |
| Tech maxPERatio | XLK P/E × 1.3 |
| Tech maxPegGate | XLK P/E ÷ 15 |
| REIT minYield | XLRE dividend yield × 0.85 |
| Bond minSpread | LQDTNX spread × 0.80 |
| ETF maxExpenseRatio | 0.75% (structural loosening) |
## Missing Data Convention
- Missing metrics use `null` (not `0`) in `_sanitize`. Gate checks skip `null` values rather than auto-failing.
- `pegRatio` falls back to `trailingPE / earningsGrowth` when Yahoo doesn't provide it.
- `quickRatio` falls back to `currentRatio` when missing.
## SimpleFIN Auth Flow
1. User gets a Setup Token from https://beta-bridge.simplefin.org
2. `SimpleFINClient.init()` base64-decodes it → POSTs once to claim URL → receives Access URL
3. Access URL is auto-appended to `.env` as `SIMPLEFIN_ACCESS_URL`
4. All subsequent requests use Access URL directly (setup token is one-time use)
## portfolio.json Format
```json
{
"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" }
]
}
```
`type` values: `stock`, `etf`, `crypto`. Crypto is priced via Yahoo (BTC-USD style) but not fundamentally scored.
## Tests
Uses Node's built-in test runner (`node:test` + `node:assert/strict`) — no test framework to install.
```
tests/
ScoringConfig.test.js ← CREDIT_RATING_SCALE, gate values, sector overrides
RuleMerger.test.js ← FUNDAMENTAL vs INFLATED modes, sector merging
MarketRegime.test.js ← inflated override formulas per asset type and sector
StockScorer.test.js ← gate failures, scoring labels, risk flags
EtfScorer.test.js ← expense gate, score tiers
BondScorer.test.js ← credit gate, spread/duration scoring, unit handling
DataMapper.test.js ← type detection, PEG computation, null convention
PortfolioAdvisor.test.js ← _position gain/loss calc, _advice signal mapping
```
**Key unit to remember:** `ytm` in `Bond.metrics` is stored as a percentage (e.g. `6.5` = 6.5%). `BondScorer._sanitize` divides by 100 before spread calculation. `minSpread` in `ScoringConfig` is also in percentage form (e.g. `1.0` = 1%).
## Conventions
- Asset `type` (uppercased) is the routing key across DataMapper, asset classes, `SCORERS` map in ScreenerEngine, and ScoringRules.
- Prefer adjusting `ScoringConfig` or `MarketRegime` over hardcoding numbers in scorers.
- BenchmarkProvider caches for 1 hour — restart the process to force a fresh fetch during development.
- All entry points live in `bin/`. Do not add logic to entry points — they call into `src/`.
## Planned Enhancements
### 1. Rate Sensitivity Flag (no new fetches needed)
Flag assets exposed to rate changes using `riskFreeRate` and `rateRegime`:
- Long-duration bonds (duration > 7) in HIGH rate regime → warn
- REITs with high D/E in HIGH rate regime → warn
- Growth stocks with negative FCF in HIGH rate regime → warn
### 2. 52-Week Positioning (data already mapped)
`week52High` and `week52Low` are in asset metrics. Add interpretation:
- `> 85%` position + high P/E = crowded/momentum trade
- `< 15%` position + passing fundamental gates = potential opportunity
### 3. Sector Rotation Cues (34 new fetches)
Add XLF, XLE, XLV, XLI to BenchmarkProvider. Compare each sector ETF's 1-year return to S&P 500 to identify leading/lagging sectors.
### 4. Client/Server Architecture
Planned split into:
- **Server** (Fastify + BullMQ) — API endpoints, webhook-driven news monitoring, WebSocket for live updates
- **Client** (SvelteKit) — interactive dashboard replacing the HTML reports
- LLM integration (Claude Haiku) wired into the server for deeper catalyst analysis
## Adding a New Asset Type
1. Create a subclass of `Asset` in `src/screener/assets/` with a flat `metrics` object and `getDisplayMetrics()`.
2. Add a per-type entry (`gates` / `weights` / `thresholds`) to `ScoringRules` in `ScoringConfig.js`.
3. Add inflated overrides in `MarketRegime.getInflatedOverrides()`.
4. Create a Scorer in `src/screener/scorers/` exposing `score(metrics, rules, marketContext)`.
5. Add a mapper in `DataMapper.js`.
6. Wire into `ScreenerEngine`: add `case` in `_buildAsset`, entry in `SCORERS` map.