diff --git a/backend/src/routes/marketAnalysis.js b/backend/src/routes/marketAnalysis.js index a279c60..030a53d 100644 --- a/backend/src/routes/marketAnalysis.js +++ b/backend/src/routes/marketAnalysis.js @@ -279,5 +279,44 @@ router.get('/search/:symbol', async (req, res) => { res.status(500).json({ success: false, error: err.message }); } }); +/** + * GET /api/market/macro + * Global Macro Indicators (Descriptive Context) + */ +router.get('/macro', async (req, res) => { + try { + const cacheKey = 'macro_indicators'; + const cached = universeCache.get(cacheKey); + if (cached) return res.json({ success: true, data: cached }); + + const MACRO_SYMBOLS = [ + { symbol: '^VIX', name: 'Volatility (VIX)' }, + { symbol: 'DX-Y.NYB', name: 'US Dollar (DXY)' }, + { symbol: '^TNX', name: '10-Yr Yield' }, + { symbol: 'GC=F', name: 'Gold' }, + { symbol: 'SI=F', name: 'Silver' }, + { symbol: 'EEM', name: 'Emerging Mkts' }, + { symbol: 'EFA', name: 'Developed Mkts' }, + { symbol: 'BTC-USD', name: 'Bitcoin' }, + ]; + + const results = await Promise.allSettled( + MACRO_SYMBOLS.map(async (entry) => { + const q = await fetchBasicQuote(entry.symbol); + return q ? { symbol: entry.symbol, name: entry.name, ...q } : null; + }) + ); + + const data = results + .filter(r => r.status === 'fulfilled' && r.value) + .map(r => r.value); + + universeCache.set(cacheKey, data, 60); // Cache for 60 seconds + res.json({ success: true, data, updatedAt: new Date().toISOString() }); + } catch (err) { + console.error('[market/macro]', err); + res.status(500).json({ success: false, error: err.message }); + } +}); export default router; diff --git a/frontend/src/components/dashboard/MacroIndicatorsPanel.jsx b/frontend/src/components/dashboard/MacroIndicatorsPanel.jsx new file mode 100644 index 0000000..d66c5e1 --- /dev/null +++ b/frontend/src/components/dashboard/MacroIndicatorsPanel.jsx @@ -0,0 +1,77 @@ +import { useState, useEffect, useCallback } from 'react'; + +const API_BASE = import.meta.env.VITE_API_URL || ''; + +function fmtPrice(n, symbol) { + if (n == null) return '—'; + // VIX and TNX are usually shown raw without $ + if (symbol === '^VIX' || symbol === '^TNX' || symbol === 'DX-Y.NYB') { + return n.toFixed(2); + } + return `$${n.toFixed(2)}`; +} + +export default function MacroIndicatorsPanel() { + const [data, setData] = useState([]); + const [loading, setLoading] = useState(true); + + const fetchData = useCallback(async () => { + try { + const res = await fetch(`${API_BASE}/api/market/macro`); + const json = await res.json(); + if (json.success) { + setData(json.data); + } + } catch (err) { + console.error('[MacroIndicatorsPanel] Error fetching macro data:', err); + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { + fetchData(); + const interval = setInterval(fetchData, 90000); // 90 seconds + return () => clearInterval(interval); + }, [fetchData]); + + if (loading && data.length === 0) { + return ( +