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:
OpenSquared
2026-06-19 19:01:58 +02:00
parent 8f15dff721
commit 08651551db
5 changed files with 383 additions and 4 deletions

View File

@@ -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 */}