517 lines
24 KiB
TypeScript
517 lines
24 KiB
TypeScript
import { useState } from 'react'
|
||
import { useRiskDashboard, usePatternCorrelations, usePnlTimeline, useRiskExposure, useSimPortfolioRisk } from '../hooks/useApi'
|
||
import { ASSET_CLASS_COLORS } from '../constants/assetColors'
|
||
import clsx from 'clsx'
|
||
import { ShieldAlert, TrendingUp, GitBranch, AlertTriangle, CheckCircle, Activity, Brain, RefreshCw, PieChart } 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 (
|
||
<div>
|
||
<div className="flex justify-between text-xs mb-1">
|
||
<span className="text-slate-400 capitalize">{label}</span>
|
||
<span className={clsx('font-mono font-bold', textColor)}>{pct.toFixed(1)}%</span>
|
||
</div>
|
||
<div className="h-2 bg-dark-700 rounded-full overflow-hidden">
|
||
<div className={clsx('h-full rounded-full transition-all', color)} style={{ width: `${Math.min(pct, 100)}%` }} />
|
||
</div>
|
||
{pct > threshold && (
|
||
<div className="text-[10px] text-red-400 mt-0.5">⚠ Saturated (>{threshold}%)</div>
|
||
)}
|
||
</div>
|
||
)
|
||
}
|
||
|
||
// ── Equity curve SVG ─────────────────────────────────────────────────────────
|
||
function EquityCurve({ timeline }: { timeline: any[] }) {
|
||
if (!timeline || timeline.length < 2) {
|
||
return (
|
||
<div className="h-24 flex items-center justify-center text-slate-600 text-xs">
|
||
No P&L data yet (requires mature trades)
|
||
</div>
|
||
)
|
||
}
|
||
|
||
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 (
|
||
<div>
|
||
<svg viewBox={`0 0 ${W} ${H}`} className="w-full h-20">
|
||
<defs>
|
||
<linearGradient id="curve-grad" x1="0" y1="0" x2="0" y2="1">
|
||
<stop offset="0%" stopColor={lineColor} stopOpacity="0.3" />
|
||
<stop offset="100%" stopColor={lineColor} stopOpacity="0" />
|
||
</linearGradient>
|
||
</defs>
|
||
{/* Zero line */}
|
||
{min < 0 && max > 0 && (
|
||
<line
|
||
x1={pad} x2={W - pad}
|
||
y1={H - pad - ((0 - min) / range) * (H - pad * 2)}
|
||
y2={H - pad - ((0 - min) / range) * (H - pad * 2)}
|
||
stroke="#475569" strokeWidth="0.5" strokeDasharray="3,3"
|
||
/>
|
||
)}
|
||
<polyline points={points} fill="none" stroke={lineColor} strokeWidth="1.5" />
|
||
</svg>
|
||
<div className="flex justify-between text-[10px] text-slate-600 mt-0.5">
|
||
<span>{timeline[0]?.entry_date?.slice(0, 7)}</span>
|
||
<span className={clsx('font-mono font-bold', finalVal >= 0 ? 'text-emerald-400' : 'text-red-400')}>
|
||
{finalVal >= 0 ? '+' : ''}{finalVal.toFixed(0)}€
|
||
</span>
|
||
<span>{timeline[timeline.length - 1]?.entry_date?.slice(0, 7)}</span>
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
// ── 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 (
|
||
<div className={clsx('flex items-center gap-3 px-3 py-2 rounded border text-xs', bg)}>
|
||
<div className="flex-1 min-w-0">
|
||
<span className="text-slate-300 truncate">{pair.name_a}</span>
|
||
<span className="text-slate-600 mx-1">↔</span>
|
||
<span className="text-slate-300 truncate">{pair.name_b}</span>
|
||
</div>
|
||
<div className={clsx('font-mono font-bold shrink-0', color)}>
|
||
{c > 0 ? '+' : ''}{c.toFixed(2)}
|
||
</div>
|
||
<div className="text-slate-600 text-[10px] shrink-0">{pair.interpretation}</div>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
// ── Recommendation card ──────────────────────────────────────────────────────
|
||
function RecommendationCard({ rec }: { rec: any }) {
|
||
if (!rec) return null
|
||
const isOk = rec.level === 'ok'
|
||
const isDanger = rec.level === 'danger'
|
||
return (
|
||
<div className={clsx('card', {
|
||
'border-red-700/40 bg-red-900/10': isDanger,
|
||
'border-amber-700/40 bg-amber-900/10': rec.level === 'warning',
|
||
'border-emerald-700/40 bg-emerald-900/10': isOk,
|
||
})}>
|
||
<div className={clsx('flex items-center gap-2 mb-2 text-sm font-semibold', {
|
||
'text-red-400': isDanger,
|
||
'text-amber-400': rec.level === 'warning',
|
||
'text-emerald-400': isOk,
|
||
})}>
|
||
{isOk ? <CheckCircle className="w-4 h-4" /> : <AlertTriangle className="w-4 h-4" />}
|
||
Risk Committee Recommendation
|
||
</div>
|
||
<div className="space-y-1.5">
|
||
{(rec.messages ?? []).map((msg: string, i: number) => (
|
||
<p key={i} className="text-xs text-slate-300 leading-relaxed">{msg}</p>
|
||
))}
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
// ── Main page ────────────────────────────────────────────────────────────────
|
||
|
||
function SimRiskPanel() {
|
||
const { data, isLoading, refetch } = useSimPortfolioRisk()
|
||
const risk = data as any
|
||
|
||
if (isLoading) return (
|
||
<div className="space-y-3">
|
||
{[1, 2, 3].map(i => <div key={i} className="card animate-pulse h-20 bg-dark-700" />)}
|
||
</div>
|
||
)
|
||
|
||
if (!risk || risk.open_count === 0) return (
|
||
<div className="card text-center py-10 text-slate-500">
|
||
<PieChart className="w-8 h-8 mx-auto mb-2 opacity-20" />
|
||
<div className="text-sm">No open positions in the simulated portfolio</div>
|
||
<div className="text-xs mt-1">Logged trades will appear here after the next AI cycle</div>
|
||
</div>
|
||
)
|
||
|
||
const concentration: Record<string, any> = 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 (
|
||
<div className="space-y-4">
|
||
{/* KPIs */}
|
||
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3">
|
||
<div className="card text-center">
|
||
<div className="text-2xl font-bold text-white font-mono">{risk.open_count}</div>
|
||
<div className="text-xs text-slate-500 mt-1">Simulated positions</div>
|
||
</div>
|
||
<div className="card text-center">
|
||
<div className={clsx('text-2xl font-bold font-mono', dangers.length > 0 ? 'text-red-400' : 'text-emerald-400')}>
|
||
{dangers.length}
|
||
</div>
|
||
<div className="text-xs text-slate-500 mt-1">Directional conflicts</div>
|
||
</div>
|
||
<div className="card text-center">
|
||
<div className={clsx('text-2xl font-bold font-mono', warnings.length > 0 ? 'text-amber-400' : 'text-emerald-400')}>
|
||
{warnings.length}
|
||
</div>
|
||
<div className="text-xs text-slate-500 mt-1">Concentration alerts</div>
|
||
</div>
|
||
<div className="card text-center">
|
||
<div className="text-2xl font-bold font-mono text-slate-300">
|
||
{Object.keys(concentration).filter(ac => ac !== 'unknown').length}
|
||
</div>
|
||
<div className="text-xs text-slate-500 mt-1">Asset classes</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Concentration + Conflicts side by side */}
|
||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
||
{/* Asset class bars */}
|
||
<div className="card">
|
||
<div className="text-sm font-semibold text-white mb-3 flex items-center justify-between">
|
||
<span className="flex items-center gap-2">
|
||
<GitBranch className="w-4 h-4 text-blue-400" /> Allocation by asset class
|
||
</span>
|
||
<button onClick={() => refetch()} className="text-slate-600 hover:text-slate-400">
|
||
<RefreshCw className="w-3.5 h-3.5" />
|
||
</button>
|
||
</div>
|
||
<div className="space-y-2.5">
|
||
{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 (
|
||
<div key={ac}>
|
||
<div className="flex items-center justify-between mb-1">
|
||
<div className="flex items-center gap-2">
|
||
<span className="text-xs font-semibold w-20 capitalize" style={{ color }}>{ac}</span>
|
||
<span className="text-[10px] text-slate-500 max-w-[120px] truncate">
|
||
{data.tickers?.slice(0, 3).join(', ')}{data.tickers?.length > 3 ? '…' : ''}
|
||
</span>
|
||
</div>
|
||
<div className="flex items-center gap-2 text-[10px]">
|
||
{exp.bullish > 0 && <span className="text-emerald-400">▲{exp.bullish}</span>}
|
||
{exp.bearish > 0 && <span className="text-red-400">▼{exp.bearish}</span>}
|
||
<span className={clsx('font-mono font-bold', isOver ? 'text-red-400' : 'text-slate-300')}>
|
||
{data.pct}%
|
||
</span>
|
||
<span className="text-slate-600">({data.count})</span>
|
||
</div>
|
||
</div>
|
||
<div className="h-2 bg-dark-700 rounded-full overflow-hidden">
|
||
<div className="h-full rounded-full"
|
||
style={{ width: `${data.pct}%`, background: isOver ? '#ef4444cc' : `${color}cc` }} />
|
||
</div>
|
||
</div>
|
||
)
|
||
})}
|
||
</div>
|
||
</div>
|
||
|
||
{/* Conflicts & alerts */}
|
||
<div className="space-y-3">
|
||
{conflicts.length === 0 && warnings.length === 0 ? (
|
||
<div className="card border-emerald-700/30 bg-emerald-900/10 flex items-center gap-3 py-4">
|
||
<CheckCircle className="w-5 h-5 text-emerald-400 shrink-0" />
|
||
<div className="text-xs text-emerald-300">Balanced portfolio — no conflicts or over-concentration detected</div>
|
||
</div>
|
||
) : null}
|
||
|
||
{conflicts.map((c: any, i: number) => (
|
||
<div key={i} className="card border-red-700/40 bg-red-900/10">
|
||
<div className="text-xs font-semibold text-red-300 mb-2 flex items-center gap-1">
|
||
<ShieldAlert className="w-3.5 h-3.5" /> Directional conflict — {c.underlying}
|
||
</div>
|
||
<div className="space-y-1">
|
||
{c.trades.map((t: any) => (
|
||
<div key={t.id} className="flex items-center gap-2 text-[11px]">
|
||
<span className={clsx('font-bold font-mono w-6',
|
||
t.direction === 'bullish' ? 'text-emerald-400' : 'text-red-400')}>
|
||
{t.direction === 'bullish' ? '▲' : '▼'}#{t.id}
|
||
</span>
|
||
<span className="text-slate-400">{t.strategy}</span>
|
||
<span className="text-slate-600">{t.entry_date}</span>
|
||
<span className="text-slate-600 truncate max-w-[140px]">{t.pattern_name}</span>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
))}
|
||
|
||
{warnings.length > 0 && (
|
||
<div className="card border-amber-700/30 bg-amber-900/10">
|
||
<div className="text-xs font-semibold text-amber-300 mb-2 flex items-center gap-1">
|
||
<AlertTriangle className="w-3.5 h-3.5" /> Concentration alerts
|
||
</div>
|
||
<div className="space-y-1">
|
||
{warnings.map((a: any, i: number) => (
|
||
<div key={i} className="text-[11px] text-amber-200/80 flex items-start gap-1.5">
|
||
<span className="text-amber-500 shrink-0">•</span>{a.message}
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
|
||
{/* AI recommendations */}
|
||
{aiMonitor && (
|
||
<div className="card border-blue-700/30 bg-blue-900/10">
|
||
<div className="flex items-center justify-between mb-2">
|
||
<div className="text-xs font-semibold text-blue-300 flex items-center gap-1.5">
|
||
<Brain className="w-3.5 h-3.5" /> AI Recommendations — Simulated portfolio monitor
|
||
</div>
|
||
{aiTs && <span className="text-[10px] text-slate-600">{aiTs.slice(0, 16).replace('T', ' ')}</span>}
|
||
</div>
|
||
{aiMonitor.assessment && <p className="text-xs text-slate-300 mb-2">{aiMonitor.assessment}</p>}
|
||
{aiMonitor.actions?.length > 0 && (
|
||
<div className="space-y-1 mb-2">
|
||
{aiMonitor.actions.map((action: any, i: number) => (
|
||
<div key={i} className={clsx(
|
||
'flex items-start gap-2 text-[11px] rounded px-2 py-1',
|
||
action.priority === 'high'
|
||
? 'bg-red-900/20 border border-red-700/20 text-red-300'
|
||
: 'bg-slate-800/40 border border-slate-700/20 text-slate-300'
|
||
)}>
|
||
<span className="shrink-0">
|
||
{action.type === 'close_trade' ? '🔒' : action.type === 'rebalance' ? '⚖️' : '👁'}
|
||
</span>
|
||
<div>
|
||
{action.underlying && <span className="font-mono font-bold mr-1">{action.underlying}</span>}
|
||
{action.trade_id && <span className="text-slate-500 mr-1">#{action.trade_id}</span>}
|
||
{action.reason}
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
)}
|
||
{aiMonitor.rebalance_suggestion && (
|
||
<div className="text-[11px] text-blue-300/70 border-t border-blue-700/20 pt-2">
|
||
⚖️ {aiMonitor.rebalance_suggestion}
|
||
</div>
|
||
)}
|
||
</div>
|
||
)}
|
||
</div>
|
||
)
|
||
}
|
||
|
||
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 (
|
||
<div className="p-6 space-y-5">
|
||
{/* Header */}
|
||
<div className="flex items-center justify-between">
|
||
<div>
|
||
<h1 className="text-xl font-bold text-white flex items-center gap-2">
|
||
<ShieldAlert className="w-5 h-5 text-red-400" /> Risk Dashboard
|
||
</h1>
|
||
<p className="text-xs text-slate-500 mt-0.5">
|
||
{mode === 'real'
|
||
? 'Concentration · Clusters · Correlations · Position Sizing — IBKR Portfolio'
|
||
: 'Directional conflicts · Concentration · AI Recommendations — Logged trades'}
|
||
</p>
|
||
</div>
|
||
{/* Mode toggle */}
|
||
<div className="flex gap-1 bg-dark-700 p-1 rounded">
|
||
<button onClick={() => 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
|
||
</button>
|
||
<button onClick={() => 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 && (
|
||
<span className="text-[10px] font-bold bg-red-500 text-white rounded-full w-4 h-4 flex items-center justify-center">
|
||
{simConflicts}
|
||
</span>
|
||
)}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
{mode === 'sim' ? <SimRiskPanel /> : isLoading ? (
|
||
<div className="grid grid-cols-4 gap-3">
|
||
{[1, 2, 3, 4].map(i => <div key={i} className="card h-20 animate-pulse bg-dark-700" />)}
|
||
</div>
|
||
) : (
|
||
<>
|
||
{/* KPI row */}
|
||
<div className="grid grid-cols-2 gap-3 sm:grid-cols-4">
|
||
<div className="card text-center">
|
||
<div className="text-2xl font-bold text-white font-mono">{d.open_trades ?? 0}</div>
|
||
<div className="text-xs text-slate-500 mt-1">Open positions</div>
|
||
</div>
|
||
<div className="card text-center">
|
||
<div className={clsx('text-2xl font-bold font-mono',
|
||
d.diversification_score >= 60 ? 'text-emerald-400'
|
||
: d.diversification_score >= 35 ? 'text-amber-400' : 'text-red-400'
|
||
)}>{d.diversification_score ?? '—'}%</div>
|
||
<div className="text-xs text-slate-500 mt-1">Diversification score</div>
|
||
<div className="text-[10px] text-slate-600">N effectif = {d.effective_n_positions ?? '—'}</div>
|
||
</div>
|
||
<div className="card text-center">
|
||
<div className={clsx('text-2xl font-bold font-mono',
|
||
(d.expected_drawdown_pct ?? 0) < 15 ? 'text-emerald-400'
|
||
: (d.expected_drawdown_pct ?? 0) < 25 ? 'text-amber-400' : 'text-red-400'
|
||
)}>{d.expected_drawdown_pct ?? '—'}%</div>
|
||
<div className="text-xs text-slate-500 mt-1">Expected drawdown</div>
|
||
</div>
|
||
<div className="card text-center">
|
||
<div className={clsx('text-2xl font-bold font-mono',
|
||
!d.saturated_factors?.length ? 'text-emerald-400' : 'text-red-400'
|
||
)}>
|
||
{d.saturated_factors?.length ?? 0}
|
||
</div>
|
||
<div className="text-xs text-slate-500 mt-1">Saturated factors</div>
|
||
{d.saturated_factors?.length > 0 && (
|
||
<div className="text-[10px] text-red-400">{d.saturated_factors.join(', ')}</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
|
||
{/* Recommendation */}
|
||
<RecommendationCard rec={d.recommendation} />
|
||
|
||
{/* Alerts */}
|
||
{(d.concentration_alerts ?? []).length > 0 && (
|
||
<div className="space-y-2">
|
||
{d.concentration_alerts.map((alert: any, i: number) => (
|
||
<div key={i} className={clsx('flex items-start gap-2 text-xs px-3 py-2 rounded border', {
|
||
'bg-red-900/20 border-red-700/30 text-red-300': alert.level === 'high',
|
||
'bg-amber-900/20 border-amber-700/30 text-amber-300': alert.level === 'warning',
|
||
})}>
|
||
<AlertTriangle className="w-3 h-3 mt-0.5 shrink-0" />
|
||
{alert.message}
|
||
</div>
|
||
))}
|
||
</div>
|
||
)}
|
||
|
||
<div className="grid grid-cols-1 gap-5 lg:grid-cols-2">
|
||
{/* Exposure by class */}
|
||
<div className="card">
|
||
<div className="text-sm font-semibold text-white mb-3 flex items-center gap-2">
|
||
<GitBranch className="w-4 h-4 text-blue-400" /> Exposure by asset class
|
||
</div>
|
||
{Object.keys(d.exposure_by_class ?? {}).length === 0 ? (
|
||
<div className="text-xs text-slate-600 text-center py-4">No open positions</div>
|
||
) : (
|
||
<div className="space-y-3">
|
||
{Object.entries(d.exposure_by_class ?? {})
|
||
.sort(([, a]: any, [, b]: any) => b.pct_of_portfolio - a.pct_of_portfolio)
|
||
.map(([cls, info]: any) => (
|
||
<ConcentrationGauge key={cls} label={cls} pct={info.pct_of_portfolio} threshold={40} />
|
||
))}
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
{/* Risk factors */}
|
||
<div className="card">
|
||
<div className="text-sm font-semibold text-white mb-3 flex items-center gap-2">
|
||
<Activity className="w-4 h-4 text-orange-400" /> Exposure by risk factor
|
||
</div>
|
||
{(d.risk_clusters ?? []).length === 0 ? (
|
||
<div className="text-xs text-slate-600 text-center py-4">No open positions</div>
|
||
) : (
|
||
<div className="space-y-3">
|
||
{(d.risk_clusters ?? []).map((c: any) => (
|
||
<ConcentrationGauge key={c.factor} label={c.factor} pct={c.pct_of_portfolio} threshold={50} />
|
||
))}
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
|
||
{/* Equity curve */}
|
||
<div className="card">
|
||
<div className="flex items-center justify-between mb-3">
|
||
<div className="text-sm font-semibold text-white flex items-center gap-2">
|
||
<TrendingUp className="w-4 h-4 text-blue-400" /> Cumulative P&L curve (mature trades)
|
||
</div>
|
||
<select value={tlDays} onChange={e => setTlDays(Number(e.target.value))}
|
||
className="bg-dark-700 border border-slate-700 rounded px-2 py-0.5 text-xs text-slate-300">
|
||
<option value={30}>30d</option>
|
||
<option value={90}>90d</option>
|
||
<option value={180}>180d</option>
|
||
<option value={365}>1 yr</option>
|
||
</select>
|
||
</div>
|
||
<EquityCurve timeline={tlData?.timeline ?? []} />
|
||
</div>
|
||
|
||
{/* Correlation pairs */}
|
||
<div className="card">
|
||
<div className="text-sm font-semibold text-white mb-3">
|
||
Pattern correlations (historical P&L)
|
||
</div>
|
||
{pairs.length === 0 ? (
|
||
<div className="text-xs text-slate-600 text-center py-4">
|
||
Requires ≥3 mature trades per pattern to calculate correlations
|
||
</div>
|
||
) : (
|
||
<div className="space-y-1.5">
|
||
{pairs.slice(0, 10).map((pair: any, i: number) => (
|
||
<CorrelationPairRow key={i} pair={pair} />
|
||
))}
|
||
{pairs.length > 10 && (
|
||
<div className="text-xs text-slate-600 text-center pt-1">{pairs.length - 10} more pairs…</div>
|
||
)}
|
||
</div>
|
||
)}
|
||
{pairs.length > 0 && (
|
||
<div className="text-[10px] text-slate-600 mt-2">
|
||
Correlation > 0.7 = concentrated risk — these patterns share the same underlying factor
|
||
</div>
|
||
)}
|
||
</div>
|
||
</>
|
||
)}
|
||
</div>
|
||
)
|
||
}
|