Files
market_screener/README.md
T
2026-06-06 22:55:43 -04:00

607 lines
18 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.
# Market Screener
A personal stock screener and portfolio tracker. Scores stocks, ETFs, and bonds under two lenses — **Market-Adjusted** (what's acceptable in today's market) and **Fundamental** (strict Graham value-investing) — then compares them to produce an actionable signal. Comes with a live SvelteKit dashboard.
---
## Table of Contents
- [Developer Setup](#developer-setup)
- [Environment Variables](#environment-variables)
- [Commands](#commands)
- [Running Tests](#running-tests)
- [Project Structure](#project-structure)
- [User Guide](#user-guide)
- [API Testing with Bruno](#api-testing-with-bruno)
---
## Developer Setup
### Prerequisites
- **Node.js 20+** (v22 recommended)
- **npm 10+**
**Check your versions:**
```bash
node --version # Should output v20.x.x or higher
npm --version # Should output 10.x.x or higher
```
**Not on Node 20+?** See [NODE_VERSION_FIX.md](./NODE_VERSION_FIX.md) for upgrade instructions.
### Install
```bash
# Install server dependencies
npm install
# Install UI dependencies (first time only)
npm run ui:install
```
### Start
```bash
npm run dev
```
This starts both the API server on **port 3000** and the SvelteKit UI on **port 5173** concurrently. Open [http://localhost:5173](http://localhost:5173).
To run the API server alone:
```bash
npm run server
```
---
## Environment Variables
Create a `.env` file in the project root. None are required to run the app — it works with Yahoo Finance data out of the box. Optional keys unlock additional features.
### `ANTHROPIC_API_KEY` — LLM news analysis *(optional)*
Powers the **Analyze** button on each screener section. Without this key the button is disabled.
1. Go to [console.anthropic.com](https://console.anthropic.com)
2. Create an API key under **API Keys**
3. Add to `.env`:
```env
ANTHROPIC_API_KEY=sk-ant-...
```
### `SIMPLEFIN_SETUP_TOKEN` — Live bank/brokerage balances *(optional)*
Powers the personal finance section of the Portfolio page (net worth, account balances, spending breakdown).
1. Go to [beta-bridge.simplefin.org](https://beta-bridge.simplefin.org) and create a Setup Token
2. Add to `.env`:
```env
SIMPLEFIN_SETUP_TOKEN=aHR0cHM6Ly...
```
On first request the token is claimed automatically and the resulting Access URL is saved back to `.env` as `SIMPLEFIN_ACCESS_URL`. Subsequent restarts use the Access URL directly.
### `API_KEY` — Bearer token auth *(optional)*
When set, every API route requires `Authorization: Bearer <key>`. Useful when the server is exposed to a network. `/health` and OPTIONS preflight are exempt.
```env
API_KEY=your-secret-key
```
### `CLIENT_ORIGIN` — CORS allowed origin *(optional)*
Defaults to `http://localhost:5173`. Change if the UI is served from a different origin.
```env
CLIENT_ORIGIN=https://yourdomain.com
```
### Complete `.env` example
```env
ANTHROPIC_API_KEY=sk-ant-...
SIMPLEFIN_SETUP_TOKEN=aHR0cHM6Ly...
API_KEY=optional-secret
CLIENT_ORIGIN=http://localhost:5173
```
---
## Commands
| Command | Description |
|---|---|
| `npm run dev` | Start API (port 3000) + UI (port 5173) together |
| `npm run server` | Start API server only |
| `npm run ui:install` | Install UI dependencies (first time / after `git pull`) |
| `npm test` | Run all unit + integration tests |
| `npm run test:watch` | Watch mode — re-run on file changes |
| `npm run typecheck` | TypeScript type check without emitting |
| `npm run format` | Format all source files with Prettier |
| `npm run format:check` | Check formatting without writing (used in CI) |
| `npm run lint` | Run ESLint on all TypeScript files |
| `npm run lint:fix` | Auto-fix ESLint issues |
---
## Running Tests
```bash
npm test
```
Uses Node's built-in `node:test` runner — no external framework. **114 test cases** across 9 files cover:
| Test File | Tests | Coverage |
|-----------|-------|----------|
| `app.test.ts` | 9 | App bootstrap, CORS, health endpoints |
| `screener-controller.test.ts` | 10 | `/api/screen` endpoints |
| `screener-engine.test.ts` | 11 | Screening orchestration logic |
| `stock-scorer.test.ts` | 13 | Stock valuation gates |
| `etf-scorer.test.ts` | 17 | ETF fund gates |
| `bond-scorer.test.ts` | 16 | Bond credit analysis |
| `portfolio-advisor.test.ts` | 12 | Portfolio advice logic |
| `portfolio-controller.test.ts` | 12 | Portfolio endpoints |
| `calls-controller.test.ts` | 14 | Market calls endpoints |
### Pre-Commit & Pre-Push Hooks
On `git commit`, the **pre-commit hook** automatically:
1. **Formats** all files with Prettier
2. **Lints & fixes** staged files with ESLint
3. **Runs tests** to catch errors early
On `git push`, the **pre-push hook** runs tests again for safety.
---
## Project Structure
**Phase 9: Domain-Driven Architecture** (completed)
```
bin/
server.ts API server entry point
server/
app.ts Fastify app factory — wires DI, rate limiting, auth hook
domains/ Domain-driven structure (shared, screener, portfolio, calls, finance)
shared/ Infrastructure & cross-domain utilities
adapters/ YahooFinanceClient, AnthropicClient, SimpleFINClient
services/ BenchmarkProvider, CatalystAnalyst, LLMAnalyst
entities/ Asset, Stock, Etf, Bond
persistence/ MarketCallRepository, PortfolioRepository
config/ ScoringConfig (gates/weights), constants
scoring/ MarketRegime, scoring overrides
types/ TypeScript interfaces (one file per domain)
screener/ Stock/ETF/Bond filtering & scoring
ScreenerEngine.ts Orchestrates: fetch → score × 2 (fundamental + inflated)
scorers/ StockScorer, EtfScorer, BondScorer
transform/ DataMapper, RuleMerger
portfolio/ Holdings management & investment advice
PortfolioAdvisor.ts Cross-references holdings with screener signals
calls/ Market call tracking & earnings calendar
CalendarService.ts Earnings calendar logic
finance/ Portfolio metrics & reporting
ui/
src/
routes/ SvelteKit pages: /, /portfolio, /calls, /safe-buys
lib/
components/ Shared UI components organized by domain
stores/ Svelte 5 reactive stores
api/ Fetch wrappers for each API domain
styles/ Global SCSS design tokens and partials
tests/ Unit + integration tests (9 files, 114 test cases)
Controllers, services, scorers fully covered
portfolio.json Your holdings (gitignored — create manually or via the UI)
market-calls.json Persisted market thesis calls (gitignored)
.benchmark-cache.json Benchmark data cache — survives server restart (gitignored)
```
See **[CLAUDE.md](./CLAUDE.md)** for detailed architecture and **[PHASES.md](./PHASES.md)** for the complete roadmap.
---
## User Guide
### Screener tab
The main view. On load it automatically fetches today's financial news, extracts the most-mentioned tickers, and screens them.
#### Market context strip
The row of chips at the top shows live benchmark data fetched from Yahoo Finance:
| Chip | Meaning |
|---|---|
| 10Y | 10-year Treasury yield — the risk-free rate |
| VIX | Volatility index — market fear gauge |
| S&P | S&P 500 index price |
| S&P P/E | Trailing P/E of the S&P 500 (via SPY) |
| Tech P/E | Trailing P/E of the tech sector (via XLK) |
| REIT Yld | REIT dividend yield (via XLRE) |
| IG Sprd | Investment-grade bond spread above risk-free (LQD TNX) |
| Rates | Rate regime: LOW / NORMAL / HIGH (based on 10Y yield) |
| Vol | Volatility regime: LOW / NORMAL / HIGH (based on VIX) |
The rate regime affects how strict the Market-Adjusted gates are — in a HIGH rate environment the P/E multiplier compresses and bond spreads tighten.
#### Signal Summary table
Quick overview of all screened tickers with their signal, market-adjusted verdict, fundamental verdict, market cap tier, and growth style.
#### Per-asset detail tables
Expand each section (STOCK / ETF / BOND) for full metrics: P/E, PEG, ROE, margins, FCF yield, D/E, analyst consensus, DCF intrinsic value, 52-week movement, and more.
#### Analyze button
Runs an Anthropic LLM over the latest Yahoo Finance news for assets in that section. Returns a sentiment summary, affected industries, and related tickers to watch. Requires `ANTHROPIC_API_KEY`.
#### Search tickers
Click **Search tickers** to screen any custom list — type tickers comma or space separated and press Enter or click Screen.
#### Signals explained
| Signal | What it means |
|---|---|
| ✅ Strong Buy | Passes both Market-Adjusted AND Fundamental gates — genuine value at current prices |
| ⚡ Momentum | Passes Market-Adjusted, holds fundamentally — good in the current market but not a bargain |
| ⚠️ Speculation | Passes Market-Adjusted, fails Fundamental — priced for perfection, high risk |
| 🔄 Neutral | Borderline in one or both lenses — hold, no clear edge |
| ❌ Avoid | Fails both lenses |
#### How scoring works
Every asset is scored twice:
**Market-Adjusted** gates move with the market. The stock P/E gate = SPY trailing P/E × 1.5 (compresses to × 1.2 in a HIGH rate regime). Tech P/E = XLK P/E × 1.3. This reflects what the market is currently willing to pay.
**Fundamental** gates are fixed Graham/value-investing standards that never change:
| Gate | Threshold | Rationale |
|---|---|---|
| Stock P/E | < 15× | Graham's actual rule |
| Stock PEG | < 1.0 | Lynch: 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 |
Sector overrides apply in both modes — e.g. tech stocks allow P/E up to 35× and D/E up to 2.0, REITs are scored on yield rather than P/E.
---
### Portfolio tab
Track your holdings and get hold/sell/add advice cross-referenced with screener signals.
**Adding holdings** — click **+ Add Holding** and fill in ticker, shares, cost basis, asset type, and source broker. Holdings are saved to `portfolio.json` on disk.
**Inline editing** — click the ✎ pencil icon on any row to edit shares, cost basis, type, or source directly in the table.
**Advice column** — each holding is screened live and the signal is combined with your gain/loss position:
| Situation | Advice |
|---|---|
| ✅ Strong Buy signal | Hold & Add |
| ⚡ Momentum + > 30% gain | Consider partial profit-taking |
| ⚠️ Speculation + > 20% gain | Reduce position |
| ❌ Avoid signal + in profit | Sell (Take Profits) |
| ❌ Avoid signal + at a loss | Sell (Cut Loss) |
| Crypto | Hold / Review position (no fundamental scoring) |
**Personal finance section** *(requires SimpleFIN)* — when configured, the page also shows net worth, total assets vs liabilities, cash vs investments ratio, monthly income/spend, account balances, and a spending breakdown by category for the last 30 days.
---
### Market Calls tab
Record and track quarterly investment theses from the day you make the call.
**Creating a call** — click ** New Call** and fill in:
- **Title** — e.g. "Q3 2025 — Rate pivot & tech rotation"
- **Quarter** — the quarter this thesis applies to
- **Thesis** — the macro reasoning behind the call (min 10 characters)
- **Tickers** — the assets you're watching for this thesis
When saved, the current price and signal for each ticker are snapshotted automatically.
**Viewing performance** — click any call card to see the current price and signal for each ticker alongside the original snapshot, so you can measure how the thesis played out.
**Calendar** — shows upcoming earnings dates, ex-dividend dates, and dividend payment dates for all tickers across your active calls.
---
### Safe Buys tab
A filtered view showing only tickers with a **✅ Strong Buy** signal across both lenses. A quick watchlist of assets passing the strictest criteria in the current market.
---
### API rate limits
`/api/screen`, `/api/screen/catalysts`, and `/api/analyze` are capped at **10 requests per minute** per IP. All other routes allow 60 per minute.
---
## API Testing with Bruno
### What is Bruno?
[Bruno](https://www.usebruno.com/) is a lightweight, open-source API client that's Git-friendly and perfect for testing REST APIs. It stores collections as plain text files instead of JSON blobs, making them easy to version control and collaborate on.
### Installing Bruno
#### macOS (via Homebrew)
```bash
brew install bruno
```
#### macOS (Direct Download)
1. Visit [usebruno.com/downloads](https://www.usebruno.com/downloads)
2. Download the macOS version
3. Drag `Bruno.app` to Applications folder
#### Windows
1. Visit [usebruno.com/downloads](https://www.usebruno.com/downloads)
2. Download the Windows installer (.exe)
3. Run the installer and follow the prompts
4. Or via Chocolatey: `choco install bruno`
#### Linux (Ubuntu/Debian)
```bash
# Add Bruno repository
curl -1sLf 'https://dl.usebruno.com/install.sh' | sudo bash
# Install
sudo apt-get install bruno
```
#### Linux (Fedora/RHEL)
```bash
curl -1sLf 'https://dl.usebruno.com/install.sh' | sudo bash
sudo dnf install bruno
```
### Installing Bruno CLI (brucli)
For running tests from the command line without the GUI:
#### macOS
```bash
brew install brucli
```
#### Windows
```bash
choco install bruno-cli
```
#### Linux
```bash
curl -1sLf 'https://dl.usebruno.com/install.sh' | sudo bash
```
### Importing the API Collection
#### Method 1: Import via Bruno GUI (Easiest)
1. **Open Bruno**
2. **File → Import Collection**
3. **Select** `api_collections/market-screener.postman_collection.json`
4. **Choose location** where to save the converted collection (e.g., `api_collections/market-screener`)
5. **Click Import** — Bruno automatically converts and structures the collection
#### Method 2: Import via Bruno CLI
```bash
# Navigate to the project root
cd market-screener
# Import the Postman collection
bru import api_collections/market-screener.postman_collection.json -o api_collections/market-screener
# Output: Collection imported to api_collections/market-screener/
```
#### Method 3: Convert Postman to Bruno Format (Manual)
If you prefer to manually convert the collection:
```bash
# Install conversion dependencies (if needed)
pip install requests
# Run the conversion script
python3 api_collections/convert_postman_to_bruno.py \
api_collections/market-screener.postman_collection.json \
api_collections/market-screener
```
### Running Tests
#### Via Bruno GUI
1. **Open the imported collection** in Bruno
2. **Set the `baseUrl` variable** (default: `http://localhost:3000`)
3. **Click the Play button** to run all tests
4. **View results** for each request in the UI
#### Via Bruno CLI (brucli)
```bash
# Navigate to the collection directory
cd api_collections/market-screener
# Run all tests in the collection
bru run
# Run with specific environment
bru run --env local
# Run with output format
bru run --output json > test-results.json
# Run specific test file
bru run "Screener/Screen - Mixed.bru"
```
### Collection Structure
After import, you'll have:
```
api_collections/market-screener/
├── bruno.json # Collection metadata
├── Health/
│ └── Health Check.bru
├── Screener/
│ ├── Screen - Mixed.bru
│ ├── Screen - Tech Stocks.bru
│ ├── Screen - REIT.bru
│ ├── Validation empty tickers.bru
│ ├── Validation 50 plus tickers.bru
│ └── Get Catalysts.bru
├── Market Context/
│ └── Get Market Context.bru
├── Portfolio/
│ ├── Add Holding AAPL.bru
│ ├── Add Holding VOO.bru
│ ├── Add Holding BTC-USD.bru
│ ├── Add Holding Validation.bru
│ ├── Get Portfolio.bru
│ ├── Remove Holding AAPL.bru
│ └── Remove Holding Non-existent.bru
├── Market Calls/
│ ├── List Calls.bru
│ ├── Create Market Call.bru
│ ├── Get Call by ID.bru
│ ├── Get Call Non-existent.bru
│ ├── Get Earnings Calendar.bru
│ ├── Get Calendar Specific Tickers.bru
│ ├── Create Call Validation.bru
│ ├── Delete Call.bru
│ └── Delete Call Already Deleted.bru
└── LLM Analysis/
├── Analyze Tickers.bru
└── Analyze Validation.bru
```
### Configuration
#### Setting Variables
Variables are stored in `bruno.json` and can be overridden per request:
**Default variables:**
- `baseUrl`: `http://localhost:3000`
- `callId`: (auto-populated by Create Market Call request)
To change variables in the GUI:
1. Right-click collection → **Settings**
2. Click **Variables** tab
3. Edit `baseUrl` or other variables
4. Click **Save**
#### Environment Files
Create a `.env.bruno` file in the collection directory for local overrides:
```env
baseUrl=http://localhost:3000
apiKey=your-secret-key
```
### Common Workflows
#### 1. Test the full API flow
```bash
cd api_collections/market-screener
bru run
```
#### 2. Test just the Screener endpoints
```bash
cd api_collections/market-screener
bru run "Screener"
```
#### 3. Test and save results
```bash
cd api_collections/market-screener
bru run --output json > test-results-$(date +%Y%m%d).json
```
#### 4. Continuous testing (while developing)
```bash
# Terminal 1: Run the API server
npm run dev
# Terminal 2: Watch and run tests every 5 seconds
cd api_collections/market-screener
watch -n 5 'bru run'
```
### Troubleshooting
#### "You can run only at the root of a collection" error
Make sure you're in the correct directory:
```bash
# ❌ Wrong — project root
cd market-screener
bru run
# ✅ Correct — collection root
cd api_collections/market-screener
bru run
```
#### Variables not found
Verify variable names in `bruno.json`:
```bash
# Check variables
cat api_collections/market-screener/bruno.json | grep -A 10 "vars"
```
#### Tests failing with "undefined" errors
Common causes:
- Variable name mismatch (case-sensitive)
- Server not running on the expected port
- Port conflict (try `lsof -i :3000` to check)
### Postman vs Bruno
| Feature | Postman | Bruno |
|---------|---------|-------|
| **Download Size** | ~380MB | ~50MB |
| **Collection Format** | Single JSON blob | Plain text `.bru` files |
| **Git-Friendly** | ❌ Binary | ✅ Text-based, diffable |
| **API Response** | UI-only | CLI + GUI |
| **Cost** | Free tier + paid | ✅ Completely free |
| **IDE Integration** | None | Can edit `.bru` files directly |
### References
- **Bruno Docs**: [docs.usebruno.com](https://docs.usebruno.com)
- **Bruno GitHub**: [github.com/usebruno/bruno](https://github.com/usebruno/bruno)
- **Postman Collection**: `api_collections/market-screener.postman_collection.json`