From 85975f6248db3a92fc9b2f7e965828f032d79e0e Mon Sep 17 00:00:00 2001 From: OpenSquared Date: Sat, 20 Jun 2026 07:10:02 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20Derniers=20Cycles=20liste=20+=20Dernier?= =?UTF-8?q?s=20trades=20logg=C3=A9s=20+=20Derniers=20Patterns?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- frontend/src/pages/Dashboard.tsx | 271 ++++++++++++++----------------- 1 file changed, 119 insertions(+), 152 deletions(-) diff --git a/frontend/src/pages/Dashboard.tsx b/frontend/src/pages/Dashboard.tsx index 0b71ee2..73fef23 100644 --- a/frontend/src/pages/Dashboard.tsx +++ b/frontend/src/pages/Dashboard.tsx @@ -92,6 +92,14 @@ export default function Dashboard() { const { data: cycleStatusData } = useCycleStatus() const { data: geoNews } = useGeoNews() + // Cycle history (last 5 for "Derniers Cycles" card) + const { data: cycleHistoryData } = useQuery({ + queryKey: ['dash-cycle-history'], + queryFn: () => fetch('/api/cycle/history?limit=5').then(r => r.ok ? r.json() : { runs: [] }), + staleTime: 60_000, + retry: 1, + }) + // Historical PnL curve + latest VaR snapshot const { data: pnlHistoryData } = useQuery({ queryKey: ['dash-pnl-history'], @@ -162,6 +170,18 @@ export default function Dashboard() { .slice(0, 4) }, [allPatterns, scoreMap, lastCycle]) + // Trades grouped by run_id for cycle history card + const tradesByRun = useMemo(() => { + const map: Record = {} + for (const t of mtmTrades) { + if (!t.run_id) continue + if (!map[t.run_id]) map[t.run_id] = { logged: 0, closed: 0 } + map[t.run_id].logged++ + if (t.status === 'closed') map[t.run_id].closed++ + } + return map + }, [mtmTrades]) + // Top impactful news const topNews = useMemo(() => [...(geoNews ?? [])] @@ -547,63 +567,50 @@ export default function Dashboard() { ) })()} - {/* Dernier Cycle — bilan complet */} + {/* Derniers Cycles — liste */} {(() => { - const last = lastCycle - 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` - const patternsAdded: number = last?.patterns_added ?? 0 - // Only count trades whose pattern was added THIS cycle (not older patterns re-scored) - const cyclePatternIds = new Set(cyclePatterns.map((p: any) => p.id)) - const cycleTradesFromNewPatterns = cycleTrades.filter((t: any) => cyclePatternIds.has(t.pattern_id)) - const tradesLogged: number = cycleTradesFromNewPatterns.length - const tradesNotLogged: number = Math.max(0, patternsAdded - tradesLogged) - const tradesClosed = cycleTradesFromNewPatterns.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 + const runs: any[] = cycleHistoryData?.runs ?? [] return ( -
- 🔄 Dernier Cycle +
+ 🔄 Derniers Cycles
-
{elapsedStr}
- {last ? ( - <> -
-
-
+{patternsAdded}
-
patterns ajoutés
-
-
-
+{tradesLogged}
-
trades ajoutés
- {tradesNotLogged > 0 && ( -
{tradesNotLogged} non-loggé{tradesNotLogged > 1 ? 's' : ''}
- )} -
-
-
{tradesLogged}
-
trades loggés
-
-
-
{tradesClosed}
-
fermés
-
-
- {commentary?.commentary && ( -
- {commentary.commentary.slice(0, 100)}… -
- )} - + {/* Column headers */} +
+ Date + Pat. + Log. + Clos + Régime +
+ {runs.length > 0 ? ( +
+ {runs.map((r: any, i: number) => { + const dt = r.started_at ? new Date(r.started_at.endsWith('Z') ? r.started_at : r.started_at + 'Z') : null + const dateStr = dt ? `${String(dt.getDate()).padStart(2,'0')}/${String(dt.getMonth()+1).padStart(2,'0')} ${String(dt.getHours()).padStart(2,'0')}:${String(dt.getMinutes()).padStart(2,'0')}` : '—' + const pat: number = r.patterns_added ?? 0 + const tStats = tradesByRun[r.run_id] ?? { logged: 0, closed: 0 } + const regime = r.dominant_regime ? (REGIME_LABELS[r.dominant_regime] ?? r.dominant_regime) : '—' + const ok = r.status === 'completed' + return ( +
+ {dateStr} + 0 ? 'text-blue-400' : 'text-slate-600')}> + +{pat} + + 0 ? 'text-emerald-400' : 'text-slate-600')}> + {tStats.logged} + + 0 ? 'text-slate-400' : 'text-slate-600')}> + {tStats.closed} + + {regime} + +
+ ) + })} +
) : (
Aucun cycle enregistré
)} @@ -777,139 +784,99 @@ export default function Dashboard() { ) })()} - {/* Trades du dernier cycle — détaillé */} + {/* Derniers trades loggés */} {(() => { - // Build a map: pattern_id → trade for quick lookup - const tradeByPattern: Record = {} - for (const t of cycleTrades) { - if (t.pattern_id) tradeByPattern[t.pattern_id] = t - } - // Show cyclePatterns with their trade status (loggé or not) - const rows = cyclePatterns.length > 0 - ? cyclePatterns - : cycleTrades.map((t: any) => ({ id: t.pattern_id, name: t.pattern_name })) + const recent = [...mtmTrades] + .filter((t: any) => t.entry_date) + .sort((a: any, b: any) => (b.entry_date ?? '').localeCompare(a.entry_date ?? '')) + .slice(0, 3) return (
- 📥 Trades du cycle + 📥 Derniers trades loggés
- {rows.length > 0 ? ( -
- {rows.slice(0, 3).map((p: any, i: number) => { - const t = tradeByPattern[p.id] - const logged = !!t - const ev: number | null = t?.ev_net ?? t?.ev_at_entry ?? null - const score: number | null = t?.score_at_entry ?? scoreMap[p.id]?.score ?? null - const pnl: number | null = t?.pnl_pct ?? null - const isBear = t?.strategy?.toLowerCase().includes('put') || t?.strategy?.toLowerCase().includes('bear') + {recent.length > 0 ? ( +
+ {recent.map((t: any, i: number) => { + const pnl: number | null = t.pnl_pct ?? null + const isBear = t.strategy?.toLowerCase().includes('put') || t.strategy?.toLowerCase().includes('bear') + const entryDate = t.entry_date ? t.entry_date.slice(0, 10) : null + const score: number | null = t.score_at_entry ?? null return (
- {logged ? ( - <> - {/* Trade row — primary */} -
- {isBear ? '🐻' : '🐂'} - {t.underlying ?? '—'} - {t.strategy ?? ''} - {ev !== null && ( - = 0.5 ? 'text-emerald-400' : ev >= 0 ? 'text-yellow-400' : 'text-red-400')}> - EV{ev >= 0 ? '+' : ''}{ev.toFixed(2)} - - )} - {score !== null && ( - {score} - )} - {pnl !== null && ( - = 0 ? 'text-emerald-400' : 'text-red-400')}> - {pnl >= 0 ? '+' : ''}{pnl.toFixed(1)}% - - )} -
- {/* Pattern name — secondary */} -
{p.name}
- - ) : ( - <> - {/* Suggested trade from scoreMap — primary */} - {(() => { - const sp = scoreMap[p.id] - const rec = sp?.recommended_trade ?? sp?.trade_rankings?.[0] - const underlying = rec?.underlying ?? rec?.ticker ?? null - const strategy = rec?.strategy ?? rec?.trade_type ?? null - const evVal: number | null = rec?.ev_net ?? rec?.ev ?? null - const isBearS = strategy?.toLowerCase().includes('put') || strategy?.toLowerCase().includes('bear') - return ( - <> -
- {isBearS ? '🐻' : '🐂'} - - {underlying ?? '—'} - - {strategy ?? 'trade suggéré'} - {evVal !== null && ( - - EV{evVal >= 0 ? '+' : ''}{evVal.toFixed(2)} - - )} - {score !== null && ( - {score} - )} - non loggé -
-
{p.name}
- - ) - })()} - +
+ {isBear ? '🐻' : '🐂'} + {t.underlying ?? '—'} + {t.strategy ?? ''} + {score !== null && ( + {score} + )} + {pnl !== null ? ( + = 0 ? 'text-emerald-400' : 'text-red-400')}> + {pnl >= 0 ? '+' : ''}{pnl.toFixed(1)}% + + ) : ( + en cours + )} +
+ {entryDate && ( +
{entryDate} · {t.pattern_name ? {t.pattern_name} : ''}
)}
) })}
) : ( -
- {lastCycle ? 'Aucun pattern ajouté ce cycle' : 'En attente du prochain cycle'} -
+
Aucun trade loggé
)} ) })()} - {/* Patterns du dernier cycle */} + {/* Derniers Patterns ajoutés */} {(() => { + const recent = [...allPatterns] + .filter((p: any) => p.created_at) + .sort((a: any, b: any) => (b.created_at ?? '').localeCompare(a.created_at ?? '')) + .slice(0, 3) return (
- ⭐ Pattern du cycle + ⭐ Derniers Patterns ajoutés
- {cyclePatterns.length > 0 ? ( -
- {cyclePatterns.map((p, i) => { + {recent.length > 0 ? ( +
+ {recent.map((p: any, i: number) => { const sp = scoreMap[p.id] - const score = sp?.score ?? null - const ticker = sp?.recommended_trade?.underlying ?? null + const score: number | null = sp?.score ?? null + const ticker: string | null = sp?.recommended_trade?.underlying ?? null + const dateStr = p.created_at ? p.created_at.slice(0, 10) : null return ( -
- {i + 1} -
-
{p.name}
- {ticker &&
{ticker}
} +
+
+ {i + 1} +
+
{p.name}
+
+ {score !== null ? ( + {score} + ) : ( + + )} +
+
+ {ticker && {ticker}} + {ticker && dateStr && ' · '} + {dateStr}
- {score !== null ? ( - {score} - ) : ( - - )}
) })}
) : ( -
- {lastCycle ? 'Aucun pattern ajouté ce cycle' : 'Aucun scoré — lancer le scoring IA'} -
+
Aucun pattern enregistré
)} )