feat: Add Global Macro Indicators strip to dashboard
Build Institutional Trader / build-and-deploy (push) Successful in 2m9s Details

This commit is contained in:
Deep Koluguri 2026-06-25 21:04:43 -04:00
parent cd78058072
commit 9625979954
3 changed files with 120 additions and 0 deletions

View File

@ -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;

View File

@ -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 (
<div className="flex gap-4 overflow-x-hidden p-1 opacity-50 mb-6">
{Array.from({ length: 6 }).map((_, i) => (
<div key={i} className="animate-pulse bg-slate-900 border border-slate-800 rounded-xl min-w-[140px] h-[72px]" />
))}
</div>
);
}
if (data.length === 0) return null;
return (
<div className="mb-6">
<div className="text-xs font-bold text-slate-500 mb-3 uppercase tracking-wider pl-1">Global Macro Context</div>
<div className="flex gap-3 overflow-x-auto pb-4 scrollbar-thin scrollbar-thumb-slate-700 scrollbar-track-transparent">
{data.map((item) => {
const isPositive = (item.change_pct ?? 0) >= 0;
return (
<div
key={item.symbol}
className="bg-slate-900 border border-slate-700/50 rounded-xl p-3 min-w-[150px] flex-shrink-0 flex flex-col justify-between hover:bg-slate-800 transition-colors shadow-sm"
>
<div className="text-xs font-semibold text-slate-400 mb-1">{item.name}</div>
<div className="flex items-end justify-between">
<span className="text-white font-mono font-bold">
{fmtPrice(item.price, item.symbol)}
</span>
<span className={`text-xs font-semibold tabular-nums ${isPositive ? 'text-emerald-400' : 'text-red-400'}`}>
{isPositive ? '+' : ''}{(item.change_pct ?? 0).toFixed(2)}%
</span>
</div>
</div>
);
})}
</div>
</div>
);
}

View File

@ -1,4 +1,5 @@
import { useState, useEffect, useCallback, useRef } from 'react';
import MacroIndicatorsPanel from './MacroIndicatorsPanel';
const API_BASE = import.meta.env.VITE_API_URL || '';
@ -246,6 +247,9 @@ export default function MarketScreenerPanel({ onSelectSymbol }) {
return (
<div className="space-y-5">
{/* Global Macro Strip */}
<MacroIndicatorsPanel />
{/* Leaderboard strip */}
{leaderboard && (
<div className="grid grid-cols-3 gap-4">