diff --git a/backend/src/routes/marketAnalysis.js b/backend/src/routes/marketAnalysis.js index 1d8bee1..a279c60 100644 --- a/backend/src/routes/marketAnalysis.js +++ b/backend/src/routes/marketAnalysis.js @@ -214,10 +214,12 @@ router.get('/leaderboard', async (req, res) => { } }); +import { buildFactorProfiles } from '../factorlab/descriptiveLens.js'; + /** * GET /api/market/screener - * Returns 3 sorted swimlanes: daytrade, shortTerm, longTerm - * Scores every universe symbol using technicals + fundamentals (all cached). + * Returns the descriptive factor profiles for the entire universe. + * Strictly descriptive. No predictive claims. */ router.get('/screener', async (req, res) => { try { @@ -225,105 +227,15 @@ router.get('/screener', async (req, res) => { const cached = universeCache.get(cacheKey); if (cached) return res.json({ success: true, data: cached, cached: true }); - const CONCURRENCY = 6; - const scored = []; - - const [spyTech, vixQuote] = await Promise.all([ - fetchTechnicals('SPY'), - fetchBasicQuote('^VIX') - ]); - - 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 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, - above_sma50: t?.above_sma50 ?? null, - q, t, f - }; - }) - ); - 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)); - } - - // Compute regime - const totalWith50 = scored.filter(s => s.above_sma50 !== null).length; - const above50 = scored.filter(s => s.above_sma50 === true).length; - const breadthPctAbove50 = totalWith50 > 0 ? (above50 / totalWith50) * 100 : 50; - const regime = classifyRegime(spyTech, vixQuote?.price ?? 20, breadthPctAbove50); - - // Compute scores and log signals - const signalsToLog = []; - const timestamp = new Date().toISOString(); - - scored.forEach(s => { - // Data freshness (using quote timestamp or assumed fresh if pulled recently) - // Usually would parse last trade time, but for phase 0 we approximate based on cache age or current time. - const dataAgeSec = s.q ? 15 : 120; // 15s if we got quote, 120s if missing - - const suitability = scoreSuitability({ - quote: s.q, - technicals: s.t, - fundamentals: s.f, - regime, - dataAgeSec - }); - s.suitability = suitability; - - // Clean up raw payloads for frontend transport size - delete s.q; delete s.t; delete s.f; delete s.above_sma50; - - // Build signals for logging (only log strong candidates to save DB space, or all. Let's log all.) - for (const lane of ['daytrade', 'shortTerm', 'longTerm']) { - signalsToLog.push({ - ticker: s.symbol, - ts: timestamp, - lane, - score: suitability[lane].score, - grade: suitability[lane].grade, - entry_price: s.price ?? 0, - features: suitability[lane].features, - regime: regime.raw, - data_age_sec: suitability[lane].dataAgeSec - }); - } - }); - - // Fire and forget logging - logSignals(signalsToLog); - - // 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); + // Fetch the descriptive factor profiles + const profiles = await buildFactorProfiles(); + const data = { - regime, - daytrade: sortByScore('daytrade'), - shortTerm: sortByScore('shortTerm'), - longTerm: sortByScore('longTerm'), + profiles: profiles, updatedAt: new Date().toISOString(), }; - universeCache.set(cacheKey, data, 90); // 90s cache for screener + universeCache.set(cacheKey, data, 120); // 120s cache res.json({ success: true, data }); } catch (err) { console.error('[market/screener]', err); diff --git a/frontend/src/components/dashboard/MarketScreenerPanel.jsx b/frontend/src/components/dashboard/MarketScreenerPanel.jsx index ea2c9ab..f61beda 100644 --- a/frontend/src/components/dashboard/MarketScreenerPanel.jsx +++ b/frontend/src/components/dashboard/MarketScreenerPanel.jsx @@ -54,137 +54,63 @@ function ScoreRing({ score, color }) { ); } -// ─── 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]; - +// ─── Factor Profile Component ─────────────────────────────────────────────────── +function pctBar(pct) { + if (pct == null) return No data; + const filled = Math.round(pct / 10); + const empty = 10 - filled; return ( -
{description}
+- Stocks ranked by trading suitability • {lastUpdated ? `Updated ${lastUpdated.toLocaleTimeString()}` : 'Loading...'} + Where stocks rank vs. peers today • {lastUpdated ? `Updated ${lastUpdated.toLocaleTimeString()}` : 'Loading...'} {loading && Refreshing...}
Descriptive only. Shows where each stock ranks against its peers across four foundational factors based on current data. We have not validated that these ranks predict forward returns. We will only upgrade these to "predictive" signals after they pass rigorous out-of-sample holdout validation.
+