import { useState } from 'react' import { useRiskDashboard, usePatternCorrelations, usePnlTimeline, useRiskExposure, useSimPortfolioRisk, usePortfolioScenarioExposure } from '../hooks/useApi' import { ASSET_CLASS_COLORS } from '../constants/assetColors' import clsx from 'clsx' import { ShieldAlert, TrendingUp, GitBranch, AlertTriangle, CheckCircle, Activity, Brain, RefreshCw, PieChart, Layers } from 'lucide-react' // ── Gauge component ────────────────────────────────────────────────────────── function ConcentrationGauge({ label, pct, threshold = 50 }: { label: string; pct: number; threshold?: number }) { const color = pct > threshold ? 'bg-red-500' : pct > threshold * 0.7 ? 'bg-amber-500' : 'bg-emerald-500' const textColor = pct > threshold ? 'text-red-400' : pct > threshold * 0.7 ? 'text-amber-400' : 'text-emerald-400' return (
{label} {pct.toFixed(1)}%
{pct > threshold && (
⚠ Saturated (>{threshold}%)
)}
) } // ── Equity curve SVG ───────────────────────────────────────────────────────── function EquityCurve({ timeline }: { timeline: any[] }) { if (!timeline || timeline.length < 2) { return (
No P&L data yet (requires mature trades)
) } const values = timeline.map(t => t.cumulative_pnl_abs ?? 0) const min = Math.min(...values) const max = Math.max(...values) const range = max - min || 1 const W = 400 const H = 80 const pad = 4 const points = values.map((v, i) => { const x = pad + (i / (values.length - 1)) * (W - pad * 2) const y = H - pad - ((v - min) / range) * (H - pad * 2) return `${x},${y}` }).join(' ') const finalVal = values[values.length - 1] const lineColor = finalVal >= 0 ? '#34d399' : '#f87171' return (
{/* Zero line */} {min < 0 && max > 0 && ( )}
{timeline[0]?.entry_date?.slice(0, 7)} = 0 ? 'text-emerald-400' : 'text-red-400')}> {finalVal >= 0 ? '+' : ''}{finalVal.toFixed(0)}€ {timeline[timeline.length - 1]?.entry_date?.slice(0, 7)}
) } // ── Correlation heatmap row ────────────────────────────────────────────────── function CorrelationPairRow({ pair }: { pair: any }) { const c = pair.correlation const abs = Math.abs(c) const color = abs > 0.7 ? 'text-red-400' : abs > 0.4 ? 'text-amber-400' : 'text-emerald-400' const bg = abs > 0.7 ? 'bg-red-900/20 border-red-700/30' : abs > 0.4 ? 'bg-amber-900/20 border-amber-700/30' : 'bg-dark-700/30 border-slate-700/20' return (
{pair.name_a} {pair.name_b}
{c > 0 ? '+' : ''}{c.toFixed(2)}
{pair.interpretation}
) } // ── Recommendation card ────────────────────────────────────────────────────── function RecommendationCard({ rec }: { rec: any }) { if (!rec) return null const isOk = rec.level === 'ok' const isDanger = rec.level === 'danger' return (
{isOk ? : } Risk Committee Recommendation
{(rec.messages ?? []).map((msg: string, i: number) => (

{msg}

))}
) } // ── Scenario concentration ("same bet, different ticker") ─────────────────── function ScenarioExposureCard() { const { data, isLoading } = usePortfolioScenarioExposure() const exp: any = data if (isLoading) return
if (!exp || !exp.positions) return null const concentration: any[] = exp.concentration ?? [] const scenarios: any[] = exp.scenarios ?? [] return (
Concentration par scénario macro
Repricing Black-Scholes réel (pricing Saxo-first) de chaque position sous 5 scénarios — révèle quand plusieurs positions différentes sont en réalité le même pari répété.
{exp.warning && (
{exp.warning}
)}
{/* Concentration bars */}
% du book dont c'est le scénario le plus favorable
{concentration.map((c: any) => (
{c.label} = 60 ? 'text-amber-400' : 'text-slate-300')}> {c.pct_of_portfolio}%
))}
{/* Sensitivity matrix */}
P&L estimé du portefeuille par scénario
{scenarios.map((s: any) => ( ))}
{s.label} = 0 ? 'text-emerald-400' : 'text-red-400')}> {s.portfolio_pnl_pct >= 0 ? '+' : ''}{s.portfolio_pnl_pct}%
{exp.unpriced?.length > 0 && (
{exp.unpriced.length} position(s) non pricée(s) (pas de legs/données) : {exp.unpriced.map((u: any) => u.title).join(', ')}
)}
) } // ── Main page ──────────────────────────────────────────────────────────────── function SimRiskPanel() { const { data, isLoading, refetch } = useSimPortfolioRisk() const risk = data as any if (isLoading) return (
{[1, 2, 3].map(i =>
)}
) if (!risk || risk.open_count === 0) return (
No open positions in the simulated portfolio
Logged trades will appear here after the next AI cycle
) const concentration: Record = risk.concentration ?? {} const alerts: any[] = risk.alerts ?? [] const conflicts: any[] = risk.conflicts ?? [] const aiMonitor: any = risk.ai_monitor const aiTs: string = risk.ai_monitor_ts const dangers = alerts.filter((a: any) => a.level === 'danger') const warnings = alerts.filter((a: any) => a.level === 'warning') return (
{/* KPIs */}
{risk.open_count}
Simulated positions
0 ? 'text-red-400' : 'text-emerald-400')}> {dangers.length}
Directional conflicts
0 ? 'text-amber-400' : 'text-emerald-400')}> {warnings.length}
Concentration alerts
{Object.keys(concentration).filter(ac => ac !== 'unknown').length}
Asset classes
{/* Concentration + Conflicts side by side */}
{/* Asset class bars */}
Allocation by asset class
{Object.entries(concentration) .sort(([, a]: any, [, b]: any) => b.pct - a.pct) .map(([ac, data]: [string, any]) => { const color = ASSET_CLASS_COLORS[ac] ?? ASSET_CLASS_COLORS.unknown const exp = risk.direction_exposure?.[ac] ?? {} const isOver = data.pct >= 35 return (
{ac} {data.tickers?.slice(0, 3).join(', ')}{data.tickers?.length > 3 ? '…' : ''}
{exp.bullish > 0 && ▲{exp.bullish}} {exp.bearish > 0 && ▼{exp.bearish}} {data.pct}% ({data.count})
) })}
{/* Conflicts & alerts */}
{conflicts.length === 0 && warnings.length === 0 ? (
Balanced portfolio — no conflicts or over-concentration detected
) : null} {conflicts.map((c: any, i: number) => (
Directional conflict — {c.underlying}
{c.trades.map((t: any) => (
{t.direction === 'bullish' ? '▲' : '▼'}#{t.id} {t.strategy} {t.entry_date} {t.pattern_name}
))}
))} {warnings.length > 0 && (
Concentration alerts
{warnings.map((a: any, i: number) => (
{a.message}
))}
)}
{/* AI recommendations */} {aiMonitor && (
AI Recommendations — Simulated portfolio monitor
{aiTs && {aiTs.slice(0, 16).replace('T', ' ')}}
{aiMonitor.assessment &&

{aiMonitor.assessment}

} {aiMonitor.actions?.length > 0 && (
{aiMonitor.actions.map((action: any, i: number) => (
{action.type === 'close_trade' ? '🔒' : action.type === 'rebalance' ? '⚖️' : '👁'}
{action.underlying && {action.underlying}} {action.trade_id && #{action.trade_id}} {action.reason}
))}
)} {aiMonitor.rebalance_suggestion && (
⚖️ {aiMonitor.rebalance_suggestion}
)}
)}
) } export default function RiskDashboard() { const { data: dashboard, isLoading } = useRiskDashboard() const { data: corrData } = usePatternCorrelations() const [tlDays, setTlDays] = useState(90) const { data: tlData } = usePnlTimeline(tlDays) const [mode, setMode] = useState<'real' | 'sim'>('real') const { data: simRisk } = useSimPortfolioRisk() const simConflicts = (simRisk as any)?.alerts?.filter((a: any) => a.level === 'danger').length ?? 0 const d: any = dashboard ?? {} const pairs: any[] = corrData?.pairs ?? [] return (
{/* Header */}

Risk Dashboard

{mode === 'real' ? 'Concentration · Clusters · Correlations · Position Sizing — IBKR Portfolio' : 'Directional conflicts · Concentration · AI Recommendations — Logged trades'}

{/* Mode toggle */}
{mode === 'sim' ? : isLoading ? (
{[1, 2, 3, 4].map(i =>
)}
) : ( <> {/* KPI row */}
{d.open_trades ?? 0}
Open positions
= 60 ? 'text-emerald-400' : d.diversification_score >= 35 ? 'text-amber-400' : 'text-red-400' )}>{d.diversification_score ?? '—'}%
Diversification score
N effectif = {d.effective_n_positions ?? '—'}
{d.expected_drawdown_pct ?? '—'}%
Expected drawdown
{d.saturated_factors?.length ?? 0}
Saturated factors
{d.saturated_factors?.length > 0 && (
{d.saturated_factors.join(', ')}
)}
{/* Recommendation */} {/* Scenario concentration — same bet, different ticker */} {/* Alerts */} {(d.concentration_alerts ?? []).length > 0 && (
{d.concentration_alerts.map((alert: any, i: number) => (
{alert.message}
))}
)}
{/* Exposure by class */}
Exposure by asset class
{Object.keys(d.exposure_by_class ?? {}).length === 0 ? (
No open positions
) : (
{Object.entries(d.exposure_by_class ?? {}) .sort(([, a]: any, [, b]: any) => b.pct_of_portfolio - a.pct_of_portfolio) .map(([cls, info]: any) => ( ))}
)}
{/* Risk factors */}
Exposure by risk factor
{(d.risk_clusters ?? []).length === 0 ? (
No open positions
) : (
{(d.risk_clusters ?? []).map((c: any) => ( ))}
)}
{/* Equity curve */}
Cumulative P&L curve (mature trades)
{/* Correlation pairs */}
Pattern correlations (historical P&L)
{pairs.length === 0 ? (
Requires ≥3 mature trades per pattern to calculate correlations
) : (
{pairs.slice(0, 10).map((pair: any, i: number) => ( ))} {pairs.length > 10 && (
{pairs.length - 10} more pairs…
)}
)} {pairs.length > 0 && (
Correlation > 0.7 = concentrated risk — these patterns share the same underlying factor
)}
)}
) }