feat: cockpit command center + skipped trades journal
Dashboard: insert 2 rows of 4 mini-cards between top row and trade ideas - Row 1: PnL Simulé, Risque Simulé, Dernier Cycle, Régime Macro - Row 2: Super Contexte, Signaux Géo, Meilleur Pattern, Patterns Actifs - All cards link to underlying pages via react-router Link Journal: add 'Non loggés' tab exposing trades suggested by cycle but skipped because no risk profile was matched - New skipped_trades table (auto-created on backend restart) - log_trade_entries() persists each skip with score/gain/asset_class - GET /api/journal/skipped-trades + useSkippedTrades hook Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -4,8 +4,10 @@ import {
|
||||
useCalendar, useAiStatus, usePortfolioSummary, useAddPosition,
|
||||
useScorePatterns, useLastScores, useAllPatterns, useMacroRegime,
|
||||
usePortfolioPositions, useTradeMtm, useRiskProfiles, useRiskDashboard,
|
||||
useSimPortfolioRisk, useCycleStatus, useKnowledgeState,
|
||||
} from '../hooks/useApi'
|
||||
import { Target, Clock, Brain, Globe, Plus, RefreshCw, ChevronDown, ChevronUp, CheckCircle2, ShieldAlert, LayoutGrid, List, Terminal } from 'lucide-react'
|
||||
import { Target, Clock, Brain, Globe, Plus, RefreshCw, ChevronDown, ChevronUp, CheckCircle2, ShieldAlert, LayoutGrid, List, Terminal, ArrowUpRight } from 'lucide-react'
|
||||
import { Link } from 'react-router-dom'
|
||||
import clsx from 'clsx'
|
||||
import type { Quote } from '../types'
|
||||
import { format } from 'date-fns'
|
||||
@@ -780,6 +782,9 @@ export default function Dashboard() {
|
||||
const { data: tradeMtmData } = useTradeMtm(30)
|
||||
const { data: riskProfilesData } = useRiskProfiles()
|
||||
const { data: riskDashboard } = useRiskDashboard()
|
||||
const { data: simRisk } = useSimPortfolioRisk()
|
||||
const { data: cycleStatusData } = useCycleStatus()
|
||||
const { data: knowledgeState } = useKnowledgeState()
|
||||
const { mutate: scorePatterns, isPending: scoring } = useScorePatterns()
|
||||
const { mutate: addPos } = useAddPosition()
|
||||
|
||||
@@ -1101,6 +1106,212 @@ export default function Dashboard() {
|
||||
</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>
|
||||
)
|
||||
})()}
|
||||
|
||||
{/* Signaux Géo */}
|
||||
{(() => {
|
||||
const topRisks: Array<[string, number]> = riskScore?.top_risks ?? []
|
||||
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]">🌍 Signaux Géo</span>
|
||||
<ArrowUpRight className="w-3 h-3 text-slate-600" />
|
||||
</div>
|
||||
<div className={clsx('text-2xl font-bold font-mono mt-1', gauge ? gauge.color : 'text-slate-500')}>
|
||||
{riskScore?.score ?? '—'}
|
||||
</div>
|
||||
<div className="space-y-0.5 mt-1">
|
||||
{topRisks.slice(0, 2).map(([cat, val]) => (
|
||||
<div key={cat} className="flex justify-between text-[10px]">
|
||||
<span className="text-slate-500 capitalize truncate">{cat.replace(/_/g, ' ')}</span>
|
||||
<span className="text-slate-400 shrink-0 ml-1">{Math.round(val * 100)}%</span>
|
||||
</div>
|
||||
))}
|
||||
</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>
|
||||
|
||||
{/* ── Trade ideas scorées par IA ── */}
|
||||
<div>
|
||||
{/* Toolbar */}
|
||||
|
||||
Reference in New Issue
Block a user