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 && (
)}
{/* 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 (
)
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
refetch()} className="text-slate-600 hover:text-slate-400">
{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 && (
{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 */}
setMode('real')}
className={clsx('px-3 py-1.5 rounded text-sm font-semibold transition-colors', {
'bg-blue-600 text-white': mode === 'real',
'text-slate-400 hover:text-slate-200': mode !== 'real',
})}>
Real Portfolio
setMode('sim')}
className={clsx('flex items-center gap-1.5 px-3 py-1.5 rounded text-sm font-semibold transition-colors', {
'bg-blue-600 text-white': mode === 'sim',
'text-slate-400 hover:text-slate-200': mode !== 'sim',
})}>
Simulated (logged)
{simConflicts > 0 && (
{simConflicts}
)}
{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) => (
))}
)}
{/* 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 */}
{(d.risk_clusters ?? []).length === 0 ? (
No open positions
) : (
{(d.risk_clusters ?? []).map((c: any) => (
))}
)}
{/* Equity curve */}
Cumulative P&L curve (mature trades)
setTlDays(Number(e.target.value))}
className="bg-dark-700 border border-slate-700 rounded px-2 py-0.5 text-xs text-slate-300">
30d
90d
180d
1 yr
{/* 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
)}
>
)}
)
}