import { useState, useEffect, useCallback } from 'react'; import { SlidersHorizontal, Search, Loader2, RefreshCw } from 'lucide-react'; import { useAppContext } from '../context/AppContext'; import { useNavigate } from 'react-router-dom'; import { getPlatformAccessToken } from '../lib/authSession'; import { tradingRuntime } from '../lib/runtime'; import { createRequestId } from '../../../shared/request-id.js'; // ─── Types ──────────────────────────────────────────────────────────────────── interface ScreenerRow { symbol: string; companyName: string; price: number; changesPercentage: number; marketCap: number; pe: number | null; sector: string; volume: number; } const SECTORS = [ 'All', 'Technology', 'Financial Services', 'Healthcare', 'Consumer Cyclical', 'Consumer Defensive', 'Industrials', 'Energy', 'Utilities', 'Real Estate', 'Communication Services', 'Basic Materials', ]; const CAP_OPTIONS: { label: string; min: number; max?: number }[] = [ { label: 'Any Cap', min: 0 }, { label: 'Mega (>$200B)', min: 200_000_000_000 }, { label: 'Large ($10B–$200B)', min: 10_000_000_000, max: 200_000_000_000 }, { label: 'Mid ($2B–$10B)', min: 2_000_000_000, max: 10_000_000_000 }, { label: 'Small (<$2B)', min: 0, max: 2_000_000_000 }, ]; const fmtCap = (n: number) => { if (n >= 1e12) return `$${(n / 1e12).toFixed(1)}T`; if (n >= 1e9) return `$${(n / 1e9).toFixed(1)}B`; if (n >= 1e6) return `$${(n / 1e6).toFixed(1)}M`; return `$${n.toLocaleString()}`; }; // ─── Screener fetch ─────────────────────────────────────────────────────────── async function runScreener(params: { sector: string; marketCapMore?: number; marketCapLess?: number; limit: number; }): Promise { const token = await getPlatformAccessToken(); const qs = new URLSearchParams({ limit: String(params.limit) }); if (params.sector && params.sector !== 'All') qs.set('sector', params.sector); if (params.marketCapMore) qs.set('marketCapMoreThan', String(params.marketCapMore)); if (params.marketCapLess) qs.set('marketCapLessThan', String(params.marketCapLess)); const res = await fetch(`${tradingRuntime.tradingApiUrl}/api/screener?${qs}`, { headers: { Authorization: `Bearer ${token}`, 'x-request-id': createRequestId('web-screener'), }, }); if (!res.ok) { const body = await res.json().catch(() => ({})) as any; throw new Error(body?.error ?? `Screener failed (${res.status})`); } const data = await res.json() as any; return Array.isArray(data) ? data : (data.results ?? []); } // ─── ScreenerView ───────────────────────────────────────────────────────────── export function ScreenerView() { const { setActiveSymbol } = useAppContext(); const navigate = useNavigate(); const [results, setResults] = useState([]); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [query, setQuery] = useState(''); const [sector, setSector] = useState('All'); const [capIdx, setCapIdx] = useState(0); const [sortKey, setSortKey] = useState('marketCap'); const [sortAsc, setSortAsc] = useState(false); const fetchResults = useCallback(async () => { setLoading(true); setError(null); const cap = CAP_OPTIONS[capIdx]; try { const rows = await runScreener({ sector, marketCapMore: cap.min > 0 ? cap.min : undefined, marketCapLess: cap.max, limit: 50, }); setResults(rows); } catch (e: any) { setError(e?.message ?? 'Screener request failed'); } finally { setLoading(false); } }, [sector, capIdx]); // Fetch on mount and when filters change useEffect(() => { fetchResults(); }, [fetchResults]); // Client-side search filter + sort const filtered = results .filter(r => { if (!query) return true; const q = query.toUpperCase(); return r.symbol?.includes(q) || r.companyName?.toLowerCase().includes(query.toLowerCase()); }) .sort((a, b) => { const av = a[sortKey] as any ?? 0; const bv = b[sortKey] as any ?? 0; return sortAsc ? (av > bv ? 1 : -1) : (av < bv ? 1 : -1); }); const handleSort = (key: keyof ScreenerRow) => { if (sortKey === key) setSortAsc(p => !p); else { setSortKey(key); setSortAsc(false); } }; const SortIcon = ({ k }: { k: keyof ScreenerRow }) => ( {sortKey === k ? (sortAsc ? '▲' : '▼') : '⇅'} ); const handleRowClick = (symbol: string) => { setActiveSymbol(symbol); navigate('/'); }; return (

Stock Screener

{/* Filters */}
{/* Search */}
setQuery(e.target.value)} style={{ width: '100%', paddingLeft: 32, paddingRight: 12, paddingTop: 8, paddingBottom: 8, border: '1px solid #E5E7EB', borderRadius: 8, fontSize: 13, outline: 'none', background: '#fff', color: '#374151', boxSizing: 'border-box', fontFamily: 'inherit', }} />
{/* Market cap */} {/* Sector pills */}
{SECTORS.slice(0, 6).map(s => ( ))} {/* Sector dropdown for rest */}
{/* Error */} {error && !loading && (
{error}
)} {/* Results table */}
{/* Header */}
{([ ['symbol', 'Symbol'], ['companyName', 'Company'], ['price', 'Price'], ['changesPercentage', 'Change %'], ['marketCap', 'Market Cap'], ['pe', 'P/E'], ['volume', 'Volume'], ] as [keyof ScreenerRow, string][]).map(([key, label]) => ( handleSort(key)} style={{ fontSize: 11, color: '#9CA3AF', fontWeight: 700, textTransform: 'uppercase', letterSpacing: '0.05em', cursor: 'pointer', userSelect: 'none', }} > {label} ))}
{/* Loading */} {loading && (
Fetching screener results…
)} {/* Rows */} {!loading && filtered.map((row, i) => (
handleRowClick(row.symbol)} style={{ display: 'grid', gridTemplateColumns: '100px 1fr 90px 90px 110px 80px 110px', padding: '11px 16px', borderBottom: i < filtered.length - 1 ? '1px solid #F9FAFB' : 'none', cursor: 'pointer', alignItems: 'center', }} onMouseEnter={e => (e.currentTarget.style.background = '#F9FAFB')} onMouseLeave={e => (e.currentTarget.style.background = 'transparent')} > {row.symbol} {row.companyName} {row.price != null ? `$${row.price.toFixed(2)}` : '—'} = 0 ? '#16A34A' : '#DC2626', }}> {row.changesPercentage >= 0 ? '+' : ''}{row.changesPercentage?.toFixed(2)}% {row.marketCap ? fmtCap(row.marketCap) : '—'} {row.pe != null && row.pe > 0 ? row.pe.toFixed(1) : '—'} {row.volume ? fmtCap(row.volume).replace('$', '') : '—'}
))} {!loading && filtered.length === 0 && !error && (
No results match your filters
)}
{!loading && filtered.length > 0 && (
{filtered.length} companies · Click any row to view chart & research
)}
); }