From d94052dd91b6e43c8c8531356bb92dead7c8b808 Mon Sep 17 00:00:00 2001 From: OpenSquared Date: Fri, 19 Jun 2026 20:49:44 +0200 Subject: [PATCH] =?UTF-8?q?fix:=20Dashboard=20cycle=20coherence=20?= =?UTF-8?q?=E2=80=94=20scoring=5Frun=5Fid=20linkage=20+=20cycle-scoped=20c?= =?UTF-8?q?ards?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- backend/services/auto_cycle.py | 19 +++ frontend/src/pages/Dashboard.tsx | 217 +++++++++++++++++++++---------- 2 files changed, 170 insertions(+), 66 deletions(-) diff --git a/backend/services/auto_cycle.py b/backend/services/auto_cycle.py index e3356b5..a53e3c3 100644 --- a/backend/services/auto_cycle.py +++ b/backend/services/auto_cycle.py @@ -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, diff --git a/frontend/src/pages/Dashboard.tsx b/frontend/src/pages/Dashboard.tsx index e6a2f75..8ce7b5f 100644 --- a/frontend/src/pages/Dashboard.tsx +++ b/frontend/src/pages/Dashboard.tsx @@ -32,6 +32,12 @@ const ASSET_COLORS: Record = { rates: '#64748b', unknown: '#334155', } +const REGIME_LABELS: Record = { + 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 = 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() { {pnlView === 'simulated' ? ( -
-
-
= 0 ? 'text-emerald-400' : 'text-red-400')}> - {avgPnl !== null ? `${avgPnl >= 0 ? '+' : ''}${avgPnl.toFixed(1)}%` : '—'} + <> +
+
+
= 0 ? 'text-emerald-400' : 'text-red-400')}> + {avgPnl !== null ? `${avgPnl >= 0 ? '+' : ''}${avgPnl.toFixed(1)}%` : '—'} +
+
+ {trades.length} trades · {winners}✓{' '} + {losers}✗ +
-
- {trades.length} trades · {winners}✓{' '} - {losers}✗ +
+ {targetHit > 0 &&
🎯 {targetHit} cible{targetHit > 1 ? 's' : ''}
} + {stopHit > 0 &&
⛔ {stopHit} stop
} +
{withPnl.length} pricés
-
- {targetHit > 0 &&
🎯 {targetHit} cible{targetHit > 1 ? 's' : ''}
} - {stopHit > 0 &&
⛔ {stopHit} stop
} -
{withPnl.length} pricés
+
+
+
Ouvert
+
{openTrades.length}
+
+
+
Fermé
+
{closedTrades.length}
+
+ {totalCapital > 0 && ( +
+
Capital
+
{totalCapital.toFixed(0)}€
+
+ )} + {totalCapital > 0 && ( +
+
Profit
+
= 0 ? 'text-emerald-400' : 'text-red-400')}> + {totalProfit >= 0 ? '+' : ''}{totalProfit.toFixed(0)}€ +
+
+ )}
-
+ ) : (
@@ -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 (
@@ -388,7 +428,6 @@ export default function Dashboard() {
0 ? 'text-red-400' : 'text-emerald-400')}> {alertCount > 0 ? `${alertCount} alerte${alertCount > 1 ? 's' : ''}` : 'OK'}
- {/* Allocation bars */} {sortedClasses.length > 0 && (
{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 (
@@ -447,20 +488,31 @@ export default function Dashboard() {
{elapsedStr}
{last ? ( -
-
-
+{patternsAdded}
-
patterns
+ <> +
+
+
+{patternsAdded}
+
patterns
+
+
+
{patternsScored}
+
scorés
+
+
+
{tradesLogged}
+
loggés
+
+
+
{tradesClosed}
+
fermés
+
-
-
{patternsScored}
-
scorés
-
-
-
{tradesLogged}
-
loggés
-
-
+ {commentary?.commentary && ( +
+ {commentary.commentary.slice(0, 100)}… +
+ )} + ) : (
Aucun cycle enregistré
)} @@ -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 (
🌐 Régime Macro
-
+
{macroInfo ? `${macroInfo.emoji} ${macroInfo.label}` : '—'}
-
- {macroInfo - ? Object.entries(macroInfo.assetBias).slice(0, 2).map(([k, v]) => `${k}→${v}`).join(' · ') - : 'Chargement...'} -
+ {ranked.length > 0 && ( +
+ {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 ( +
+ + {REGIME_LABELS[key] ?? key} + +
+
+
+ + {score} + +
+ ) + })} +
+ )} ) })()} @@ -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 (
📥 Trades du cycle
-
+{added}
+
+{patternsAdded} patterns
{cycleTrades.length > 0 ? (
{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 (
{isBear ? '🐻' : '🐂'} {t.underlying ?? '—'} {t.strategy ?? ''} - {pnl !== null && ( - = 0 ? 'text-emerald-400' : 'text-red-400')}> - {pnl >= 0 ? '+' : ''}{pnl.toFixed(1)}% + {ev !== null && ( + = 0.5 ? 'text-emerald-400' : ev >= 0 ? 'text-yellow-400' : 'text-red-400')}> + EV{ev >= 0 ? '+' : ''}{ev.toFixed(2)} )} {t.score_at_entry != null && ( @@ -555,28 +629,35 @@ export default function Dashboard() { {t.score_at_entry} )} + {pnl !== null && ( + = 0 ? 'text-emerald-400' : 'text-red-400')}> + {pnl >= 0 ? '+' : ''}{pnl.toFixed(1)}% + + )}
) })}
) : ( -
Aucun trade loggé
+
+ {scoringRunId ? 'Aucun trade loggé ce cycle' : 'En attente du prochain cycle'} +
)} ) })()} - {/* Top patterns scorés du cycle */} + {/* Patterns du dernier cycle */} {(() => { return (
- ⭐ Top Patterns + ⭐ Pattern du cycle
- {topScoredPatterns.length > 0 ? ( + {cyclePatterns.length > 0 ? (
- {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() {
{p.name}
{ticker &&
{ticker}
}
- {score !== null && ( + {score !== null ? ( {score} + ) : ( + )}
) })}
) : ( -
Aucun scoré — lancer le scoring IA
+
+ {lastCycle ? 'Aucun pattern ajouté ce cycle' : 'Aucun scoré — lancer le scoring IA'} +
)} )