fix: Dashboard cycle coherence — scoring_run_id linkage + cycle-scoped cards

- Backend: get_status() now resolves scoring_run_id by querying
  trade_entry_prices within the cycle time window, fixing the mismatch
  between cycle run_id and the id actually written to trades
- Dashboard: Trades du cycle filters by scoring_run_id (no stale fallback)
- Dashboard: Pattern du cycle shows only patterns added in last cycle
  (created_at >= started_at), renamed from Top Patterns
- Dashboard: Dernier Cycle now shows 4 stats (patterns/scorés/loggés/fermés)
  + IA commentary snippet
- Dashboard: P&L simulated mode bottom half shows open/closed/capital/profit
- Dashboard: Régime Macro shows top 4 scenario score bars

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
OpenSquared
2026-06-19 20:49:44 +02:00
parent c415248bb1
commit d94052dd91
2 changed files with 170 additions and 66 deletions

View File

@@ -1090,6 +1090,25 @@ def get_status() -> Dict[str, Any]:
recent = get_cycle_runs(limit=1)
last = recent[0] if recent else None
# Enrich last_cycle with the scoring_run_id used for trade_entry_prices
if last:
try:
from services.database import get_conn
_conn = get_conn()
started = (last.get("started_at") or "").replace(" ", "T")
completed = last.get("completed_at") or ""
if started and completed:
row = _conn.execute(
"SELECT run_id FROM trade_entry_prices WHERE run_id >= ? AND run_id <= ?"
" ORDER BY run_id DESC LIMIT 1",
(started, completed),
).fetchone()
if row:
last = dict(last)
last["scoring_run_id"] = row["run_id"]
except Exception:
pass
return {
**_current_status,
"enabled": enabled,

View File

@@ -32,6 +32,12 @@ const ASSET_COLORS: Record<string, string> = {
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)
@@ -113,7 +119,9 @@ export default function Dashboard() {
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 }
// 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
@@ -121,19 +129,21 @@ export default function Dashboard() {
? Object.entries(riskScore.breakdown).map(([k, v]) => ({ subject: k.replace('_', ' '), score: v }))
: []
// Last cycle run_id for filtering trades
const lastRunId: string | null = (cycleStatusData as any)?.last_cycle?.run_id ?? null
// 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 cycleTradesAll = lastRunId ? mtmTrades.filter((t: any) => t.run_id === lastRunId) : []
const cycleTrades = cycleTradesAll.length > 0 ? cycleTradesAll : mtmTrades.slice(0, 4)
const cycleTrades = scoringRunId ? mtmTrades.filter((t: any) => t.run_id === scoringRunId) : []
// Top scored patterns
const topScoredPatterns = useMemo(() =>
[...allPatterns]
.filter(p => scoreMap[p.id]?.score != null)
.sort((a, b) => (scoreMap[b.id]?.score ?? 0) - (scoreMap[a.id]?.score ?? 0))
// 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])
}, [allPatterns, scoreMap, lastCycle])
// Top impactful news
const topNews = useMemo(() =>
@@ -302,6 +312,11 @@ export default function Dashboard() {
: 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')
const totalCapital = trades.reduce((s: number, t: any) => s + (t.capital_invested ?? 0), 0)
const totalProfit = withPnl.reduce((s: number, t: any) =>
s + ((t.capital_invested ?? 0) * t.pnl_pct / 100), 0)
const targetHit = trades.filter((t: any) => t.alert_type === 'target_reached').length
const stopHit = trades.filter((t: any) => t.alert_type === 'stop_loss').length
@@ -321,23 +336,49 @@ export default function Dashboard() {
</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 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-[10px] text-slate-500 mt-1">
{trades.length} trades · <span className="text-emerald-500">{winners}</span>{' '}
<span className="text-red-400">{losers}</span>
<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="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 className="mt-2 pt-2 border-t border-slate-700/30 flex justify-between gap-2">
<div className="text-center">
<div className="text-[10px] text-slate-600">Ouvert</div>
<div className="text-xs font-bold text-slate-300">{openTrades.length}</div>
</div>
<div className="text-center">
<div className="text-[10px] text-slate-600">Fermé</div>
<div className="text-xs font-bold text-slate-400">{closedTrades.length}</div>
</div>
{totalCapital > 0 && (
<div className="text-center">
<div className="text-[10px] text-slate-600">Capital</div>
<div className="text-xs font-bold text-slate-300 font-mono">{totalCapital.toFixed(0)}</div>
</div>
)}
{totalCapital > 0 && (
<div className="text-center">
<div className="text-[10px] text-slate-600">Profit</div>
<div className={clsx('text-xs font-bold font-mono', totalProfit >= 0 ? 'text-emerald-400' : 'text-red-400')}>
{totalProfit >= 0 ? '+' : ''}{totalProfit.toFixed(0)}
</div>
</div>
)}
</div>
</div>
</>
) : (
<div className="flex items-end justify-between mt-1">
<div>
@@ -375,7 +416,6 @@ export default function Dashboard() {
.sort(([, a], [, b]) => (b as any).pct - (a as any).pct)
.slice(0, 5)
const pf = portfolio as any
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">
@@ -388,7 +428,6 @@ export default function Dashboard() {
<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>
{/* Allocation bars */}
{sortedClasses.length > 0 && (
<div className="mt-2 space-y-1">
{sortedClasses.map(([cls, data]: [string, any]) => (
@@ -425,7 +464,7 @@ export default function Dashboard() {
{/* Dernier Cycle — bilan complet */}
{(() => {
const last = (cycleStatusData as any)?.last_cycle
const last = lastCycle
const ts = last?.ts
? new Date(last.ts.endsWith('Z') ? last.ts : last.ts + 'Z')
: null
@@ -436,9 +475,11 @@ export default function Dashboard() {
: `${Math.round(elapsed / 1440)}j`
const patternsAdded: number = last?.patterns_added ?? 0
const patternsScored: number = last?.patterns_scored ?? 0
const tradesLogged: number = lastRunId
? mtmTrades.filter((t: any) => t.run_id === lastRunId).length
: 0
const tradesLogged: number = cycleTrades.length
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">
@@ -447,20 +488,31 @@ export default function Dashboard() {
</div>
<div className="text-xl font-bold text-blue-400 mt-0.5 font-mono">{elapsedStr}</div>
{last ? (
<div className="mt-1.5 grid grid-cols-3 gap-1 text-center">
<div className="bg-dark-700/60 rounded px-1 py-1">
<div className="text-sm font-bold text-slate-200">+{patternsAdded}</div>
<div className="text-[9px] text-slate-600 leading-tight">patterns</div>
<>
<div className="mt-1.5 grid grid-cols-4 gap-1 text-center">
<div className="bg-dark-700/60 rounded px-1 py-1">
<div className="text-sm font-bold text-slate-200">+{patternsAdded}</div>
<div className="text-[9px] text-slate-600 leading-tight">patterns</div>
</div>
<div className="bg-dark-700/60 rounded px-1 py-1">
<div className="text-sm font-bold text-emerald-400">{patternsScored}</div>
<div className="text-[9px] text-slate-600 leading-tight">scorés</div>
</div>
<div className="bg-dark-700/60 rounded px-1 py-1">
<div className="text-sm font-bold text-blue-400">{tradesLogged}</div>
<div className="text-[9px] text-slate-600 leading-tight">loggés</div>
</div>
<div className="bg-dark-700/60 rounded px-1 py-1">
<div className="text-sm font-bold text-slate-500">{tradesClosed}</div>
<div className="text-[9px] text-slate-600 leading-tight">fermés</div>
</div>
</div>
<div className="bg-dark-700/60 rounded px-1 py-1">
<div className="text-sm font-bold text-emerald-400">{patternsScored}</div>
<div className="text-[9px] text-slate-600 leading-tight">scorés</div>
</div>
<div className="bg-dark-700/60 rounded px-1 py-1">
<div className="text-sm font-bold text-blue-400">{tradesLogged}</div>
<div className="text-[9px] text-slate-600 leading-tight">loggé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>
)}
@@ -471,24 +523,46 @@ export default function Dashboard() {
{/* 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'
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-lg font-bold mt-1', colorClass)}>
<div className={clsx('text-base 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>
{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>
)
})()}
@@ -526,28 +600,28 @@ export default function Dashboard() {
{/* Trades du dernier cycle — détaillé */}
{(() => {
const last = (cycleStatusData as any)?.last_cycle
const added: number = last?.patterns_added ?? 0
const patternsAdded: number = lastCycle?.patterns_added ?? 0
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-xl font-bold text-emerald-400 font-mono">+{added}</div>
<div className="text-xl font-bold text-emerald-400 font-mono">+{patternsAdded} patterns</div>
{cycleTrades.length > 0 ? (
<div className="space-y-1 mt-1.5">
{cycleTrades.slice(0, 3).map((t: any, i) => {
const pnl: number | null = t.pnl_pct ?? null
const isBear = t.direction === 'bearish'
const ev: number | null = t.ev_net ?? t.ev_at_entry ?? null
const isBear = t.strategy?.toLowerCase().includes('put') || t.strategy?.toLowerCase().includes('bear')
return (
<div key={i} className="flex items-center gap-1.5 text-[10px]">
<span className="shrink-0">{isBear ? '🐻' : '🐂'}</span>
<span className="font-mono text-slate-300 font-semibold w-10 truncate">{t.underlying ?? '—'}</span>
<span className="text-slate-600 truncate flex-1">{t.strategy ?? ''}</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)}%
{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>
)}
{t.score_at_entry != null && (
@@ -555,28 +629,35 @@ export default function Dashboard() {
{t.score_at_entry}
</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>
)
})}
</div>
) : (
<div className="text-[10px] text-slate-600 mt-1">Aucun trade loggé</div>
<div className="text-[10px] text-slate-600 mt-1">
{scoringRunId ? 'Aucun trade loggé ce cycle' : 'En attente du prochain cycle'}
</div>
)}
</Link>
)
})()}
{/* Top patterns scorés du cycle */}
{/* 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]"> Top Patterns</span>
<span className="section-title mb-0 text-[10px]"> Pattern du cycle</span>
<ArrowUpRight className="w-3 h-3 text-slate-600" />
</div>
{topScoredPatterns.length > 0 ? (
{cyclePatterns.length > 0 ? (
<div className="space-y-1.5 mt-1">
{topScoredPatterns.map((p, i) => {
{cyclePatterns.map((p, i) => {
const sp = scoreMap[p.id]
const score = sp?.score ?? null
const ticker = sp?.recommended_trade?.underlying ?? null
@@ -587,15 +668,19 @@ export default function Dashboard() {
<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 && (
{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">Aucun scoré lancer le scoring IA</div>
<div className="text-[10px] text-slate-600 mt-1">
{lastCycle ? 'Aucun pattern ajouté ce cycle' : 'Aucun scoré — lancer le scoring IA'}
</div>
)}
</Link>
)