Files
OpenFin/frontend/src/pages/Dashboard.tsx
OpenSquared bbc9461a8b fix: Trades du cycle — show suggested trade details for non-logged patterns
For patterns added in the cycle but below log threshold, display the
recommended_trade (underlying + strategy + score) from scoreMap instead
of just the pattern name. Pattern name moves to secondary small italic line.

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

791 lines
40 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 { useMemo, useState } from 'react'
import {
useGeoRiskScore, useAllQuotes,
useCalendar, usePortfolioSummary, useLastScores, useAllPatterns, useMacroRegime,
useTradeMtm, useRiskDashboard, useGeoNews,
useSimPortfolioRisk, useCycleStatus, useKnowledgeState,
} from '../hooks/useApi'
import { Clock, Globe, ShieldAlert, ArrowUpRight, Brain, Newspaper } from 'lucide-react'
import { Link } from 'react-router-dom'
import clsx from 'clsx'
import type { Quote } from '../types'
import { format } from 'date-fns'
import { fr } from 'date-fns/locale'
import { RadarChart, PolarGrid, PolarAngleAxis, Radar, ResponsiveContainer } from 'recharts'
import { scoreColor } from '../components/TradeIdeas'
const riskGauge = (score: number) => {
if (score < 25) return { color: 'text-emerald-400', bg: 'bg-emerald-500', label: 'FAIBLE' }
if (score < 50) return { color: 'text-yellow-400', bg: 'bg-yellow-500', label: 'MODÉRÉ' }
if (score < 75) return { color: 'text-orange-400', bg: 'bg-orange-500', label: 'ÉLEVÉ' }
return { color: 'text-red-400', bg: 'bg-red-500', label: 'EXTRÊME' }
}
const assetEmoji: Record<string, string> = {
energy: '⛽', metals: '🥇', agriculture: '🌾', equities: '📈',
indices: '📊', forex: '💱', crypto: '₿', rates: '📉',
}
const ASSET_COLORS: Record<string, string> = {
energy: '#f97316', metals: '#eab308', agriculture: '#22c55e',
indices: '#3b82f6', equities: '#06b6d4', forex: '#8b5cf6',
rates: '#64748b', unknown: '#334155',
}
const REGIME_LABELS: Record<string, string> = {
goldilocks: 'Goldilocks', desinflation: 'Désinflation', stagflation: 'Stagflation',
recession: 'Récession', crise_liquidite: 'Crise liq.', reflation: 'Reflation',
soft_landing: 'Soft Landing', inflation_shock: 'Infl. Choc', incertain: 'Incertain',
}
function ViewToggle({ value, onChange }: { value: 'simulated' | 'portfolio'; onChange: (v: 'simulated' | 'portfolio') => void }) {
const click = (v: 'simulated' | 'portfolio') => (e: React.MouseEvent) => {
e.preventDefault(); e.stopPropagation(); onChange(v)
}
return (
<div className="flex gap-0.5 bg-dark-800 rounded p-0.5 text-[9px]">
<button onClick={click('simulated')}
className={clsx('px-1.5 py-0.5 rounded transition-colors',
value === 'simulated' ? 'bg-blue-600 text-white' : 'text-slate-600 hover:text-slate-400')}>
Simulé
</button>
<button onClick={click('portfolio')}
className={clsx('px-1.5 py-0.5 rounded transition-colors',
value === 'portfolio' ? 'bg-blue-600 text-white' : 'text-slate-600 hover:text-slate-400')}>
Portfolio
</button>
</div>
)
}
function QuoteRow({ q }: { q: Quote }) {
if (!q.price) return null
const pos = q.change_pct >= 0
return (
<div className="flex items-center justify-between py-1.5 border-b border-slate-700/20 last:border-0">
<span className="text-xs text-white truncate max-w-[140px]">{q.name || q.symbol}</span>
<div className="text-right ml-2">
<div className="text-xs text-white font-mono">{q.price.toFixed(2)}</div>
<div className={clsx('text-xs font-mono', pos ? 'positive' : 'negative')}>
{pos ? '+' : ''}{q.change_pct.toFixed(2)}%
</div>
</div>
</div>
)
}
export default function Dashboard() {
const { data: riskScore, isLoading: riskLoading } = useGeoRiskScore()
const { data: allQuotes } = useAllQuotes()
const { data: calendar } = useCalendar()
const { data: portfolio } = usePortfolioSummary()
const { data: lastScoresData } = useLastScores()
const { data: allPatternsData } = useAllPatterns()
const { data: macroData } = useMacroRegime()
const { data: tradeMtmData } = useTradeMtm(30)
const { data: riskDashboard } = useRiskDashboard()
const { data: simRisk } = useSimPortfolioRisk()
const { data: cycleStatusData } = useCycleStatus()
const { data: knowledgeState } = useKnowledgeState()
const { data: geoNews } = useGeoNews()
const [pnlView, setPnlView] = useState<'simulated' | 'portfolio'>(() =>
(localStorage.getItem('dash_pnl_view') as any) ?? 'simulated'
)
const [riskView, setRiskView] = useState<'simulated' | 'portfolio'>(() =>
(localStorage.getItem('dash_risk_view') as any) ?? 'simulated'
)
const setPnlViewPersist = (v: 'simulated' | 'portfolio') => {
setPnlView(v); localStorage.setItem('dash_pnl_view', v)
}
const setRiskViewPersist = (v: 'simulated' | 'portfolio') => {
setRiskView(v); localStorage.setItem('dash_risk_view', v)
}
const allPatterns: any[] = allPatternsData ?? []
const scoreMap = useMemo(() => {
const map: Record<string, any> = {}
for (const sp of (lastScoresData?.scored_patterns ?? [])) {
if (sp.pattern_id) map[sp.pattern_id] = sp
}
return map
}, [lastScoresData])
const macroInfo = useMemo(() => {
if (!macroData?.scenarios) return null
const sc = macroData.scenarios
const dom = sc.dominant ?? 'incertain'
const m = sc.meta?.[dom] ?? { label: dom, color: '#94a3b8', emoji: '?' }
const assetBias: Record<string, string> = sc.asset_bias?.[dom] ?? {}
// Top 3 scenario scores (excluding dominant, sorted desc)
const ranked: [string, number][] = (sc.ranked ?? []).slice(0, 4)
return { dominant: dom, label: m.label, color: m.color, emoji: m.emoji, assetBias, ranked }
}, [macroData])
const gauge = riskScore ? riskGauge(riskScore.score) : null
const radarData = riskScore?.breakdown
? Object.entries(riskScore.breakdown).map(([k, v]) => ({ subject: k.replace('_', ' '), score: v }))
: []
// Cycle trades: use scoring_run_id (which differs from cycle run_id)
const lastCycle = (cycleStatusData as any)?.last_cycle ?? null
const scoringRunId: string | null = lastCycle?.scoring_run_id ?? null
const mtmTrades: any[] = (tradeMtmData as any)?.trades ?? []
const cycleTrades = scoringRunId ? mtmTrades.filter((t: any) => t.run_id === scoringRunId) : []
// Patterns from the last cycle only (filter by created_at >= cycle started_at)
const cyclePatterns = useMemo(() => {
const started = lastCycle?.started_at ?? null
if (!started) return []
return [...allPatterns]
.filter(p => p.created_at && p.created_at >= started)
.sort((a, b) => (scoreMap[b.id]?.score ?? -1) - (scoreMap[a.id]?.score ?? -1))
.slice(0, 4)
}, [allPatterns, scoreMap, lastCycle])
// Top impactful news
const topNews = useMemo(() =>
[...(geoNews ?? [])]
.sort((a, b) => b.impact_score - a.impact_score)
.slice(0, 3)
, [geoNews])
return (
<div className="p-6 space-y-6">
{/* Header */}
<div className="flex items-center justify-between">
<div>
<h1 className="text-xl font-bold text-white">Cockpit GeoOptions</h1>
<p className="text-xs text-slate-500 mt-0.5">
{format(new Date(), "EEEE d MMMM yyyy · HH:mm", { locale: fr })}
</p>
</div>
<div className="flex items-center gap-3">
{portfolio && portfolio.open_positions > 0 && (
<div className={clsx('text-sm font-bold', portfolio.unrealized_pnl >= 0 ? 'text-emerald-400' : 'text-red-400')}>
Portfolio: {portfolio.unrealized_pnl >= 0 ? '+' : ''}{portfolio.unrealized_pnl?.toFixed(0)}
</div>
)}
<div className="flex items-center gap-2 text-xs text-slate-500">
<div className="w-2 h-2 rounded-full bg-emerald-400 animate-pulse"></div>
Live
</div>
</div>
</div>
{/* Risk concentration banner */}
{(riskDashboard as any)?.concentration_alerts?.length > 0 && (
<div className="space-y-1.5">
{((riskDashboard as any).concentration_alerts as any[]).slice(0, 2).map((alert: any, i: number) => (
<div key={i} className={clsx('flex items-center gap-2 text-xs px-3 py-2 rounded border', {
'bg-red-900/20 border-red-700/30 text-red-300': alert.level === 'high',
'bg-amber-900/20 border-amber-700/30 text-amber-300': alert.level === 'warning',
})}>
<ShieldAlert className="w-3 h-3 shrink-0" />
{alert.message}
<a href="/risk" className="ml-auto text-slate-500 hover:text-slate-300 underline">Risk Dashboard </a>
</div>
))}
</div>
)}
{/* Top row */}
<div className="grid grid-cols-4 gap-4">
{/* Geo Risk */}
<div className="card col-span-1">
<div className="flex items-center justify-between mb-2">
<div className="section-title flex items-center gap-1 mb-0">
<Globe className="w-3 h-3" /> Risque Géopolitique
</div>
<Link to="/geo" className="flex items-center gap-0.5 text-[10px] text-slate-600 hover:text-slate-300 transition-colors">
News géo <ArrowUpRight className="w-2.5 h-2.5" />
</Link>
</div>
{riskLoading ? (
<div className="animate-pulse h-16 bg-dark-600 rounded"></div>
) : riskScore && gauge ? (
<>
<div className={clsx('text-5xl font-bold', gauge.color)}>{riskScore.score}</div>
<div className={clsx('text-sm font-semibold mt-1', gauge.color)}>{gauge.label}</div>
<div className="mt-3 bg-dark-700 rounded-full h-2">
<div className={clsx('h-2 rounded-full', gauge.bg)} style={{ width: `${riskScore.score}%` }} />
</div>
<div className="mt-2 space-y-1">
{riskScore.top_risks?.map(([cat, val]) => (
<div key={cat} className="flex justify-between text-xs">
<span className="text-slate-500 capitalize">{(cat as string).replace('_', ' ')}</span>
<span className="text-slate-300">{Math.round((val as number) * 100)}%</span>
</div>
))}
</div>
</>
) : <div className="text-slate-500 text-xs">Backend requis</div>}
</div>
{/* Radar */}
<div className="card col-span-1">
<div className="section-title">Cartographie risques</div>
{radarData.length > 0 ? (
<ResponsiveContainer width="100%" height={160}>
<RadarChart data={radarData}>
<PolarGrid stroke="#1e2d4d" />
<PolarAngleAxis dataKey="subject" tick={{ fill: '#64748b', fontSize: 9 }} />
<Radar dataKey="score" stroke="#3b82f6" fill="#3b82f6" fillOpacity={0.25} />
</RadarChart>
</ResponsiveContainer>
) : <div className="h-40 flex items-center justify-center text-slate-600 text-xs">Chargement...</div>}
</div>
{/* Patterns — AI scores */}
<div className="card col-span-1 overflow-y-auto max-h-64">
<div className="section-title flex items-center gap-1">
<Brain className="w-3 h-3" /> Scores patterns
{allPatterns.length > 0 && <span className="text-slate-600 text-xs ml-auto font-normal">{allPatterns.length} patterns</span>}
</div>
<div className="space-y-1.5 mt-1">
{allPatterns.length === 0 ? (
<div className="text-slate-600 text-xs">Backend requis</div>
) : (
[...allPatterns]
.sort((a, b) => {
const spA = scoreMap[a.id], spB = scoreMap[b.id]
const effA = spA ? Math.max(...[0, ...(spA.trade_rankings ?? []).map((r: any) => (spA.score ?? 0) + (r.score_delta ?? 0))]) : -1
const effB = spB ? Math.max(...[0, ...(spB.trade_rankings ?? []).map((r: any) => (spB.score ?? 0) + (r.score_delta ?? 0))]) : -1
return effB - effA
})
.map(p => {
const sp = scoreMap[p.id]
const rawScore = sp?.score ?? null
const bestEff = sp
? Math.max(rawScore ?? 0, ...(sp.trade_rankings ?? []).map((r: any) =>
Math.max(0, Math.min(100, (rawScore ?? 0) + (r.score_delta ?? 0)))))
: null
return (
<div key={p.id} className="flex items-center justify-between gap-2">
<span className="text-xs text-slate-300 truncate flex-1 min-w-0">{p.name}</span>
{bestEff !== null ? (
<span className={clsx('text-xs font-bold shrink-0', scoreColor(bestEff))}>{bestEff}</span>
) : (
<span className="text-xs text-slate-600 shrink-0"></span>
)}
</div>
)
})
)}
</div>
</div>
{/* Calendrier */}
<div className="card col-span-1 space-y-3">
<div className="section-title flex items-center gap-1"><Clock className="w-3 h-3" /> Prochains catalyseurs</div>
<div className="space-y-1.5">
{calendar?.slice(0, 4).map((ev, i) => (
<div key={i} className="flex items-center gap-2 text-xs">
<span className={clsx('badge', {
'badge-red': ev.importance === 'high',
'badge-yellow': ev.importance === 'medium',
'badge-blue': ev.importance === 'low',
})}>
{'!'.repeat(ev.importance === 'high' ? 3 : ev.importance === 'medium' ? 2 : 1)}
</span>
<div className="min-w-0">
<div className="text-white line-clamp-1">{ev.title}</div>
<div className="text-slate-600">{ev.date?.slice(0, 10)}</div>
</div>
</div>
))}
</div>
</div>
</div>
{/* ── Command Center: Résumé Opérationnel ── */}
<div className="grid grid-cols-4 gap-3">
{/* PnL */}
{(() => {
const trades: any[] = mtmTrades
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
const winners = withPnl.filter((t: any) => t.pnl_pct > 0).length
const losers = withPnl.filter((t: any) => t.pnl_pct < 0).length
const closedTrades = trades.filter((t: any) => t.status === 'closed')
const openTrades = trades.filter((t: any) => t.status !== 'closed')
// Use capital_invested if set, otherwise entry_price as proxy cost
const totalCapital = trades.reduce((s: number, t: any) =>
s + (t.capital_invested ?? t.entry_price ?? 0), 0)
const totalProfit = withPnl.reduce((s: number, t: any) =>
s + ((t.capital_invested ?? t.entry_price ?? 0) * t.pnl_pct / 100), 0)
const isEstimated = trades.some((t: any) => t.capital_invested == null && t.entry_price != null)
const targetHit = trades.filter((t: any) => t.alert_type === 'target_reached').length
const stopHit = trades.filter((t: any) => t.alert_type === 'stop_loss').length
const pf = portfolio as any
const pfPnlPct = pf?.unrealized_pnl_pct ?? null
const pfPnl = pf?.unrealized_pnl ?? null
const pfInvest = pf?.total_invested ?? null
const pfReal = pf?.realized_pnl ?? null
return (
<Link to="/journal" className="card block hover:border-slate-600/60 transition-all cursor-pointer">
<div className="flex items-center justify-between mb-1">
<span className="section-title mb-0 text-[10px]">📊 P&L</span>
<div className="flex items-center gap-1.5">
<ViewToggle value={pnlView} onChange={setPnlViewPersist} />
<ArrowUpRight className="w-3 h-3 text-slate-600" />
</div>
</div>
{pnlView === 'simulated' ? (
<>
<div className="flex items-end justify-between mt-1">
<div>
<div className={clsx('text-2xl font-bold font-mono',
avgPnl === null ? 'text-slate-500' : avgPnl >= 0 ? 'text-emerald-400' : 'text-red-400')}>
{avgPnl !== null ? `${avgPnl >= 0 ? '+' : ''}${avgPnl.toFixed(1)}%` : '—'}
</div>
<div className="text-[10px] text-slate-500 mt-1">
{trades.length} trades · <span className="text-emerald-500">{winners}</span>{' '}
<span className="text-red-400">{losers}</span>
</div>
</div>
<div className="text-right space-y-0.5">
{targetHit > 0 && <div className="text-[10px] text-emerald-400">🎯 {targetHit} cible{targetHit > 1 ? 's' : ''}</div>}
{stopHit > 0 && <div className="text-[10px] text-red-400"> {stopHit} stop</div>}
<div className="text-[10px] text-slate-600">{withPnl.length} pricés</div>
</div>
</div>
<div className="mt-2 pt-2 border-t border-slate-700/30 flex justify-between items-end">
<div>
<div className="text-[10px] text-slate-500 mb-0.5">
Investi{isEstimated ? ' ~' : ''}
</div>
<div className="text-2xl font-bold font-mono text-slate-300">
{totalCapital > 0 ? `${totalCapital.toFixed(0)}` : '—'}
</div>
</div>
<div className="text-right">
<div className="text-[10px] text-slate-500 mb-0.5">P&L </div>
<div className={clsx('text-2xl font-bold font-mono',
totalCapital === 0 ? 'text-slate-600' : totalProfit >= 0 ? 'text-emerald-400' : 'text-red-400')}>
{totalCapital > 0 ? `${totalProfit >= 0 ? '+' : ''}${totalProfit.toFixed(0)}` : '—'}
</div>
</div>
</div>
</>
) : (
<div className="flex items-end justify-between mt-1">
<div>
<div className={clsx('text-2xl font-bold font-mono',
pfPnlPct === null ? 'text-slate-500' : pfPnlPct >= 0 ? 'text-emerald-400' : 'text-red-400')}>
{pfPnlPct !== null ? `${pfPnlPct >= 0 ? '+' : ''}${pfPnlPct.toFixed(1)}%` : '—'}
</div>
<div className="text-[10px] text-slate-500 mt-1">
{pf?.open_positions ?? 0} positions ouvertes
</div>
</div>
<div className="text-right space-y-0.5">
{pfInvest != null && <div className="text-[10px] text-slate-400 font-mono">{pfInvest.toFixed(0)} investi</div>}
{pfPnl != null && <div className={clsx('text-[10px] font-mono font-bold', pfPnl >= 0 ? 'text-emerald-400' : 'text-red-400')}>
{pfPnl >= 0 ? '+' : ''}{pfPnl.toFixed(0)}
</div>}
{pfReal != null && pfReal !== 0 && <div className={clsx('text-[10px] font-mono', pfReal >= 0 ? 'text-emerald-600' : 'text-red-600')}>
réal. {pfReal >= 0 ? '+' : ''}{pfReal.toFixed(0)}
</div>}
</div>
</div>
)}
</Link>
)
})()}
{/* Risk */}
{(() => {
const risk = simRisk as any
const alertCount: number = risk?.alerts?.length ?? 0
const conflictCount: number = risk?.conflicts?.length ?? 0
const openCount: number = risk?.open_count ?? 0
const concentration: Record<string, any> = risk?.concentration ?? {}
const sortedClasses = Object.entries(concentration)
.sort(([, a], [, b]) => (b as any).pct - (a as any).pct)
.slice(0, 5)
return (
<Link to="/risk" className="card block hover:border-slate-600/60 transition-all cursor-pointer">
<div className="flex items-center justify-between mb-1">
<span className="section-title mb-0 text-[10px]">🛡 Risque</span>
<div className="flex items-center gap-1.5">
<ViewToggle value={riskView} onChange={setRiskViewPersist} />
<ArrowUpRight className="w-3 h-3 text-slate-600" />
</div>
</div>
<div className={clsx('text-lg font-bold mt-0.5', alertCount > 0 ? 'text-red-400' : 'text-emerald-400')}>
{alertCount > 0 ? `${alertCount} alerte${alertCount > 1 ? 's' : ''}` : 'OK'}
</div>
{sortedClasses.length > 0 && (
<div className="mt-2 space-y-1">
{sortedClasses.map(([cls, data]: [string, any]) => (
<div key={cls} className="flex items-center gap-1.5">
<span className="text-[9px] text-slate-500 w-12 truncate capitalize">{cls}</span>
<div className="flex-1 h-2 bg-dark-700 rounded-full overflow-hidden flex">
{data.bullish > 0 && (
<div className="h-full rounded-l-full" title={`${data.bullish} haussier`}
style={{ width: `${(data.bullish / openCount) * 100}%`, background: ASSET_COLORS[cls] ?? '#3b82f6', opacity: 0.9 }} />
)}
{data.bearish > 0 && (
<div className="h-full rounded-r-full" title={`${data.bearish} baissier`}
style={{ width: `${(data.bearish / openCount) * 100}%`, background: '#ef4444', opacity: 0.7 }} />
)}
</div>
<span className="text-[9px] text-slate-600 w-7 text-right font-mono">{data.pct}%</span>
</div>
))}
<div className="flex items-center gap-1 text-[9px] text-slate-700 pt-0.5">
<span className="inline-block w-2 h-1.5 rounded-sm bg-blue-500/60"></span>haussier
<span className="inline-block w-2 h-1.5 rounded-sm bg-red-500/60 ml-1"></span>baissier
{conflictCount > 0 && <span className="ml-auto text-red-400">{conflictCount} conflit{conflictCount > 1 ? 's' : ''}</span>}
</div>
</div>
)}
{sortedClasses.length === 0 && (
<div className="text-[10px] text-slate-600 mt-1">
{openCount > 0 ? `${openCount} positions` : 'Aucune position'}
</div>
)}
</Link>
)
})()}
{/* Dernier Cycle — bilan complet */}
{(() => {
const last = lastCycle
const ts = last?.ts
? new Date(last.ts.endsWith('Z') ? last.ts : last.ts + 'Z')
: null
const elapsed = ts ? Math.round((Date.now() - ts.getTime()) / 60_000) : null
const elapsedStr = elapsed === null ? '—'
: elapsed < 60 ? `${elapsed}min`
: elapsed < 1440 ? `${Math.round(elapsed / 60)}h`
: `${Math.round(elapsed / 1440)}j`
const patternsAdded: number = last?.patterns_added ?? 0
const patternsScored: number = last?.patterns_scored ?? 0
const tradesLogged: number = cycleTrades.length
const tradesNotLogged: number = Math.max(0, patternsAdded - tradesLogged)
const tradesClosed = cycleTrades.filter((t: any) => t.status === 'closed').length
const commentary = last?.commentary
? (() => { try { return typeof last.commentary === 'string' ? JSON.parse(last.commentary) : last.commentary } catch { return null } })()
: null
return (
<Link to="/journal" className="card block hover:border-slate-600/60 transition-all cursor-pointer">
<div className="flex items-center justify-between mb-1">
<span className="section-title mb-0 text-[10px]">🔄 Dernier Cycle</span>
<ArrowUpRight className="w-3 h-3 text-slate-600" />
</div>
<div className="text-xl font-bold text-blue-400 mt-0.5 font-mono">{elapsedStr}</div>
{last ? (
<>
<div className="mt-2 grid grid-cols-2 gap-1.5 text-center">
<div className="bg-dark-700/60 rounded px-2 py-2">
<div className="text-xl font-bold text-slate-200">+{patternsAdded}</div>
<div className="text-[9px] text-slate-500 mt-0.5">patterns ajoutés</div>
</div>
<div className="bg-dark-700/60 rounded px-2 py-2">
<div className="text-xl font-bold text-blue-400">+{tradesLogged}</div>
<div className="text-[9px] text-slate-500 mt-0.5">trades ajoutés</div>
{tradesNotLogged > 0 && (
<div className="text-[8px] text-slate-700 mt-0.5">{tradesNotLogged} non-loggé{tradesNotLogged > 1 ? 's' : ''}</div>
)}
</div>
<div className="bg-dark-700/60 rounded px-2 py-1.5">
<div className="text-base font-bold text-emerald-400">{patternsScored}</div>
<div className="text-[9px] text-slate-600">scorés</div>
</div>
<div className="bg-dark-700/60 rounded px-2 py-1.5">
<div className="text-base font-bold text-slate-500">{tradesClosed}</div>
<div className="text-[9px] text-slate-600">fermés</div>
</div>
</div>
{commentary?.commentary && (
<div className="mt-1.5 text-[9px] text-slate-500 line-clamp-2 italic leading-tight">
{commentary.commentary.slice(0, 100)}
</div>
)}
</>
) : (
<div className="text-[10px] text-slate-600 mt-1">Aucun cycle enregistré</div>
)}
</Link>
)
})()}
{/* Régime Macro */}
{(() => {
const dom = macroInfo?.dominant
const colorClass = dom === 'growth' || dom === 'goldilocks' || dom === 'soft_landing' ? 'text-emerald-400'
: dom === 'stagflation' || dom === 'recession' || dom === 'crise_liquidite' ? 'text-red-400'
: dom === 'deflation' || dom === 'desinflation' ? 'text-blue-300'
: dom === 'inflation_shock' ? 'text-orange-400'
: 'text-slate-400'
const ranked = macroInfo?.ranked ?? []
return (
<Link to="/macro" className="card block hover:border-slate-600/60 transition-all cursor-pointer">
<div className="flex items-center justify-between mb-1">
<span className="section-title mb-0 text-[10px]">🌐 Régime Macro</span>
<ArrowUpRight className="w-3 h-3 text-slate-600" />
</div>
<div className={clsx('text-base font-bold mt-1', colorClass)}>
{macroInfo ? `${macroInfo.emoji} ${macroInfo.label}` : '—'}
</div>
{ranked.length > 0 && (
<div className="mt-1.5 space-y-1">
{ranked.slice(0, 4).map(([key, score]: [string, number]) => {
const maxScore = ranked[0]?.[1] ?? 1
const pct = Math.round((score / Math.max(maxScore, 1)) * 100)
const isDom = key === dom
return (
<div key={key} className="flex items-center gap-1.5">
<span className={clsx('text-[9px] w-16 truncate', isDom ? 'text-slate-200 font-semibold' : 'text-slate-500')}>
{REGIME_LABELS[key] ?? key}
</span>
<div className="flex-1 h-1.5 bg-dark-700 rounded-full overflow-hidden">
<div
className={clsx('h-full rounded-full transition-all', isDom ? 'bg-blue-500' : 'bg-slate-600')}
style={{ width: `${pct}%` }}
/>
</div>
<span className={clsx('text-[9px] font-mono w-5 text-right', isDom ? 'text-blue-400' : 'text-slate-600')}>
{score}
</span>
</div>
)
})}
</div>
)}
</Link>
)
})()}
</div>
{/* ── Command Center: Intelligence & Contexte ── */}
<div className="grid grid-cols-4 gap-3">
{/* Super Contexte IA */}
{(() => {
const state = (knowledgeState as any)?.state
const synthesis = state?.synthesis
const insights = (synthesis?.regime_insights?.length ?? 0)
+ (synthesis?.pattern_insights?.length ?? 0)
+ (synthesis?.recurring_mistakes?.length ?? 0)
const priority = synthesis?.strategic_priorities?.[0]
const excerpt = typeof priority === 'string' ? priority
: typeof state?.narrative === 'string' ? state.narrative.slice(0, 90)
: null
return (
<Link to="/super-contexte" className="card block hover:border-slate-600/60 transition-all cursor-pointer">
<div className="flex items-center justify-between mb-1">
<span className="section-title mb-0 text-[10px]">🧠 Super Contexte</span>
<ArrowUpRight className="w-3 h-3 text-slate-600" />
</div>
<div className="text-sm font-bold text-purple-400 mt-1">
{state ? `${insights} insights actifs` : 'Aucune synthèse'}
</div>
<div className="text-[10px] text-slate-500 mt-1 line-clamp-2">
{excerpt ?? 'Lancer une synthèse dans Super Contexte'}
</div>
</Link>
)
})()}
{/* Trades du dernier cycle — détaillé */}
{(() => {
// Build a map: pattern_id → trade for quick lookup
const tradeByPattern: Record<string, any> = {}
for (const t of cycleTrades) {
if (t.pattern_id) tradeByPattern[t.pattern_id] = t
}
// Show cyclePatterns with their trade status (loggé or not)
const rows = cyclePatterns.length > 0
? cyclePatterns
: cycleTrades.map((t: any) => ({ id: t.pattern_id, name: t.pattern_name }))
return (
<Link to="/journal" className="card block hover:border-slate-600/60 transition-all cursor-pointer">
<div className="flex items-center justify-between mb-1">
<span className="section-title mb-0 text-[10px]">📥 Trades du cycle</span>
<ArrowUpRight className="w-3 h-3 text-slate-600" />
</div>
{rows.length > 0 ? (
<div className="space-y-2 mt-1">
{rows.slice(0, 3).map((p: any, i: number) => {
const t = tradeByPattern[p.id]
const logged = !!t
const ev: number | null = t?.ev_net ?? t?.ev_at_entry ?? null
const score: number | null = t?.score_at_entry ?? scoreMap[p.id]?.score ?? null
const pnl: number | null = t?.pnl_pct ?? null
const isBear = t?.strategy?.toLowerCase().includes('put') || t?.strategy?.toLowerCase().includes('bear')
return (
<div key={i}>
{logged ? (
<>
{/* Trade row — primary */}
<div className="flex items-center gap-1.5 text-[10px]">
<span className="shrink-0">{isBear ? '🐻' : '🐂'}</span>
<span className="font-mono text-slate-200 font-bold w-10 truncate">{t.underlying ?? '—'}</span>
<span className="text-slate-400 truncate flex-1">{t.strategy ?? ''}</span>
{ev !== null && (
<span className={clsx('font-mono shrink-0 text-[9px]', ev >= 0.5 ? 'text-emerald-400' : ev >= 0 ? 'text-yellow-400' : 'text-red-400')}>
EV{ev >= 0 ? '+' : ''}{ev.toFixed(2)}
</span>
)}
{score !== null && (
<span className={clsx('font-mono shrink-0 font-bold', scoreColor(score))}>{score}</span>
)}
{pnl !== null && (
<span className={clsx('font-mono font-bold shrink-0', pnl >= 0 ? 'text-emerald-400' : 'text-red-400')}>
{pnl >= 0 ? '+' : ''}{pnl.toFixed(1)}%
</span>
)}
</div>
{/* Pattern name — secondary */}
<div className="text-[8px] text-slate-600 italic truncate mt-0.5 ml-4">{p.name}</div>
</>
) : (
<>
{/* Suggested trade from scoreMap — primary */}
{(() => {
const sp = scoreMap[p.id]
const rec = sp?.recommended_trade ?? sp?.trade_rankings?.[0]
const underlying = rec?.underlying ?? rec?.ticker ?? null
const strategy = rec?.strategy ?? rec?.trade_type ?? null
const evVal: number | null = rec?.ev_net ?? rec?.ev ?? null
const isBearS = strategy?.toLowerCase().includes('put') || strategy?.toLowerCase().includes('bear')
return (
<>
<div className="flex items-center gap-1.5 text-[10px]">
<span className="text-slate-600 shrink-0">{isBearS ? '🐻' : '🐂'}</span>
<span className={clsx('font-mono font-bold w-10 truncate shrink-0', underlying ? 'text-slate-400' : 'text-slate-700')}>
{underlying ?? '—'}
</span>
<span className="text-slate-600 truncate flex-1">{strategy ?? 'trade suggéré'}</span>
{evVal !== null && (
<span className="font-mono text-[9px] text-slate-600">
EV{evVal >= 0 ? '+' : ''}{evVal.toFixed(2)}
</span>
)}
{score !== null && (
<span className={clsx('font-mono shrink-0 font-bold', scoreColor(score))}>{score}</span>
)}
<span className="text-[8px] text-slate-700 shrink-0">non loggé</span>
</div>
<div className="text-[8px] text-slate-700 italic truncate mt-0.5 ml-4">{p.name}</div>
</>
)
})()}
</>
)}
</div>
)
})}
</div>
) : (
<div className="text-[10px] text-slate-600 mt-1">
{lastCycle ? 'Aucun pattern ajouté ce cycle' : 'En attente du prochain cycle'}
</div>
)}
</Link>
)
})()}
{/* Patterns du dernier cycle */}
{(() => {
return (
<Link to="/patterns" className="card block hover:border-slate-600/60 transition-all cursor-pointer">
<div className="flex items-center justify-between mb-1">
<span className="section-title mb-0 text-[10px]"> Pattern du cycle</span>
<ArrowUpRight className="w-3 h-3 text-slate-600" />
</div>
{cyclePatterns.length > 0 ? (
<div className="space-y-1.5 mt-1">
{cyclePatterns.map((p, i) => {
const sp = scoreMap[p.id]
const score = sp?.score ?? null
const ticker = sp?.recommended_trade?.underlying ?? null
return (
<div key={p.id} className="flex items-center gap-1.5">
<span className="text-[10px] text-slate-700 w-3 shrink-0 font-mono">{i + 1}</span>
<div className="flex-1 min-w-0">
<div className="text-[10px] text-slate-300 truncate leading-tight">{p.name}</div>
{ticker && <div className="text-[9px] text-slate-600 font-mono">{ticker}</div>}
</div>
{score !== null ? (
<span className={clsx('text-sm font-bold font-mono shrink-0', scoreColor(score))}>{score}</span>
) : (
<span className="text-[9px] text-slate-700 shrink-0"></span>
)}
</div>
)
})}
</div>
) : (
<div className="text-[10px] text-slate-600 mt-1">
{lastCycle ? 'Aucun pattern ajouté ce cycle' : 'Aucun scoré — lancer le scoring IA'}
</div>
)}
</Link>
)
})()}
{/* Top news impactantes */}
{(() => {
return (
<Link to="/geo" className="card block hover:border-slate-600/60 transition-all cursor-pointer">
<div className="flex items-center justify-between mb-1">
<span className="section-title mb-0 text-[10px] flex items-center gap-1">
<Newspaper className="w-2.5 h-2.5" /> Top News
</span>
<ArrowUpRight className="w-3 h-3 text-slate-600" />
</div>
{topNews.length > 0 ? (
<div className="space-y-2 mt-1">
{topNews.map((n, i) => {
const impact = Math.round(n.impact_score * 100)
const impactColor = impact >= 70 ? 'text-red-400' : impact >= 40 ? 'text-orange-400' : 'text-yellow-400'
const barColor = impact >= 70 ? 'bg-red-500' : impact >= 40 ? 'bg-orange-500' : 'bg-yellow-500'
return (
<div key={i} className="space-y-0.5">
<div className="flex items-start gap-1.5">
<span className="text-[9px] text-slate-700 font-mono mt-0.5 shrink-0">{i + 1}</span>
<p className="text-[10px] text-slate-300 leading-tight line-clamp-2 flex-1">{n.title}</p>
<span className={clsx('text-[10px] font-bold font-mono shrink-0', impactColor)}>{impact}</span>
</div>
<div className="ml-3 bg-dark-700 rounded-full h-0.5">
<div className={clsx('h-0.5 rounded-full', barColor)} style={{ width: `${impact}%` }} />
</div>
</div>
)
})}
</div>
) : (
<div className="text-[10px] text-slate-600 mt-1">Chargement des news</div>
)}
</Link>
)
})()}
</div>
{/* Markets mini overview */}
<div className="grid grid-cols-4 gap-3">
{['energy', 'metals', 'indices', 'forex'].map(cls => (
<div key={cls} className="card">
<div className="section-title">{assetEmoji[cls]} {cls}</div>
{allQuotes?.[cls]?.map(q => <QuoteRow key={q.symbol} q={q} />) ?? (
<div className="text-xs text-slate-600">Chargement...</div>
)}
</div>
))}
</div>
</div>
)
}