feat: Add Global Macro Indicators strip to dashboard
Build Institutional Trader / build-and-deploy (push) Successful in 2m9s
Details
Build Institutional Trader / build-and-deploy (push) Successful in 2m9s
Details
This commit is contained in:
parent
cd78058072
commit
9625979954
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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">
|
||||
|
|
|
|||
Loading…
Reference in New Issue