import { useState, useEffect, useCallback, useRef } from 'react'; const API_BASE = import.meta.env.VITE_API_URL || ''; // ─── Formatters ─────────────────────────────────────────────────────────────── function fmtPrice(n) { if (n == null) return '—'; return `$${n.toFixed(2)}`; } function fmtChange(n) { if (n == null) return null; return { value: n, label: `${n >= 0 ? '+' : ''}${n.toFixed(2)}%`, positive: n >= 0 }; } function fmtMktCap(n) { if (n == null) return '—'; 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.toFixed(0)}`; } // ─── Score ring component ───────────────────────────────────────────────────── function ScoreRing({ score, color }) { const r = 20; const circ = 2 * Math.PI * r; const fill = (score / 100) * circ; const colors = { emerald: '#10b981', blue: '#3b82f6', yellow: '#eab308', orange: '#f97316', red: '#ef4444', }; const stroke = colors[color] || colors.blue; return (
{score}
); } // ─── Swimlane card ──────────────────────────────────────────────────────────── function StockCard({ item, lane, onSelect }) { const s = item.suitability?.[lane] || { score: 0, reasons: [], grade: { label: 'N/A', color: 'red' } }; const change = fmtChange(item.change_pct); const laneColors = { daytrade: { accent: 'border-orange-500/30', badge: 'bg-orange-500/10 text-orange-400' }, shortTerm: { accent: 'border-blue-500/30', badge: 'bg-blue-500/10 text-blue-400' }, longTerm: { accent: 'border-emerald-500/30',badge: 'bg-emerald-500/10 text-emerald-400' }, }; const lc = laneColors[lane]; return (
onSelect?.(item.symbol)} className={`group bg-slate-900 border ${lc.accent} rounded-xl p-3.5 cursor-pointer hover:bg-slate-800/80 hover:border-opacity-70 transition-all duration-150 hover:shadow-lg hover:shadow-black/20 active:scale-[0.99]`} > {/* Top row: symbol + score */}
{item.symbol} {item.capTier && ( {item.capTier} )}
{item.name}
{/* Price + change */}
{fmtPrice(item.price)} {change && ( {change.label} )}
{/* Signals / reasons */} {s.reasons.length > 0 && (
{s.reasons.map((r, i) => (
{r}
))}
)} {/* Footer: sector + grade badge */}
{item.sector} {s.grade.label}
); } // ─── Swimlane column ────────────────────────────────────────────────────────── function Swimlane({ title, icon, description, items = [], lane, onSelect, loading, accentClass }) { return (
{/* Lane header */}
{icon}

{title}

{!loading && items.length > 0 && ( {items.length} stocks )}

{description}

{/* Cards */}
{loading ? ( Array.from({ length: 6 }).map((_, i) => (
)) ) : items.length === 0 ? (
No data available
) : ( items.map(item => ( )) )}
); } // ─── Search bar ─────────────────────────────────────────────────────────────── function SearchBar({ onResult, onClear }) { const [query, setQuery] = useState(''); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const inputRef = useRef(null); const handleSearch = async () => { const sym = query.trim().toUpperCase(); if (!sym) return; setLoading(true); setError(null); try { const res = await fetch(`${API_BASE}/api/market/search/${sym}`); const data = await res.json(); if (data.success) { onResult(sym, data.data); } else { setError(data.error || 'Symbol not found'); } } catch (e) { setError('Network error. Check backend.'); } finally { setLoading(false); } }; const handleKeyDown = (e) => { if (e.key === 'Enter') handleSearch(); if (e.key === 'Escape') { setQuery(''); onClear(); } }; return (
🔍 { setQuery(e.target.value.toUpperCase()); setError(null); }} onKeyDown={handleKeyDown} className="bg-slate-800 border border-slate-700 rounded-xl pl-9 pr-4 py-2 text-sm text-white placeholder-slate-500 focus:outline-none focus:border-blue-500 focus:ring-1 focus:ring-blue-500/30 w-72 transition-all" /> {query && ( )}
{error && {error}}
); } // ─── Main Component ─────────────────────────────────────────────────────────── export default function MarketScreenerPanel({ onSelectSymbol }) { const [screenerData, setScreenerData] = useState(null); const [leaderboard, setLeaderboard] = useState(null); const [loading, setLoading] = useState(true); const [lastUpdated, setLastUpdated] = useState(null); const [capFilter, setCapFilter] = useState('all'); const fetchData = useCallback(async () => { try { const [screenerRes, lbRes] = await Promise.allSettled([ fetch(`${API_BASE}/api/market/screener`).then(r => r.json()), fetch(`${API_BASE}/api/market/leaderboard`).then(r => r.json()), ]); if (screenerRes.status === 'fulfilled' && screenerRes.value.success) { setScreenerData(screenerRes.value.data); setLastUpdated(new Date()); } if (lbRes.status === 'fulfilled' && lbRes.value.success) { setLeaderboard(lbRes.value.data); } } catch (err) { console.error('[MarketScreenerPanel]', err); } finally { setLoading(false); } }, []); useEffect(() => { setLoading(true); fetchData(); const interval = setInterval(fetchData, 90000); return () => clearInterval(interval); }, [fetchData]); // Filter lanes by cap tier const filterByCap = (arr) => { if (!arr || capFilter === 'all') return arr || []; return arr.filter(s => { if (capFilter === 'etf') return s.isETF; return s.capTier === capFilter.toUpperCase() && !s.isETF; }); }; const CAP_FILTERS = [ { id: 'all', label: 'All' }, { id: 'large', label: '🏛 Large' }, { id: 'mid', label: '🏢 Mid' }, { id: 'small', label: '🔬 Small' }, { id: 'etf', label: '📦 ETF' }, ]; return (
{/* Leaderboard strip */} {leaderboard && (
)} {/* Screener header */}

📊 Trading Screener

Stocks ranked by trading suitability • {lastUpdated ? `Updated ${lastUpdated.toLocaleTimeString()}` : 'Loading...'} {loading && Refreshing...}

{/* Cap tier filter pills */}
{CAP_FILTERS.map(f => ( ))}
{/* Search */}
onSelectSymbol?.(sym)} onClear={() => {}} />
{/* ─── 3 Swimlanes ─────────────────────────────────────────────────── */}
{/* Footer */}
Scores: Daytrade (vol+beta+RSI+MACD) · Short Term (MACD+RSI zone+SMA50+upside) · Long Term (SMA200+earnings+analyst consensus) Data: Yahoo Finance · Refreshes every 90s
); } // ─── Leaderboard helper ─────────────────────────────────────────────────────── function LeaderCard({ title, items = [], type, onSelect }) { return (
{title}
{items.slice(0, 5).map(item => (
onSelect?.(item.symbol)} className="flex items-center justify-between cursor-pointer hover:bg-slate-800/50 rounded-lg px-2 py-1 transition-colors group" >
{item.symbol} {fmtPrice(item.price)}
{type === 'volume' ? ( {item.volume >= 1e9 ? `${(item.volume / 1e9).toFixed(1)}B` : item.volume >= 1e6 ? `${(item.volume / 1e6).toFixed(1)}M` : item.volume} ) : ( = 0 ? 'text-emerald-400' : 'text-red-400' }`}> {item.change_pct != null ? `${item.change_pct >= 0 ? '+' : ''}${item.change_pct.toFixed(2)}%` : '—'} )}
))} {items.length === 0 &&
Loading...
}
); }