Files
OpenFin/frontend/src/pages/JournalDeBord.tsx
2026-07-14 11:21:43 +02:00

1968 lines
94 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, CATEGORIES } 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, useDeleteTrade, api } from '../hooks/useApi'
import { ASSET_CLASS_COLORS } from '../constants/assetColors'
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: 'Disinflation', 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: 'Recession', color: '#8b5cf6', emoji: '📉' },
crise_liquidite: { label: 'Liquidity Crisis', color: '#ec4899', emoji: '🚨' },
incertain: { label: 'Uncertain', 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}> adjusted</span>}
</div>
)
}
// ── Close Trade Modal ─────────────────────────────────────────────────────────
const CLOSE_REASONS = [
{ value: 'target', label: '🎯 Target reached' },
{ value: 'stop_loss', label: '🛑 Stop-loss triggered' },
{ value: 'signal_reversal', label: '🔄 AI signal reversed' },
{ value: 'manual', label: '✍️ Manual close' },
]
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" /> Close trade
</div>
<div className="text-xs text-slate-500 mt-0.5">
{trade.underlying} · {trade.strategy} · entry {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' ? '🎯 Target reached!' : '🛑 Stop-loss hit'}
{' — 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">Close price</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="Current underlying price"
/>
{pnlPreview !== null && (
<div className={clsx('text-xs mt-1 font-mono font-bold',
pnlPreview >= 0 ? 'text-emerald-400' : 'text-red-400')}>
Realized P&L: {pnlPreview >= 0 ? '+' : ''}{pnlPreview}%
</div>
)}
</div>
<div>
<label className="block text-xs text-slate-500 mb-1">Close reason</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 (optional)</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="Context, reason…"
maxLength={200}
/>
</div>
</div>
{isError && <div className="text-xs text-red-400">Error closing 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">
Cancel
</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 ? 'Closing…' : 'Confirm close'}
</button>
</div>
</div>
</div>
)
}
// ── Shared filter bar ────────────────────────────────────────────────────────
const DIRECTIONS = [
{ key: 'all', label: 'All' },
{ key: 'bullish', label: '🐂 Bullish' },
{ key: 'bearish', label: '🐻 Bearish' },
]
function _isBearishStr(strategy: string) {
return /put|bear|short|sell|vente|baissier/i.test(strategy ?? '')
}
// Normalize AI-returned asset_class variants to canonical CATEGORIES keys
const _TICKER_TO_CLASS: Record<string, string> = {
// Energy
'CL=F': 'energy', 'BZ=F': 'energy', 'NG=F': 'energy', 'RB=F': 'energy', 'HO=F': 'energy',
'XLE': 'energy', 'XOP': 'energy', 'USO': 'energy', 'UCO': 'energy', 'BOIL': 'energy', 'UNG': 'energy',
// Metals
'GC=F': 'metals', 'SI=F': 'metals', 'HG=F': 'metals', 'PA=F': 'metals', 'PL=F': 'metals',
'GLD': 'metals', 'IAU': 'metals', 'SLV': 'metals', 'GDX': 'metals', 'GDXJ': 'metals',
'PPLT': 'metals', 'DBP': 'metals', 'GLDM': 'metals',
// Agriculture
'ZC=F': 'agriculture', 'ZS=F': 'agriculture', 'ZW=F': 'agriculture',
'CC=F': 'agriculture', 'KC=F': 'agriculture', 'CT=F': 'agriculture', 'OJ=F': 'agriculture',
'CORN': 'agriculture', 'WEAT': 'agriculture', 'SOYB': 'agriculture', 'DBA': 'agriculture',
// Indices
'^GSPC': 'indices', '^NDX': 'indices', '^DJI': 'indices', '^RUT': 'indices',
'SPY': 'indices', 'QQQ': 'indices', 'IWM': 'indices', 'DIA': 'indices', 'RSP': 'indices',
'VGK': 'indices', 'EEM': 'indices', 'EWZ': 'indices', 'FXI': 'indices',
'EWJ': 'indices', 'EFA': 'indices', 'ACWI': 'indices', 'INDA': 'indices',
'^NSEI': 'indices', '^HSI': 'indices', 'EWG': 'indices', 'EWU': 'indices', 'EWI': 'indices',
// Equities (sector ETFs)
'XLF': 'equities', 'XLK': 'equities', 'XLV': 'equities', 'XLI': 'equities',
'XLP': 'equities', 'XLU': 'equities', 'XLY': 'equities', 'XLRE': 'equities',
'XLB': 'equities', 'XLC': 'equities',
// Forex
'DX-Y.NYB': 'forex', 'UUP': 'forex', 'FXE': 'forex', 'FXY': 'forex',
'EUO': 'forex', 'YCS': 'forex', 'FXA': 'forex', 'FXB': 'forex', 'FXF': 'forex',
}
function _normalizeAssetClass(cls: string | null | undefined, ticker?: string | null): string {
if (cls) {
const c = cls.toLowerCase().trim()
if (/energy|oil|gas|petrol|brent|wti/.test(c)) return 'energy'
if (/metal|gold|silver|copper|mining|precious/.test(c)) return 'metals'
if (/agri|grain|corn|wheat|soy|crop|coton|coffee|cocoa/.test(c)) return 'agriculture'
if (/index|indic|indices|spx|nasdaq|dow|s&p|russell|cac|dax/.test(c)) return 'indices'
if (/equit|stock|action|share|sector/.test(c)) return 'equities'
if (/forex|currency|fx|devise|change|eur|usd|jpy|dxy/.test(c)) return 'forex'
if (['energy', 'metals', 'agriculture', 'indices', 'equities', 'forex'].includes(c)) return c
}
// Fallback: infer from ticker symbol
if (ticker) {
const t = ticker.toUpperCase().trim()
if (_TICKER_TO_CLASS[t]) return _TICKER_TO_CLASS[t]
// NSE: or other exchange-prefixed equities
if (t.includes(':')) return 'equities'
// Remaining =F futures: crude/energy or precious/metals heuristic
if (t.endsWith('=F')) {
const stem = t.slice(0, 2)
if (['CL', 'RB', 'HO', 'NG', 'BZ'].includes(stem)) return 'energy'
if (['GC', 'SI', 'HG', 'PA', 'PL'].includes(stem)) return 'metals'
if (['ZC', 'ZS', 'ZW', 'CC', 'KC', 'CT'].includes(stem)) return 'agriculture'
}
// Currency pairs
if (t.includes('=X') || t.includes('/')) return 'forex'
}
return cls?.toLowerCase().trim() ?? ''
}
interface FilterBarProps {
search: string; onSearch: (v: string) => void
assetClass: string; onAssetClass: (v: string) => void
direction?: string; onDirection?: (v: string) => void
pnlFilter?: string; onPnlFilter?: (v: string) => void
}
function FilterBar({ search, onSearch, assetClass, onAssetClass, direction, onDirection, pnlFilter, onPnlFilter }: FilterBarProps) {
return (
<div className="flex items-center gap-2 flex-wrap">
{/* Text search */}
<div className="relative">
<Search className="absolute left-2 top-1/2 -translate-y-1/2 w-3 h-3 text-slate-600 pointer-events-none" />
<input
type="text"
value={search}
onChange={e => onSearch(e.target.value)}
placeholder="Ticker, strategy…"
className="pl-6 pr-2 py-1 bg-dark-700 border border-slate-700/50 rounded text-xs text-slate-300 placeholder-slate-600 w-36 focus:outline-none focus:border-slate-500"
/>
{search && (
<button onClick={() => onSearch('')} className="absolute right-1.5 top-1/2 -translate-y-1/2 text-slate-600 hover:text-slate-400">
<X className="w-3 h-3" />
</button>
)}
</div>
{/* Asset class */}
<div className="flex items-center gap-0.5 bg-dark-700 rounded p-0.5">
{CATEGORIES.map(c => (
<button key={c.key} onClick={() => onAssetClass(c.key)}
className={clsx('px-2 py-1 rounded text-xs transition-colors', {
'bg-blue-600 text-white': assetClass === c.key,
'text-slate-400 hover:text-slate-200': assetClass !== c.key,
})}>
{c.label}
</button>
))}
</div>
{/* Direction */}
{onDirection && (
<div className="flex items-center gap-0.5 bg-dark-700 rounded p-0.5">
{DIRECTIONS.map(d => (
<button key={d.key} onClick={() => onDirection(d.key)}
className={clsx('px-2 py-1 rounded text-xs transition-colors', {
'bg-slate-600 text-white': direction === d.key,
'text-slate-400 hover:text-slate-200': direction !== d.key,
})}>
{d.label}
</button>
))}
</div>
)}
{/* P&L filter (Fermés only) */}
{onPnlFilter && (
<div className="flex items-center gap-0.5 bg-dark-700 rounded p-0.5">
{[
{ key: 'all', label: 'All' },
{ key: 'winners', label: '✓ Winners' },
{ key: 'losers', label: '✗ Losers' },
].map(p => (
<button key={p.key} onClick={() => onPnlFilter(p.key)}
className={clsx('px-2 py-1 rounded text-xs transition-colors', {
'bg-slate-600 text-white': pnlFilter === p.key,
'text-slate-400 hover:text-slate-200': pnlFilter !== p.key,
})}>
{p.label}
</button>
))}
</div>
)}
</div>
)
}
// ── Closed Trades Section ─────────────────────────────────────────────────────
function ClosedTradesSection({ days }: { days: number }) {
const { data, isLoading, refetch, isFetching } = useClosedTrades(days)
const { mutate: deleteTrade, isPending: deleting } = useDeleteTrade()
const allTrades: any[] = (data as any)?.trades ?? []
const stats: any = (data as any)?.stats ?? {}
const [search, setSearch] = useState('')
const [assetClass, setAssetClass] = useState('all')
const [direction, setDirection] = useState('all')
const [pnlFilter, setPnlFilter] = useState('all')
const [confirmDeleteId, setConfirmDeleteId] = useState<number | null>(null)
const trades = allTrades.filter((t: any) => {
const q = search.toLowerCase()
if (q && !`${t.underlying} ${t.strategy ?? ''} ${t.pattern_name ?? ''}`.toLowerCase().includes(q)) return false
if (assetClass !== 'all' && _normalizeAssetClass(t.asset_class, t.underlying) !== assetClass) return false
const bearish = _isBearishStr(t.strategy ?? '')
if (direction === 'bullish' && bearish) return false
if (direction === 'bearish' && !bearish) return false
if (pnlFilter === 'winners' && (t.pnl_realized ?? 0) < 0) return false
if (pnlFilter === 'losers' && (t.pnl_realized ?? 0) >= 0) return false
return true
})
const REASON_META: Record<string, { label: string; color: string }> = {
target: { label: '🎯 Target', color: 'text-emerald-400' },
stop_loss: { label: '🛑 Stop-loss', color: 'text-red-400' },
signal_reversal: { label: '🔄 AI Signal', color: 'text-blue-400' },
manual: { label: '✍️ Manual', color: 'text-slate-400' },
}
return (
<div className="space-y-4">
{/* Stats banner */}
{allTrades.length > 0 && (
<div className="grid grid-cols-4 gap-3">
{[
{ label: 'Closed trades', value: stats.total ?? 0, sub: '', color: 'text-white' },
{ label: 'Win rate', value: stats.win_rate != null ? `${stats.win_rate}%` : '—', sub: '', color: stats.win_rate >= 50 ? 'text-emerald-400' : 'text-red-400' },
{ label: 'Avg P&L', 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: 'Total P&L', value: stats.total_pnl != null ? `${stats.total_pnl >= 0 ? '+' : ''}${stats.total_pnl?.toFixed(1)}%` : '—', sub: `best ${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 flex-wrap gap-2">
<div className="text-xs font-semibold text-slate-500 uppercase tracking-wide">
{trades.length}/{allTrades.length} closed trades · last {days} days
</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>
<FilterBar
search={search} onSearch={setSearch}
assetClass={assetClass} onAssetClass={setAssetClass}
direction={direction} onDirection={setDirection}
pnlFilter={pnlFilter} onPnlFilter={setPnlFilter}
/>
{isLoading ? (
<div className="card h-32 animate-pulse bg-dark-700" />
) : allTrades.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" />
No closed trades in the last {days} days
</div>
) : trades.length === 0 ? (
<div className="card text-center py-8 text-slate-600 text-sm">
No results for these filters
</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">Strategy</th>
<th className="text-left px-3 py-2 font-medium">Ticker</th>
<th className="text-right px-3 py-2 font-medium">Entry</th>
<th className="text-right px-3 py-2 font-medium">Exit</th>
<th className="text-right px-3 py-2 font-medium">Entry date</th>
<th className="text-right px-3 py-2 font-medium">Close date</th>
<th className="text-right px-3 py-2 font-medium">Duration</th>
<th className="text-right px-3 py-2 font-medium">Realized P&L</th>
<th className="text-left px-3 py-2 font-medium">Reason</th>
<th className="text-left px-3 py-2 font-medium">Note</th>
<th className="px-2 py-2 w-8"></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
const isConfirming = confirmDeleteId === t.id
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]',
_isBearishStr(t.strategy ?? '') ? '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>
<td className="px-2 py-2">
{isConfirming ? (
<div className="flex items-center gap-1">
<button
onClick={() => { deleteTrade(t.id, { onSuccess: () => setConfirmDeleteId(null) }) }}
disabled={deleting}
className="text-[10px] font-semibold text-red-400 hover:text-red-300 bg-red-900/30 border border-red-700/40 rounded px-1.5 py-0.5 disabled:opacity-40"
>
{deleting ? '…' : 'Delete'}
</button>
<button onClick={() => setConfirmDeleteId(null)} className="text-slate-600 hover:text-slate-400">
<X className="w-3 h-3" />
</button>
</div>
) : (
<button
onClick={() => setConfirmDeleteId(t.id)}
className="p-1 rounded text-slate-700 hover:text-red-400 hover:bg-red-900/20 transition-colors"
title="Delete this trade"
>
<Trash2 className="w-3.5 h-3.5" />
</button>
)}
</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" /> IBKR Ticket
{!entryPrice && <span className="ml-auto text-slate-600 font-normal normal-case">Price unavailable check in IBKR</span>}
</div>
<div className="grid grid-cols-3 gap-3 text-[10px]">
<div>
<div className="text-slate-600 mb-0.5">Underlying</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">Strategy</div>
<div className="text-slate-200 font-semibold">{strategy}</div>
{entryPrice && <div className="text-slate-600 text-[9px]">Underlying: ${entryPrice.toFixed(2)}</div>}
</div>
<div>
<div className="text-slate-600 mb-0.5">Expiry</div>
<div className="text-slate-200 font-mono">
{expiryDate ? format(expiryDate, 'd MMM yyyy', { locale: fr }) : expiryDays ? `~${expiryDays}d` : '—'}
</div>
<div className="text-slate-600 text-[9px]">{expiryDays ? `${expiryDays}d 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 contract</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">Order type</div>
<div className="text-slate-300 font-semibold">LIMIT</div>
<div className="text-slate-600 text-[9px]">Price = premium in IBKR</div>
</div>
<div>
<div className="text-slate-600 mb-0.5">Max budget</div>
<div className="text-slate-300 font-mono">{maxLoss ? `${Math.abs(maxLoss).toFixed(0)}` : '~1 000€'}</div>
<div className="text-slate-600 text-[9px]">Estimated max loss</div>
</div>
<div>
<div className="text-slate-600 mb-0.5">Target</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 · last {days} days
</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" />
No snapshots click "Re-analyze" in Macro Regime to start logging
</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 (regime change)
</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">History:</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 ?? ''} geo {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" />}
Why scored {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>Regime: <ScenarioBadge dominant={sc.macro_dominant} /></span>
)}
{scCtx.asset_bias && (
<span>Asset bias: <span className="text-slate-300">{scCtx.asset_bias}</span></span>
)}
{sc?.geo_score != null && (
<span>Geo: <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" />}
Why this pattern was created
</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>Created under: <ScenarioBadge dominant={sg.macro_dominant} /></span>
{sg.geo_score != null && <span>Geo: <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 ? 'GPT-4o analysis running…' : 'Analyze with 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" />
GPT-4o Analysis
</div>
{[
{ key: 'diagnostic', label: 'Diagnostic', color: 'text-slate-200' },
{ key: 'what_worked', label: 'What worked', color: 'text-emerald-400' },
{ key: 'what_missed', label: 'What was missed', color: 'text-red-400' },
{ key: 'regime_alignment', label: 'Regime alignment', color: 'text-yellow-400' },
{ key: 'contra_assessment', label: 'Contra-signals', color: 'text-orange-400' },
{ key: 'lesson', label: 'Lesson', color: 'text-blue-300' },
{ key: 'next_cycle', label: 'Next 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 [search, setSearch] = useState('')
const [assetClass, setAssetClass] = useState('all')
const [direction, setDirection] = useState('all')
const allTrades: any[] = ((data as any)?.trades ?? []).filter((t: any) => t.status !== 'closed')
const trades = allTrades.filter((t: any) => {
if ((t.latest_score ?? t.score_at_entry ?? 0) < minScoreFilter) return false
const q = search.toLowerCase()
if (q && !`${t.underlying} ${t.strategy ?? ''} ${t.pattern_name ?? ''}`.toLowerCase().includes(q)) return false
if (assetClass !== 'all' && _normalizeAssetClass(t.asset_class, t.underlying) !== assetClass) return false
const bearish = _isBearishStr(t.strategy ?? '')
if (direction === 'bullish' && bearish) return false
if (direction === 'bearish' && !bearish) return false
return true
})
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 flex-wrap">
<div className="text-xs font-semibold text-slate-500 uppercase tracking-wide">
{trades.length}/{allTrades.length} trades · {withPnl.length} priced
{avgPnl != null && (
<span className={clsx('ml-2 font-bold', avgPnl >= 0 ? 'text-emerald-400' : 'text-red-400')}>
· avg {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>
<FilterBar
search={search} onSearch={setSearch}
assetClass={assetClass} onAssetClass={setAssetClass}
direction={direction} onDirection={setDirection}
/>
{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
? 'No open trades — score patterns to start tracking'
: 'No results for these filters'}
</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">Profile</th>
<th className="text-left px-3 py-2 font-medium">Strategy</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">Net EV</th>
<th className="text-right px-3 py-2 font-medium">Expected move</th>
<th className="text-right px-3 py-2 font-medium">Date</th>
<th className="text-right px-3 py-2 font-medium">Entry price</th>
<th className="text-right px-3 py-2 font-medium">Current price</th>
<th className="text-right px-3 py-2 font-medium">Target/Stop</th>
<th className="text-right px-3 py-2 font-medium">Maturity</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}d` : t.horizon_days != null ? `${t.horizon_days}d` : '—'}
</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' ? 'No price data — monitoring unavailable' : t.price_warning === 'no_entry_price' ? 'Missing entry price' : 'Live price unavailable'}
>
{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}d / {t.horizon_days ?? 90}d ({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}d` : '—'}
</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="Close 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} alerts · last {days} days
</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" />
No alerts score patterns to start tracking
</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` : 'manual only'}
</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 ? 'Launching...' : 'Run a 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" />
No cycles enable auto-cycle in Configuration or run manually
</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' ? 'Manual' : `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} suggested</span>
<span> {run.patterns_added} added</span>
<span>🎯 {run.patterns_scored} scored</span>
{run.geo_score != null && <span>🌐 Geo {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" /> AI Comment
</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">
🎯 Key pattern: <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" />
Lessons from the {commentary.lessons_from_report} report integrated in this 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" />
No performance report available generate a report in "AI Report"
</div>
)}
</div>
)}
{!commentary && ok && (
<div className="text-[11px] text-slate-700 italic">No AI comment generated for this cycle</div>
)}
</div>
)
})}
</div>
)}
</div>
)
}
// ── Page principale ────────────────────────────────────────────────────────────
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">No open positions in the simulated portfolio</div>
<div className="text-xs mt-1">Logged trades will appear here after the next 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">Simulated Portfolio Risk Analysis</span>
<span className="text-xs text-slate-500">{risk.open_count} open positions</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} CONFLICT{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">Breakdown by asset class</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">
Directional conflict {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" /> Concentration alerts
</div>
<div className="space-y-1">
{warnings.map((a: any, i: number) => (
<div key={i} className="text-[11px] text-amber-200/80 flex items-start gap-1.5">
<span className="text-amber-500 shrink-0"></span>
{a.message}
</div>
))}
</div>
</div>
)}
</div>
)}
{/* 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" /> AI Recommendations Portfolio Monitor
</div>
{aiTs && (
<span className="text-[10px] text-slate-600">{aiTs.slice(0, 16).replace('T', ' ')}</span>
)}
</div>
{aiMonitor.assessment && (
<p className="text-xs text-slate-300 mb-2">{aiMonitor.assessment}</p>
)}
{aiMonitor.actions?.length > 0 && (
<div className="space-y-1 mb-2">
{aiMonitor.actions.map((action: any, i: number) => (
<div key={i} className={clsx(
'flex items-start gap-2 text-[11px] rounded px-2 py-1',
action.priority === 'high'
? 'bg-red-900/20 border border-red-700/20 text-red-300'
: 'bg-slate-800/40 border border-slate-700/20 text-slate-300'
)}>
<span className="shrink-0 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">Balanced portfolio no conflicts or over-concentration detected</div>
</div>
)}
</div>
)
}
const TABS = [
{ key: 'cycles', label: 'AI Cycles', icon: Zap },
{ key: 'macro', label: 'Macro Regimes', icon: Activity },
{ key: 'ideas', label: 'Trade Ideas', icon: Target },
{ key: 'mtm', label: 'Open', icon: TrendingUp },
{ key: 'closed', label: 'Closed', icon: Lock },
{ key: 'geo', label: 'Geo Alerts', icon: AlertTriangle },
{ key: 'skipped', label: 'Not logged', icon: XCircle },
] as const
function SkippedTradesSection({ days }: { days: number }) {
const { data, isLoading } = useSkippedTrades(days)
const allTrades: any[] = (data as any)?.trades ?? []
const [search, setSearch] = useState('')
const [assetClass, setAssetClass] = useState('all')
const [reasonFilter, setReasonFilter] = useState('all')
const ASSET_CLASS_BADGE: 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',
}
const allReasons = Array.from(new Set(allTrades.map((t: any) => t.skip_reason).filter(Boolean)))
const trades = allTrades.filter((t: any) => {
const q = search.toLowerCase()
if (q && !`${t.underlying} ${t.strategy ?? ''} ${t.pattern_name ?? ''}`.toLowerCase().includes(q)) return false
if (assetClass !== 'all' && _normalizeAssetClass(t.asset_class, t.underlying) !== assetClass) return false
if (reasonFilter !== 'all' && t.skip_reason !== reasonFilter) return false
return true
})
const byReason = allTrades.reduce((acc: Record<string, number>, t: any) => {
acc[t.skip_reason] = (acc[t.skip_reason] ?? 0) + 1
return acc
}, {})
if (isLoading) return <div className="card animate-pulse h-24" />
if (allTrades.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" />
No unlogged suggestions in the last {days}d all scored trades match at least one risk profile.
</div>
)
}
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" />
Suggested trades not logged {trades.length}/{allTrades.length} over {days}d
<span className="ml-2 text-slate-600 font-normal text-xs">
(score or gain insufficient for any active profile)
</span>
</div>
{/* Reason summary chips */}
<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>
{/* Filters */}
<div className="flex items-center gap-2 flex-wrap mb-4">
<FilterBar
search={search} onSearch={setSearch}
assetClass={assetClass} onAssetClass={setAssetClass}
/>
{/* Reason filter */}
{allReasons.length > 1 && (
<div className="flex items-center gap-0.5 bg-dark-700 rounded p-0.5">
<button onClick={() => setReasonFilter('all')}
className={clsx('px-2 py-1 rounded text-xs transition-colors',
reasonFilter === 'all' ? 'bg-slate-600 text-white' : 'text-slate-400 hover:text-slate-200')}>
All
</button>
{allReasons.map(r => (
<button key={r} onClick={() => setReasonFilter(r)}
className={clsx('px-2 py-1 rounded text-xs transition-colors',
reasonFilter === r ? 'bg-slate-600 text-white' : 'text-slate-400 hover:text-slate-200')}>
{r}
</button>
))}
</div>
)}
</div>
{trades.length === 0 ? (
<div className="text-center py-6 text-slate-600 text-sm">No results for these filters</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">Strategy</th>
<th className="px-2 py-2 text-right">Score</th>
<th className="px-2 py-2 text-right">Expected gain</th>
<th className="px-2 py-2 text-left">Class</th>
<th className="pr-3 py-2 text-left">Skip reason</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_BADGE[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 reset')
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('Reset error')
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" /> Trading Journal
</h1>
<p className="text-xs text-slate-500 mt-0.5">
Macro regime history · Trade mark-to-market · Geopolitical risk evolution
</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">Confirm?</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 ? 'Resetting...' : confirmReset ? 'Clear all' : '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}d
</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">macro snapshots</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">regime transitions</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">avg geo score</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">logged trades</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} directional conflict{riskAlertCount > 1 ? 's' : ''} detected
</span>
<span className="text-xs text-slate-500"> see Risk Dashboard Simulated</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>
)
}