import { useMemo, useState, useRef, useLayoutEffect, useEffect } from 'react' import { useQuery } from '@tanstack/react-query' import { useGeoRiskScore, useAllQuotes, useEcoCalendar, usePortfolioSummary, usePortfolioPositions, usePnlHistory, useLastScores, useAllPatterns, useMacroRegime, useRiskDashboard, useRiskRadar, useGeoNews, useCycleStatus, useInstrumentsWatchlist, useInstrumentsWatchlistQuotes, useWatchlistHistory, useInstrumentCatalogIds, useSaxoIvWatchlist, useLatestCycleReport, useWaveletWatchlistSignals, } from '../hooks/useApi' import { Clock, Globe, ShieldAlert, ArrowUpRight, Newspaper, Waves, Link2 } from 'lucide-react' import { Link, useNavigate } from 'react-router-dom' import clsx from 'clsx' import type { Quote } from '../types' import { format } from 'date-fns' import { fr } from 'date-fns/locale' import { RadarChart, PolarGrid, PolarAngleAxis, Radar, ResponsiveContainer, AreaChart, Area, XAxis, YAxis, Tooltip, CartesianGrid, Cell, BarChart, Bar, ReferenceLine, } from 'recharts' import { scoreColor } from '../components/TradeIdeas' import { ASSET_CLASS_COLORS } from '../constants/assetColors' import TradeRankList from '../components/TradeRankList' import { fmtPrice, fmtAsOf } from '../lib/format' const riskGauge = (score: number) => { if (score < 25) return { color: 'text-emerald-400', bg: 'bg-emerald-500', label: 'LOW' } if (score < 50) return { color: 'text-yellow-400', bg: 'bg-yellow-500', label: 'MODERATE' } if (score < 75) return { color: 'text-orange-400', bg: 'bg-orange-500', label: 'HIGH' } return { color: 'text-red-400', bg: 'bg-red-500', label: 'EXTREME' } } const assetEmoji: Record = { energy: 'β›½', metals: 'πŸ₯‡', agriculture: '🌾', equities: 'πŸ“ˆ', indices: 'πŸ“Š', forex: 'πŸ’±', crypto: 'β‚Ώ', rates: 'πŸ“‰', } const REGIME_LABELS: Record = { goldilocks: 'Goldilocks', desinflation: 'Disinflation', stagflation: 'Stagflation', recession: 'Recession', crise_liquidite: 'Liq. Crisis', reflation: 'Reflation', 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-emerald-500', } // The Economic Events card's currency filter is a closed set of 4 major economies // (not every SUPPORTED_CURRENCIES value) β€” deliberately simple: toggle chips, not a // full currency picker. GBP as the 4th pick: heavily represented in the feed already // and a major G7/FX market alongside US/EUR/China. const ECO_CURRENCY_FILTERS = ['USD', 'EUR', 'CNY', 'GBP'] as const // Instrument Analysis's catalog keys FX pairs with yfinance's "=X" suffix (EURUSD=X), // while the Cockpit watchlist stores the plain ticker (EURUSD) β€” try both forms before // concluding an instrument isn't registered there. const resolveCatalogId = (ticker: string, catalogIds?: Set): string | undefined => { if (!catalogIds) return undefined const upper = ticker.toUpperCase() if (catalogIds.has(upper)) return upper if (catalogIds.has(`${upper}=X`)) return `${upper}=X` return undefined } 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' }) } const formatTimeShort = (isoStr: string) => { const d = new Date(isoStr.includes('Z') || isoStr.includes('+') ? isoStr : isoStr.replace(' ', 'T') + 'Z') return d.toLocaleTimeString('fr-FR', { hour: '2-digit', minute: '2-digit' }) } function QuoteRow({ q }: { q: Quote }) { if (!q.price) return null const pos = q.change_pct >= 0 return (
{q.name || q.symbol}
{fmtPrice(q.price)}
{pos ? '+' : ''}{q.change_pct.toFixed(2)}%
) } function EcoEventRow({ ev, showActual }: { ev: any; showActual: boolean }) { return (
{CURRENCY_FLAGS[ev.currency] ?? ev.currency} {ev.event_name}
{showActual && ev.actual_value ? ( {ev.actual_value} {ev.forecast_value && /{ev.forecast_value}} ) : ev.forecast_value ? ( F {ev.forecast_value} ) : null} {ev.event_time && {ev.event_time}}
) } export default function Dashboard() { const navigate = useNavigate() const { data: riskScore, isLoading: riskLoading } = useGeoRiskScore() const { data: allQuotes } = useAllQuotes() const { data: ecoCalendarData } = useEcoCalendar({ period: 'recent', limit: 150, impacts: 'high,medium,low' }) const { data: portfolio } = usePortfolioSummary() const { data: lastScoresData } = useLastScores() const { data: allPatternsData } = useAllPatterns() const { data: macroData } = useMacroRegime() const { data: openPositionsData } = usePortfolioPositions('open') const { data: riskDashboard } = useRiskDashboard() const { data: riskRadarData } = useRiskRadar() const { data: cycleStatusData } = useCycleStatus() const { data: geoNews } = useGeoNews() const { data: watchlistItems } = useInstrumentsWatchlist() const { data: watchlistQuotesData } = useInstrumentsWatchlistQuotes() const { data: instrumentCatalogIds, isError: instrumentCatalogError, error: instrumentCatalogErrorObj } = useInstrumentCatalogIds() const [ecoImpactFilter, setEcoImpactFilter] = useState>(new Set(['high', 'medium', 'low'])) const [ecoCurrencyFilter, setEcoCurrencyFilter] = useState>(new Set(ECO_CURRENCY_FILTERS)) const [ecoShowNextWeek, setEcoShowNextWeek] = useState(false) const [watchlistChartTicker, setWatchlistChartTicker] = useState(null) const [watchlistChartPeriod, setWatchlistChartPeriod] = useState('3m') const activeWatchlistTicker = watchlistChartTicker ?? (watchlistItems as any)?.[0]?.ticker ?? '' const activeWatchlistName = ((watchlistItems as any) ?? []).find((w: any) => w.ticker === activeWatchlistTicker)?.name || activeWatchlistTicker const { data: watchlistHistoryData, isLoading: watchlistHistoryLoading } = useWatchlistHistory(activeWatchlistTicker, watchlistChartPeriod) const { data: latestCycleReportData } = useLatestCycleReport() const { data: waveletSignalsData } = useWaveletWatchlistSignals() const { data: saxoIvWatchlistData } = useSaxoIvWatchlist() // Diagnostic for the Watchlist card's "open in Instrument Analysis" arrow/double-click: // logs whether the catalog query succeeded and how each watchlist ticker resolves // against it, so a missing arrow can be traced to "catalog query failed" vs. "ticker // genuinely isn't registered in instruments.json" without needing a debugger. useEffect(() => { if (instrumentCatalogError) { console.error('[Watchlist] instrument catalog query failed:', instrumentCatalogErrorObj) return } if (!instrumentCatalogIds || !watchlistQuotesData) return const items: any[] = (watchlistQuotesData as any)?.items ?? [] console.debug('[Watchlist] catalog ids:', Array.from(instrumentCatalogIds as Set)) console.debug('[Watchlist] ticker -> catalogId resolution:', items.map((it: any) => ({ ticker: it.ticker, resolved: resolveCatalogId(it.ticker, instrumentCatalogIds as Set) ?? null, }))) }, [instrumentCatalogIds, instrumentCatalogError, instrumentCatalogErrorObj, watchlistQuotesData]) // Historical PnL curve (realized, from closed portfolio positions) + latest VaR snapshot const { data: pnlHistoryData } = usePnlHistory() const { data: latestVarData } = useQuery({ queryKey: ['dash-var-latest'], queryFn: () => fetch('/api/var/latest').then(r => r.ok ? r.json() : { snapshot: null }), staleTime: 120_000, retry: 1, }) // Row height is dictated by the config-driven watchlist cards (Watchlist Radar for row 1, // Options Lab for row 2) β€” the other cards in each row are capped to match, with internal scroll. const watchlistCardRef = useRef(null) const optionsLabCardRef = useRef(null) const [row1Height, setRow1Height] = useState(undefined) const [row2Height, setRow2Height] = useState(undefined) // Floors avoid the row collapsing to near-zero while the master card is still loading useLayoutEffect(() => { const el = watchlistCardRef.current if (!el) return const measure = () => setRow1Height(Math.max(el.offsetHeight, 220)) measure() const ro = new ResizeObserver(measure) ro.observe(el) return () => ro.disconnect() }, []) useLayoutEffect(() => { const el = optionsLabCardRef.current if (!el) return const measure = () => setRow2Height(Math.max(el.offsetHeight, 180)) measure() const ro = new ResizeObserver(measure) ro.observe(el) return () => ro.disconnect() }, []) const allPatterns: any[] = allPatternsData ?? [] const scoreMap = useMemo(() => { const map: Record = {} for (const sp of (lastScoresData?.scored_patterns ?? [])) { if (sp.pattern_id) map[sp.pattern_id] = sp } return map }, [lastScoresData]) const macroInfo = useMemo(() => { if (!macroData?.scenarios) return null const sc = macroData.scenarios const dom = sc.dominant ?? 'incertain' const m = sc.meta?.[dom] ?? { label: dom, color: '#94a3b8', emoji: '?' } const assetBias: Record = sc.asset_bias?.[dom] ?? {} // Top 3 scenario scores (excluding dominant, sorted desc) const ranked: [string, number][] = (sc.ranked ?? []).slice(0, 4) return { dominant: dom, label: m.label, color: m.color, emoji: m.emoji, assetBias, ranked } }, [macroData]) const gauge = riskScore ? riskGauge(riskScore.score) : null const lastCycle = (cycleStatusData as any)?.last_cycle ?? null const openPositions: any[] = (openPositionsData as any) ?? [] // Patterns from the last cycle only (filter by created_at >= cycle started_at) const cyclePatterns = useMemo(() => { const started = lastCycle?.started_at ?? null if (!started) return [] return [...allPatterns] .filter(p => p.created_at && p.created_at >= started) .sort((a, b) => (scoreMap[b.id]?.score ?? -1) - (scoreMap[a.id]?.score ?? -1)) .slice(0, 4) }, [allPatterns, scoreMap, lastCycle]) // 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, 30) , [geoNews]) const watchlistAsOf = useMemo(() => { const items: any[] = (watchlistQuotesData as any)?.items ?? [] return fmtAsOf(items.map((it: any) => it.timestamp).filter(Boolean).sort().pop()) }, [watchlistQuotesData]) // FF economic events β€” today (with actual/forecast as they release) + rest of the // current week (Mon-Sun) + optionally next week, grouped by date. Filtered by impact // (high/medium/low, toggleable) and currency (closed set of 4 majors, toggleable). // Past events beyond today are dropped β€” the point of this card is "am I up to date". const groupByDate = (evs: any[]) => { const sorted = [...evs].sort((a, b) => `${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 } const { todayEvents, restOfWeekGrouped, nextWeekGrouped } = useMemo(() => { const all: any[] = (ecoCalendarData as any)?.events ?? [] const events = all.filter((ev: any) => ecoImpactFilter.has(ev.impact) && ecoCurrencyFilter.has(ev.currency)) const now = new Date() const todayISO = now.toISOString().slice(0, 10) const dow = now.getUTCDay() // 0=Sun..6=Sat const sunday = new Date(now) sunday.setUTCDate(now.getUTCDate() + (dow === 0 ? 0 : 7 - dow)) const weekEndISO = sunday.toISOString().slice(0, 10) const nextSunday = new Date(sunday) nextSunday.setUTCDate(sunday.getUTCDate() + 7) const nextWeekEndISO = nextSunday.toISOString().slice(0, 10) const today = events .filter((ev: any) => ev.event_date === todayISO) .sort((a: any, b: any) => (a.event_time ?? '').localeCompare(b.event_time ?? '')) const restOfWeek = events.filter((ev: any) => ev.event_date > todayISO && ev.event_date <= weekEndISO) const nextWeek = events.filter((ev: any) => ev.event_date > weekEndISO && ev.event_date <= nextWeekEndISO) return { todayEvents: today, restOfWeekGrouped: groupByDate(restOfWeek), nextWeekGrouped: groupByDate(nextWeek) } }, [ecoCalendarData, ecoImpactFilter, ecoCurrencyFilter]) return (
{/* Header */}

OpenFin Cockpit

{format(new Date(), "EEEE d MMMM yyyy Β· HH:mm", { locale: fr })}

{portfolio && portfolio.open_positions > 0 && (
= 0 ? 'text-emerald-400' : 'text-red-400')}> Portfolio: {portfolio.unrealized_pnl >= 0 ? '+' : ''}{portfolio.unrealized_pnl?.toFixed(0)}€
)}
Live
{/* Risk concentration banner */} {(riskDashboard as any)?.concentration_alerts?.length > 0 && (
{((riskDashboard as any).concentration_alerts as any[]).slice(0, 2).map((alert: any, i: number) => (
{alert.message} Risk Dashboard β†’
))}
)} {/* Top row β€” height capped to Watchlist Radar's natural size (the only card driven by user config) */}
{/* Geo Risk */}
Geopolitical Risk
Geo news
{riskLoading ? (
) : riskScore && gauge ? (
{riskScore.score}
{gauge.label}
{Object.entries(riskScore.breakdown ?? {}) .sort((a, b) => b[1] - a[1]) .slice(0, 3) .map(([cat, val]) => (
{cat.replace('_', ' ')} {Math.round(val)}%
))}
{riskScore.computed_at && (
AI Score β€” frozen at last cycle ({formatTimeShort(riskScore.computed_at)}) {riskScore.rationale && (

{riskScore.rationale}

)}
)} {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}
) })}
)}
) :
Backend required
}
{/* Watchlist β€” natural height (config-driven), measured and used to cap the other row-1 cards */}
πŸ“‘ Watchlist
{watchlistAsOf && MAJ {watchlistAsOf}} Manage
{((watchlistQuotesData as any)?.items ?? []).length > 0 ? ( <>
{activeWatchlistName}
{['1w', '1m', '3m', '6m', '1y', '5y', 'max'].map(p => ( ))}
{watchlistHistoryLoading ? (
Loading…
) : ((watchlistHistoryData as any)?.bars?.length ?? 0) > 1 ? ( [fmtPrice(v), activeWatchlistName]} /> ) : (
No chart data for {activeWatchlistName}
)}
{((watchlistQuotesData as any)?.items ?? []).map((it: any) => { const catalogId = resolveCatalogId(it.ticker, instrumentCatalogIds as Set | undefined) return (
{ if (catalogId) navigate(`/instruments/${encodeURIComponent(catalogId)}`) }} className={clsx( 'flex items-center justify-between gap-1.5 text-[10px] whitespace-nowrap rounded px-1 -mx-1 py-0.5 transition-colors', it.ticker === activeWatchlistTicker ? 'bg-blue-900/20' : 'hover:bg-dark-700/40', catalogId && 'cursor-pointer' )} title={catalogId ? `Double-click to open ${it.name || it.ticker} in Instrument Analysis` : undefined} >
{fmtPrice(it.price)} = 0 ? 'text-emerald-400' : 'text-red-400')}> {it.change_pct != null ? `${it.change_pct >= 0 ? '+' : ''}${it.change_pct.toFixed(2)}%` : 'β€”'} {it.volatility_pct != null ? `${it.volatility_pct.toFixed(1)}%` : 'β€”'} {it.volatility_change_pct != null && Math.abs(it.volatility_change_pct) >= 0.1 && ( 0 ? 'text-orange-400' : 'text-blue-400'}> {' '}{it.volatility_change_pct >= 0 ? '+' : ''}{it.volatility_change_pct.toFixed(1)} )}
) })}
) : (
No instruments watched Add instruments β†’
)}
{/* RΓ©gime Macro */} {(() => { const dom = macroInfo?.dominant const colorClass = dom === 'growth' || dom === 'goldilocks' || dom === 'soft_landing' ? 'text-emerald-400' : dom === 'stagflation' || dom === 'recession' || dom === 'crise_liquidite' ? 'text-red-400' : dom === 'deflation' || dom === 'desinflation' ? 'text-blue-300' : dom === 'inflation_shock' ? 'text-orange-400' : 'text-slate-400' const ranked = macroInfo?.ranked ?? [] // Key gauges from macroData const gauges = (macroData as any)?.gauges ?? {} const reasons: string[] = dom ? ((macroData as any)?.scenarios?.reasons?.[dom] ?? []) : [] // 5 key indicators that explain the regime const KEY_GAUGES = [ { key: 'vix', label: 'VIX', color: (v: number) => v < 18 ? 'text-emerald-400' : v < 25 ? 'text-yellow-400' : 'text-red-400', fmt: (v: number) => v.toFixed(1), hint: (v: number) => v < 18 ? '↓ low risk' : v < 25 ? '↑ alert' : '↑ crisis', }, { key: 'spx_vs_200d', label: 'S&P/200d', color: (v: number) => v >= 0 ? 'text-emerald-400' : 'text-red-400', fmt: (v: number) => `${v >= 0 ? '+' : ''}${v.toFixed(1)}%`, hint: (v: number) => v >= 5 ? 'strong bull' : v >= 0 ? 'mod. bull' : 'bear', }, { key: 'slope_10y3m', label: 'Slope 10Y-3M', color: (v: number) => v >= 0.5 ? 'text-emerald-400' : v >= 0 ? 'text-yellow-400' : 'text-red-400', fmt: (v: number) => `${v >= 0 ? '+' : ''}${v.toFixed(2)}`, hint: (v: number) => v >= 0.5 ? 'normal' : v >= 0 ? 'flat' : 'inverted', }, { key: 'copper', label: 'Copper', color: (v: number, chg: number) => chg >= 0 ? 'text-emerald-400' : 'text-red-400', fmt: (v: number, chg: number) => `${chg >= 0 ? '+' : ''}${chg.toFixed(2)}%`, hint: (v: number, chg: number) => chg >= 0 ? '↑ growth' : '↓ slowing', }, { key: 'gold', label: 'Gold', color: (v: number, chg: number) => chg >= 1 ? 'text-amber-400' : chg <= -1 ? 'text-emerald-400' : 'text-slate-400', fmt: (v: number, chg: number) => `${chg >= 0 ? '+' : ''}${chg.toFixed(2)}%`, hint: (v: number, chg: number) => chg >= 1 ? '↑ safe haven' : chg <= -1 ? '↓ risk-on' : 'β‰ˆ neutral', }, ] return (
🌐 Macro Regime
{fmtAsOf((macroData as any)?.fetched_at) && ( MAJ {fmtAsOf((macroData as any)?.fetched_at)} )}
{macroInfo ? `${macroInfo.emoji} ${macroInfo.label}` : 'β€”'}
{ranked.length > 0 && (
{ranked.slice(0, 4).map(([key, score]: [string, number]) => { const maxScore = ranked[0]?.[1] ?? 1 const pct = Math.round((score / Math.max(maxScore, 1)) * 100) const isDom = key === dom return (
{REGIME_LABELS[key] ?? key}
{score}
) })}
)} {/* Key macro gauges */} {Object.keys(gauges).length > 0 && (
{KEY_GAUGES.map(({ key, label, color, fmt, hint }) => { const g = gauges[key] if (!g) return null const val: number = g.value ?? 0 const chg: number = g.change_pct ?? 0 const c = color(val, chg) const h = hint(val, chg) return (
{label} {fmt(val, chg)} {h}
) })}
)} {/* Trigger signals for dominant regime */} {reasons.length > 0 && (
{reasons.slice(0, 4).map((r: string, i: number) => ( {r} ))}
)} ) })()} {/* Economic Events β€” real Forex Factory feed: today (actual as it releases) + rest of week (+ next week, optional) */}
Economic Events
{(['high', 'medium', 'low'] as const).map(imp => (
{ECO_CURRENCY_FILTERS.map(cur => ( ))}
{(todayEvents.length > 0 || restOfWeekGrouped.length > 0 || (ecoShowNextWeek && nextWeekGrouped.length > 0)) ? (
{todayEvents.length > 0 && (
Today TODAY
{todayEvents.map((ev: any, i: number) => )}
)} {restOfWeekGrouped.map(group => (
{formatDateShort(group.date)}
{group.events.map((ev: any, i: number) => )}
))} {ecoShowNextWeek && nextWeekGrouped.length > 0 && ( <>
Next week
{nextWeekGrouped.map(group => (
{formatDateShort(group.date)}
{group.events.map((ev: any, i: number) => )}
))} )}
) :
No events match filters
}
{/* ── Command Center: RΓ©sumΓ© OpΓ©rationnel ── height capped to Options Lab's natural size (config-driven) ── */}
{/* PnL β€” real portfolio (services/database.py `portfolio` table), mark-to-market via Black-Scholes */} {(() => { const pf = portfolio as any const pfPnlPct = pf?.unrealized_pnl_pct ?? null const pfPnl = pf?.unrealized_pnl ?? null const pfInvest = pf?.total_invested ?? null const pfReal = pf?.realized_pnl ?? null const pfNet = pf?.net_pnl ?? null return (
πŸ“Š P&L
{/* ── Side-by-side: Unrealized | Realized ── */}
{/* Left: Unrealized */}
Open
= 0 ? 'text-emerald-400' : 'text-red-400')}> {pfPnlPct !== null ? `${pfPnlPct >= 0 ? '+' : ''}${pfPnlPct.toFixed(1)}%` : 'β€”'}
= 0 ? 'text-emerald-500' : 'text-red-400')}> {pfPnl != null ? `${pfPnl >= 0 ? '+' : ''}${pfPnl.toFixed(0)}€` : 'β€”'}
{pf?.open_positions ?? 0} positions
{/* Right: Realized */}
Realized
0 ? 'text-emerald-400' : 'text-red-400')}> {pfReal != null && pfReal !== 0 ? `${pfReal >= 0 ? '+' : ''}${pfReal.toFixed(0)}€` : 'β€”'}
{pfNet != null && (
= 0 ? 'text-emerald-500/70' : 'text-red-400/70')}> net {pfNet >= 0 ? '+' : ''}{pfNet.toFixed(0)}€
)}
{pf?.closed_positions ?? 0} closed
{/* Total line */}
Invested {pfInvest != null ? `${pfInvest.toFixed(0)}€` : 'β€”'} = 0 ? 'text-emerald-400' : 'text-red-400')}> Total {pfNet != null ? `${pfNet >= 0 ? '+' : ''}${pfNet.toFixed(0)}€` : 'β€”'}
{/* Realized PnL curve β€” cumulative, from closed portfolio positions */} {(() => { const curve: any[] = (pnlHistoryData as any) ?? [] if (curve.length < 2) return null const chartData = curve.map((s: any) => ({ date: s.date?.slice(5, 10), pnl: s.cumulative ?? 0, })) const minVal = Math.min(...chartData.map((d: any) => d.pnl)) const maxVal = Math.max(...chartData.map((d: any) => d.pnl)) const isPositive = chartData[chartData.length - 1]?.pnl >= 0 const strokeColor = isPositive ? '#34d399' : '#f87171' const gradId = 'pnlGrad' return (
Realized PnL curve ({chartData.length} closed)
[`${v >= 0 ? '+' : ''}${Number(v).toFixed(0)}€`, 'Cumulative PnL']} />
) })()} ) })()} {/* Risk β€” real portfolio. 5-axis radar (Concentration/Volatility/Correlation/ Exposure/Drawdown) from services/portfolio_risk.py `compute_real_portfolio_risk_radar()` (`/api/risk/radar`), concentration alerts + asset-class breakdown from `get_risk_dashboard()` (`/api/risk/dashboard`) β€” both built on the `portfolio` table. */} {(() => { const risk = riskDashboard as any const alertCount: number = risk?.concentration_alerts?.length ?? 0 const openCount: number = risk?.open_trades ?? 0 const byClass: Record = risk?.exposure_by_class ?? {} const pieData = Object.entries(byClass) .map(([cls, data]: [string, any]) => ({ name: cls, value: data.pct_of_portfolio ?? 0 })) .filter(d => d.value > 0) .sort((a, b) => b.value - a.value) const radarAxes = ((riskRadarData as any)?.axes ?? []).map((a: any) => ({ ...a, value: a.value ?? 0 })) return (
πŸ›‘οΈ Risk
0 ? 'text-red-400' : 'text-emerald-400')}> {alertCount > 0 ? `${alertCount} alert${alertCount > 1 ? 's' : ''}` : 'OK'}
{radarAxes.length > 0 && ( [props.payload.detail, props.payload.axis]} /> )} {pieData.length > 0 ? ( <>
0 && 'mt-1 pt-1 border-t border-slate-700/30')}> {pieData.map(d => (
{d.name} {d.value}%
))}
{(risk?.concentration_alerts ?? []).length > 0 && (
{(risk.concentration_alerts as any[]).map((a: any, i: number) => (
{a.level === 'high' ? '●' : 'β–²'} {a.message}
))}
)} ) : (
{openCount > 0 ? `${openCount} positions` : 'No positions'}
)} ) })()} {/* VaR β€” histogram + rolling trend */} {(() => { const snap = latestVarData?.snapshot const hv = snap?.hist_var_1d_pct const cv = snap?.hist_cvar_pct const mv = snap?.mc_var_1d_pct const ts = snap?.computed_at?.slice(0, 16).replace('T', ' ') const histogram: any[] = snap?.full_result?.histogram ?? [] const rollingVar: any[] = (snap?.full_result?.rolling_var ?? []).slice(-30) return (
πŸ“‰ VaR
{snap ? ( <>
VaR 95% Β· {ts} UTC
{[ { label: 'Hist.', val: hv, color: 'text-blue-400' }, { label: 'CVaR', val: cv, color: 'text-orange-400' }, { label: 'MCΓ—1.5', val: mv, color: 'text-red-400' }, ].map(({ label, val, color }) => (
{label}
{val != null ? `${val >= 0 ? '+' : ''}${val.toFixed(2)}%` : 'β€”'}
))}
{histogram.length > 0 && (
P&L distribution
[v, 'count']} labelFormatter={(l: any) => `${l}%`} /> {hv != null && } {histogram.map((h: any, i: number) => ( ))}
)} {rollingVar.length > 1 && (
)} ) : (
No VaR snapshot yet β€” run one from /var
)} ) })()} {/* Options Lab β€” Saxo-only IV highlights, scoped to watchlist instruments that have a Saxo OPTION symbol linked (Config -> Instruments Watchlist, "Option" picker β€” distinct from the "Quote" picker used for Watchlist Radar's price/vol source). Deliberately not the yfinance-based IV batch β€” matches Options Lab itself, which now only shows Saxo. */} {(() => { const watchlist: any[] = watchlistItems ?? [] const linked = watchlist.filter((w: any) => w.saxo_option_symbol) const bySaxoSymbol: Record = {} for (const item of ((saxoIvWatchlistData as any)?.items ?? [])) bySaxoSymbol[item.ticker] = item const highlights = linked .map((w: any) => bySaxoSymbol[w.saxo_option_symbol] ? { ...bySaxoSymbol[w.saxo_option_symbol], watchlistTicker: w.ticker, watchlistName: w.name || w.ticker } : null) .filter(Boolean) .sort((a: any, b: any) => Math.abs((b.iv_rank ?? 50) - 50) - Math.abs((a.iv_rank ?? 50) - 50)) return (
πŸ§ͺ Options Lab
Saxo
{linked.length === 0 ? (
Link a watchlist instrument to a Saxo option symbol (Config β†’ Instruments Watchlist β†’ "Option") to see IV highlights here
) : highlights.length > 0 ? (
{highlights.map((h: any) => { const rank = h.iv_rank const ivCur = h.iv_current_pct const ivChg = h.iv_change_1d_pct return (
{rank != null && rank > 80 ? 'πŸ”΄' : rank != null && rank < 20 ? '🟒' : 'βšͺ'} {h.watchlistName} IVR {rank != null ? rank.toFixed(0) : 'β€”'} {ivCur != null && Β· IV {ivCur.toFixed(1)}%} {ivChg != null && Math.abs(ivChg) >= 0.1 && ( 0 ? 'text-orange-400' : 'text-blue-400')}> {ivChg >= 0 ? '+' : ''}{ivChg.toFixed(1)}pt )} {h.history_days > 0 && ( {h.history_days}d )}
) })}
) : (
Waiting for Saxo snapshots to accumulate…
)} ) })()}
{/* ── Command Center: Intelligence & Contexte ── */}
{/* Wavelets Signal β€” latest per-ticker signal from the watchlist scan (computed each cycle). Single click on the card = Wavelets Simulation overview; double-click a row = that instrument's Analysis page, Wavelets panel open directly. */} {(() => { const signals: any[] = waveletSignalsData?.signals ?? [] const nameByTicker: Record = {} for (const w of ((watchlistItems as any[]) ?? [])) nameByTicker[w.ticker] = w.name || w.ticker return (
Wavelets Signal {signals.length > 0 ? (
{signals.slice(0, 5).map((s: any, i: number) => (
navigate(`/instruments/${encodeURIComponent(s.ticker)}?tab=wavelets`)} title="Double-click to open in Instrument Analysis β†’ Wavelets" className="flex items-center gap-1.5 text-[10px] rounded px-1 -mx-1 py-0.5 cursor-pointer hover:bg-dark-700/40 transition-colors" > {s.direction === 'up' ? '↑' : '↓'} {nameByTicker[s.ticker] || s.ticker} {s.band_label} Β· {s.signal_kind} {s.computed_at && {s.computed_at.slice(0, 10)}}
))}
) : (
No wavelet signal detected β€” le prochain cycle en calculera
)}
) })()} {/* Recommandations du jour β€” patterns from the most recent cycle only */}
🎯 Recommandations du jour
{cyclePatterns.length > 0 ? (
{cyclePatterns.map((p: any, i: number) => { const sp = scoreMap[p.id] const score: number | null = sp?.score ?? null const ticker: string | null = sp?.recommended_trade?.underlying ?? null const dateStr = p.created_at ? p.created_at.slice(0, 10) : null return (
{i + 1}
{p.name}
{score !== null ? ( {score} ) : ( β€” )}
{ticker && {ticker}} {ticker && dateStr && ' Β· '} {dateStr}
) })}
) : (
Aucun cycle exΓ©cutΓ© aujourd'hui
)} {/* Narration du cycle β€” GPT-4o context narrative from the latest cycle report */} {(() => { const report = (latestCycleReportData as any)?.report const narrative: string = report?.context_narrative?.narrative ?? '' const keySignals: string[] = report?.context_narrative?.key_signals ?? [] return (
Cycle Narrative
{narrative ? ( <>

{narrative}

{keySignals.length > 0 && (
{keySignals.slice(0, 4).map((s: string, i: number) => ( {s} ))}
)} ) : (
No cycle narrative yet
)} ) })()}
{/* Markets mini overview */}
{['energy', 'metals', 'indices', 'forex'].map(cls => (
{assetEmoji[cls]} {cls}
{allQuotes?.[cls]?.map(q => ) ?? (
Loading...
)}
))}
) }