2026-06-06 22:55:43 -04:00
2026-06-06 22:55:43 -04:00
2026-06-06 22:55:43 -04:00
2026-06-06 22:55:43 -04:00
2026-06-06 22:55:43 -04:00
2026-06-06 22:55:43 -04:00
2026-06-06 22:55:43 -04:00
2026-06-06 22:55:43 -04:00
2026-06-06 22:55:43 -04:00
2026-06-06 22:55:43 -04:00
2026-06-06 22:55:43 -04:00
2026-06-06 22:55:43 -04:00
2026-06-06 22:55:43 -04:00
2026-06-06 22:55:43 -04:00
2026-06-06 22:55:43 -04:00

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

Prerequisites

  • Node.js 20+ (v22 recommended)
  • npm 10+

Check your versions:

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 for upgrade instructions.

Install

# Install server dependencies
npm install

# Install UI dependencies (first time only)
npm run ui:install

Start

npm run dev

This starts both the API server on port 3000 and the SvelteKit UI on port 5173 concurrently. Open http://localhost:5173.

To run the API server alone:

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
  2. Create an API key under API Keys
  3. Add to .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 and create a Setup Token
  2. Add to .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.

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.

CLIENT_ORIGIN=https://yourdomain.com

Complete .env example

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

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 for detailed architecture and 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 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)

brew install bruno

macOS (Direct Download)

  1. Visit usebruno.com/downloads
  2. Download the macOS version
  3. Drag Bruno.app to Applications folder

Windows

  1. Visit 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)

# Add Bruno repository
curl -1sLf 'https://dl.usebruno.com/install.sh' | sudo bash

# Install
sudo apt-get install bruno

Linux (Fedora/RHEL)

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

brew install brucli

Windows

choco install bruno-cli

Linux

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

# 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:

# 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)

# 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:

baseUrl=http://localhost:3000
apiKey=your-secret-key

Common Workflows

1. Test the full API flow

cd api_collections/market-screener
bru run

2. Test just the Screener endpoints

cd api_collections/market-screener
bru run "Screener"

3. Test and save results

cd api_collections/market-screener
bru run --output json > test-results-$(date +%Y%m%d).json

4. Continuous testing (while developing)

# 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:

# ❌ 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:

# 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

S
Description
No description provided
Readme 1.7 MiB
Languages
TypeScript 49%
Svelte 30.2%
HTML 11.7%
SCSS 8.7%
JavaScript 0.2%
Other 0.2%