diff --git a/frontend/src/pages/Dashboard.tsx b/frontend/src/pages/Dashboard.tsx index 64eb644..30967db 100644 --- a/frontend/src/pages/Dashboard.tsx +++ b/frontend/src/pages/Dashboard.tsx @@ -40,6 +40,26 @@ const REGIME_LABELS: Record = { soft_landing: 'Soft Landing', inflation_shock: 'Infl. Shock', incertain: 'Uncertain', } +const CURRENCY_FLAGS: Record = { + USD: 'πŸ‡ΊπŸ‡Έ', EUR: 'πŸ‡ͺπŸ‡Ί', GBP: 'πŸ‡¬πŸ‡§', JPY: 'πŸ‡―πŸ‡΅', + AUD: 'πŸ‡¦πŸ‡Ί', CAD: 'πŸ‡¨πŸ‡¦', NZD: 'πŸ‡³πŸ‡Ώ', CHF: 'πŸ‡¨πŸ‡­', CNY: 'πŸ‡¨πŸ‡³', +} + +const IMPACT_DOT: Record = { + high: 'bg-red-500', medium: 'bg-yellow-400', low: 'bg-slate-400', +} + +const isTodayISO = (dateStr: string) => { + const t = new Date() + const utc = `${t.getUTCFullYear()}-${String(t.getUTCMonth() + 1).padStart(2, '0')}-${String(t.getUTCDate()).padStart(2, '0')}` + return dateStr === utc +} + +const formatDateShort = (dateStr: string) => { + const d = new Date(dateStr + 'T00:00:00Z') + return d.toLocaleDateString('fr-FR', { weekday: 'short', day: 'numeric', month: 'short', timeZone: 'UTC' }) +} + function ViewToggle({ value, onChange }: { value: 'simulated' | 'portfolio'; onChange: (v: 'simulated' | 'portfolio') => void }) { const click = (v: 'simulated' | 'portfolio') => (e: React.MouseEvent) => { e.preventDefault(); e.stopPropagation(); onChange(v) @@ -160,11 +180,11 @@ export default function Dashboard() { .slice(0, 4) }, [allPatterns, scoreMap, lastCycle]) - // Top impactful news (compact list embedded in the Geopolitical Risk card) + // Top impactful news (fills the Geopolitical Risk card's available height, no fixed cap) const topNews = useMemo(() => [...(geoNews ?? [])] .sort((a, b) => b.impact_score - a.impact_score) - .slice(0, 5) + .slice(0, 30) , [geoNews]) // Watchlist radar: change_pct normalized to a 0-100 scale (50 = flat) @@ -176,19 +196,20 @@ export default function Dashboard() { }) }, [watchlistQuotesData]) - // Upcoming FF economic events, ranked by impact then chronology - const upcomingEvents = useMemo(() => { + // Upcoming FF economic events, chronological, grouped by date (fills available height, no fixed cap) + const upcomingEventsGrouped = useMemo(() => { const events: any[] = (ecoCalendarData as any)?.events ?? [] const todayISO = new Date().toISOString().slice(0, 10) - const rank: Record = { high: 3, medium: 2, low: 1 } - return events + const sorted = [...events] .filter((ev: any) => ev.event_date >= todayISO) - .sort((a: any, b: any) => { - const r = (rank[b.impact] ?? 0) - (rank[a.impact] ?? 0) - if (r !== 0) return r - return `${a.event_date}${a.event_time ?? ''}`.localeCompare(`${b.event_date}${b.event_time ?? ''}`) - }) - .slice(0, 4) + .sort((a: any, b: any) => `${a.event_date}${a.event_time ?? ''}`.localeCompare(`${b.event_date}${b.event_time ?? ''}`)) + const groups: { date: string; events: any[] }[] = [] + for (const ev of sorted) { + const last = groups[groups.length - 1] + if (!last || last.date !== ev.event_date) groups.push({ date: ev.event_date, events: [ev] }) + else last.events.push(ev) + } + return groups }, [ecoCalendarData]) return ( @@ -233,8 +254,8 @@ export default function Dashboard() { {/* Top row */}
{/* Geo Risk */} -
-
+
+
Geopolitical Risk
@@ -245,8 +266,8 @@ export default function Dashboard() { {riskLoading ? (
) : riskScore && gauge ? ( - <> -
+
+
{riskScore.score}
{gauge.label}
@@ -260,33 +281,35 @@ export default function Dashboard() { ))}
-
+
{topNews.length > 0 && ( -
-
+
+
Top news
- {topNews.map((n, i) => { - const impact = Math.round(n.impact_score * 100) - const impactColor = impact >= 75 ? 'text-red-400' : impact >= 50 ? 'text-orange-400' : impact >= 25 ? 'text-yellow-400' : 'text-emerald-400' - const dotColor = impact >= 75 ? 'bg-red-500' : impact >= 50 ? 'bg-orange-500' : impact >= 25 ? 'bg-yellow-500' : 'bg-emerald-500' - return ( -
- - {n.title} - {impact} -
- ) - })} +
+ {topNews.map((n, i) => { + const impact = Math.round(n.impact_score * 100) + const impactColor = impact >= 75 ? 'text-red-400' : impact >= 50 ? 'text-orange-400' : impact >= 25 ? 'text-yellow-400' : 'text-emerald-400' + const dotColor = impact >= 75 ? 'bg-red-500' : impact >= 50 ? 'bg-orange-500' : impact >= 25 ? 'bg-yellow-500' : 'bg-emerald-500' + return ( +
+ + {n.title} + {impact} +
+ ) + })} +
)} - +
) :
Backend required
}
- {/* Watchlist Radar */} + {/* Watchlist Radar β€” its natural height (no cap) drives row 1's height */}
πŸ“‘ Watchlist Radar
@@ -304,7 +327,7 @@ export default function Dashboard() { -
+
{((watchlistQuotesData as any)?.items ?? []).map((it: any) => (
{it.ticker} @@ -446,26 +469,31 @@ export default function Dashboard() { ) })()} - {/* Economic Events β€” real Forex Factory feed, ranked by impact */} -
-
Economic Events
-
- {upcomingEvents.length > 0 ? upcomingEvents.map((ev: any, i: number) => ( -
- - {'!'.repeat(ev.impact === 'high' ? 3 : ev.impact === 'medium' ? 2 : 1)} - -
-
{ev.event_name}
-
{ev.event_date} {ev.currency && Β· {ev.currency}}
+ {/* Economic Events β€” real Forex Factory feed, grouped by date */} +
+
Economic Events
+ {upcomingEventsGrouped.length > 0 ? ( +
+ {upcomingEventsGrouped.map(group => ( +
+
+ {formatDateShort(group.date)} + {isTodayISO(group.date) && TODAY} +
+
+ {group.events.map((ev: any, i: number) => ( +
+ {CURRENCY_FLAGS[ev.currency] ?? ev.currency} + + {ev.event_name} + {ev.event_time && {ev.event_time}} +
+ ))} +
-
- )) :
No upcoming events
} -
+ ))} +
+ ) :
No upcoming events
}
@@ -511,7 +539,7 @@ export default function Dashboard() { const pfNet = pf?.net_pnl ?? null return ( - +
πŸ“Š P&L
@@ -678,7 +706,7 @@ export default function Dashboard() { .sort((a, b) => b.value - a.value) return ( - +
πŸ›‘οΈ Risk @@ -689,9 +717,9 @@ export default function Dashboard() {
{pieData.length > 0 ? ( <> - + - + {pieData.map((d, i) => ( ))} @@ -702,15 +730,31 @@ export default function Dashboard() { /> -
- {pieData.map(d => ( -
- - {d.name} - {d.value}% -
- ))} +
+ {pieData.map(d => { + const bias = d.bullish > d.bearish ? 'bullish' : d.bearish > d.bullish ? 'bearish' : 'neutral' + return ( +
+ + {d.name} + + {bias === 'bullish' ? '↑' : bias === 'bearish' ? '↓' : 'β€”'} + + {d.value}% +
+ ) + })}
+ {(risk?.alerts ?? []).length > 0 && ( +
+ {(risk.alerts as any[]).map((a: any, i: number) => ( +
+ {a.level === 'danger' ? '●' : 'β–²'} + {a.message} +
+ ))} +
+ )} ) : (
@@ -732,7 +776,7 @@ export default function Dashboard() { const rollingVar: any[] = (snap?.full_result?.rolling_var ?? []).slice(-30) return ( - +
πŸ“‰ VaR @@ -804,10 +848,9 @@ export default function Dashboard() { .map((w: any) => snapshots[w.ticker]) .filter(Boolean) .sort((a: any, b: any) => Math.abs((b.iv_rank ?? 50) - 50) - Math.abs((a.iv_rank ?? 50) - 50)) - .slice(0, 6) return ( - +
πŸ§ͺ Options Lab @@ -817,7 +860,7 @@ export default function Dashboard() { Add instruments to your watchlist (Config) to see IV highlights
) : highlights.length > 0 ? ( -
+
{highlights.map((h: any) => { const rank = h.iv_rank const skewPct = h.skew?.skew_pct