import { useState, useEffect, useRef, Fragment } from 'react' import { BookOpen, TrendingUp, TrendingDown, Activity, AlertTriangle, RefreshCw, Zap, CheckCircle, XCircle, Brain, Trash2, Search, X, ChevronDown, ChevronUp, Terminal, Lock, ShieldAlert, PieChart, Target } from 'lucide-react' import { TradeIdeasTab } from '../components/TradeIdeas' import clsx from 'clsx' import { useJournalSummary, useMacroHistory, useGeoHistory, useTradeMtm, useClosedTrades, useCloseTrade, useUpdateExitParams, useExitDefaults, useCycleHistory, useCycleStatus, useTriggerCycle, useTradePostmortem, useAnalyzePostmortem, useIvForTrade, useKellySizing, useSimPortfolioRisk, useSkippedTrades, api } from '../hooks/useApi' import { useQueryClient } from '@tanstack/react-query' import { format } from 'date-fns' import { fr } from 'date-fns/locale' const SCENARIO_META: Record = { 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 ScenarioBadge({ dominant, size = 'sm' }: { dominant: string; size?: 'sm' | 'lg' }) { const m = SCENARIO_META[dominant] ?? SCENARIO_META.incertain return ( {m.emoji} {m.label} ) } function PnlBadge({ pnl }: { pnl: number | null | undefined }) { if (pnl == null) return const pos = pnl >= 0 return ( {pos ? : } {pos ? '+' : ''}{pnl.toFixed(2)}% ) } function ScoreDelta({ entry, latest }: { entry: number | null; latest: number | null }) { if (entry == null || latest == null) return const delta = latest - entry return ( = 50 ? 'text-emerald-400' : latest >= 25 ? 'text-yellow-400' : 'text-slate-500')}> {latest} {delta !== 0 && ( 0 ? 'text-emerald-600' : 'text-red-600')}> {delta > 0 ? '+' : ''}{delta} )} ) } // ── Section 1 : Historique des régimes macro ────────────────────────────────── function IvRankCell({ underlying }: { underlying: string }) { const { data, isLoading } = useIvForTrade(underlying) if (isLoading) return const rank = data?.iv_rank const iv = data?.iv_current_pct if (rank == null) return const color = rank >= 80 ? 'text-red-400' : rank >= 50 ? 'text-amber-400' : rank >= 20 ? 'text-emerald-400' : 'text-blue-400' const signal = rank >= 80 ? '↓vol' : rank < 20 ? '↑vol' : '' return (
{rank}% {iv != null ? `IV ${iv}%` : ''} {signal && {signal}}
) } function KellyCell({ patternId, capital = 10000 }: { patternId: string; capital?: number }) { const { data, isLoading } = useKellySizing(patternId, capital) if (!patternId || isLoading) return if (!data || data.error) return const pct = data.suggested_capital_pct const eur = data.suggested_capital_eur const color = pct <= 0 ? 'text-slate-600' : pct < 5 ? 'text-amber-400' : 'text-emerald-400' const adjusted = data.cluster_adjustment_reason || data.reliability_adjustment return (
{pct?.toFixed(1)}% {eur != null ? `${eur}€` : ''} {adjusted && ⚠ ajusté}
) } // ── Close Trade Modal ───────────────────────────────────────────────────────── const CLOSE_REASONS = [ { value: 'target', label: '🎯 Objectif atteint' }, { value: 'stop_loss', label: '🛑 Stop-loss déclenché' }, { value: 'signal_reversal', label: '🔄 Signal IA retourné' }, { value: 'manual', label: '✍️ Fermeture manuelle' }, ] function CloseTradeModal({ trade, onClose }: { trade: any; onClose: () => void }) { const { mutate, isPending, isError } = useCloseTrade() const [closePrice, setClosePrice] = useState(String(trade.current_price ?? trade.entry_price ?? '')) const [reason, setReason] = useState(trade.alert_type === 'target_reached' ? 'target' : trade.alert_type === 'stop_loss' ? 'stop_loss' : 'manual') const [note, setNote] = useState('') const ep = parseFloat(closePrice) || 0 const isBearish = trade.direction === 'bearish' const pnlPreview = trade.entry_price && ep > 0 ? (() => { const raw = (ep - trade.entry_price) / trade.entry_price * 100 return round2(isBearish ? -raw : raw) })() : null function round2(n: number) { return Math.round(n * 100) / 100 } const handleSubmit = () => { mutate({ id: trade.id, body: { close_price: ep, close_reason: reason, close_note: note } }, { onSuccess: onClose }) } return (
Fermer le trade
{trade.underlying} · {trade.strategy} · entrée {trade.entry_price?.toFixed(2) ?? '—'}
{trade.alert_type && (
{trade.alert_type === 'target_reached' ? '🎯 Objectif atteint !' : '🛑 Stop-loss atteint'} {' — P&L actuel '} {trade.pnl_pct >= 0 ? '+' : ''}{trade.pnl_pct?.toFixed(2)}%
)}
setClosePrice(e.target.value)} className="w-full bg-dark-700 border border-slate-600/50 rounded px-3 py-1.5 text-sm text-white font-mono" placeholder="Prix actuel du sous-jacent" /> {pnlPreview !== null && (
= 0 ? 'text-emerald-400' : 'text-red-400')}> P&L réalisé : {pnlPreview >= 0 ? '+' : ''}{pnlPreview}%
)}
setNote(e.target.value)} className="w-full bg-dark-700 border border-slate-600/50 rounded px-3 py-1.5 text-sm text-white" placeholder="Contexte, raison…" maxLength={200} />
{isError &&
Erreur lors de la fermeture du trade.
}
) } // ── Closed Trades Section ───────────────────────────────────────────────────── function ClosedTradesSection({ days }: { days: number }) { const { data, isLoading, refetch, isFetching } = useClosedTrades(days) const trades: any[] = (data as any)?.trades ?? [] const stats: any = (data as any)?.stats ?? {} const REASON_META: Record = { target: { label: '🎯 Objectif', color: 'text-emerald-400' }, stop_loss: { label: '🛑 Stop-loss', color: 'text-red-400' }, signal_reversal: { label: '🔄 Signal IA', color: 'text-blue-400' }, manual: { label: '✍️ Manuel', color: 'text-slate-400' }, } return (
{/* Stats banner */} {trades.length > 0 && (
{[ { label: 'Trades fermés', value: stats.total ?? 0, sub: '', color: 'text-white' }, { label: 'Taux de succès', value: stats.win_rate != null ? `${stats.win_rate}%` : '—', sub: '', color: stats.win_rate >= 50 ? 'text-emerald-400' : 'text-red-400' }, { label: 'P&L moyen', value: stats.avg_pnl != null ? `${stats.avg_pnl >= 0 ? '+' : ''}${stats.avg_pnl?.toFixed(1)}%` : '—', sub: '', color: (stats.avg_pnl ?? 0) >= 0 ? 'text-emerald-400' : 'text-red-400' }, { label: 'P&L total', value: stats.total_pnl != null ? `${stats.total_pnl >= 0 ? '+' : ''}${stats.total_pnl?.toFixed(1)}%` : '—', sub: `meilleur ${stats.best?.toFixed(1) ?? '—'}%`, color: (stats.total_pnl ?? 0) >= 0 ? 'text-emerald-400' : 'text-red-400' }, ].map(s => (
{s.value}
{s.label}
{s.sub &&
{s.sub}
}
))}
)}
{trades.length} trades clôturés · {days} derniers jours
{isLoading ? (
) : trades.length === 0 ? (
Aucun trade clôturé dans les {days} derniers jours
) : (
{trades.map((t: any) => { const meta = REASON_META[t.close_reason] ?? REASON_META.manual const daysHeld = t.entry_date && t.closed_at ? Math.round((new Date(t.closed_at).getTime() - new Date(t.entry_date).getTime()) / 86400000) : null return ( ) })}
Pattern Stratégie Ticker Entrée Sortie Date entrée Date sortie Durée P&L réalisé Motif Note
{t.pattern_name || t.pattern_id} {t.strategy || '—'} {t.underlying} {t.entry_price != null ? t.entry_price.toFixed(2) : '—'} {t.close_price != null ? t.close_price.toFixed(2) : '—'} {t.entry_date ?? '—'} {t.closed_at ? t.closed_at.slice(0, 10) : '—'} {daysHeld != null ? `${daysHeld}j` : '—'} {t.pnl_realized != null ? ( = 0 ? 'text-emerald-400' : 'text-red-400')}> {t.pnl_realized >= 0 ? '+' : ''}{t.pnl_realized.toFixed(2)}% ) : } {meta.label} {t.close_note || '—'}
)}
) } // ── IBKR Ticket helpers ─────────────────────────────────────────────────────── function computeStrikeDollars(price: number, guidance: string, strategy: string): number { const g = guidance.toUpperCase() const match = g.match(/(\d+)\s*%\s*OTM/) if (!match) return price const pct = parseInt(match[1]) / 100 const isBearish = /put|bear/i.test(strategy) const raw = isBearish ? price * (1 - pct) : price * (1 + pct) const inc = price > 1000 ? 10 : price > 200 ? 5 : price > 50 ? 1 : 0.5 return Math.round(raw / inc) * inc } function estimateExpiryDate(horizonDays: number): Date { const d = new Date() d.setDate(d.getDate() + horizonDays) const dow = d.getDay() if (dow !== 5) d.setDate(d.getDate() + ((5 - dow + 7) % 7)) return d } function buildLegs(strategy: string, strike: number | null, price: number | null) { const s = strategy.toLowerCase() const atm = price ? (() => { const inc = price > 1000 ? 10 : price > 200 ? 5 : price > 50 ? 1 : 0.5 return Math.round(price / inc) * inc })() : null if (s.includes('bull call spread')) return [ { action: 'BUY', type: 'CALL', strike: atm }, { action: 'SELL', type: 'CALL', strike }, ] if (s.includes('bear put spread')) return [ { action: 'BUY', type: 'PUT', strike: atm }, { action: 'SELL', type: 'PUT', strike }, ] if (s.includes('straddle')) return [ { action: 'BUY', type: 'CALL', strike: atm ?? strike }, { action: 'BUY', type: 'PUT', strike: atm ?? strike }, ] if (s.includes('strangle')) return [ { action: 'BUY', type: 'CALL', strike: price ? Math.round(price * 1.05) : strike }, { action: 'BUY', type: 'PUT', strike: price ? Math.round(price * 0.95) : strike }, ] if (s.includes('call')) return [{ action: 'BUY', type: 'CALL', strike }] if (s.includes('put')) return [{ action: 'BUY', type: 'PUT', strike }] return [{ action: 'BUY', type: strategy.toUpperCase(), strike }] } function IBKRTicket({ strategy, underlying, strikeGuidance, expiryDays, entryPrice, maxLoss, targetGain }: { strategy: string underlying: string strikeGuidance?: string | null expiryDays?: number | null entryPrice?: number | null maxLoss?: number | null targetGain?: number | null }) { const strike = entryPrice && strikeGuidance ? computeStrikeDollars(entryPrice, strikeGuidance, strategy) : (entryPrice ?? null) const expiryDate = expiryDays ? estimateExpiryDate(expiryDays) : null const legs = buildLegs(strategy, strike, entryPrice ?? null) const fmtStrike = (s: number | null | undefined) => s == null ? '—' : s >= 100 ? `$${s.toFixed(0)}` : `$${s.toFixed(2)}` return (
Ticket IBKR {!entryPrice && Prix non disponible — vérifier dans IBKR}
Sous-jacent
{underlying}
Security Type: OPT
Stratégie
{strategy}
{entryPrice &&
Sous-jacent: ${entryPrice.toFixed(2)}
}
Expiration
{expiryDate ? format(expiryDate, 'd MMM yyyy', { locale: fr }) : expiryDays ? `~${expiryDays}j` : '—'}
{expiryDays ? `${expiryDays}j DTE` : ''}
{legs.map((leg, i) => (
{leg.action} 1 contrat {leg.type} Strike {fmtStrike(leg.strike)} {leg.strike === null && strikeGuidance && ( ({strikeGuidance}) )}
))}
Type d'ordre
LIMIT
Prix = prime dans IBKR
Budget max
{maxLoss ? `${Math.abs(maxLoss).toFixed(0)}€` : '~1 000€'}
Perte max estimée
Cible
{targetGain ? `+${targetGain.toFixed(0)}€` : '—'}
) } function MacroHistorySection({ days }: { days: number }) { const { data, isLoading, refetch, isFetching } = useMacroHistory(days) const history: any[] = (data as any)?.history ?? [] return (
{history.length} snapshots · {days} derniers jours
{isLoading ? (
{[1,2,3].map(i =>
)}
) : history.length === 0 ? (
Aucun snapshot — cliquer "Ré-analyser" dans Régime Macro pour commencer à logger
) : (
{history.map((entry: any, i: number) => { const scores: Record = entry.scores ?? {} const maxScore = Math.max(...Object.values(scores), 1) const ts = entry.timestamp?.slice(0, 16).replace('T', ' ') ?? '' const reasons: string[] = entry.reasons?.[entry.dominant] ?? [] const isTransition = i > 0 && history[i - 1].dominant !== entry.dominant return (
{isTransition && ( ↕ Transition )} {ts} UTC
{Object.entries(scores) .sort(([, a], [, b]) => (b as number) - (a as number)) .slice(0, 8) .map(([key, score]) => { const m = SCENARIO_META[key] ?? SCENARIO_META.incertain return (
{m.label}
{score}
) })}
{reasons.length > 0 && (
{reasons.slice(0, 4).map((r: string, ri: number) => ( {r} ))}
)}
) })}
)}
) } // ── Post-mortem panel ───────────────────────────────────────────────────────── function PostmortemPanel({ tradeId, onClose }: { tradeId: number; onClose: () => void }) { const { data, isLoading } = useTradePostmortem(tradeId) const { mutate: analyze, isPending: analyzing, data: analysisData } = useAnalyzePostmortem() const [showScoring, setShowScoring] = useState(true) const [showSuggestion, setShowSuggestion] = useState(false) if (isLoading) { return (
) } if (!data) return null const { trade, scoring_context: sc, suggestion_context: sg, score_history } = data as any const scOut = sc?.output ?? {} const sgOut = sg?.output ?? {} const scCtx = sc?.input_context ?? {} const buckets: any[] = scOut.buckets ?? [] const analysis: any = analysisData?.analysis ?? null return (
{/* Header */}
Post-mortem — {trade?.pattern_name} | {trade?.underlying} {trade?.strategy}
{/* Score history sparkline */} {score_history?.length > 0 && (
Historique :
{[...score_history].reverse().map((h: any, i: number) => (
= 60 ? '#22c55e' : (h.score ?? 0) >= 40 ? '#eab308' : '#64748b', }} /> {h.score ?? '?'}
))}
{score_history.length > 0 && ( {score_history[0]?.macro_dominant ?? ''} — géo {score_history[0]?.geo_score ?? '?'} )}
)} {/* Scoring context */}
{showScoring && (
{/* Regime / bias */}
{sc?.macro_dominant && ( Régime : )} {scCtx.asset_bias && ( Biais asset : {scCtx.asset_bias} )} {sc?.geo_score != null && ( Géo : {sc.geo_score}/100 )}
{/* Buckets */} {buckets.length > 0 && (
{buckets.map((b: any, i: number) => { const pct = b.max ? Math.round((b.score / b.max) * 100) : 0 const color = pct >= 66 ? '#22c55e' : pct >= 33 ? '#eab308' : '#ef4444' return (
{b.label ?? b.id} {b.score}/{b.max}
{b.comment && ( {b.comment} )}
) })}
)} {scOut.summary && (

{scOut.summary}

)}
)}
{/* Suggestion context */} {sg && (
{showSuggestion && (
{sgOut.macro_fit &&

{sgOut.macro_fit}

} {sgOut.description &&

{sgOut.description}

} {sg.macro_dominant && (
Créé sous : {sg.geo_score != null && Géo : {sg.geo_score}/100}
)}
)}
)} {/* GPT-4o analysis */}
{!analysis ? ( ) : (
Analyse GPT-4o
{[ { key: 'diagnostic', label: 'Diagnostic', color: 'text-slate-200' }, { key: 'what_worked', label: 'Ce qui a marché', color: 'text-emerald-400' }, { key: 'what_missed', label: 'Ce qui a manqué', color: 'text-red-400' }, { key: 'regime_alignment', label: 'Alignement régime', color: 'text-yellow-400' }, { key: 'contra_assessment', label: 'Contra-signals', color: 'text-orange-400' }, { key: 'lesson', label: 'Leçon', color: 'text-blue-300' }, { key: 'next_cycle', label: 'Prochain cycle', color: 'text-purple-400' }, ].map(({ key, label, color }) => analysis[key] ? (
{label} : {analysis[key]}
) : null )}
)}
) } // ── Section 2 : Mark-to-Market des trades ───────────────────────────────────── function TradeMtmSection({ days }: { days: number }) { const { data, isLoading, refetch, isFetching } = useTradeMtm(days) const [minScoreFilter, setMinScoreFilter] = useState(0) const [selectedTradeId, setSelectedTradeId] = useState(null) const [closingTrade, setClosingTrade] = useState(null) const allTrades: any[] = (data as any)?.trades ?? [] const trades = allTrades.filter((t: any) => (t.latest_score ?? t.score_at_entry ?? 0) >= minScoreFilter) const withPnl = trades.filter((t: any) => t.pnl_pct != null) const avgPnl = withPnl.length ? withPnl.reduce((s: number, t: any) => s + t.pnl_pct, 0) / withPnl.length : null return (
{trades.length}/{allTrades.length} trades · {withPnl.length} pricés {avgPnl != null && ( = 0 ? 'text-emerald-400' : 'text-red-400')}> · moy {avgPnl >= 0 ? '+' : ''}{avgPnl.toFixed(1)}% )}
{/* Score filter */}
Score ≥ setMinScoreFilter(parseInt(e.target.value))} className="w-24 accent-blue-500" /> {minScoreFilter}
{closingTrade && ( setClosingTrade(null)} /> )} {isLoading ? (
) : trades.length === 0 ? (
{allTrades.length === 0 ? 'Aucun trade logué — scorer des patterns pour commencer le suivi' : `Aucun trade avec score ≥ ${minScoreFilter}`}
) : (
{trades.map((t: any) => { const evNet = t.ev_net ?? null const tradeScore = t.trade_score ?? null const isExpanded = selectedTradeId === t.id return ( setSelectedTradeId(isExpanded ? null : t.id)} > {isExpanded && ( )} ) })}
Pattern Profil Stratégie Strike DTE Ticker Score Trade Score EV nette Gain prévu Date Prix entrée Prix actuel Cible/Stop Maturité IV Rank Kelly P&L
{t.pattern_name || t.pattern_id} {t.matched_profile ? ( {t.matched_profile} ) : } {t.direction === 'bearish' ? '🐻' : '🐂'} {t.strategy || '—'} {t.strike_guidance ?? '—'} {t.expiry_days_at_entry != null ? `${t.expiry_days_at_entry}j` : t.horizon_days != null ? `${t.horizon_days}j` : '—'}
{t.underlying} {t.price_warning && ( ⚠ {t.price_warning === 'no_price_data' ? 'NO DATA' : t.price_warning === 'no_entry_price' ? 'NO ENTRY' : 'NO LIVE'} )}
{tradeScore != null ? ( = 55 ? 'text-emerald-400' : tradeScore >= 45 ? 'text-yellow-400' : 'text-slate-500')}> {tradeScore.toFixed(1)} ) : } {evNet != null ? ( 0.05 ? 'text-emerald-400' : evNet >= -0.01 ? 'text-yellow-400' : 'text-slate-600')}> {evNet > 0 ? '+' : ''}{(evNet * 100).toFixed(0)}% ) : } {t.expected_move_pct != null ? ( {t.expected_move_pct.toFixed(0)}% ) : } {t.entry_date} {t.entry_price != null ? t.entry_price.toFixed(2) : '—'} {t.current_price != null ? t.current_price.toFixed(2) : '—'} {t.pnl_pct != null ? (
{t.alert_type === 'target_reached' && ( 🎯 )} {t.alert_type === 'stop_loss' && ( 🛑 )}
{/* progress bar: stop_loss (negative) to target (positive) */}
= 0 ? 'bg-emerald-500' : 'bg-red-500')} style={{ width: `${Math.min( 100, t.pnl_pct >= 0 ? (t.pnl_pct / (t.target_pct || 30)) * 100 : (Math.abs(t.pnl_pct) / Math.abs(t.stop_loss_pct || 50)) * 100 )}%` }} />
{t.stop_loss_pct ?? -50}% +{t.target_pct ?? 30}%
) : }
{t.maturity ? (
{t.maturity.emoji} {t.maturity.label} {t.days_held ?? 0}j / {t.horizon_days ?? 90}j ({t.maturity.ratio_pct}%)
) : ( {t.days_held != null ? `${t.days_held}j` : '—'} )}
e.stopPropagation()}> e.stopPropagation()}>
setSelectedTradeId(null)} />
)}
) } // ── Section 3 : Historique du score géopolitique ────────────────────────────── function GeoHistorySection({ days }: { days: number }) { const { data, isLoading, refetch, isFetching } = useGeoHistory(days) const history: any[] = (data as any)?.history ?? [] const maxScore = Math.max(...history.map((h: any) => h.geo_score), 1) return (
{history.length} alertes · {days} derniers jours
{isLoading ? (
{[1,2].map(i =>
)}
) : history.length === 0 ? (
Aucune alerte — scorer des patterns pour commencer le suivi
) : (
{history.map((entry: any) => { const score = entry.geo_score ?? 0 const level = score >= 70 ? 'extreme' : score >= 50 ? 'high' : score >= 30 ? 'medium' : 'low' const barColor = { low: '#22c55e', medium: '#eab308', high: '#f97316', extreme: '#ef4444' }[level] const ts = entry.timestamp?.slice(0, 16).replace('T', ' ') ?? '' const patterns: any[] = entry.top_patterns ?? [] return (
{score}
{level}
{ts}
{entry.news_count} news
{patterns.length > 0 && (
{patterns.slice(0, 5).map((p: any, i: number) => ( {p.score} {p.name || p.pattern_id} ))}
)}
) })}
)}
) } // ── Section 4 : Cycles d'intelligence ──────────────────────────────────────── function CyclesSection() { const { data: histData, isLoading, refetch, isFetching } = useCycleHistory(20) const { data: statusData } = useCycleStatus() const { mutate: triggerCycle, isPending: triggering } = useTriggerCycle() const runs: any[] = (histData as any)?.runs ?? [] const cs = statusData as any // Auto-refetch history list when the running cycle finishes const wasRunning = useRef(false) useEffect(() => { if (cs?.running) { wasRunning.current = true } else if (wasRunning.current) { wasRunning.current = false; refetch() } }, [cs?.running]) return (
{runs.length} cycles · {cs?.enabled ? `auto ${cs.interval_hours}h` : 'manuel uniquement'}
{isLoading ? (
{[1,2,3].map(i =>
)}
) : runs.length === 0 ? (
Aucun cycle — activer l'auto-cycle dans Configuration ou lancer manuellement
) : (
{runs.map((run: any) => { const commentary = run.commentary_parsed const ok = run.status === 'completed' const ts = run.started_at?.slice(0, 16).replace('T', ' ') ?? '' const duration = run.completed_at ? Math.round((new Date(run.completed_at).getTime() - new Date(run.started_at).getTime()) / 1000) : null return (
{ok ? : }
{ts} UTC {run.trigger === 'manual' ? 'Manuel' : `Auto ${cs?.interval_hours ?? ''}h`} {run.dominant_regime && } {duration != null && {duration}s}
💡 {run.patterns_suggested} suggérés ✚ {run.patterns_added} ajoutés 🎯 {run.patterns_scored} scorés {run.geo_score != null && 🌐 Géo {run.geo_score}/100}
{commentary && (
Commentaire IA

{commentary.commentary}

{commentary.key_risk && (
{commentary.key_risk}
)} {commentary.top_pattern && (
🎯 Pattern clé : {commentary.top_pattern}
)} {commentary.lessons_from_report ? (
Leçons du rapport du {commentary.lessons_from_report} intégrées dans ce cycle {commentary.lessons_headline && ( · {commentary.lessons_headline} )}
) : (
Aucun rapport de performance disponible — générer un rapport dans "Rapport IA"
)}
)} {!commentary && ok && (
Aucun commentaire IA généré pour ce cycle
)}
) })}
)}
) } // ── Page principale ──────────────────────────────────────────────────────────── const ASSET_CLASS_COLORS: Record = { energy: '#f97316', metals: '#eab308', agriculture: '#84cc16', equities: '#22c55e', indices: '#3b82f6', forex: '#8b5cf6', rates: '#06b6d4', unknown: '#64748b', } function SimPortfolioRiskPanel() { 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 (
Aucune position ouverte dans le portefeuille simulé
Les trades logués apparaîtront ici après le prochain 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 (
{/* Header */}
Portefeuille Simulé — Analyse de Risque {risk.open_count} positions ouvertes {dangers.length > 0 && ( {dangers.length} CONFLIT{dangers.length > 1 ? 'S' : ''} )}
{/* Asset class concentration bars */}
Répartition par classe d'actif
{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 exposure = risk.direction_exposure?.[ac] ?? {} return (
{ac} {data.tickers?.slice(0, 3).join(', ')}{data.tickers?.length > 3 ? '…' : ''}
{exposure.bullish > 0 && ( {exposure.bullish} )} {exposure.bearish > 0 && ( {exposure.bearish} )} {data.pct}% ({data.count})
) })}
{/* Alerts */} {alerts.length > 0 && (
{/* Conflicts */} {conflicts.map((c: any, i: number) => (
Conflit directionnel — {c.underlying}
{c.trades.map((t: any) => (
{t.direction === 'bullish' ? '▲' : '▼'} #{t.id} {t.strategy} {t.entry_date} {t.pattern_name && ( {t.pattern_name} )}
))}
))} {/* Concentration & overweight warnings */} {warnings.length > 0 && (
Alertes de concentration
{warnings.map((a: any, i: number) => (
{a.message}
))}
)}
)} {/* AI Monitor recommendations */} {aiMonitor && (
Recommandations IA — Moniteur de portefeuille
{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}
)}
)} {alerts.length === 0 && (
Portefeuille équilibré — aucun conflit ni sur-concentration détectés
)}
) } const TABS = [ { key: 'cycles', label: 'Cycles IA', icon: Zap }, { key: 'macro', label: 'Régimes Macro', icon: Activity }, { key: 'ideas', label: 'Idées de trade', icon: Target }, { key: 'mtm', label: 'Ouverts', icon: TrendingUp }, { key: 'closed', label: 'Fermés', icon: Lock }, { key: 'geo', label: 'Alertes Géo', icon: AlertTriangle }, { key: 'skipped', label: 'Non loggés', icon: XCircle }, ] as const function SkippedTradesSection({ days }: { days: number }) { const { data, isLoading } = useSkippedTrades(days) const trades: any[] = (data as any)?.trades ?? [] const ASSET_CLASS_COLORS: Record = { energy: 'bg-orange-900/30 text-orange-300 border-orange-700/30', metals: 'bg-yellow-900/30 text-yellow-300 border-yellow-700/30', agriculture: 'bg-green-900/30 text-green-300 border-green-700/30', indices: 'bg-blue-900/30 text-blue-300 border-blue-700/30', forex: 'bg-purple-900/30 text-purple-300 border-purple-700/30', rates: 'bg-slate-800/60 text-slate-300 border-slate-600/30', equities: 'bg-cyan-900/30 text-cyan-300 border-cyan-700/30', } if (isLoading) return
if (trades.length === 0) { return (
Aucune suggestion non loggée sur {days}j — tous les trades scorés passent au moins un profil de risque.
) } const byReason = trades.reduce((acc: Record, t: any) => { acc[t.skip_reason] = (acc[t.skip_reason] ?? 0) + 1 return acc }, {}) return (
Trades suggérés non loggés — {trades.length} sur {days}j (score ou gain insuffisant pour tout profil actif)
{Object.entries(byReason).map(([reason, count]) => (
{reason}: {count}
))}
{trades.map((t: any) => ( ))}
Pattern Ticker Stratégie Score Gain attendu Classe Raison du skip
{t.pattern_name || '—'} {t.underlying} {t.strategy || '—'} = 50 ? 'text-emerald-400' : t.score >= 25 ? 'text-yellow-400' : 'text-slate-600')}> {t.score ?? '—'} {t.expected_move_pct != null ? `${t.expected_move_pct.toFixed(0)}%` : '—'} {t.asset_class ? ( {t.asset_class} ) : } {t.skip_detail || t.skip_reason}
) } export default function JournalDeBord() { const [tab, setTab] = useState<'cycles' | 'macro' | 'ideas' | 'mtm' | 'closed' | 'geo' | 'skipped'>('cycles') const [days, setDays] = useState(15) const [confirmReset, setConfirmReset] = useState(false) const [resetting, setResetting] = useState(false) const [resetMsg, setResetMsg] = useState('') const { data: summary, refetch: refetchSummary } = useJournalSummary() const { data: riskData } = useSimPortfolioRisk() const riskAlertCount = (riskData as any)?.alerts?.filter((a: any) => a.level === 'danger').length ?? 0 const qc = useQueryClient() const s = summary as any const handleReset = async () => { if (!confirmReset) { setConfirmReset(true) return } setResetting(true) try { await api.delete('/journal/reset') setResetMsg('Journal réinitialisé') setConfirmReset(false) // Invalidate all journal queries await qc.invalidateQueries({ queryKey: ['journal-summary'] }) await qc.invalidateQueries({ queryKey: ['macro-history'] }) await qc.invalidateQueries({ queryKey: ['geo-history'] }) await qc.invalidateQueries({ queryKey: ['trade-mtm'] }) await qc.invalidateQueries({ queryKey: ['cycle-history'] }) setTimeout(() => setResetMsg(''), 3000) } catch { setResetMsg('Erreur lors du reset') setTimeout(() => setResetMsg(''), 3000) } finally { setResetting(false) } } return (
{/* Header */}

Journal de Bord

Historique des régimes macro · Mark-to-market des trades · Évolution du risque géopolitique

{/* Reset button */}
{resetMsg && ( {resetMsg} )} {confirmReset && !resetting && ( Confirmer ? )}
{/* Period selector */}
{[15, 30, 60, 90].map(d => ( ))}
{/* Summary cards */} {s && (
{s.macro_snapshots ?? 0}
snapshots macro
{s.current_dominant && (
)}
{s.regime_transitions?.length ?? 0}
transitions de régime
{s.regime_transitions?.length > 0 && (
↕ {s.regime_transitions[0].from} → {s.regime_transitions[0].to}
)}
= 70 ? 'text-red-400' : (s.avg_geo_score ?? 0) >= 40 ? 'text-orange-400' : 'text-emerald-400')}> {s.avg_geo_score != null ? s.avg_geo_score.toFixed(0) : '—'}
score géo moyen
{s.max_geo_score != null && (
max {s.max_geo_score}
)}
{s.trade_entries_logged ?? 0}
trades logués
{riskAlertCount > 0 && (
{riskAlertCount} conflit{riskAlertCount > 1 ? 's' : ''} directionnel{riskAlertCount > 1 ? 's' : ''} détecté{riskAlertCount > 1 ? 's' : ''} — voir Risk Dashboard → Simulé
)}
)} {/* Tabs */}
{TABS.map(({ key, label, icon: Icon }) => ( ))}
{/* Content */} {tab === 'cycles' && } {tab === 'macro' && } {tab === 'ideas' && } {tab === 'mtm' && } {tab === 'closed' && } {tab === 'geo' && } {tab === 'skipped' && }
) }