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

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

463 lines
21 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 } from 'react'
import {
useGeoRiskScore, useAllQuotes,
useCalendar, usePortfolioSummary, useLastScores, useAllPatterns, useMacroRegime,
useTradeMtm, useRiskDashboard,
useSimPortfolioRisk, useCycleStatus, useKnowledgeState,
} from '../hooks/useApi'
import { Clock, Globe, ShieldAlert, ArrowUpRight, Brain } 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: '📉',
}
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 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] ?? {}
return { dominant: dom, label: m.label, color: m.color, emoji: m.emoji, assetBias }
}, [macroData])
const gauge = riskScore ? riskGauge(riskScore.score) : null
const radarData = riskScore?.breakdown
? Object.entries(riskScore.breakdown).map(([k, v]) => ({ subject: k.replace('_', ' '), score: v }))
: []
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 Simulé */}
{(() => {
const trades: any[] = (tradeMtmData as any)?.trades ?? []
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
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]">📊 PnL Simulé</span>
<ArrowUpRight className="w-3 h-3 text-slate-600" />
</div>
<div className={clsx('text-2xl font-bold font-mono mt-1',
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>
</Link>
)
})()}
{/* Risque Simulé */}
{(() => {
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
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 Simulé</span>
<ArrowUpRight className="w-3 h-3 text-slate-600" />
</div>
<div className={clsx('text-2xl font-bold mt-1', alertCount > 0 ? 'text-red-400' : 'text-emerald-400')}>
{alertCount > 0 ? `${alertCount} alerte${alertCount > 1 ? 's' : ''}` : 'OK'}
</div>
<div className="text-[10px] text-slate-500 mt-1">
{openCount} positions
{conflictCount > 0 && <span className="text-red-400 ml-1">· {conflictCount} conflit{conflictCount > 1 ? 's' : ''}</span>}
</div>
</Link>
)
})()}
{/* Dernier Cycle */}
{(() => {
const last = (cycleStatusData as any)?.last_cycle
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`
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-2xl font-bold text-blue-400 mt-1">{elapsedStr}</div>
<div className="text-[10px] text-slate-500 mt-1">
{last
? `${last.patterns_added ?? 0} loggés · géo ${last.geo_score ?? '—'}`
: 'Aucun cycle enregistré'}
</div>
</Link>
)
})()}
{/* Régime Macro */}
{(() => {
const dom = macroInfo?.dominant
const colorClass = dom === 'growth' ? 'text-emerald-400'
: dom === 'stagflation' || dom === 'recession' ? 'text-red-400'
: dom === 'deflation' ? 'text-blue-300'
: 'text-slate-400'
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-lg font-bold mt-1', colorClass)}>
{macroInfo ? `${macroInfo.emoji} ${macroInfo.label}` : '—'}
</div>
<div className="text-[10px] text-slate-500 mt-1 line-clamp-1">
{macroInfo
? Object.entries(macroInfo.assetBias).slice(0, 2).map(([k, v]) => `${k}${v}`).join(' · ')
: 'Chargement...'}
</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 */}
{(() => {
const last = (cycleStatusData as any)?.last_cycle
const added: number = last?.patterns_added ?? 0
const scored: number = last?.patterns_scored ?? 0
const geoScore: number | null = last?.geo_score ?? null
const mtmTrades: any[] = (tradeMtmData as any)?.trades ?? []
const recent = mtmTrades.slice(0, 3)
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>
<div className="text-2xl font-bold text-emerald-400 mt-1">+{added}</div>
{recent.length > 0 ? (
<div className="space-y-0.5 mt-1">
{recent.map((t: any) => (
<div key={t.id} className="flex justify-between text-[10px]">
<span className="text-slate-500 truncate font-mono">{t.underlying ?? '—'}</span>
<span className="text-slate-400 shrink-0 ml-1">
{t.direction === 'bearish' ? '🐻' : '🐂'}
</span>
</div>
))}
</div>
) : (
<div className="text-[10px] text-slate-600 mt-1">
{scored > 0 ? `${scored} scorés` : 'Aucun cycle enregistré'}
{geoScore !== null && ` · géo ${geoScore}`}
</div>
)}
</Link>
)
})()}
{/* Meilleur Pattern scoré */}
{(() => {
const best = [...allPatterns]
.map(p => ({ p, sp: scoreMap[p.id] }))
.filter(x => x.sp?.score != null)
.sort((a, b) => (b.sp.score ?? 0) - (a.sp.score ?? 0))[0]
const score = best?.sp?.score ?? null
const name = best?.p?.name ?? null
const ticker = best?.sp?.recommended_trade?.underlying ?? null
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]"> Meilleur Pattern</span>
<ArrowUpRight className="w-3 h-3 text-slate-600" />
</div>
<div className={clsx('text-2xl font-bold font-mono mt-1', score !== null ? scoreColor(score) : 'text-slate-500')}>
{score ?? '—'}
</div>
<div className="text-[10px] text-slate-500 mt-1 line-clamp-1">
{name ?? 'Aucun scoré'}{ticker ? ` · ${ticker}` : ''}
</div>
</Link>
)
})()}
{/* Patterns Actifs */}
{(() => {
const total = allPatterns.length
const scored = allPatterns.filter(p => scoreMap[p.id]).length
const unscored = total - scored
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]">📋 Patterns Actifs</span>
<ArrowUpRight className="w-3 h-3 text-slate-600" />
</div>
<div className="text-2xl font-bold text-slate-200 mt-1">{total}</div>
<div className="text-[10px] text-slate-500 mt-1">
<span className="text-emerald-500">{scored} scorés</span>
{unscored > 0 && <span className="text-slate-600"> · {unscored} à scorer</span>}
</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>
)
}