Files
market_screener/ui/CLAUDE.md
T
2026-06-04 01:32:05 -04:00

5.9 KiB

CLAUDE.md

Guidance for working in this repository.

Overview

market-screener-ui is a SvelteKit 5 single-page application (CSR, no SSR) that serves as the interactive dashboard for the market_screener Fastify API.

  • All data comes from the API at http://localhost:3000 (proxied through Vite in dev)
  • No SSR — +layout.js exports ssr = false
  • Svelte 5 runes syntax ($state, $derived, $effect, $props)

Commands

npm install          # install dependencies (SvelteKit, Vite, Svelte 5)
npm run dev          # dev server on port 5173
npm run build        # production build → .svelte-kit/output
npm run preview      # preview production build

To run the full stack, use npm run dev from the API repo (market_screener/) instead — it starts both servers together using concurrently.


Architecture

No SSR

src/routes/+layout.js exports ssr = false. All data fetching happens in the browser. This avoids Svelte 5 SSR compatibility issues and makes sense for a live-data dashboard.

API Proxy

vite.config.js proxies /api/*http://localhost:3000 in dev. In production, configure your reverse proxy (nginx/Caddy) to do the same.

Data Loading

  • Screener page (/): data loaded client-side on button click via $lib/api.js
  • Portfolio page (/portfolio): data loaded via SvelteKit +page.js load() function — this fires on navigation and is the correct SvelteKit pattern for CSR page data

Do not use onMount for initial data fetching — use load() in +page.js instead. onMount does not reliably fire in SvelteKit CSR for page-level data.


Project Structure

src/
  app.html             ← HTML shell
  app.css              ← Global reset + body styles (no :global() in .svelte files)
  routes/
    +layout.js         ← exports ssr = false
    +layout.svelte     ← nav bar (Screener / Portfolio links)
    +page.svelte        ← Screener page
    portfolio/
      +page.js         ← load() function — fetches /api/finance/portfolio
      +page.svelte     ← Portfolio + SimpleFIN page

  lib/
    api.js             ← All fetch calls to the Fastify API
    SignalBadge.svelte ← Signal pill component (Strong Buy / Avoid / etc.)
    MarketContext.svelte ← Benchmark strip component

.claude/
  launch.json          ← Preview server config for Claude Code

vite.config.js         ← Vite config with /api proxy
svelte.config.js       ← SvelteKit config (adapter-auto)

Key Files

src/lib/api.js

All API calls in one place. If the API base URL changes, change it here only.

screenTickers(tickers)    // POST /api/screen
fetchCatalysts()          // GET /api/screen/catalysts
fetchPortfolio()          // GET /api/finance/portfolio
fetchMarketContext()       // GET /api/finance/market-context

src/routes/+page.svelte (Screener)

  • Ticker input pre-filled with a default watchlist
  • screen() calls API and stores results in $state
  • loadCatalysts() fetches news tickers then immediately calls screen() — one click, full results
  • results is null until first screen — nothing renders below the toolbar
  • verdictShort() abbreviates long verdict strings ("🟢 BUY (High Conviction)""Strong")

src/routes/portfolio/+page.svelte

  • Receives data from +page.js load function via let { data } = $props()
  • Shows data.error if load failed, data.advice for holdings, data.personalFinance for SimpleFIN section

Svelte 5 Patterns Used

<!-- Reactive state -->
let loading = $state(false);

<!-- Derived values -->
const totalGL = $derived(totalValue - totalCost);

<!-- Derived with block -->
const cards = $derived.by(() => { ... return [...] });

<!-- Props -->
let { ctx } = $props();
let { data } = $props();

<!-- Event handlers (no on:click, use onclick) -->
<button onclick={screen}>Screen</button>

<!-- Conditionals in template -->
{@const mode = getTab(type)}

API Response Shape

The Fastify API serializes asset class instances before sending — asset.getDisplayMetrics() is called server-side and included as asset.displayMetrics. In the browser, use r.asset.displayMetrics directly (not r.asset.getDisplayMetrics() which doesn't exist on plain JSON objects).

// Screener response shape
{
  STOCK: [{ asset: { ticker, type, currentPrice, metrics, displayMetrics }, fundamental, inflated, signal }],
  ETF:   [...],
  BOND:  [...],
  ERROR: [...],
  marketContext: { sp500Price, riskFreeRate, vixLevel, rateRegime, volatilityRegime, benchmarks }
}

Styling Conventions

  • Dark theme throughout: page background #0f1117, card sections #0d1117/#111827
  • All colors are CSS custom values inline (no CSS variables yet — keep consistent with existing palette)
  • Tables: width: max-content; min-width: 100% inside a .table-wrap { overflow-x: auto } container
  • First column sticky: position: sticky; left: 0; background: inherit
  • Verdict pills: .verdict-pill.green/yellow/red — colored background tint + text
  • Monospace font for the ticker input field
  • white-space: nowrap on tbody td — tables scroll horizontally, not wrap

Color palette:

page bg:       #0f1117
card bg:       #0d1117  /  #111827 (header rows)
border:        #1e293b
muted:         #64748b  /  #475569
text:          #e2e8f0  /  #f1f5f9
green:         #4ade80  (bg tint: #14532d33)
yellow:        #facc15  (bg tint: #71350033)
red:           #f87171  (bg tint: #450a0a33)
blue accent:   #2563eb  /  #3b82f6

Conventions

  • Do not use :global() in <style> blocks — put global styles in src/app.css
  • Use load() in +page.js for page-level data, not onMount
  • $derived for computed values — do not recalculate in templates
  • Keep api.js as the single place for fetch calls
  • If adding a new page: create +page.js with a load() that fetches the needed API endpoint, receive via $props() in the component