feat: new cockpit

This commit is contained in:
OpenSquared
2026-07-14 13:22:13 +02:00
parent 09b9efeda7
commit 922bb76567

View File

@@ -40,6 +40,26 @@ const REGIME_LABELS: Record<string, string> = {
soft_landing: 'Soft Landing', inflation_shock: 'Infl. Shock', incertain: 'Uncertain',
}
const CURRENCY_FLAGS: Record<string, string> = {
USD: '🇺🇸', EUR: '🇪🇺', GBP: '🇬🇧', JPY: '🇯🇵',
AUD: '🇦🇺', CAD: '🇨🇦', NZD: '🇳🇿', CHF: '🇨🇭', CNY: '🇨🇳',
}
const IMPACT_DOT: Record<string, string> = {
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<string, number> = { 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 */}
<div className="grid grid-cols-4 gap-4">
{/* Geo Risk */}
<div className="card col-span-1">
<div className="flex items-center justify-between mb-2">
<div className="card col-span-1 flex flex-col">
<div className="flex items-center justify-between mb-2 shrink-0">
<div className="section-title flex items-center gap-1 mb-0">
<Globe className="w-3 h-3" /> Geopolitical Risk
</div>
@@ -245,8 +266,8 @@ export default function Dashboard() {
{riskLoading ? (
<div className="animate-pulse h-16 bg-dark-600 rounded"></div>
) : riskScore && gauge ? (
<>
<div className="flex items-start justify-between gap-3">
<div className="flex flex-col flex-1 min-h-0">
<div className="flex items-start justify-between gap-3 shrink-0">
<div>
<div className={clsx('text-5xl font-bold', gauge.color)}>{riskScore.score}</div>
<div className={clsx('text-sm font-semibold mt-1', gauge.color)}>{gauge.label}</div>
@@ -260,33 +281,35 @@ export default function Dashboard() {
))}
</div>
</div>
<div className="mt-3 bg-dark-700 rounded-full h-2">
<div className="mt-3 bg-dark-700 rounded-full h-2 shrink-0">
<div className={clsx('h-2 rounded-full', gauge.bg)} style={{ width: `${riskScore.score}%` }} />
</div>
{topNews.length > 0 && (
<div className="mt-2.5 pt-2 border-t border-slate-700/30 space-y-1.5">
<div className="flex items-center gap-1 text-[9px] text-slate-600 mb-0.5">
<div className="mt-2.5 pt-2 border-t border-slate-700/30 flex flex-col flex-1 min-h-0">
<div className="flex items-center gap-1 text-[9px] text-slate-600 mb-1 shrink-0">
<Newspaper className="w-2.5 h-2.5" /> Top news
</div>
{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 (
<div key={i} className="flex items-center gap-1.5 text-[10px]">
<span className={clsx('w-1.5 h-1.5 rounded-full shrink-0', dotColor)} />
<span className="text-slate-300 truncate flex-1 line-clamp-1">{n.title}</span>
<span className={clsx('font-mono font-bold shrink-0', impactColor)}>{impact}</span>
</div>
)
})}
<div className="space-y-1.5 overflow-y-auto flex-1 min-h-0">
{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 (
<div key={i} className="flex items-center gap-1.5 text-[10px]">
<span className={clsx('w-1.5 h-1.5 rounded-full shrink-0', dotColor)} />
<span className="text-slate-300 truncate flex-1 line-clamp-1">{n.title}</span>
<span className={clsx('font-mono font-bold shrink-0', impactColor)}>{impact}</span>
</div>
)
})}
</div>
</div>
)}
</>
</div>
) : <div className="text-slate-500 text-xs">Backend required</div>}
</div>
{/* Watchlist Radar */}
{/* Watchlist Radar — its natural height (no cap) drives row 1's height */}
<div className="card col-span-1">
<div className="flex items-center justify-between mb-2">
<div className="section-title mb-0">📡 Watchlist Radar</div>
@@ -304,7 +327,7 @@ export default function Dashboard() {
<Radar dataKey="value" stroke="#3b82f6" fill="#3b82f6" fillOpacity={0.25} />
</RadarChart>
</ResponsiveContainer>
<div className="mt-1.5 pt-1.5 border-t border-slate-700/30 space-y-1 max-h-40 overflow-y-auto">
<div className="mt-1.5 pt-1.5 border-t border-slate-700/30 space-y-1">
{((watchlistQuotesData as any)?.items ?? []).map((it: any) => (
<div key={it.ticker} className="flex items-center justify-between text-[10px]">
<span className="text-slate-400 font-mono">{it.ticker}</span>
@@ -446,26 +469,31 @@ export default function Dashboard() {
)
})()}
{/* Economic Events — real Forex Factory feed, ranked by impact */}
<div className="card col-span-1 space-y-3">
<div className="section-title flex items-center gap-1"><Clock className="w-3 h-3" /> Economic Events</div>
<div className="space-y-1.5">
{upcomingEvents.length > 0 ? upcomingEvents.map((ev: any, i: number) => (
<div key={i} className="flex items-center gap-2 text-xs">
<span className={clsx('badge', {
'badge-red': ev.impact === 'high',
'badge-yellow': ev.impact === 'medium',
'badge-blue': ev.impact === 'low',
})}>
{'!'.repeat(ev.impact === 'high' ? 3 : ev.impact === 'medium' ? 2 : 1)}
</span>
<div className="min-w-0">
<div className="text-white line-clamp-1">{ev.event_name}</div>
<div className="text-slate-600">{ev.event_date} {ev.currency && <span className="text-slate-700">· {ev.currency}</span>}</div>
{/* Economic Events — real Forex Factory feed, grouped by date */}
<div className="card col-span-1 flex flex-col">
<div className="section-title flex items-center gap-1 shrink-0"><Clock className="w-3 h-3" /> Economic Events</div>
{upcomingEventsGrouped.length > 0 ? (
<div className="mt-2 space-y-2 overflow-y-auto flex-1 min-h-0">
{upcomingEventsGrouped.map(group => (
<div key={group.date}>
<div className="flex items-center gap-1.5 text-[9px] font-semibold text-slate-300 bg-dark-700/50 rounded px-1.5 py-0.5 mb-1">
{formatDateShort(group.date)}
{isTodayISO(group.date) && <span className="badge-blue text-[8px] px-1 py-0">TODAY</span>}
</div>
<div className="space-y-1 pl-0.5">
{group.events.map((ev: any, i: number) => (
<div key={i} className="flex items-center gap-1.5 text-[10px]">
<span className="shrink-0">{CURRENCY_FLAGS[ev.currency] ?? ev.currency}</span>
<span className={clsx('w-1.5 h-1.5 rounded-full shrink-0', IMPACT_DOT[ev.impact] ?? 'bg-slate-400')} />
<span className="text-white truncate flex-1 line-clamp-1">{ev.event_name}</span>
{ev.event_time && <span className="text-slate-700 text-[9px] shrink-0 font-mono">{ev.event_time}</span>}
</div>
))}
</div>
</div>
</div>
)) : <div className="text-slate-600 text-xs">No upcoming events</div>}
</div>
))}
</div>
) : <div className="text-slate-600 text-xs mt-2">No upcoming events</div>}
</div>
</div>
@@ -511,7 +539,7 @@ export default function Dashboard() {
const pfNet = pf?.net_pnl ?? null
return (
<Link to="/journal" className="card flex flex-col h-[288px] overflow-hidden hover:border-slate-600/60 transition-all cursor-pointer">
<Link to="/journal" className="card flex flex-col hover:border-slate-600/60 transition-all cursor-pointer">
<div className="flex items-center justify-between mb-1.5">
<span className="section-title mb-0 text-[10px]">📊 P&L</span>
<div className="flex items-center gap-1.5">
@@ -678,7 +706,7 @@ export default function Dashboard() {
.sort((a, b) => b.value - a.value)
return (
<Link to="/risk" className="card flex flex-col h-[288px] overflow-hidden hover:border-slate-600/60 transition-all cursor-pointer">
<Link to="/risk" className="card flex flex-col hover:border-slate-600/60 transition-all cursor-pointer">
<div className="flex items-center justify-between mb-1">
<span className="section-title mb-0 text-[10px]">🛡 Risk</span>
<ArrowUpRight className="w-3 h-3 text-slate-600" />
@@ -689,9 +717,9 @@ export default function Dashboard() {
</div>
{pieData.length > 0 ? (
<>
<ResponsiveContainer width="100%" height={110}>
<ResponsiveContainer width="100%" height={100}>
<PieChart>
<Pie data={pieData} dataKey="value" nameKey="name" innerRadius={32} outerRadius={50} paddingAngle={2}>
<Pie data={pieData} dataKey="value" nameKey="name" innerRadius={30} outerRadius={48} paddingAngle={2}>
{pieData.map((d, i) => (
<Cell key={i} fill={ASSET_CLASS_COLORS[d.name] ?? ASSET_CLASS_COLORS.unknown} />
))}
@@ -702,15 +730,31 @@ export default function Dashboard() {
/>
</PieChart>
</ResponsiveContainer>
<div className="mt-1 space-y-0.5 overflow-y-auto flex-1">
{pieData.map(d => (
<div key={d.name} className="flex items-center gap-1.5 text-[9px]">
<span className="w-2 h-2 rounded-full shrink-0" style={{ background: ASSET_CLASS_COLORS[d.name] ?? ASSET_CLASS_COLORS.unknown }} />
<span className="text-slate-500 capitalize flex-1 truncate">{d.name}</span>
<span className="text-slate-600 font-mono">{d.value}%</span>
</div>
))}
<div className="mt-1 space-y-0.5">
{pieData.map(d => {
const bias = d.bullish > d.bearish ? 'bullish' : d.bearish > d.bullish ? 'bearish' : 'neutral'
return (
<div key={d.name} className="flex items-center gap-1.5 text-[9px]">
<span className="w-2 h-2 rounded-full shrink-0" style={{ background: ASSET_CLASS_COLORS[d.name] ?? ASSET_CLASS_COLORS.unknown }} />
<span className="text-slate-500 capitalize flex-1 truncate">{d.name}</span>
<span className={clsx('text-[8px] w-3', bias === 'bullish' ? 'text-emerald-500' : bias === 'bearish' ? 'text-red-400' : 'text-slate-700')}>
{bias === 'bullish' ? '↑' : bias === 'bearish' ? '↓' : '—'}
</span>
<span className="text-slate-600 font-mono w-9 text-right">{d.value}%</span>
</div>
)
})}
</div>
{(risk?.alerts ?? []).length > 0 && (
<div className="mt-2 pt-2 border-t border-slate-700/30 space-y-1">
{(risk.alerts as any[]).map((a: any, i: number) => (
<div key={i} className={clsx('flex items-start gap-1 text-[9px] leading-snug', a.level === 'danger' ? 'text-red-400' : 'text-amber-400')}>
<span className="shrink-0 mt-[3px]">{a.level === 'danger' ? '●' : '▲'}</span>
<span className="line-clamp-2">{a.message}</span>
</div>
))}
</div>
)}
</>
) : (
<div className="text-[10px] text-slate-600 mt-1">
@@ -732,7 +776,7 @@ export default function Dashboard() {
const rollingVar: any[] = (snap?.full_result?.rolling_var ?? []).slice(-30)
return (
<Link to="/var" className="card flex flex-col h-[288px] overflow-hidden hover:border-slate-600/60 transition-all cursor-pointer">
<Link to="/var" className="card flex flex-col hover:border-slate-600/60 transition-all cursor-pointer">
<div className="flex items-center justify-between mb-1">
<span className="section-title mb-0 text-[10px]">📉 VaR</span>
<ArrowUpRight className="w-3 h-3 text-slate-600" />
@@ -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 (
<Link to="/options" className="card flex flex-col h-[288px] overflow-hidden hover:border-slate-600/60 transition-all cursor-pointer">
<Link to="/options" className="card flex flex-col hover:border-slate-600/60 transition-all cursor-pointer">
<div className="flex items-center justify-between mb-1">
<span className="section-title mb-0 text-[10px]">🧪 Options Lab</span>
<ArrowUpRight className="w-3 h-3 text-slate-600" />
@@ -817,7 +860,7 @@ export default function Dashboard() {
Add instruments to your watchlist (Config) to see IV highlights
</div>
) : highlights.length > 0 ? (
<div className="space-y-1.5 mt-1 overflow-y-auto flex-1">
<div className="space-y-1.5 mt-1">
{highlights.map((h: any) => {
const rank = h.iv_rank
const skewPct = h.skew?.skew_pct