diff --git a/backend/src/routes/marketAnalysis.js b/backend/src/routes/marketAnalysis.js index a01f47f..8d63d6f 100644 --- a/backend/src/routes/marketAnalysis.js +++ b/backend/src/routes/marketAnalysis.js @@ -10,6 +10,7 @@ import { UNIVERSE, getSymbolMeta, classifyCapTier } from '../services/stockUnive import { fetchBasicQuote, fetchQuoteSummary, fetchTechnicals, fetchFullAnalysis } from '../services/fundamentalsService.js'; import { fetchInstitutionalHolders, fetchInsiderTransactions } from '../services/institutionalHoldersService.js'; import { fetchNewsForSymbol, fetchMarketNews } from '../services/newsService.js'; +import { scoreSuitability } from '../services/suitabilityService.js'; const router = express.Router(); const universeCache = new NodeCache({ stdTTL: 60 }); @@ -212,4 +213,108 @@ router.get('/leaderboard', async (req, res) => { } }); +/** + * GET /api/market/screener + * Returns 3 sorted swimlanes: daytrade, shortTerm, longTerm + * Scores every universe symbol using technicals + fundamentals (all cached). + */ +router.get('/screener', async (req, res) => { + try { + const cacheKey = 'screener'; + const cached = universeCache.get(cacheKey); + if (cached) return res.json({ success: true, data: cached, cached: true }); + + const CONCURRENCY = 6; + const scored = []; + + for (let i = 0; i < UNIVERSE.length; i += CONCURRENCY) { + const batch = UNIVERSE.slice(i, i + CONCURRENCY); + const results = await Promise.allSettled( + batch.map(async (entry) => { + const [quote, technicals, fundamentals] = await Promise.allSettled([ + fetchBasicQuote(entry.symbol), + fetchTechnicals(entry.symbol), + fetchQuoteSummary(entry.symbol), + ]); + const q = quote.status === 'fulfilled' ? quote.value : null; + const t = technicals.status === 'fulfilled' ? technicals.value : null; + const f = fundamentals.status === 'fulfilled' ? fundamentals.value : null; + + const suitability = scoreSuitability({ quote: q, technicals: t, fundamentals: f }); + const capTier = classifyCapTier(q?.market_cap ?? f?.market_cap ?? null, entry.isETF); + + return { + symbol: entry.symbol, + name: entry.name, + sector: entry.sector, + isETF: entry.isETF, + capTier, + price: q?.price ?? null, + change_pct: q?.change_pct ?? null, + volume: q?.volume ?? null, + market_cap: q?.market_cap ?? f?.market_cap ?? null, + beta: f?.beta ?? null, + rsi_14: t?.rsi_14 ?? null, + above_sma200: t?.above_sma200 ?? null, + recommendation: f?.recommendation ?? null, + suitability, + }; + }) + ); + results.forEach(r => { if (r.status === 'fulfilled' && r.value) scored.push(r.value); }); + if (i + CONCURRENCY < UNIVERSE.length) await new Promise(r => setTimeout(r, 100)); + } + + // Sort each lane by score desc, take top 15 for each + const sortByScore = (key) => [...scored].sort((a, b) => b.suitability[key].score - a.suitability[key].score).slice(0, 15); + const data = { + daytrade: sortByScore('daytrade'), + shortTerm: sortByScore('shortTerm'), + longTerm: sortByScore('longTerm'), + updatedAt: new Date().toISOString(), + }; + + universeCache.set(cacheKey, data, 90); // 90s cache for screener + res.json({ success: true, data }); + } catch (err) { + console.error('[market/screener]', err); + res.status(500).json({ success: false, error: err.message }); + } +}); + +/** + * GET /api/market/search/:symbol + * Search ANY ticker (not just universe). Returns full analysis + suitability. + */ +router.get('/search/:symbol', async (req, res) => { + try { + const symbol = req.params.symbol.toUpperCase().trim(); + const meta = getSymbolMeta(symbol); // may be null for out-of-universe tickers + + const analysis = await fetchFullAnalysis(symbol); + if (!analysis.quote) { + return res.status(404).json({ success: false, error: `No data found for symbol ${symbol}` }); + } + + const suitability = scoreSuitability({ + quote: analysis.quote, + technicals: analysis.technicals, + fundamentals: analysis.fundamentals, + }); + + const capTier = classifyCapTier( + analysis.quote?.market_cap ?? analysis.fundamentals?.market_cap ?? null, + meta?.isETF ?? false + ); + + res.json({ + success: true, + data: { ...analysis, meta, capTier, suitability }, + }); + } catch (err) { + console.error(`[market/search/${req.params.symbol}]`, err); + res.status(500).json({ success: false, error: err.message }); + } +}); + export default router; diff --git a/backend/src/services/suitabilityService.js b/backend/src/services/suitabilityService.js new file mode 100644 index 0000000..f3d8f0e --- /dev/null +++ b/backend/src/services/suitabilityService.js @@ -0,0 +1,183 @@ +/** + * Suitability Service + * Scores stocks across three trading horizons using available market data. + * + * Daytrade: Volatility × Relative Volume × Momentum + * Short Term: Technical setup (MACD, RSI zone, SMA proximity) + * Long Term: Analyst consensus × Earnings trend × SMA200 uptrend + * + * All scores are 0–100. Returns top candidates per lane. + */ + +/** + * Score a single stock entry (quote + technicals + optional fundamentals) + * Returns { daytrade, shortTerm, longTerm, reasons } + */ +export function scoreSuitability({ quote, technicals, fundamentals }) { + const q = quote || {}; + const t = technicals || {}; + const f = fundamentals || {}; + + // ─── Daytrade Score ─────────────────────────────────────────────────────── + // Needs: liquidity spike, beta, momentum in a tradeable RSI zone, MACD + let dt = 0; + + // Relative volume (current vs 10-day avg): 0–35 pts + const relVol = q.volume && f.avg_volume_10d + ? q.volume / f.avg_volume_10d + : q.volume && f.avg_volume_3m + ? q.volume / f.avg_volume_3m + : null; + const dtReasons = []; + if (relVol != null) { + if (relVol >= 3.0) { dt += 35; dtReasons.push(`🔥 ${relVol.toFixed(1)}x rel. volume`); } + else if (relVol >= 2.0) { dt += 28; dtReasons.push(`⚡ ${relVol.toFixed(1)}x rel. volume`); } + else if (relVol >= 1.5) { dt += 20; dtReasons.push(`📈 ${relVol.toFixed(1)}x rel. volume`); } + else if (relVol >= 1.0) { dt += 10; dtReasons.push(`Avg volume`); } + else { dt += 3; dtReasons.push(`Low volume`); } + } + + // Beta: 0–25 pts + const beta = f.beta ?? null; + if (beta != null) { + if (beta >= 2.0) { dt += 25; dtReasons.push(`Beta ${beta.toFixed(1)} (high vol)`); } + else if (beta >= 1.5) { dt += 20; dtReasons.push(`Beta ${beta.toFixed(1)}`); } + else if (beta >= 1.2) { dt += 13; } + else if (beta >= 0.8) { dt += 5; } + else { dt += 0; dtReasons.push(`Low beta`); } + } + + // RSI zone (tradeable, not exhausted): 0–20 pts + const rsi = t.rsi_14 ?? null; + if (rsi != null) { + if (rsi >= 40 && rsi <= 65) { dt += 20; dtReasons.push(`RSI ${rsi.toFixed(0)} (momentum zone)`); } + else if (rsi >= 30 && rsi < 40) { dt += 14; dtReasons.push(`RSI ${rsi.toFixed(0)} (bouncing)`); } + else if (rsi > 65 && rsi <= 75) { dt += 10; dtReasons.push(`RSI ${rsi.toFixed(0)} (extended but running)`); } + else if (rsi > 75) { dt += 2; dtReasons.push(`RSI ${rsi.toFixed(0)} (overbought)`); } + else { dt += 2; dtReasons.push(`RSI ${rsi.toFixed(0)} (oversold)`); } + } + + // MACD histogram positive: 0–20 pts + const macdH = t.macd_histogram ?? null; + if (macdH != null) { + if (macdH > 0) { dt += 20; dtReasons.push(`MACD bullish`); } + else if (macdH > -0.1) { dt += 10; } + else { dt += 0; dtReasons.push(`MACD bearish`); } + } + + // ─── Short Term (Swing) Score ───────────────────────────────────────────── + // Needs: MACD setup, RSI not extended, near SMA50, analyst upside + let st = 0; + const stReasons = []; + + // MACD histogram: 0–30 pts + if (macdH != null) { + if (macdH > 0.5) { st += 30; stReasons.push(`Strong MACD bullish`); } + else if (macdH > 0) { st += 22; stReasons.push(`MACD bullish`); } + else if (macdH > -0.2) { st += 10; stReasons.push(`MACD near crossover`); } + else { st += 0; } + } + + // RSI sweet spot (not extended, not exhausted): 0–25 pts + if (rsi != null) { + if (rsi >= 40 && rsi <= 60) { st += 25; stReasons.push(`RSI ${rsi.toFixed(0)} (ideal swing zone)`); } + else if (rsi >= 30 && rsi < 40) { st += 20; stReasons.push(`RSI ${rsi.toFixed(0)} (reset - buy dip)`); } + else if (rsi > 60 && rsi <= 70) { st += 15; stReasons.push(`RSI ${rsi.toFixed(0)} (momentum run)`); } + else if (rsi <= 30) { st += 10; stReasons.push(`RSI oversold`); } + else { st += 2; } + } + + // % from SMA50: -10% to +10% = best setup for swing: 0–25 pts + const pctSma50 = t.pct_from_sma50 ?? null; + if (pctSma50 != null) { + const abs = Math.abs(pctSma50); + if (abs <= 3) { st += 25; stReasons.push(`Near SMA50 (${pctSma50.toFixed(1)}%) - coiled`); } + else if (abs <= 7) { st += 18; stReasons.push(`${pctSma50 > 0 ? 'Above' : 'Below'} SMA50 by ${abs.toFixed(1)}%`); } + else if (abs <= 15) { st += 8; } + else { st += 0; stReasons.push(`Extended from SMA50`); } + } + + // Analyst upside: 0–20 pts + const price = q.price ?? null; + const targetMean = f.target_mean ?? null; + if (price && targetMean) { + const upside = ((targetMean - price) / price) * 100; + if (upside >= 20) { st += 20; stReasons.push(`${upside.toFixed(0)}% analyst upside`); } + else if (upside >= 10) { st += 14; stReasons.push(`${upside.toFixed(0)}% analyst upside`); } + else if (upside >= 5) { st += 8; } + else if (upside > 0) { st += 3; } + else { st += 0; stReasons.push(`Analyst consensus cautious`); } + } + + // ─── Long Term Score ────────────────────────────────────────────────────── + // Needs: above SMA200 uptrend, earnings growth, analyst conviction, margins + let lt = 0; + const ltReasons = []; + + // Above SMA200 (price in long-term uptrend): 0–30 pts + const above200 = t.above_sma200 ?? null; + const pctSma200 = t.pct_from_sma200 ?? null; + if (above200 != null) { + if (above200 && pctSma200 != null && pctSma200 > 10) { lt += 30; ltReasons.push(`${pctSma200.toFixed(0)}% above SMA200`); } + else if (above200) { lt += 22; ltReasons.push(`Above SMA200`); } + else if (pctSma200 != null && pctSma200 > -5) { lt += 10; ltReasons.push(`Near SMA200 support`); } + else { lt += 0; ltReasons.push(`Below SMA200`); } + } + + // Earnings growth: 0–25 pts + const eg = f.earnings_growth ?? null; + if (eg != null) { + if (eg >= 0.30) { lt += 25; ltReasons.push(`${(eg * 100).toFixed(0)}% earnings growth`); } + else if (eg >= 0.15) { lt += 18; ltReasons.push(`${(eg * 100).toFixed(0)}% earnings growth`); } + else if (eg >= 0.05) { lt += 11; } + else if (eg >= 0) { lt += 5; } + else { lt += 0; ltReasons.push(`Negative earnings growth`); } + } + + // Analyst conviction (strong buy/buy count as % of total ratings): 0–25 pts + const totalRec = (f.rec_strong_buy || 0) + (f.rec_buy || 0) + (f.rec_hold || 0) + (f.rec_sell || 0) + (f.rec_strong_sell || 0); + const bullishRec = (f.rec_strong_buy || 0) + (f.rec_buy || 0); + if (totalRec > 0) { + const bullPct = bullishRec / totalRec; + if (bullPct >= 0.75) { lt += 25; ltReasons.push(`${(bullPct * 100).toFixed(0)}% analysts bullish (${totalRec} coverage)`); } + else if (bullPct >= 0.55) { lt += 16; ltReasons.push(`${(bullPct * 100).toFixed(0)}% analysts bullish`); } + else if (bullPct >= 0.40) { lt += 8; } + else { lt += 0; ltReasons.push(`Bearish analyst consensus`); } + } + + // Profit margins (durable business): 0–20 pts + const pm = f.profit_margins ?? null; + if (pm != null) { + if (pm >= 0.25) { lt += 20; ltReasons.push(`${(pm * 100).toFixed(0)}% profit margin`); } + else if (pm >= 0.15) { lt += 14; ltReasons.push(`${(pm * 100).toFixed(0)}% profit margin`); } + else if (pm >= 0.05) { lt += 7; } + else if (pm >= 0) { lt += 2; } + else { lt += 0; ltReasons.push(`Unprofitable`); } + } + + return { + daytrade: { + score: Math.min(100, Math.round(dt)), + reasons: dtReasons.slice(0, 3), + grade: gradeLabel(dt), + }, + shortTerm: { + score: Math.min(100, Math.round(st)), + reasons: stReasons.slice(0, 3), + grade: gradeLabel(st), + }, + longTerm: { + score: Math.min(100, Math.round(lt)), + reasons: ltReasons.slice(0, 3), + grade: gradeLabel(lt), + }, + }; +} + +function gradeLabel(score) { + if (score >= 80) return { label: 'Strong', color: 'emerald' }; + if (score >= 60) return { label: 'Good', color: 'blue' }; + if (score >= 40) return { label: 'Moderate', color: 'yellow' }; + if (score >= 20) return { label: 'Weak', color: 'orange' }; + return { label: 'Poor', color: 'red' }; +} diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index feecfd9..1cc0864 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -3,14 +3,11 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import OptionsFlowPanel from '@/components/dashboard/OptionsFlowPanel'; import ScannerPanel from '@/components/dashboard/ScannerPanel'; import BacktestPanel from '@/components/dashboard/BacktestPanel'; -import AlertsFeed from '@/components/dashboard/AlertsFeed'; -import Watchlist from '@/components/dashboard/Watchlist'; import PerformanceTrackingPanel from '@/components/dashboard/PerformanceTrackingPanel'; import ReversalAlerts from '@/components/alerts/ReversalAlerts'; import ConvergenceAlerts from '@/components/alerts/ConvergenceAlerts'; import MarketScreenerPanel from '@/components/dashboard/MarketScreenerPanel'; import StockDetailPanel from '@/components/dashboard/StockDetailPanel'; -import NewsFeedPanel from '@/components/dashboard/NewsFeedPanel'; export default function App() { const [selectedSymbol, setSelectedSymbol] = useState(null); @@ -19,82 +16,69 @@ export default function App() {
+ {/* Header */} -
-
+
+

🚀 Institutional Flow Platform

-

+

Real-time options flow · Tape analysis · Pro-grade signals

Market Status
-
- RTH • LIVE -
+
RTH • LIVE
- {/* Main Content */} + {/* Main Content — full width */}
-
- {/* Left: Main Panels */} -
- - - - 📊 Market Analysis - - - 🎯 Options Flow - - - 🔍 Multi-Signal Scanner - - - 📊 Backtests - - + + + + 📊 Market Analysis + + + 🎯 Options Flow + + + 🔍 Multi-Signal Scanner + + + 📊 Backtests + + - - - {selectedSymbol && ( - setSelectedSymbol(null)} - /> - )} - + + + {selectedSymbol && ( + setSelectedSymbol(null)} + /> + )} + - - - + + + - - - + + + - - - - -
- - {/* Right: News, Watchlist & Alerts Feed */} -
- - - -
-
+ + + + {/* Bottom: Performance Tracking */}
diff --git a/frontend/src/components/dashboard/MarketScreenerPanel.jsx b/frontend/src/components/dashboard/MarketScreenerPanel.jsx index ad3b56a..68465d4 100644 --- a/frontend/src/components/dashboard/MarketScreenerPanel.jsx +++ b/frontend/src/components/dashboard/MarketScreenerPanel.jsx @@ -1,299 +1,426 @@ -import { useState, useEffect, useCallback } from 'react'; +import { useState, useEffect, useCallback, useRef } from 'react'; const API_BASE = import.meta.env.VITE_API_URL || 'http://localhost:3010'; -const CAP_TABS = [ - { id: 'all', label: '🌐 All', color: 'text-slate-300' }, - { id: 'large', label: '🏛️ Large Cap', color: 'text-blue-400' }, - { id: 'mid', label: '🏢 Mid Cap', color: 'text-purple-400' }, - { id: 'small', label: '🔬 Small Cap', color: 'text-orange-400' }, - { id: 'etf', label: '📦 ETFs', color: 'text-green-400' }, -]; - +// ─── 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(2)}T`; - if (n >= 1e9) return `$${(n / 1e9).toFixed(2)}B`; - if (n >= 1e6) return `$${(n / 1e6).toFixed(2)}M`; + 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)}`; } -function fmtVolume(n) { - if (n == null) return '—'; - if (n >= 1e9) return `${(n / 1e9).toFixed(1)}B`; - if (n >= 1e6) return `${(n / 1e6).toFixed(1)}M`; - if (n >= 1e3) return `${(n / 1e3).toFixed(0)}K`; - return `${n}`; -} - -function ChangeBadge({ value }) { - if (value == null) return ; - const positive = value >= 0; - return ( - - {positive ? '+' : ''}{value.toFixed(2)}% - - ); -} - -function CapBadge({ tier }) { - const styles = { - LARGE: 'bg-blue-500/20 text-blue-400 border-blue-500/30', - MID: 'bg-purple-500/20 text-purple-400 border-purple-500/30', - SMALL: 'bg-orange-500/20 text-orange-400 border-orange-500/30', - ETF: 'bg-emerald-500/20 text-emerald-400 border-emerald-500/30', - UNKNOWN: 'bg-slate-700 text-slate-400 border-slate-600', +// ─── 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 ( - - {tier} - +
+ + + + +
+ {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 [activeTab, setActiveTab] = useState('all'); - const [sortBy, setSortBy] = useState('market_cap'); - const [sortDir, setSortDir] = useState('desc'); - const [search, setSearch] = useState(''); - const [data, setData] = useState([]); - const [leaderboard, setLeaderboard] = useState(null); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); - const [lastUpdated, setLastUpdated] = useState(null); + 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 cap = activeTab === 'all' ? '' : activeTab; - const url = `${API_BASE}/api/market/universe${cap ? `?cap=${cap}` : ''}`; - const [universeRes, lbRes] = await Promise.allSettled([ - fetch(url).then(r => r.json()), + 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 (universeRes.status === 'fulfilled' && universeRes.value.success) { - setData(universeRes.value.data || []); + 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); } - setError(null); } catch (err) { - setError(err.message); + console.error('[MarketScreenerPanel]', err); } finally { setLoading(false); } - }, [activeTab]); + }, []); useEffect(() => { setLoading(true); fetchData(); - const interval = setInterval(fetchData, 60000); + const interval = setInterval(fetchData, 90000); return () => clearInterval(interval); }, [fetchData]); - const processed = [...data] - .filter(r => { - if (!search) return true; - const q = search.toUpperCase(); - return r.symbol.includes(q) || r.name.toUpperCase().includes(q) || r.sector?.toUpperCase().includes(q); - }) - .sort((a, b) => { - const av = a[sortBy] ?? -Infinity; - const bv = b[sortBy] ?? -Infinity; - return sortDir === 'desc' ? bv - av : av - bv; + // 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 toggleSort = (col) => { - if (sortBy === col) setSortDir(d => d === 'desc' ? 'asc' : 'desc'); - else { setSortBy(col); setSortDir('desc'); } }; - const SortIcon = ({ col }) => { - if (sortBy !== col) return ; - return {sortDir === 'desc' ? '↓' : '↑'}; - }; + 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 && ( -
- - - +
+ + +
)} - {/* Main screener card */} -
- {/* Header */} -
-
-

📊 Market Screener

- {lastUpdated && ( - - Updated {lastUpdated.toLocaleTimeString()} - - )} - {loading && Refreshing...} + {/* Screener header */} +
+
+
+
+

📊 Trading Screener

+

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

+
+ + {/* Cap tier filter pills */} +
+ {CAP_FILTERS.map(f => ( + + ))} +
-
- setSearch(e.target.value)} - className="bg-slate-800 border border-slate-600 rounded-lg px-3 py-1.5 text-sm text-white placeholder-slate-500 focus:outline-none focus:border-blue-500 w-52" + + {/* Search */} +
+ onSelectSymbol?.(sym)} + onClear={() => {}} />
- {/* Cap tier tabs */} -
- {CAP_TABS.map(tab => ( - - ))} + {/* ─── 3 Swimlanes ─────────────────────────────────────────────────── */} +
+ + +
- {/* Table */} - {error ? ( -
-

⚠️ {error}

- -
- ) : ( -
- - - - - - - - - - - - - - - {loading && data.length === 0 ? ( - Array.from({ length: 10 }).map((_, i) => ( - - {Array.from({ length: 8 }).map((_, j) => ( - - ))} - - )) - ) : processed.length === 0 ? ( - - ) : ( - processed.map(row => ( - onSelectSymbol?.(row.symbol)} - className="hover:bg-slate-800/40 cursor-pointer transition-colors group" - > - - - - - - - - - - )) - )} - -
SymbolNameSector toggleSort('price')} - >Price toggleSort('change_pct')} - >Change toggleSort('market_cap')} - >Mkt Cap toggleSort('volume')} - >Volume Cap Tier
-
-
No results found
- - {row.symbol} - - {row.name} - - {row.sector} - - - - {row.price != null ? `$${row.price.toFixed(2)}` : '—'} - - - - - {fmtMktCap(row.market_cap)} - - {fmtVolume(row.volume)} - - -
-
- )} - {/* Footer */} -
- {processed.length} of {data.length} symbols - Powered by Yahoo Finance • Free data • 60s refresh +
+ + 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
); } -function LeaderCard({ title, items = [], type }) { +// ─── Leaderboard helper ─────────────────────────────────────────────────────── +function LeaderCard({ title, items = [], type, onSelect }) { return ( -
-
{title}
-
+
+
{title}
+
{items.slice(0, 5).map(item => ( -
- {item.symbol} +
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 >= 1e6 ? `${(item.volume / 1e6).toFixed(1)}M` : item.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...
} + {items.length === 0 &&
Loading...
}
);