import { useMemo, useState } from 'react' import { useQuery } from '@tanstack/react-query' import { useGeoRiskScore, useAllQuotes, useEcoCalendar, usePortfolioSummary, useLastScores, useAllPatterns, useMacroRegime, useTradeMtm, useRiskDashboard, useGeoNews, useSimPortfolioRisk, useCycleStatus, useClosedTrades, useInstrumentsWatchlist, useInstrumentsWatchlistQuotes, useIvBatch, useLatestCycleReport, } from '../hooks/useApi' import { Clock, Globe, ShieldAlert, ArrowUpRight, Newspaper } from 'lucide-react' import { Link } 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, PieChart, Pie, Cell, BarChart, Bar, ReferenceLine, } from 'recharts' import { scoreColor } from '../components/TradeIdeas' import { ASSET_CLASS_COLORS } from '../constants/assetColors' import TradeRankList from '../components/TradeRankList' 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-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) } return (
) } function QuoteRow({ q }: { q: Quote }) { if (!q.price) return null const pos = q.change_pct >= 0 return (
{q.name || q.symbol}
{q.price.toFixed(2)}
{pos ? '+' : ''}{q.change_pct.toFixed(2)}%
) } export default function Dashboard() { const { data: riskScore, isLoading: riskLoading } = useGeoRiskScore() const { data: allQuotes } = useAllQuotes() const { data: ecoCalendarData } = useEcoCalendar({ period: 'recent', limit: 30 }) const { data: portfolio } = usePortfolioSummary() const { data: lastScoresData } = useLastScores() const { data: allPatternsData } = useAllPatterns() const { data: macroData } = useMacroRegime() const { data: tradeMtmData } = useTradeMtm(30) const { data: closedTradesData } = useClosedTrades(90) const { data: riskDashboard } = useRiskDashboard() const { data: simRisk } = useSimPortfolioRisk() const { data: cycleStatusData } = useCycleStatus() const { data: geoNews } = useGeoNews() const { data: watchlistItems } = useInstrumentsWatchlist() const { data: watchlistQuotesData } = useInstrumentsWatchlistQuotes() const { data: latestCycleReportData } = useLatestCycleReport() const watchlistTickers: string[] = (watchlistItems ?? []).map((w: any) => w.ticker) const { data: ivBatchData } = useIvBatch(watchlistTickers.join(',')) // Historical PnL curve + latest VaR snapshot const { data: pnlHistoryData } = useQuery({ queryKey: ['dash-pnl-history'], queryFn: () => fetch('/api/var/pnl/snapshots?limit=200').then(r => r.ok ? r.json() : { snapshots: [] }), staleTime: 120_000, retry: 1, }) 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, }) const [pnlView, setPnlView] = useState<'simulated' | 'portfolio'>(() => (localStorage.getItem('dash_pnl_view') as any) ?? 'simulated' ) const [riskView, setRiskView] = useState<'simulated' | 'portfolio'>(() => (localStorage.getItem('dash_risk_view') as any) ?? 'simulated' ) const setPnlViewPersist = (v: 'simulated' | 'portfolio') => { setPnlView(v); localStorage.setItem('dash_pnl_view', v) } const setRiskViewPersist = (v: 'simulated' | 'portfolio') => { setRiskView(v); localStorage.setItem('dash_risk_view', v) } 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 mtmTrades: any[] = (tradeMtmData as any)?.trades ?? [] // 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]) // Watchlist radar: change_pct normalized to a 0-100 scale (50 = flat) const watchlistRadarData = useMemo(() => { const items: any[] = (watchlistQuotesData as any)?.items ?? [] return items.slice(0, 8).map((it: any) => { const chg = Math.max(-5, Math.min(5, it.change_pct ?? 0)) return { subject: it.ticker, value: 50 + chg * 10, ref: 50 } }) }, [watchlistQuotesData]) // 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 sorted = [...events] .filter((ev: any) => ev.event_date >= todayISO) .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 (
{/* 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 */}
{/* Geo Risk */}
Geopolitical Risk
Geo news
{riskLoading ? (
) : riskScore && gauge ? (
{riskScore.score}
{gauge.label}
{riskScore.top_risks?.map(([cat, val]) => (
{(cat as string).replace('_', ' ')} {Math.round((val as number) * 100)}%
))}
{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 Radar β€” its natural height (no cap) drives row 1's height */}
πŸ“‘ Watchlist Radar
Manage
{watchlistRadarData.length > 0 ? ( <>
{((watchlistQuotesData as any)?.items ?? []).map((it: any) => (
{it.ticker}
{it.price != null ? it.price.toFixed(2) : 'β€”'} = 0 ? 'text-emerald-400' : 'text-red-400')}> {it.change_pct != null ? `${it.change_pct >= 0 ? '+' : ''}${it.change_pct.toFixed(2)}%` : 'β€”'}
))}
) : (
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
{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, 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
}
{/* ── Command Center: RΓ©sumΓ© OpΓ©rationnel ── */}
{/* PnL */} {(() => { // Unrealized β€” open trades only (get_trade_entry_prices excludes closed) const openTrades = mtmTrades const openWithPnl = openTrades.filter((t: any) => t.pnl_pct != null) const openCapital = openTrades.reduce((s: number, t: any) => s + (t.capital_invested ?? t.entry_price ?? 0), 0) const openProfit = openWithPnl.reduce((s: number, t: any) => s + ((t.capital_invested ?? t.entry_price ?? 0) * t.pnl_pct / 100), 0) const openPnlPct = openCapital > 0 ? openProfit / openCapital * 100 : null const openWinners = openWithPnl.filter((t: any) => t.pnl_pct > 0).length const openLosers = openWithPnl.filter((t: any) => t.pnl_pct < 0).length const targetHit = openTrades.filter((t: any) => t.alert_type === 'target_reached').length const stopHit = openTrades.filter((t: any) => t.alert_type === 'stop_loss').length // Realized β€” from dedicated closed-trades endpoint (pnl_realized in %, capital for EUR) const closedTrades = (closedTradesData as any)?.trades ?? [] const closedCapital = closedTrades.reduce((s: number, t: any) => s + (t.capital_invested ?? t.entry_price ?? 0), 0) const closedProfitEur = closedTrades.reduce((s: number, t: any) => { const cap = t.capital_invested ?? t.entry_price ?? 0 return s + (cap * (t.pnl_realized ?? 0) / 100) }, 0) const closedWithPnl = closedTrades.filter((t: any) => t.pnl_realized != null) const avgClosedPct = closedWithPnl.length > 0 ? closedWithPnl.reduce((s: number, t: any) => s + t.pnl_realized, 0) / closedWithPnl.length : null const closedWinners = closedTrades.filter((t: any) => (t.pnl_realized ?? 0) > 0).length const closedLosers = closedTrades.filter((t: any) => (t.pnl_realized ?? 0) < 0).length const isEstimated = openTrades.some((t: any) => t.capital_invested == null && t.entry_price != null) const totalCapital = openCapital + closedCapital const totalProfit = openProfit + closedProfitEur 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
{pnlView === 'simulated' ? ( <>
= 0 ? 'text-emerald-400' : 'text-red-400')}> {openPnlPct !== null ? `${openPnlPct >= 0 ? '+' : ''}${openPnlPct.toFixed(1)}%` : 'β€”'}
= 0 ? 'text-emerald-500' : 'text-red-400')}> {openCapital > 0 ? `${openProfit >= 0 ? '+' : ''}${openProfit.toFixed(0)}€` : 'β€”'}
{openTrades.length} trades Β· {openWinners}βœ“{' '} {openLosers}βœ—
) : ( <>
= 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
)} {(targetHit > 0 || stopHit > 0) && (
{targetHit > 0 && 🎯{targetHit}} {stopHit > 0 && β›”{stopHit}}
)}
{/* Right: Realized */}
Realized
{pnlView === 'simulated' ? ( <>
= 0 ? 'text-emerald-400' : 'text-red-400')}> {avgClosedPct !== null ? `${avgClosedPct >= 0 ? '+' : ''}${avgClosedPct.toFixed(2)}%` : closedTrades.length === 0 ? 'β€”' : '+0.00%'}
= 0 ? 'text-emerald-500' : 'text-red-400')}> {closedCapital > 0 ? `${closedProfitEur >= 0 ? '+' : ''}${closedProfitEur.toFixed(0)}€` : closedTrades.length > 0 ? 'no capital' : ''}
{closedTrades.length} closed Β· {closedWinners}βœ“{' '} {closedLosers}βœ—
) : ( <>
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{isEstimated ? ' ~' : ''} {pnlView === 'simulated' ? (totalCapital > 0 ? `${totalCapital.toFixed(0)}€` : 'β€”') : (pfInvest != null ? `${pfInvest.toFixed(0)}€` : 'β€”')} = 0 ? 'text-emerald-400' : 'text-red-400')}> Total {pnlView === 'simulated' ? (totalCapital > 0 ? `${totalProfit >= 0 ? '+' : ''}${totalProfit.toFixed(0)}€` : 'β€”') : (pfNet != null ? `${pfNet >= 0 ? '+' : ''}${pfNet.toFixed(0)}€` : 'β€”')}
{/* PnL historical sparkline */} {(() => { const snaps: any[] = [...(pnlHistoryData?.snapshots ?? [])].reverse() if (snaps.length < 2) return null const chartData = snaps.map(s => ({ date: s.snapped_at?.slice(5, 10), pnl: s.total_pnl_pct ?? 0, })) const minVal = Math.min(...chartData.map(d => d.pnl)) const maxVal = Math.max(...chartData.map(d => d.pnl)) const isPositive = chartData[chartData.length - 1]?.pnl >= 0 const strokeColor = isPositive ? '#34d399' : '#f87171' const gradId = 'pnlGrad' return (
Historical PnL curve ({snaps.length} pts)
[`${v >= 0 ? '+' : ''}${Number(v).toFixed(3)}%`, 'PnL']} />
) })()} ) })()} {/* Risk β€” donut chart of asset allocation */} {(() => { const risk = simRisk as any const alertCount: number = risk?.alerts?.length ?? 0 const conflictCount: number = risk?.conflicts?.length ?? 0 const openCount: number = risk?.open_count ?? 0 const concentration: Record = risk?.concentration ?? {} const pieData = Object.entries(concentration) .map(([cls, data]: [string, any]) => ({ name: cls, value: data.pct, bullish: data.bullish, bearish: data.bearish })) .filter(d => d.value > 0) .sort((a, b) => b.value - a.value) return (
πŸ›‘οΈ Risk
0 ? 'text-red-400' : 'text-emerald-400')}> {alertCount > 0 ? `${alertCount} alert${alertCount > 1 ? 's' : ''}` : 'OK'} {conflictCount > 0 && {conflictCount} conflict{conflictCount > 1 ? 's' : ''}}
{pieData.length > 0 ? ( <> {pieData.map((d, i) => ( ))} [`${value}% (${props.payload.bullish}↑ ${props.payload.bearish}↓)`, name]} />
{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}
))}
)} ) : (
{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 β€” IV highlights scoped to the watchlist */} {(() => { const watchlist: any[] = watchlistItems ?? [] const snapshots: Record = (ivBatchData as any)?.snapshots ?? {} const highlights = watchlist .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)) return (
πŸ§ͺ Options Lab
{watchlist.length === 0 ? (
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 const ivCur = h.iv_current_pct const ivPctl = h.iv_percentile const flow: string | null = h.options_flow?.flow_bias ?? null const pcRatio = h.options_flow?.pc_oi_ratio return (
{rank != null && rank > 80 ? 'πŸ”΄' : rank != null && rank < 20 ? '🟒' : 'βšͺ'} {h.ticker} IVR {rank != null ? rank.toFixed(0) : 'β€”'} {ivCur != null && Β· IV {ivCur.toFixed(1)}%} {skewPct != null && Math.abs(skewPct) > 3 && ( 0 ? 'text-orange-400' : 'text-blue-400')}> skew {skewPct >= 0 ? '+' : ''}{skewPct.toFixed(1)} )}
{(ivPctl != null || flow != null || pcRatio != null) && (
{ivPctl != null && Pctl {ivPctl.toFixed(0)}} {ivPctl != null && pcRatio != null && ' Β· '} {pcRatio != null && P/C {pcRatio.toFixed(2)}} {(ivPctl != null || pcRatio != null) && flow && ' Β· '} {flow && {flow}}
)}
) })}
) : (
Loading IV data…
)} ) })()}
{/* ── Command Center: Intelligence & Contexte ── */}
{/* 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...
)}
))}
) }