Files
OpenFin/frontend/src/pages/JournalDeBord.tsx
OpenSquared f2b020ee4b refactor: move trade ideas to Journal tab, tabify Config, trim Dashboard
- Extract TradeCard/TradeRow/IBKRTicket + TradeIdeasTab self-contained
  component to frontend/src/components/TradeIdeas.tsx
- Dashboard: remove trade ideas section, MtM section, all related hooks/
  state/computations; replace Signaux Géo mini-card with Trades du cycle;
  add News géo link on Risque Géopolitique card
- JournalDeBord: add Idées de trade tab (left of Ouverts) using TradeIdeasTab
- Config: reorganize into 5 tabs — IA & Analyse, Auto-Cycle & Logging,
  Sources, Journal & Sortie, Profils de risque; expose min_score_threshold
  as a prominent slider in the Auto-Cycle tab

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

1709 lines
82 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, 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<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 ScenarioBadge({ dominant, size = 'sm' }: { dominant: string; size?: 'sm' | 'lg' }) {
const m = SCENARIO_META[dominant] ?? SCENARIO_META.incertain
return (
<span
className={clsx('inline-flex items-center gap-1 rounded font-semibold',
size === 'lg' ? 'px-2.5 py-1 text-sm' : 'px-1.5 py-0.5 text-[11px]')}
style={{ background: `${m.color}22`, color: m.color, border: `1px solid ${m.color}44` }}
>
{m.emoji} {m.label}
</span>
)
}
function PnlBadge({ pnl }: { pnl: number | null | undefined }) {
if (pnl == null) return <span className="text-slate-600 text-xs"></span>
const pos = pnl >= 0
return (
<span className={clsx('inline-flex items-center gap-0.5 text-xs font-bold font-mono',
pos ? 'text-emerald-400' : 'text-red-400')}>
{pos ? <TrendingUp className="w-3 h-3" /> : <TrendingDown className="w-3 h-3" />}
{pos ? '+' : ''}{pnl.toFixed(2)}%
</span>
)
}
function ScoreDelta({ entry, latest }: { entry: number | null; latest: number | null }) {
if (entry == null || latest == null) return <span className="text-slate-600 text-[10px]"></span>
const delta = latest - entry
return (
<span className="flex flex-col items-end gap-0.5">
<span className={clsx('font-bold font-mono text-xs',
latest >= 50 ? 'text-emerald-400' : latest >= 25 ? 'text-yellow-400' : 'text-slate-500')}>
{latest}
</span>
{delta !== 0 && (
<span className={clsx('text-[10px] font-mono', delta > 0 ? 'text-emerald-600' : 'text-red-600')}>
{delta > 0 ? '+' : ''}{delta}
</span>
)}
</span>
)
}
// ── Section 1 : Historique des régimes macro ──────────────────────────────────
function IvRankCell({ underlying }: { underlying: string }) {
const { data, isLoading } = useIvForTrade(underlying)
if (isLoading) return <span className="text-slate-700 text-[10px]"></span>
const rank = data?.iv_rank
const iv = data?.iv_current_pct
if (rank == null) return <span className="text-slate-700 text-[10px]"></span>
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 (
<div className="flex flex-col items-end gap-0.5">
<span className={clsx('text-[10px] font-bold font-mono', color)}>
{rank}%
</span>
<span className="text-[8px] text-slate-600">{iv != null ? `IV ${iv}%` : ''}</span>
{signal && <span className={clsx('text-[8px] font-semibold', color)}>{signal}</span>}
</div>
)
}
function KellyCell({ patternId, capital = 10000 }: { patternId: string; capital?: number }) {
const { data, isLoading } = useKellySizing(patternId, capital)
if (!patternId || isLoading) return <span className="text-slate-700 text-[10px]"></span>
if (!data || data.error) return <span className="text-slate-700 text-[10px]"></span>
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 (
<div className="flex flex-col items-end gap-0.5">
<span className={clsx('text-[10px] font-bold font-mono', color)}>{pct?.toFixed(1)}%</span>
<span className="text-[8px] text-slate-600">{eur != null ? `${eur}` : ''}</span>
{adjusted && <span className="text-[8px] text-orange-400" title={adjusted}> ajusté</span>}
</div>
)
}
// ── 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 (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm">
<div className="bg-dark-800 border border-slate-700/60 rounded-xl w-full max-w-md p-6 space-y-5 shadow-2xl">
<div className="flex items-center justify-between">
<div>
<div className="text-sm font-bold text-white flex items-center gap-2">
<Lock className="w-4 h-4 text-amber-400" /> Fermer le trade
</div>
<div className="text-xs text-slate-500 mt-0.5">
{trade.underlying} · {trade.strategy} · entrée {trade.entry_price?.toFixed(2) ?? '—'}
</div>
</div>
<button onClick={onClose} className="text-slate-600 hover:text-slate-400"><X className="w-4 h-4" /></button>
</div>
{trade.alert_type && (
<div className={clsx('text-xs rounded px-3 py-2 font-semibold border',
trade.alert_type === 'target_reached'
? 'bg-emerald-900/30 border-emerald-700/40 text-emerald-300'
: 'bg-red-900/30 border-red-700/40 text-red-300')}>
{trade.alert_type === 'target_reached' ? '🎯 Objectif atteint !' : '🛑 Stop-loss atteint'}
{' — P&L actuel '}
<span className="font-mono">{trade.pnl_pct >= 0 ? '+' : ''}{trade.pnl_pct?.toFixed(2)}%</span>
</div>
)}
<div className="space-y-3">
<div>
<label className="block text-xs text-slate-500 mb-1">Prix de clôture</label>
<input
type="number" step="0.01"
value={closePrice}
onChange={e => 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 && (
<div className={clsx('text-xs mt-1 font-mono font-bold',
pnlPreview >= 0 ? 'text-emerald-400' : 'text-red-400')}>
P&L réalisé : {pnlPreview >= 0 ? '+' : ''}{pnlPreview}%
</div>
)}
</div>
<div>
<label className="block text-xs text-slate-500 mb-1">Motif de fermeture</label>
<select
value={reason}
onChange={e => setReason(e.target.value)}
className="w-full bg-dark-700 border border-slate-600/50 rounded px-3 py-1.5 text-sm text-white"
>
{CLOSE_REASONS.map(r => (
<option key={r.value} value={r.value}>{r.label}</option>
))}
</select>
</div>
<div>
<label className="block text-xs text-slate-500 mb-1">Note (optionnel)</label>
<input
type="text"
value={note}
onChange={e => 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}
/>
</div>
</div>
{isError && <div className="text-xs text-red-400">Erreur lors de la fermeture du trade.</div>}
<div className="flex gap-3 pt-1">
<button onClick={onClose}
className="flex-1 px-4 py-2 rounded border border-slate-700/50 text-slate-400 hover:text-slate-200 text-sm">
Annuler
</button>
<button
onClick={handleSubmit}
disabled={isPending || ep === 0}
className="flex-1 px-4 py-2 rounded bg-amber-600 hover:bg-amber-500 disabled:opacity-50 text-white text-sm font-semibold flex items-center justify-center gap-2"
>
<Lock className="w-3.5 h-3.5" />
{isPending ? 'Fermeture…' : 'Confirmer la fermeture'}
</button>
</div>
</div>
</div>
)
}
// ── 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<string, { label: string; color: string }> = {
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 (
<div className="space-y-4">
{/* Stats banner */}
{trades.length > 0 && (
<div className="grid grid-cols-4 gap-3">
{[
{ 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 => (
<div key={s.label} className="card text-center py-3">
<div className={clsx('text-lg font-bold font-mono', s.color)}>{s.value}</div>
<div className="text-[11px] text-slate-600">{s.label}</div>
{s.sub && <div className="text-[10px] text-slate-700 mt-0.5">{s.sub}</div>}
</div>
))}
</div>
)}
<div className="flex items-center justify-between">
<div className="text-xs font-semibold text-slate-500 uppercase tracking-wide">
{trades.length} trades clôturés · {days} derniers jours
</div>
<button onClick={() => refetch()} disabled={isFetching}
className="text-slate-600 hover:text-slate-400 disabled:opacity-40">
<RefreshCw className={clsx('w-3.5 h-3.5', isFetching && 'animate-spin')} />
</button>
</div>
{isLoading ? (
<div className="card h-32 animate-pulse bg-dark-700" />
) : trades.length === 0 ? (
<div className="card text-center py-12 text-slate-600 text-sm">
<Lock className="w-8 h-8 mx-auto mb-2 opacity-20" />
Aucun trade clôturé dans les {days} derniers jours
</div>
) : (
<div className="overflow-x-auto rounded-lg border border-slate-700/40">
<table className="w-full text-xs">
<thead>
<tr className="text-slate-600 border-b border-slate-700/40">
<th className="text-left px-3 py-2 font-medium">Pattern</th>
<th className="text-left px-3 py-2 font-medium">Stratégie</th>
<th className="text-left px-3 py-2 font-medium">Ticker</th>
<th className="text-right px-3 py-2 font-medium">Entrée</th>
<th className="text-right px-3 py-2 font-medium">Sortie</th>
<th className="text-right px-3 py-2 font-medium">Date entrée</th>
<th className="text-right px-3 py-2 font-medium">Date sortie</th>
<th className="text-right px-3 py-2 font-medium">Durée</th>
<th className="text-right px-3 py-2 font-medium">P&L réalisé</th>
<th className="text-left px-3 py-2 font-medium">Motif</th>
<th className="text-left px-3 py-2 font-medium">Note</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-800/60">
{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 (
<tr key={t.id} className="hover:bg-dark-700/30 transition-colors">
<td className="px-3 py-2 text-slate-300 max-w-[100px] truncate">{t.pattern_name || t.pattern_id}</td>
<td className="px-3 py-2">
<span className={clsx('badge text-[10px]',
t.strategy?.toLowerCase().includes('put') || t.strategy?.toLowerCase().includes('bear')
? 'badge-red' : 'badge-green')}>
{t.strategy || '—'}
</span>
</td>
<td className="px-3 py-2 font-mono text-slate-300">{t.underlying}</td>
<td className="px-3 py-2 text-right font-mono text-slate-500 text-[11px]">
{t.entry_price != null ? t.entry_price.toFixed(2) : '—'}
</td>
<td className="px-3 py-2 text-right font-mono text-slate-400 text-[11px]">
{t.close_price != null ? t.close_price.toFixed(2) : '—'}
</td>
<td className="px-3 py-2 text-right text-slate-600 text-[11px]">{t.entry_date ?? '—'}</td>
<td className="px-3 py-2 text-right text-slate-600 text-[11px]">
{t.closed_at ? t.closed_at.slice(0, 10) : '—'}
</td>
<td className="px-3 py-2 text-right text-slate-600 text-[11px] font-mono">
{daysHeld != null ? `${daysHeld}j` : '—'}
</td>
<td className="px-3 py-2 text-right">
{t.pnl_realized != null ? (
<span className={clsx('font-bold font-mono text-xs',
t.pnl_realized >= 0 ? 'text-emerald-400' : 'text-red-400')}>
{t.pnl_realized >= 0 ? '+' : ''}{t.pnl_realized.toFixed(2)}%
</span>
) : <span className="text-slate-600"></span>}
</td>
<td className="px-3 py-2">
<span className={clsx('text-[10px] font-medium', meta.color)}>{meta.label}</span>
</td>
<td className="px-3 py-2 text-slate-600 text-[10px] max-w-[120px] truncate" title={t.close_note}>
{t.close_note || '—'}
</td>
</tr>
)
})}
</tbody>
</table>
</div>
)}
</div>
)
}
// ── 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 (
<div className="bg-blue-950/20 border border-blue-700/30 rounded p-3 space-y-2.5">
<div className="text-[10px] font-bold text-blue-300 uppercase tracking-wide flex items-center gap-1.5">
<Terminal className="w-3 h-3" /> Ticket IBKR
{!entryPrice && <span className="ml-auto text-slate-600 font-normal normal-case">Prix non disponible vérifier dans IBKR</span>}
</div>
<div className="grid grid-cols-3 gap-3 text-[10px]">
<div>
<div className="text-slate-600 mb-0.5">Sous-jacent</div>
<div className="text-white font-mono font-bold text-sm">{underlying}</div>
<div className="text-slate-600 text-[9px]">Security Type: OPT</div>
</div>
<div>
<div className="text-slate-600 mb-0.5">Stratégie</div>
<div className="text-slate-200 font-semibold">{strategy}</div>
{entryPrice && <div className="text-slate-600 text-[9px]">Sous-jacent: ${entryPrice.toFixed(2)}</div>}
</div>
<div>
<div className="text-slate-600 mb-0.5">Expiration</div>
<div className="text-slate-200 font-mono">
{expiryDate ? format(expiryDate, 'd MMM yyyy', { locale: fr }) : expiryDays ? `~${expiryDays}j` : '—'}
</div>
<div className="text-slate-600 text-[9px]">{expiryDays ? `${expiryDays}j DTE` : ''}</div>
</div>
</div>
<div className="space-y-1">
{legs.map((leg, i) => (
<div key={i} className="flex items-center gap-2 bg-slate-900/70 rounded px-2.5 py-1.5 font-mono text-[10px]">
<span className={clsx('w-8 font-bold', leg.action === 'BUY' ? 'text-emerald-400' : 'text-red-400')}>{leg.action}</span>
<span className="text-slate-400">1 contrat</span>
<span className={clsx('font-bold w-10 text-center', leg.type === 'CALL' ? 'text-blue-300' : leg.type === 'PUT' ? 'text-orange-300' : 'text-slate-300')}>{leg.type}</span>
<span className="text-slate-600">Strike</span>
<span className="text-yellow-300 font-bold">{fmtStrike(leg.strike)}</span>
{leg.strike === null && strikeGuidance && (
<span className="text-slate-500">({strikeGuidance})</span>
)}
</div>
))}
</div>
<div className="grid grid-cols-3 gap-3 text-[10px]">
<div>
<div className="text-slate-600 mb-0.5">Type d'ordre</div>
<div className="text-slate-300 font-semibold">LIMIT</div>
<div className="text-slate-600 text-[9px]">Prix = prime dans IBKR</div>
</div>
<div>
<div className="text-slate-600 mb-0.5">Budget max</div>
<div className="text-slate-300 font-mono">{maxLoss ? `${Math.abs(maxLoss).toFixed(0)}€` : '~1 000'}</div>
<div className="text-slate-600 text-[9px]">Perte max estimée</div>
</div>
<div>
<div className="text-slate-600 mb-0.5">Cible</div>
<div className="text-emerald-400 font-mono">{targetGain ? `+${targetGain.toFixed(0)}€` : ''}</div>
</div>
</div>
</div>
)
}
function MacroHistorySection({ days }: { days: number }) {
const { data, isLoading, refetch, isFetching } = useMacroHistory(days)
const history: any[] = (data as any)?.history ?? []
return (
<div className="space-y-3">
<div className="flex items-center justify-between">
<div className="text-xs font-semibold text-slate-500 uppercase tracking-wide">
{history.length} snapshots · {days} derniers jours
</div>
<button onClick={() => refetch()} disabled={isFetching}
className="text-slate-600 hover:text-slate-400 disabled:opacity-40">
<RefreshCw className={clsx('w-3.5 h-3.5', isFetching && 'animate-spin')} />
</button>
</div>
{isLoading ? (
<div className="space-y-2">{[1,2,3].map(i => <div key={i} className="card h-14 animate-pulse bg-dark-700" />)}</div>
) : history.length === 0 ? (
<div className="card text-center py-10 text-slate-600 text-sm">
<Activity className="w-8 h-8 mx-auto mb-2 opacity-20" />
Aucun snapshot — cliquer "Ré-analyser" dans Régime Macro pour commencer à logger
</div>
) : (
<div className="space-y-2">
{history.map((entry: any, i: number) => {
const scores: Record<string, number> = 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 (
<div key={entry.id} className={clsx('card', isTransition && 'border-amber-700/30')}>
<div className="flex items-center gap-3 mb-2">
{isTransition && (
<span className="text-[10px] bg-amber-900/30 text-amber-400 border border-amber-700/30 rounded px-1.5 py-0.5 shrink-0">
↕ Transition
</span>
)}
<ScenarioBadge dominant={entry.dominant} size="lg" />
<span className="text-xs text-slate-600 ml-auto shrink-0">{ts} UTC</span>
</div>
<div className="grid grid-cols-4 gap-x-3 gap-y-1 mb-2">
{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 (
<div key={key} className="flex items-center gap-1">
<div className="w-16 text-[10px] text-slate-600 truncate">{m.label}</div>
<div className="flex-1 h-1.5 bg-dark-700 rounded-full overflow-hidden">
<div className="h-full rounded-full transition-all"
style={{ width: `${((score as number) / maxScore) * 100}%`, background: m.color }} />
</div>
<span className="text-[10px] font-mono text-slate-500 w-6 text-right">{score}</span>
</div>
)
})}
</div>
{reasons.length > 0 && (
<div className="flex flex-wrap gap-1">
{reasons.slice(0, 4).map((r: string, ri: number) => (
<span key={ri} className="text-[10px] text-slate-500 bg-dark-700 px-1.5 py-0.5 rounded">{r}</span>
))}
</div>
)}
</div>
)
})}
</div>
)}
</div>
)
}
// ── 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 (
<div className="card mt-3 p-4 border border-blue-700/30 animate-pulse">
<div className="h-4 bg-dark-700 rounded w-48 mb-3" />
<div className="h-3 bg-dark-700 rounded w-full mb-2" />
<div className="h-3 bg-dark-700 rounded w-3/4" />
</div>
)
}
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 (
<div className="mt-3 rounded-lg border border-blue-700/30 bg-dark-900/80 p-4 space-y-4 text-xs">
{/* Header */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<Brain className="w-4 h-4 text-blue-400" />
<span className="font-semibold text-blue-300">Post-mortem — {trade?.pattern_name}</span>
<span className="text-slate-500">|</span>
<span className="font-mono text-slate-400">{trade?.underlying} {trade?.strategy}</span>
</div>
<button onClick={onClose} className="text-slate-600 hover:text-slate-400">
<X className="w-4 h-4" />
</button>
</div>
{/* Score history sparkline */}
{score_history?.length > 0 && (
<div className="flex items-center gap-3">
<span className="text-slate-600 shrink-0">Historique :</span>
<div className="flex items-center gap-1.5 flex-wrap">
{[...score_history].reverse().map((h: any, i: number) => (
<div key={i} className="flex flex-col items-center gap-0.5">
<div
className="w-6 rounded-sm"
style={{
height: `${Math.max(4, (h.score ?? 0) / 2)}px`,
background: (h.score ?? 0) >= 60 ? '#22c55e' : (h.score ?? 0) >= 40 ? '#eab308' : '#64748b',
}}
/>
<span className="text-[9px] font-mono text-slate-600">{h.score ?? '?'}</span>
</div>
))}
</div>
{score_history.length > 0 && (
<span className="text-slate-600 ml-auto shrink-0">
{score_history[0]?.macro_dominant ?? ''} — géo {score_history[0]?.geo_score ?? '?'}
</span>
)}
</div>
)}
{/* Scoring context */}
<div>
<button
className="flex items-center gap-1.5 text-slate-400 hover:text-slate-200 font-semibold mb-2"
onClick={() => setShowScoring(v => !v)}
>
{showScoring ? <ChevronUp className="w-3.5 h-3.5" /> : <ChevronDown className="w-3.5 h-3.5" />}
Pourquoi noté {scOut.score ?? '?'}/100
{scOut.key_catalyst && <span className="text-slate-600 font-normal ml-1">· {scOut.key_catalyst}</span>}
</button>
{showScoring && (
<div className="pl-5 space-y-2">
{/* Regime / bias */}
<div className="flex flex-wrap gap-3 text-[11px] text-slate-500">
{sc?.macro_dominant && (
<span>Régime : <ScenarioBadge dominant={sc.macro_dominant} /></span>
)}
{scCtx.asset_bias && (
<span>Biais asset : <span className="text-slate-300">{scCtx.asset_bias}</span></span>
)}
{sc?.geo_score != null && (
<span>Géo : <span className="font-mono text-slate-300">{sc.geo_score}/100</span></span>
)}
</div>
{/* Buckets */}
{buckets.length > 0 && (
<div className="grid grid-cols-2 gap-1.5">
{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 (
<div key={i} className="flex flex-col gap-0.5 bg-dark-800 rounded p-2">
<div className="flex items-center justify-between">
<span className="text-slate-400 font-medium truncate mr-2">{b.label ?? b.id}</span>
<span className="font-mono shrink-0" style={{ color }}>{b.score}/{b.max}</span>
</div>
<div className="h-1 bg-dark-700 rounded-full overflow-hidden">
<div className="h-full rounded-full" style={{ width: `${pct}%`, background: color }} />
</div>
{b.comment && (
<span className="text-[10px] text-slate-600 line-clamp-2">{b.comment}</span>
)}
</div>
)
})}
</div>
)}
{scOut.summary && (
<p className="text-slate-500 italic">{scOut.summary}</p>
)}
</div>
)}
</div>
{/* Suggestion context */}
{sg && (
<div>
<button
className="flex items-center gap-1.5 text-slate-400 hover:text-slate-200 font-semibold mb-2"
onClick={() => setShowSuggestion(v => !v)}
>
{showSuggestion ? <ChevronUp className="w-3.5 h-3.5" /> : <ChevronDown className="w-3.5 h-3.5" />}
Pourquoi ce pattern a été créé
</button>
{showSuggestion && (
<div className="pl-5 space-y-1.5 text-[11px] text-slate-400">
{sgOut.macro_fit && <p className="italic">{sgOut.macro_fit}</p>}
{sgOut.description && <p className="text-slate-500">{sgOut.description}</p>}
{sg.macro_dominant && (
<div className="flex gap-3 mt-1">
<span>Créé sous : <ScenarioBadge dominant={sg.macro_dominant} /></span>
{sg.geo_score != null && <span>Géo : <span className="font-mono text-slate-300">{sg.geo_score}/100</span></span>}
</div>
)}
</div>
)}
</div>
)}
{/* GPT-4o analysis */}
<div className="border-t border-slate-800 pt-3">
{!analysis ? (
<button
onClick={() => analyze(tradeId)}
disabled={analyzing}
className="flex items-center gap-2 px-3 py-1.5 rounded bg-blue-900/30 border border-blue-700/40 text-blue-300 hover:bg-blue-900/50 disabled:opacity-50 text-xs font-medium"
>
<Brain className={clsx('w-3.5 h-3.5', analyzing && 'animate-pulse')} />
{analyzing ? 'Analyse GPT-4o en cours' : 'Analyser avec GPT-4o'}
</button>
) : (
<div className="space-y-3">
<div className="flex items-center gap-2 text-blue-400 font-semibold">
<Brain className="w-3.5 h-3.5" />
Analyse GPT-4o
</div>
{[
{ 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] ? (
<div key={key}>
<span className={clsx('font-semibold', color)}>{label} : </span>
<span className="text-slate-400">{analysis[key]}</span>
</div>
) : null
)}
</div>
)}
</div>
</div>
)
}
// ── 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<number | null>(null)
const [closingTrade, setClosingTrade] = useState<any | null>(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 (
<div className="space-y-3">
<div className="flex items-center justify-between gap-4">
<div className="text-xs font-semibold text-slate-500 uppercase tracking-wide">
{trades.length}/{allTrades.length} trades · {withPnl.length} pricés
{avgPnl != null && (
<span className={clsx('ml-2 font-bold', avgPnl >= 0 ? 'text-emerald-400' : 'text-red-400')}>
· moy {avgPnl >= 0 ? '+' : ''}{avgPnl.toFixed(1)}%
</span>
)}
</div>
<div className="flex items-center gap-3">
{/* Score filter */}
<div className="flex items-center gap-2 text-xs text-slate-500">
<span className="shrink-0">Score ≥</span>
<input type="range" min="0" max="80" step="5"
value={minScoreFilter}
onChange={e => setMinScoreFilter(parseInt(e.target.value))}
className="w-24 accent-blue-500" />
<span className="w-5 font-mono text-blue-400">{minScoreFilter}</span>
</div>
<button onClick={() => refetch()} disabled={isFetching}
className="text-slate-600 hover:text-slate-400 disabled:opacity-40">
<RefreshCw className={clsx('w-3.5 h-3.5', isFetching && 'animate-spin')} />
</button>
</div>
</div>
{closingTrade && (
<CloseTradeModal trade={closingTrade} onClose={() => setClosingTrade(null)} />
)}
{isLoading ? (
<div className="card h-32 animate-pulse bg-dark-700" />
) : trades.length === 0 ? (
<div className="card text-center py-10 text-slate-600 text-sm">
<TrendingUp className="w-8 h-8 mx-auto mb-2 opacity-20" />
{allTrades.length === 0
? 'Aucun trade logué scorer des patterns pour commencer le suivi'
: `Aucun trade avec score ≥ ${minScoreFilter}`}
</div>
) : (
<div className="overflow-x-auto rounded-lg border border-slate-700/40">
<table className="w-full text-xs">
<thead>
<tr className="text-slate-600 border-b border-slate-700/40">
<th className="text-left px-3 py-2 font-medium">Pattern</th>
<th className="text-left px-3 py-2 font-medium">Profil</th>
<th className="text-left px-3 py-2 font-medium">Stratégie</th>
<th className="text-right px-3 py-2 font-medium">Strike</th>
<th className="text-right px-3 py-2 font-medium">DTE</th>
<th className="text-left px-3 py-2 font-medium">Ticker</th>
<th className="text-right px-3 py-2 font-medium">Score</th>
<th className="text-right px-3 py-2 font-medium">Trade Score</th>
<th className="text-right px-3 py-2 font-medium">EV nette</th>
<th className="text-right px-3 py-2 font-medium">Gain prévu</th>
<th className="text-right px-3 py-2 font-medium">Date</th>
<th className="text-right px-3 py-2 font-medium">Prix entrée</th>
<th className="text-right px-3 py-2 font-medium">Prix actuel</th>
<th className="text-right px-3 py-2 font-medium">Cible/Stop</th>
<th className="text-right px-3 py-2 font-medium">Maturité</th>
<th className="text-right px-3 py-2 font-medium">IV Rank</th>
<th className="text-right px-3 py-2 font-medium">Kelly</th>
<th className="text-right px-3 py-2 font-medium">P&L</th>
<th className="px-3 py-2 font-medium w-8"></th>
<th className="px-3 py-2 font-medium w-8"></th>
</tr>
</thead>
<tbody className="divide-y divide-slate-800/60">
{trades.map((t: any) => {
const evNet = t.ev_net ?? null
const tradeScore = t.trade_score ?? null
const isExpanded = selectedTradeId === t.id
return (
<Fragment key={t.id}>
<tr
className={clsx('hover:bg-dark-700/30 transition-colors cursor-pointer', isExpanded && 'bg-dark-700/40 border-b-0')}
onClick={() => setSelectedTradeId(isExpanded ? null : t.id)}
>
<td className="px-3 py-2 text-slate-300 max-w-[110px] truncate">{t.pattern_name || t.pattern_id}</td>
<td className="px-3 py-2">
{t.matched_profile ? (
<span className="text-[10px] font-semibold text-slate-400 bg-dark-700 px-1.5 py-0.5 rounded border border-slate-700/50">
{t.matched_profile}
</span>
) : <span className="text-slate-700 text-[10px]">—</span>}
</td>
<td className="px-3 py-2">
<span className={clsx('badge text-[10px]',
t.direction === 'bearish' ? 'badge-red' : 'badge-green')}>
{t.direction === 'bearish' ? '🐻' : '🐂'} {t.strategy || ''}
</span>
</td>
<td className="px-3 py-2 text-right font-mono text-[11px] text-slate-400">
{t.strike_guidance ?? ''}
</td>
<td className="px-3 py-2 text-right font-mono text-[11px] text-slate-400">
{t.expiry_days_at_entry != null ? `${t.expiry_days_at_entry}j` : t.horizon_days != null ? `${t.horizon_days}j` : ''}
</td>
<td className="px-3 py-2 font-mono">
<div className="flex items-center gap-1">
<span className={clsx(t.price_warning ? 'text-amber-300' : 'text-slate-300')}>{t.underlying}</span>
{t.price_warning && (
<span
className="text-[8px] font-bold bg-amber-900/40 border border-amber-600/40 text-amber-400 rounded px-1 py-0.5 leading-none shrink-0"
title={t.price_warning === 'no_price_data' ? 'Aucune donnée prix monitoring impossible' : t.price_warning === 'no_entry_price' ? 'Prix d\'entrée manquant' : 'Prix live indisponible'}
>
{t.price_warning === 'no_price_data' ? 'NO DATA' : t.price_warning === 'no_entry_price' ? 'NO ENTRY' : 'NO LIVE'}
</span>
)}
</div>
</td>
<td className="px-3 py-2 text-right">
<ScoreDelta entry={t.score_at_entry} latest={t.latest_score ?? t.score_at_entry} />
</td>
<td className="px-3 py-2 text-right">
{tradeScore != null ? (
<span className={clsx('font-bold font-mono text-xs',
tradeScore >= 55 ? 'text-emerald-400' : tradeScore >= 45 ? 'text-yellow-400' : 'text-slate-500')}>
{tradeScore.toFixed(1)}
</span>
) : <span className="text-slate-700 text-xs"></span>}
</td>
<td className="px-3 py-2 text-right">
{evNet != null ? (
<span className={clsx('font-mono text-[11px]',
evNet > 0.05 ? 'text-emerald-400' : evNet >= -0.01 ? 'text-yellow-400' : 'text-slate-600')}>
{evNet > 0 ? '+' : ''}{(evNet * 100).toFixed(0)}%
</span>
) : <span className="text-slate-700 text-xs"></span>}
</td>
<td className="px-3 py-2 text-right">
{t.expected_move_pct != null ? (
<span className="font-mono text-[11px] text-slate-500">{t.expected_move_pct.toFixed(0)}%</span>
) : <span className="text-slate-700 text-xs"></span>}
</td>
<td className="px-3 py-2 text-right text-slate-600 whitespace-nowrap text-[11px]">{t.entry_date}</td>
<td className={clsx('px-3 py-2 text-right font-mono text-[11px]', t.price_warning === 'no_entry_price' || t.price_warning === 'no_price_data' ? 'text-amber-600' : 'text-slate-400')}>
{t.entry_price != null ? t.entry_price.toFixed(2) : '—'}
</td>
<td className={clsx('px-3 py-2 text-right font-mono text-[11px]', t.price_warning === 'no_live_price' || t.price_warning === 'no_price_data' ? 'text-amber-600' : 'text-slate-300')}>
{t.current_price != null ? t.current_price.toFixed(2) : '—'}
</td>
<td className="px-3 py-2 text-right">
{t.pnl_pct != null ? (
<div className="flex flex-col items-end gap-1">
<div className="flex items-center gap-1.5">
{t.alert_type === 'target_reached' && (
<span className="text-[9px] font-bold text-emerald-400 bg-emerald-900/30 border border-emerald-700/30 rounded px-1">🎯</span>
)}
{t.alert_type === 'stop_loss' && (
<span className="text-[9px] font-bold text-red-400 bg-red-900/30 border border-red-700/30 rounded px-1">🛑</span>
)}
</div>
{/* progress bar: stop_loss (negative) to target (positive) */}
<div className="w-16 relative">
<div className="h-1.5 bg-slate-700 rounded-full overflow-hidden">
<div
className={clsx('h-full rounded-full',
t.pnl_pct >= 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
)}%`
}}
/>
</div>
<div className="flex justify-between text-[8px] text-slate-700 mt-0.5 font-mono">
<span>{t.stop_loss_pct ?? -50}%</span>
<span>+{t.target_pct ?? 30}%</span>
</div>
</div>
</div>
) : <span className="text-slate-700 text-[10px]"></span>}
</td>
<td className="px-3 py-2 text-right">
{t.maturity ? (
<div className="flex flex-col items-end gap-0.5">
<span className={clsx('text-[10px] font-semibold',
t.maturity.status === 'trop_tot' ? 'text-slate-500' :
t.maturity.status === 'debut' ? 'text-yellow-400' :
t.maturity.status === 'mature' ? 'text-emerald-400' :
'text-orange-400'
)}>
{t.maturity.emoji} {t.maturity.label}
</span>
<span className="text-[9px] text-slate-600 font-mono">
{t.days_held ?? 0}j / {t.horizon_days ?? 90}j ({t.maturity.ratio_pct}%)
</span>
<div className="w-16 h-1 bg-slate-700 rounded-full overflow-hidden">
<div
className={clsx('h-full rounded-full',
t.maturity.status === 'trop_tot' ? 'bg-slate-600' :
t.maturity.status === 'debut' ? 'bg-yellow-500' :
t.maturity.status === 'mature' ? 'bg-emerald-500' :
'bg-orange-500'
)}
style={{ width: `${Math.min(t.maturity.ratio_pct, 100)}%` }}
/>
</div>
</div>
) : (
<span className="text-slate-700 text-[11px]">
{t.days_held != null ? `${t.days_held}j` : '—'}
</span>
)}
</td>
<td className="px-3 py-2 text-right">
<IvRankCell underlying={t.underlying} />
</td>
<td className="px-3 py-2 text-right">
<KellyCell patternId={t.pattern_id} />
</td>
<td className="px-3 py-2 text-right">
<PnlBadge pnl={t.pnl_pct} />
</td>
<td className="px-2 py-2" onClick={e => e.stopPropagation()}>
<button
onClick={() => setSelectedTradeId(isExpanded ? null : t.id)}
className={clsx(
'p-1 rounded transition-colors',
isExpanded
? 'text-blue-400 bg-blue-900/30'
: 'text-slate-600 hover:text-blue-400 hover:bg-blue-900/20'
)}
title="Post-mortem IA"
>
{isExpanded ? <ChevronUp className="w-3.5 h-3.5" /> : <Search className="w-3.5 h-3.5" />}
</button>
</td>
<td className="px-2 py-2" onClick={e => e.stopPropagation()}>
<button
onClick={() => setClosingTrade(t)}
className={clsx(
'p-1 rounded transition-colors',
t.alert_type
? 'text-amber-400 bg-amber-900/30 hover:bg-amber-900/50'
: 'text-slate-700 hover:text-amber-400 hover:bg-amber-900/20'
)}
title="Fermer le trade"
>
<Lock className="w-3.5 h-3.5" />
</button>
</td>
</tr>
{isExpanded && (
<tr className="bg-dark-900/60">
<td colSpan={20} className="px-4 py-3 border-b border-blue-700/20 space-y-3">
<IBKRTicket
strategy={t.strategy ?? ''}
underlying={t.underlying}
strikeGuidance={t.strike_guidance}
expiryDays={t.expiry_days_at_entry ?? t.horizon_days}
entryPrice={t.entry_price}
maxLoss={t.max_loss_eur}
targetGain={t.target_gain_eur}
/>
<PostmortemPanel tradeId={t.id} onClose={() => setSelectedTradeId(null)} />
</td>
</tr>
)}
</Fragment>
)
})}
</tbody>
</table>
</div>
)}
</div>
)
}
// ── 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 (
<div className="space-y-3">
<div className="flex items-center justify-between">
<div className="text-xs font-semibold text-slate-500 uppercase tracking-wide">
{history.length} alertes · {days} derniers jours
</div>
<button onClick={() => refetch()} disabled={isFetching}
className="text-slate-600 hover:text-slate-400 disabled:opacity-40">
<RefreshCw className={clsx('w-3.5 h-3.5', isFetching && 'animate-spin')} />
</button>
</div>
{isLoading ? (
<div className="space-y-2">{[1,2].map(i => <div key={i} className="card h-12 animate-pulse bg-dark-700" />)}</div>
) : history.length === 0 ? (
<div className="card text-center py-10 text-slate-600 text-sm">
<AlertTriangle className="w-8 h-8 mx-auto mb-2 opacity-20" />
Aucune alerte scorer des patterns pour commencer le suivi
</div>
) : (
<div className="space-y-2">
{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 (
<div key={entry.id} className="card">
<div className="flex items-center gap-3 mb-2">
<div className="flex items-center gap-2 shrink-0">
<div className="w-10 h-10 rounded-lg flex items-center justify-center text-sm font-bold"
style={{ background: `${barColor}22`, color: barColor, border: `1px solid ${barColor}44` }}>
{score}
</div>
<div>
<div className="text-[10px] text-slate-600 uppercase">{level}</div>
<div className="text-[10px] text-slate-700">{ts}</div>
</div>
</div>
<div className="flex-1">
<div className="h-2 bg-dark-700 rounded-full overflow-hidden">
<div className="h-full rounded-full" style={{ width: `${(score / 100) * 100}%`, background: barColor }} />
</div>
</div>
<span className="text-[10px] text-slate-700 shrink-0">{entry.news_count} news</span>
</div>
{patterns.length > 0 && (
<div className="flex flex-wrap gap-1">
{patterns.slice(0, 5).map((p: any, i: number) => (
<span key={i} className="inline-flex items-center gap-1 text-[10px] bg-dark-700 text-slate-400 rounded px-1.5 py-0.5">
<span className="font-mono text-slate-500">{p.score}</span>
<span className="max-w-[120px] truncate">{p.name || p.pattern_id}</span>
</span>
))}
</div>
)}
</div>
)
})}
</div>
)}
</div>
)
}
// ── 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 (
<div className="space-y-3">
<div className="flex items-center justify-between">
<div className="text-xs font-semibold text-slate-500 uppercase tracking-wide">
{runs.length} cycles · {cs?.enabled ? `auto ${cs.interval_hours}h` : 'manuel uniquement'}
</div>
<div className="flex gap-2">
<button
onClick={() => triggerCycle(undefined, { onSuccess: () => setTimeout(() => refetch(), 3000) })}
disabled={triggering}
className="flex items-center gap-1 text-xs border border-blue-500/40 text-blue-400 hover:bg-blue-900/20 px-2.5 py-1 rounded disabled:opacity-40">
<Zap className={clsx('w-3 h-3', triggering && 'animate-pulse')} />
{triggering ? 'Lancement...' : 'Lancer un cycle'}
</button>
<button onClick={() => refetch()} disabled={isFetching}
className="text-slate-600 hover:text-slate-400 disabled:opacity-40">
<RefreshCw className={clsx('w-3.5 h-3.5', isFetching && 'animate-spin')} />
</button>
</div>
</div>
{isLoading ? (
<div className="space-y-2">{[1,2,3].map(i => <div key={i} className="card h-20 animate-pulse bg-dark-700" />)}</div>
) : runs.length === 0 ? (
<div className="card text-center py-10 text-slate-600 text-sm">
<Zap className="w-8 h-8 mx-auto mb-2 opacity-20" />
Aucun cycle activer l'auto-cycle dans Configuration ou lancer manuellement
</div>
) : (
<div className="space-y-3">
{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 (
<div key={run.id} className={clsx('card', ok ? 'border-slate-700/40' : 'border-red-800/30')}>
<div className="flex items-start gap-3 mb-2">
{ok
? <CheckCircle className="w-4 h-4 text-emerald-400 shrink-0 mt-0.5" />
: <XCircle className="w-4 h-4 text-red-400 shrink-0 mt-0.5" />}
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 flex-wrap">
<span className="text-xs text-slate-300 font-semibold">{ts} UTC</span>
<span className={clsx('badge text-[10px]', run.trigger === 'manual' ? 'badge-blue' : 'badge-purple')}>
{run.trigger === 'manual' ? 'Manuel' : `Auto ${cs?.interval_hours ?? ''}h`}
</span>
{run.dominant_regime && <ScenarioBadge dominant={run.dominant_regime} />}
{duration != null && <span className="text-[10px] text-slate-600">{duration}s</span>}
</div>
<div className="flex gap-3 mt-1 text-[11px] text-slate-500 flex-wrap">
<span>💡 {run.patterns_suggested} suggérés</span>
<span>✚ {run.patterns_added} ajoutés</span>
<span>🎯 {run.patterns_scored} scorés</span>
{run.geo_score != null && <span>🌐 Géo {run.geo_score}/100</span>}
</div>
</div>
</div>
{commentary && (
<div className="bg-blue-900/10 border border-blue-700/20 rounded p-3 space-y-2">
<div className="flex items-center gap-1.5 text-[10px] text-blue-400 font-semibold uppercase tracking-wide">
<Brain className="w-3 h-3" /> Commentaire IA
</div>
<p className="text-xs text-slate-300 leading-relaxed">{commentary.commentary}</p>
{commentary.key_risk && (
<div className="flex items-start gap-1.5 text-[11px] text-orange-400/80 bg-orange-900/10 border border-orange-700/20 rounded px-2 py-1">
<span className="shrink-0">⚠</span> {commentary.key_risk}
</div>
)}
{commentary.top_pattern && (
<div className="text-[11px] text-emerald-400/80">
🎯 Pattern clé : <span className="font-semibold">{commentary.top_pattern}</span>
</div>
)}
{commentary.lessons_from_report ? (
<div className="flex items-center gap-1.5 text-[10px] text-purple-400/80 bg-purple-900/10 border border-purple-700/20 rounded px-2 py-1">
<BookOpen className="w-3 h-3 shrink-0" />
Leçons du rapport du {commentary.lessons_from_report} intégrées dans ce cycle
{commentary.lessons_headline && (
<span className="text-slate-600 ml-1">· {commentary.lessons_headline}</span>
)}
</div>
) : (
<div className="flex items-center gap-1.5 text-[10px] text-slate-700 italic">
<BookOpen className="w-3 h-3 shrink-0" />
Aucun rapport de performance disponible — générer un rapport dans "Rapport IA"
</div>
)}
</div>
)}
{!commentary && ok && (
<div className="text-[11px] text-slate-700 italic">Aucun commentaire IA généré pour ce cycle</div>
)}
</div>
)
})}
</div>
)}
</div>
)
}
// ── Page principale ────────────────────────────────────────────────────────────
const ASSET_CLASS_COLORS: Record<string, string> = {
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 (
<div className="card animate-pulse">
<div className="h-5 bg-dark-700 rounded w-48 mb-4" />
<div className="space-y-2">{[1, 2, 3].map(i => <div key={i} className="h-8 bg-dark-700 rounded" />)}</div>
</div>
)
}
if (!risk || risk.open_count === 0) {
return (
<div className="card text-center py-8 text-slate-500">
<PieChart className="w-8 h-8 mx-auto mb-2 opacity-20" />
<div className="text-sm">Aucune position ouverte dans le portefeuille simulé</div>
<div className="text-xs mt-1">Les trades logués apparaîtront ici après le prochain 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-3">
{/* Header */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<PieChart className="w-4 h-4 text-blue-400" />
<span className="text-sm font-semibold text-white">Portefeuille Simulé — Analyse de Risque</span>
<span className="text-xs text-slate-500">{risk.open_count} positions ouvertes</span>
{dangers.length > 0 && (
<span className="inline-flex items-center gap-1 text-[10px] font-bold bg-red-900/30 border border-red-600/40 text-red-400 rounded px-1.5 py-0.5">
<ShieldAlert className="w-3 h-3" /> {dangers.length} CONFLIT{dangers.length > 1 ? 'S' : ''}
</span>
)}
</div>
<button onClick={() => refetch()} className="text-slate-600 hover:text-slate-400 transition-colors">
<RefreshCw className="w-3.5 h-3.5" />
</button>
</div>
{/* Asset class concentration bars */}
<div className="card">
<div className="text-xs text-slate-500 mb-3 font-semibold uppercase tracking-wide">Répartition par classe d'actif</div>
<div className="space-y-2">
{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 (
<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">{data.tickers?.slice(0, 3).join(', ')}{data.tickers?.length > 3 ? '…' : ''}</span>
</div>
<div className="flex items-center gap-2 text-[10px]">
{exposure.bullish > 0 && (
<span className="text-emerald-400 flex items-center gap-0.5">
<TrendingUp className="w-2.5 h-2.5" /> {exposure.bullish}
</span>
)}
{exposure.bearish > 0 && (
<span className="text-red-400 flex items-center gap-0.5">
<TrendingDown className="w-2.5 h-2.5" /> {exposure.bearish}
</span>
)}
<span className="text-slate-400 font-mono font-bold">{data.pct}%</span>
<span className="text-slate-600">({data.count})</span>
</div>
</div>
<div className="h-1.5 bg-dark-700 rounded-full overflow-hidden">
<div
className="h-full rounded-full transition-all"
style={{ width: `${data.pct}%`, background: `${color}cc` }}
/>
</div>
</div>
)
})}
</div>
</div>
{/* Alerts */}
{alerts.length > 0 && (
<div className="space-y-2">
{/* Conflicts */}
{conflicts.map((c: any, i: number) => (
<div key={i} className="card border-red-700/40 bg-red-900/10">
<div className="flex items-start gap-2">
<ShieldAlert className="w-4 h-4 text-red-400 shrink-0 mt-0.5" />
<div className="flex-1 min-w-0">
<div className="text-xs font-semibold text-red-300 mb-1">
Conflit directionnel {c.underlying}
</div>
<div className="space-y-0.5">
{c.trades.map((t: any) => (
<div key={t.id} className="flex items-center gap-2 text-[11px]">
<span className={clsx('font-bold font-mono',
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>
{t.pattern_name && (
<span className="text-slate-600 truncate max-w-[180px]">{t.pattern_name}</span>
)}
</div>
))}
</div>
</div>
</div>
</div>
))}
{/* Concentration & overweight warnings */}
{warnings.length > 0 && (
<div className="card border-amber-700/30 bg-amber-900/10">
<div className="text-xs font-semibold text-amber-300 mb-1.5 flex items-center gap-1">
<AlertTriangle className="w-3.5 h-3.5" /> Alertes de concentration
</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>
)}
{/* AI Monitor 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" /> Recommandations IA Moniteur de portefeuille
</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 font-bold">
{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 mt-1">
{aiMonitor.rebalance_suggestion}
</div>
)}
</div>
)}
{alerts.length === 0 && (
<div className="card border-emerald-700/30 bg-emerald-900/10 text-center py-3">
<CheckCircle className="w-5 h-5 text-emerald-400 mx-auto mb-1" />
<div className="text-xs text-emerald-300">Portefeuille équilibré aucun conflit ni sur-concentration détectés</div>
</div>
)}
</div>
)
}
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<string, string> = {
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 <div className="card animate-pulse h-24" />
if (trades.length === 0) {
return (
<div className="card text-center py-8 text-slate-500 text-sm">
<XCircle className="w-6 h-6 mx-auto mb-2 text-slate-600" />
Aucune suggestion non loggée sur {days}j tous les trades scorés passent au moins un profil de risque.
</div>
)
}
const byReason = trades.reduce((acc: Record<string, number>, t: any) => {
acc[t.skip_reason] = (acc[t.skip_reason] ?? 0) + 1
return acc
}, {})
return (
<div className="space-y-4">
<div className="card">
<div className="section-title flex items-center gap-1 mb-3">
<XCircle className="w-3 h-3 text-amber-400" />
Trades suggérés non loggés {trades.length} sur {days}j
<span className="ml-2 text-slate-600 font-normal text-xs">
(score ou gain insuffisant pour tout profil actif)
</span>
</div>
<div className="flex gap-3 mb-4 flex-wrap">
{Object.entries(byReason).map(([reason, count]) => (
<div key={reason} className="badge badge-yellow text-[10px]">
{reason}: {count}
</div>
))}
</div>
<div className="overflow-x-auto rounded-lg border border-slate-700/40">
<table className="w-full text-xs">
<thead>
<tr className="border-b border-slate-700/60 text-slate-600 text-[10px] uppercase tracking-wide">
<th className="pl-3 py-2 text-left">Pattern</th>
<th className="px-2 py-2 text-left">Ticker</th>
<th className="px-2 py-2 text-left">Stratégie</th>
<th className="px-2 py-2 text-right">Score</th>
<th className="px-2 py-2 text-right">Gain attendu</th>
<th className="px-2 py-2 text-left">Classe</th>
<th className="pr-3 py-2 text-left">Raison du skip</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-800/40">
{trades.map((t: any) => (
<tr key={t.id} className="hover:bg-dark-700/30">
<td className="pl-3 py-2 text-slate-300 max-w-[140px] truncate">{t.pattern_name || '—'}</td>
<td className="px-2 py-2 font-mono text-slate-400">{t.underlying}</td>
<td className="px-2 py-2 text-slate-400">{t.strategy || '—'}</td>
<td className={clsx('px-2 py-2 text-right font-bold font-mono',
t.score >= 50 ? 'text-emerald-400' : t.score >= 25 ? 'text-yellow-400' : 'text-slate-600')}>
{t.score ?? '—'}
</td>
<td className="px-2 py-2 text-right text-slate-400">
{t.expected_move_pct != null ? `${t.expected_move_pct.toFixed(0)}%` : '—'}
</td>
<td className="px-2 py-2">
{t.asset_class ? (
<span className={clsx('badge text-[10px] border', ASSET_CLASS_COLORS[t.asset_class] ?? 'badge-blue')}>
{t.asset_class}
</span>
) : <span className="text-slate-600"></span>}
</td>
<td className="pr-3 py-2 text-slate-600 max-w-[200px] truncate" title={t.skip_detail}>
{t.skip_detail || t.skip_reason}
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>
)
}
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 (
<div className="p-6 space-y-5">
{/* Header */}
<div className="flex items-start justify-between">
<div>
<h1 className="text-xl font-bold text-white flex items-center gap-2">
<BookOpen className="w-5 h-5 text-blue-400" /> Journal de Bord
</h1>
<p className="text-xs text-slate-500 mt-0.5">
Historique des régimes macro · Mark-to-market des trades · Évolution du risque géopolitique
</p>
</div>
<div className="flex items-center gap-3">
{/* Reset button */}
<div className="flex items-center gap-2">
{resetMsg && (
<span className={clsx('text-xs', resetMsg.includes('Erreur') ? 'text-red-400' : 'text-emerald-400')}>
{resetMsg}
</span>
)}
{confirmReset && !resetting && (
<span className="text-xs text-amber-400">Confirmer ?</span>
)}
<button
onClick={confirmReset ? handleReset : () => setConfirmReset(true)}
disabled={resetting}
onBlur={() => setTimeout(() => setConfirmReset(false), 300)}
className={clsx(
'flex items-center gap-1.5 px-3 py-1.5 rounded text-xs font-semibold border transition-all disabled:opacity-40',
confirmReset
? 'bg-red-900/30 border-red-600/50 text-red-400 hover:bg-red-900/50'
: 'border-slate-700/40 text-slate-600 hover:text-slate-400 hover:border-slate-600'
)}>
<Trash2 className="w-3.5 h-3.5" />
{resetting ? 'Réinit...' : confirmReset ? 'Effacer tout' : 'Reset journal'}
</button>
</div>
{/* Period selector */}
<div className="flex gap-1 bg-dark-700 p-1 rounded text-xs">
{[15, 30, 60, 90].map(d => (
<button key={d} onClick={() => setDays(d)}
className={clsx('px-2.5 py-1 rounded transition-colors', {
'bg-blue-600 text-white': days === d,
'text-slate-400 hover:text-slate-200': days !== d,
})}>
{d}j
</button>
))}
</div>
</div>
</div>
{/* Summary cards */}
{s && (
<div className="grid grid-cols-4 gap-3">
<div className="card text-center">
<div className="text-2xl font-bold text-white">{s.macro_snapshots ?? 0}</div>
<div className="text-xs text-slate-600 mt-0.5">snapshots macro</div>
{s.current_dominant && (
<div className="mt-1"><ScenarioBadge dominant={s.current_dominant} /></div>
)}
</div>
<div className="card text-center">
<div className="text-2xl font-bold text-white">{s.regime_transitions?.length ?? 0}</div>
<div className="text-xs text-slate-600 mt-0.5">transitions de régime</div>
{s.regime_transitions?.length > 0 && (
<div className="text-[10px] text-amber-400 mt-1">
{s.regime_transitions[0].from} {s.regime_transitions[0].to}
</div>
)}
</div>
<div className="card text-center">
<div className={clsx('text-2xl font-bold',
(s.avg_geo_score ?? 0) >= 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) : '—'}
</div>
<div className="text-xs text-slate-600 mt-0.5">score géo moyen</div>
{s.max_geo_score != null && (
<div className="text-[10px] text-slate-600 mt-1">max {s.max_geo_score}</div>
)}
</div>
<div className="card text-center">
<div className="text-2xl font-bold text-white">{s.trade_entries_logged ?? 0}</div>
<div className="text-xs text-slate-600 mt-0.5">trades logués</div>
</div>
{riskAlertCount > 0 && (
<div className="card text-center border-red-700/40 bg-red-900/10 col-span-full">
<div className="flex items-center justify-center gap-2">
<ShieldAlert className="w-4 h-4 text-red-400" />
<span className="text-sm font-semibold text-red-300">
{riskAlertCount} conflit{riskAlertCount > 1 ? 's' : ''} directionnel{riskAlertCount > 1 ? 's' : ''} détecté{riskAlertCount > 1 ? 's' : ''}
</span>
<span className="text-xs text-slate-500"> voir Risk Dashboard Simulé</span>
</div>
</div>
)}
</div>
)}
{/* Tabs */}
<div className="flex gap-1 bg-dark-700 p-1 rounded w-fit">
{TABS.map(({ key, label, icon: Icon }) => (
<button key={key} onClick={() => setTab(key as any)}
className={clsx('flex items-center gap-1.5 px-3 py-1.5 rounded text-sm transition-colors', {
'bg-blue-600 text-white': tab === key,
'text-slate-400 hover:text-slate-200': tab !== key,
})}>
<Icon className="w-3.5 h-3.5" />
{label}
</button>
))}
</div>
{/* Content */}
{tab === 'cycles' && <CyclesSection />}
{tab === 'macro' && <MacroHistorySection days={days} />}
{tab === 'ideas' && <TradeIdeasTab />}
{tab === 'mtm' && <TradeMtmSection days={days} />}
{tab === 'closed' && <ClosedTradesSection days={days} />}
{tab === 'geo' && <GeoHistorySection days={days} />}
{tab === 'skipped' && <SkippedTradesSection days={days} />}
</div>
)
}