Files
OpenFin/frontend/src/pages/RapportIA.tsx
OpenSquared 3ee39d5f08 feat: options technical agent — IV/skew/term structure validation per trade
- New options_technical_agent.py: rule engine (IVR, skew, term structure, flow)
  + GPT-4o narrative per trade; verdict OK/WARN/ALERT + fit_score
- options_trade_assessments table in DB for Journal badge persistence
- auto_cycle.py step 5.2: assess newly logged trades after log_trade_entries;
  results embedded in cycle report
- suggest_patterns_from_market_context: +iv_context param + explicit IV→strategy
  rules in prompt (IVR<30%→Long, 30-60%→Spread, >60%→no naked long, >80%→short)
- Pre-fetch iv_context at step 1.9 so suggestion step gets strategy rules
- reports.py: /api/reports/assessments/latest + /assessments/{run_id} endpoints
- RapportIA.tsx: "Validation Technique Options" section with per-trade IVBar,
  VerdictBadge, issues list, GPT-4o analysis, optimal strategy suggestion

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-20 09:36:35 +02:00

596 lines
30 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useState } from 'react'
import { useQuery } from '@tanstack/react-query'
import clsx from 'clsx'
import {
Brain, TrendingUp, TrendingDown, RefreshCw, ShieldAlert,
GitCompare, Layers, Zap, BookOpen, Clock, ChevronRight,
CheckCircle2, AlertTriangle, XCircle, BarChart2,
} from 'lucide-react'
const SCENARIO_META: Record<string, { label: string; color: string; emoji: string }> = {
goldilocks: { label: 'Goldilocks', color: '#22c55e', emoji: '🌟' },
desinflation: { label: 'Désinflation', color: '#06b6d4', emoji: '❄️' },
soft_landing: { label: 'Soft Landing', color: '#3b82f6', emoji: '🛬' },
reflation: { label: 'Reflation', color: '#f97316', emoji: '🔥' },
stagflation: { label: 'Stagflation', color: '#eab308', emoji: '⚡' },
inflation_shock: { label: 'Inflation Shock', color: '#ef4444', emoji: '💥' },
recession: { label: 'Récession', color: '#8b5cf6', emoji: '📉' },
crise_liquidite: { label: 'Crise Liquidité', color: '#ec4899', emoji: '🚨' },
incertain: { label: 'Incertain', color: '#64748b', emoji: '❓' },
}
function RegimeBadge({ dominant }: { dominant?: string }) {
if (!dominant) return null
const m = SCENARIO_META[dominant] ?? SCENARIO_META.incertain
return (
<span
className="inline-flex items-center gap-1 rounded px-2 py-0.5 text-xs font-semibold"
style={{ background: `${m.color}22`, color: m.color, border: `1px solid ${m.color}44` }}
>
{m.emoji} {m.label}
</span>
)
}
function Section({ icon, title, children }: { icon: React.ReactNode; title: string; children: React.ReactNode }) {
return (
<div className="card mb-4">
<div className="flex items-center gap-2 mb-3 pb-2 border-b border-slate-700/40">
<span className="text-slate-400">{icon}</span>
<span className="text-sm font-semibold text-slate-200">{title}</span>
</div>
{children}
</div>
)
}
const VERDICT_META = {
OK: { color: 'text-emerald-400', bg: 'bg-emerald-900/10 border-emerald-700/30', icon: CheckCircle2 },
WARN: { color: 'text-yellow-400', bg: 'bg-yellow-900/10 border-yellow-700/30', icon: AlertTriangle },
ALERT: { color: 'text-red-400', bg: 'bg-red-900/10 border-red-700/30', icon: XCircle },
}
function VerdictBadge({ verdict }: { verdict: 'OK' | 'WARN' | 'ALERT' }) {
const m = VERDICT_META[verdict] ?? VERDICT_META.WARN
const Icon = m.icon
return (
<span className={clsx('inline-flex items-center gap-1 rounded px-1.5 py-0.5 text-[10px] font-bold border', m.bg, m.color)}>
<Icon className="w-2.5 h-2.5" /> {verdict}
</span>
)
}
function IVBar({ ivRank }: { ivRank?: number | null }) {
if (ivRank == null) return <span className="text-slate-600 text-[10px]">IVR </span>
const color = ivRank >= 60 ? 'bg-red-500' : ivRank >= 30 ? 'bg-yellow-500' : 'bg-emerald-500'
return (
<div className="flex items-center gap-1.5">
<div className="w-16 h-1 bg-slate-700 rounded-full overflow-hidden">
<div className={clsx('h-full rounded-full', color)} style={{ width: `${Math.min(ivRank, 100)}%` }} />
</div>
<span className={clsx('text-[10px] font-mono', ivRank >= 60 ? 'text-red-400' : ivRank >= 30 ? 'text-yellow-400' : 'text-emerald-400')}>
IVR {ivRank.toFixed(0)}%
</span>
</div>
)
}
function DeltaRow({ emoji, label, count, items, colorClass }: {
emoji: string; label: string; count: number;
items: any[]; colorClass: string;
}) {
const [expanded, setExpanded] = useState(false)
return (
<div className="mb-2">
<button
onClick={() => setExpanded(e => !e)}
className="flex items-center gap-2 w-full text-left"
>
<span className="text-base">{emoji}</span>
<span className={clsx('text-sm font-bold', colorClass)}>{count}</span>
<span className="text-xs text-slate-400">{label}</span>
{items.length > 0 && (
<ChevronRight className={clsx('w-3 h-3 text-slate-600 ml-auto transition-transform', expanded && 'rotate-90')} />
)}
</button>
{expanded && items.length > 0 && (
<div className="mt-1.5 ml-6 space-y-1">
{items.map((item: any, i: number) => (
<div key={i} className="text-xs text-slate-400 flex items-start gap-2">
<span className="text-slate-600 shrink-0 mt-0.5"></span>
<div>
<span className="text-slate-200 font-medium">
{item.name ?? item.underlying ?? '?'}
{item.underlying && item.strategy ? ` · ${item.strategy}` : ''}
</span>
{item.description && (
<div className="text-slate-600 text-[10px] mt-0.5 line-clamp-2">{item.description}</div>
)}
{item.triggers?.length > 0 && (
<div className="text-slate-700 text-[10px]">Déclencheurs: {item.triggers.join(', ')}</div>
)}
{item.pnl_pct != null && (
<span className={clsx('font-mono text-[10px] ml-1', item.pnl_pct >= 0 ? 'text-emerald-400' : 'text-red-400')}>
{item.pnl_pct >= 0 ? '+' : ''}{item.pnl_pct.toFixed(1)}%
</span>
)}
{item.macro_fit && (
<div className="text-slate-600 text-[10px] italic">{item.macro_fit}</div>
)}
</div>
</div>
))}
</div>
)}
</div>
)
}
export default function RapportCycle() {
const [selectedRunId, setSelectedRunId] = useState<string | null>(null)
const { data: listData, isLoading: listLoading } = useQuery({
queryKey: ['cycle-reports-list'],
queryFn: () => fetch('/api/reports/cycle/list?limit=20').then(r => r.ok ? r.json() : { reports: [] }),
staleTime: 30_000,
})
const { data: latestReport, isLoading: latestLoading } = useQuery({
queryKey: ['cycle-report-latest'],
queryFn: () => fetch('/api/reports/cycle/latest').then(r => r.ok ? r.json() : { report: null }),
staleTime: 30_000,
})
const { data: selectedReport, isLoading: selectedLoading } = useQuery({
queryKey: ['cycle-report', selectedRunId],
queryFn: () => fetch(`/api/reports/cycle/${selectedRunId}`).then(r => r.ok ? r.json() : null),
enabled: !!selectedRunId,
staleTime: 60_000,
})
const reports: any[] = listData?.reports ?? []
const report: any = selectedRunId ? selectedReport : latestReport?.report
const isLoading = selectedRunId ? selectedLoading : latestLoading
return (
<div className="p-6 flex gap-4 h-full min-h-screen">
{/* Left panel — history list */}
<div className="w-56 shrink-0">
<div className="section-title flex items-center gap-1 mb-3">
<BookOpen className="w-3.5 h-3.5" /> Historique
</div>
{listLoading ? (
<div className="text-xs text-slate-600">Chargement</div>
) : reports.length === 0 ? (
<div className="text-xs text-slate-600">Aucun rapport. Le premier sera généré au prochain cycle.</div>
) : (
<div className="space-y-1">
{reports.map((r: any) => {
const dt = r.generated_at ? new Date(r.generated_at.endsWith('Z') ? r.generated_at : r.generated_at + 'Z') : null
const dateStr = dt
? dt.toLocaleDateString('fr-FR', { day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit' })
: '—'
const isActive = selectedRunId === r.run_id || (!selectedRunId && r === reports[0])
const m = SCENARIO_META[r.macro_dominant] ?? SCENARIO_META.incertain
return (
<button
key={r.run_id}
onClick={() => setSelectedRunId(r.run_id)}
className={clsx(
'w-full text-left px-2.5 py-2 rounded border text-xs transition-all',
isActive
? 'border-blue-600/60 bg-blue-900/20 text-slate-200'
: 'border-slate-700/30 bg-dark-800 text-slate-500 hover:border-slate-600/40 hover:text-slate-300'
)}
>
<div className="font-mono text-[10px] text-slate-600 mb-0.5">{dateStr}</div>
<div className="flex items-center gap-1.5 mb-1">
<span>{m.emoji}</span>
<span className="font-medium truncate" style={{ color: isActive ? m.color : undefined }}>{m.label}</span>
</div>
<div className="flex gap-2 text-[10px] text-slate-600">
<span className="text-blue-400">+{r.patterns_added ?? 0} pat.</span>
<span className="text-emerald-500">{r.trades_logged ?? 0} log.</span>
{(r.trades_closed ?? 0) > 0 && <span className="text-slate-400">{r.trades_closed} clos</span>}
</div>
</button>
)
})}
</div>
)}
</div>
{/* Right panel — report */}
<div className="flex-1 min-w-0">
{isLoading ? (
<div className="flex items-center gap-2 text-slate-500 text-sm mt-12 justify-center">
<RefreshCw className="w-4 h-4 animate-spin" /> Chargement
</div>
) : !report ? (
<div className="card text-center py-12">
<Brain className="w-10 h-10 text-slate-700 mx-auto mb-3" />
<div className="text-slate-500 text-sm mb-1">Aucun rapport de cycle disponible</div>
<div className="text-xs text-slate-600">Les rapports sont générés automatiquement à la fin de chaque cycle.</div>
</div>
) : (
<>
{/* Header */}
<div className="card mb-4">
<div className="flex items-start justify-between mb-3">
<div>
<div className="flex items-center gap-2 mb-1">
<RegimeBadge dominant={report.macro_dominant} />
<span className="text-xs text-slate-500 font-mono">
Géo {report.geo_score ?? '—'}/100
</span>
</div>
<div className="text-[10px] text-slate-600 font-mono">
Cycle du {report.generated_at
? new Date(report.generated_at.endsWith('Z') ? report.generated_at : report.generated_at + 'Z')
.toLocaleString('fr-FR')
: '—'}
{report.prev_cycle_at && (
<span className="ml-2 text-slate-700">
· vs cycle du {new Date(report.prev_cycle_at.endsWith('Z') ? report.prev_cycle_at : report.prev_cycle_at + 'Z')
.toLocaleDateString('fr-FR')}
</span>
)}
</div>
</div>
{/* Macro scores mini */}
{report.macro_scores && (
<div className="flex gap-1.5 flex-wrap justify-end max-w-[220px]">
{Object.entries(report.macro_scores as Record<string, number>)
.sort(([, a], [, b]) => b - a)
.slice(0, 4)
.map(([k, v]: [string, number]) => {
const m = SCENARIO_META[k] ?? SCENARIO_META.incertain
return (
<span key={k}
className="text-[9px] px-1.5 py-0.5 rounded font-mono"
style={{ background: `${m.color}18`, color: m.color }}>
{m.label} {v}%
</span>
)
})}
</div>
)}
</div>
{/* PnL + VaR summary row */}
<div className="grid grid-cols-2 gap-3 mt-2">
{report.pnl_summary && Object.keys(report.pnl_summary).length > 0 && (
<div className="bg-dark-700/50 rounded px-3 py-2">
<div className="text-[9px] text-slate-600 mb-1 uppercase tracking-wide">PnL au moment du cycle</div>
<div className="flex items-baseline gap-2">
<span className={clsx('text-lg font-bold font-mono',
(report.pnl_summary.total_pnl_pct ?? 0) >= 0 ? 'text-emerald-400' : 'text-red-400')}>
{report.pnl_summary.total_pnl_pct != null
? `${report.pnl_summary.total_pnl_pct >= 0 ? '+' : ''}${report.pnl_summary.total_pnl_pct.toFixed(2)}%`
: '—'}
</span>
{report.pnl_summary.total_pnl_eur != null && (
<span className="text-xs text-slate-500 font-mono">
{report.pnl_summary.total_pnl_eur >= 0 ? '+' : ''}
{report.pnl_summary.total_pnl_eur.toFixed(0)}
</span>
)}
</div>
<div className="text-[9px] text-slate-600 mt-0.5">
{report.pnl_summary.n_open ?? 0} ouvertes · {report.pnl_summary.n_closed ?? 0} fermées
</div>
</div>
)}
{report.var_summary && Object.keys(report.var_summary).length > 0 && (
<div className="bg-dark-700/50 rounded px-3 py-2">
<div className="text-[9px] text-slate-600 mb-1 uppercase tracking-wide">VaR 95% au moment du cycle</div>
<div className="grid grid-cols-3 gap-1">
{[
{ label: 'Hist.', val: report.var_summary.hist_var_1d_pct, color: 'text-blue-400' },
{ label: 'CVaR', val: report.var_summary.hist_cvar_pct, color: 'text-orange-400' },
{ label: 'MC', val: report.var_summary.mc_var_1d_pct, color: 'text-red-400' },
].map(({ label, val, color }) => (
<div key={label} className="text-center">
<div className="text-[8px] text-slate-700">{label}</div>
<div className={clsx('text-xs font-mono font-bold', color)}>
{val != null ? `${val >= 0 ? '+' : ''}${val.toFixed(2)}%` : '—'}
</div>
</div>
))}
</div>
</div>
)}
</div>
</div>
{/* Delta section */}
<Section icon={<GitCompare className="w-4 h-4" />} title="Changements du cycle">
<DeltaRow
emoji="⭐" label="patterns ajoutés"
count={report.patterns_added ?? 0}
items={report.patterns_added_list ?? []}
colorClass="text-blue-400"
/>
<DeltaRow
emoji="📥" label="trades loggés"
count={report.trades_logged ?? 0}
items={(report.trades_logged_list ?? []).map((t: any) => ({
underlying: t.underlying, strategy: t.strategy,
pnl_pct: t.pnl_pct, description: t.pattern_name
}))}
colorClass="text-emerald-400"
/>
<DeltaRow
emoji="✅" label="trades fermés depuis le cycle précédent"
count={report.trades_closed ?? 0}
items={(report.trades_closed_list ?? []).map((t: any) => ({
underlying: t.underlying, strategy: t.strategy,
pnl_pct: t.pnl_pct, description: t.pattern_name
}))}
colorClass={report.trades_closed > 0 ? 'text-slate-300' : 'text-slate-600'}
/>
{/* Top scored */}
{(report.top_scored ?? []).length > 0 && (
<div className="mt-3 pt-3 border-t border-slate-700/30">
<div className="text-xs text-slate-500 mb-2">Top patterns scorés ce cycle</div>
<div className="space-y-1.5">
{(report.top_scored as any[]).map((p: any, i: number) => (
<div key={i} className="flex items-start gap-2 text-xs">
<span className="text-slate-700 font-mono w-4 shrink-0">{i + 1}</span>
<div className="flex-1 min-w-0">
<span className="text-slate-300">{p.name ?? '—'}</span>
{p.key_catalyst && (
<div className="text-[10px] text-slate-600 truncate">{p.key_catalyst}</div>
)}
</div>
<span className={clsx('font-mono font-bold shrink-0',
(p.score ?? 0) >= 60 ? 'text-emerald-400' : (p.score ?? 0) >= 40 ? 'text-yellow-400' : 'text-slate-500')}>
{p.score ?? '—'}
</span>
</div>
))}
</div>
</div>
)}
</Section>
{/* Context narrative */}
{report.context_narrative?.narrative && (
<Section icon={<Brain className="w-4 h-4" />} title="Raisonnement IA — ce cycle">
<p className="text-sm text-slate-300 leading-relaxed mb-4">
{report.context_narrative.narrative}
</p>
<div className="grid grid-cols-2 gap-4">
{(report.context_narrative.context_driven ?? []).length > 0 && (
<div>
<div className="text-[10px] text-emerald-400 font-semibold uppercase tracking-wide mb-2">
📡 Du contexte transmis
</div>
<ul className="space-y-1">
{(report.context_narrative.context_driven as string[]).map((s, i) => (
<li key={i} className="text-xs text-slate-400 flex items-start gap-1.5">
<span className="text-emerald-600 shrink-0 mt-0.5"></span> {s}
</li>
))}
</ul>
</div>
)}
{(report.context_narrative.general_knowledge ?? []).length > 0 && (
<div>
<div className="text-[10px] text-blue-400 font-semibold uppercase tracking-wide mb-2">
🧠 Connaissance générale
</div>
<ul className="space-y-1">
{(report.context_narrative.general_knowledge as string[]).map((s, i) => (
<li key={i} className="text-xs text-slate-400 flex items-start gap-1.5">
<span className="text-blue-600 shrink-0 mt-0.5"></span> {s}
</li>
))}
</ul>
</div>
)}
</div>
{(report.context_narrative.key_signals ?? []).length > 0 && (
<div className="mt-3 pt-3 border-t border-slate-700/30">
<div className="text-[10px] text-slate-500 mb-2">Signaux déterminants</div>
<div className="flex flex-wrap gap-1.5">
{(report.context_narrative.key_signals as string[]).map((s, i) => (
<span key={i} className="text-[10px] bg-slate-800 text-slate-300 border border-slate-700/50 px-2 py-0.5 rounded">
<Zap className="w-2.5 h-2.5 inline mr-1 text-yellow-500" />{s}
</span>
))}
</div>
</div>
)}
{/* Context log */}
{report.context_narrative.context_log && (
<div className="mt-3 pt-3 border-t border-slate-700/30">
<div className="text-[10px] text-slate-500 mb-2">Log contexte état transmis à l'IA</div>
<div className="grid grid-cols-2 gap-x-4 gap-y-1 text-[10px]">
{Object.entries(report.context_narrative.context_log.key_gauges ?? {}).map(([k, v]: [string, any]) => (
<div key={k} className="flex items-center gap-1.5">
<span className="text-slate-600 w-20">{k}</span>
<span className="font-mono text-slate-400">{v}</span>
</div>
))}
</div>
{(report.context_narrative.context_log.dominant_news ?? []).length > 0 && (
<div className="mt-2">
<div className="text-[9px] text-slate-700 mb-1">News transmises (top impact)</div>
{(report.context_narrative.context_log.dominant_news as string[]).map((n, i) => (
<div key={i} className="text-[9px] text-slate-600 truncate">· {n}</div>
))}
</div>
)}
</div>
)}
</Section>
)}
{/* Cycle commentary */}
{report.commentary?.commentary && (
<Section icon={<Layers className="w-4 h-4" />} title="Analyse macro du cycle">
<p className="text-sm text-slate-300 leading-relaxed mb-3">{report.commentary.commentary}</p>
{report.commentary.key_risk && (
<div className="flex items-start gap-2 text-xs text-orange-400 bg-orange-900/10 border border-orange-800/20 rounded px-3 py-2">
<ShieldAlert className="w-3.5 h-3.5 shrink-0 mt-0.5" />
<span>{report.commentary.key_risk}</span>
</div>
)}
{report.commentary.top_pattern && (
<div className="mt-2 text-xs text-slate-500">
Pattern le plus pertinent : <span className="text-blue-400 font-medium">{report.commentary.top_pattern}</span>
</div>
)}
</Section>
)}
{/* Options Technical Agent */}
{report.options_technical && (report.options_technical.assessments?.length > 0 || report.options_technical.global_assessment) && (
<Section icon={<BarChart2 className="w-4 h-4" />} title="Validation Technique Options">
{/* Global score + summary */}
<div className="flex items-start gap-3 mb-4">
{report.options_technical.global_score != null && (
<div className="shrink-0 text-center">
<div className={clsx('text-2xl font-bold font-mono',
report.options_technical.global_score >= 70 ? 'text-emerald-400'
: report.options_technical.global_score >= 45 ? 'text-yellow-400'
: 'text-red-400'
)}>
{report.options_technical.global_score}
</div>
<div className="text-[9px] text-slate-600">Score vol</div>
</div>
)}
<div className="flex-1 min-w-0">
{report.options_technical.global_assessment && (
<p className="text-xs text-slate-300 leading-relaxed">
{report.options_technical.global_assessment}
</p>
)}
<div className="flex gap-3 mt-2 text-[10px]">
{report.options_technical.n_ok > 0 && (
<span className="text-emerald-400 flex items-center gap-0.5">
<CheckCircle2 className="w-3 h-3" /> {report.options_technical.n_ok} OK
</span>
)}
{report.options_technical.n_warn > 0 && (
<span className="text-yellow-400 flex items-center gap-0.5">
<AlertTriangle className="w-3 h-3" /> {report.options_technical.n_warn} WARN
</span>
)}
{report.options_technical.n_alert > 0 && (
<span className="text-red-400 flex items-center gap-0.5">
<XCircle className="w-3 h-3" /> {report.options_technical.n_alert} ALERT
</span>
)}
</div>
</div>
</div>
{/* Per-trade assessments */}
<div className="space-y-3">
{(report.options_technical.assessments as any[]).map((a: any, i: number) => (
<div key={i} className={clsx(
'rounded border px-3 py-2.5',
VERDICT_META[a.verdict as 'OK' | 'WARN' | 'ALERT']?.bg ?? 'border-slate-700/30 bg-dark-700/40'
)}>
{/* Trade header */}
<div className="flex items-center gap-2 mb-2">
<VerdictBadge verdict={a.verdict} />
<span className="text-xs font-semibold text-slate-200">{a.ticker}</span>
<span className="text-[10px] text-slate-500">{a.strategy}</span>
<div className="ml-auto">
<IVBar ivRank={a.iv_rank} />
</div>
</div>
{/* IV data row */}
<div className="flex gap-3 text-[10px] text-slate-500 mb-2">
{a.skew_pct != null && (
<span>Skew: <span className={clsx('font-mono', a.skew_pct > 5 ? 'text-orange-400' : 'text-slate-400')}>
{a.skew_pct > 0 ? '+' : ''}{a.skew_pct.toFixed(1)}pts
</span></span>
)}
{a.term_structure && (
<span>Structure: <span className={clsx('font-mono',
a.term_structure === 'backwardation' ? 'text-red-400'
: a.term_structure === 'contango' ? 'text-emerald-400' : 'text-slate-400'
)}>{a.term_structure}</span></span>
)}
{a.flow_bias && (
<span>Flow: <span className="font-mono text-slate-400">{a.flow_bias}</span></span>
)}
<span className="ml-auto text-slate-600">Score: <span className="font-mono text-slate-400">{a.fit_score}</span></span>
</div>
{/* Issues + positives */}
{((a.issues ?? []).length > 0 || (a.positives ?? []).length > 0) && (
<div className="space-y-0.5 mb-2">
{(a.issues as string[]).map((issue: string, j: number) => (
<div key={j} className="flex items-start gap-1.5 text-[10px] text-orange-300">
<span className="shrink-0 mt-0.5">⚠</span> {issue}
</div>
))}
{(a.positives as string[]).map((pos: string, j: number) => (
<div key={j} className="flex items-start gap-1.5 text-[10px] text-emerald-400">
<span className="shrink-0 mt-0.5">✓</span> {pos}
</div>
))}
</div>
)}
{/* GPT-4o analysis */}
{a.analysis && (
<p className="text-[11px] text-slate-400 leading-relaxed mb-1">{a.analysis}</p>
)}
{/* Optimal strategy + when to enter */}
<div className="flex items-start gap-3 mt-1.5 pt-1.5 border-t border-slate-700/20 text-[10px]">
{a.optimal_strategy && a.optimal_strategy !== a.strategy && (
<span className="text-slate-500">
Stratégie optimale: <span className="text-blue-400 font-medium">{a.optimal_strategy}</span>
</span>
)}
{a.when_to_enter && (
<span className="text-slate-600 italic">{a.when_to_enter}</span>
)}
</div>
</div>
))}
</div>
</Section>
)}
{/* Risk / portfolio monitor */}
{report.portfolio_monitor && (
<Section icon={<ShieldAlert className="w-4 h-4" />} title="Risque & Alertes portefeuille">
<p className="text-sm text-slate-400 mb-3">{report.portfolio_monitor.assessment}</p>
{(report.portfolio_monitor.actions ?? []).length > 0 && (
<div className="space-y-2">
{(report.portfolio_monitor.actions as any[]).map((a: any, i: number) => (
<div key={i} className={clsx(
'flex items-start gap-2 text-xs px-2.5 py-2 rounded border',
a.priority === 'high'
? 'border-red-700/30 bg-red-900/10 text-red-300'
: 'border-yellow-700/30 bg-yellow-900/10 text-yellow-300'
)}>
<span className="shrink-0 font-mono font-bold">{a.priority === 'high' ? '' : '👁'}</span>
<span>{a.underlying && <strong>{a.underlying} </strong>}{a.reason}</span>
</div>
))}
</div>
)}
{report.portfolio_monitor.rebalance_suggestion && (
<div className="mt-2 text-[11px] text-slate-500 italic">
💡 {report.portfolio_monitor.rebalance_suggestion}
</div>
)}
</Section>
)}
</>
)}
</div>
</div>
)
}