feat: Derniers Cycles liste + Derniers trades loggés + Derniers Patterns

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
OpenSquared
2026-06-20 07:10:02 +02:00
parent a27949e9f7
commit 85975f6248

View File

@@ -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<string, { logged: number; closed: number }> = {}
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 Cyclesliste */}
{(() => {
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 (
<Link to="/journal" className="card flex flex-col h-[288px] overflow-hidden 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>
<div className="flex items-center justify-between mb-2">
<span className="section-title mb-0 text-[10px]">🔄 Derniers Cycles</span>
<ArrowUpRight className="w-3 h-3 text-slate-600" />
</div>
<div className="text-xl font-bold text-blue-400 mt-0.5 font-mono">{elapsedStr}</div>
{last ? (
<>
<div className="mt-2 grid grid-cols-2 gap-1.5 text-center">
<div className="bg-dark-700/60 rounded px-2 py-2">
<div className="text-xl font-bold text-slate-200">+{patternsAdded}</div>
<div className="text-[9px] text-slate-500 mt-0.5">patterns ajoutés</div>
</div>
<div className="bg-dark-700/60 rounded px-2 py-2">
<div className="text-xl font-bold text-blue-400">+{tradesLogged}</div>
<div className="text-[9px] text-slate-500 mt-0.5">trades ajoutés</div>
{tradesNotLogged > 0 && (
<div className="text-[8px] text-slate-700 mt-0.5">{tradesNotLogged} non-loggé{tradesNotLogged > 1 ? 's' : ''}</div>
)}
</div>
<div className="bg-dark-700/60 rounded px-2 py-1.5">
<div className="text-base font-bold text-emerald-400">{tradesLogged}</div>
<div className="text-[9px] text-slate-600">trades loggés</div>
</div>
<div className="bg-dark-700/60 rounded px-2 py-1.5">
<div className="text-base font-bold text-slate-500">{tradesClosed}</div>
<div className="text-[9px] text-slate-600">fermé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>
)}
</>
{/* Column headers */}
<div className="flex items-center gap-1 mb-1 pb-1 border-b border-slate-700/30">
<span className="text-[8px] text-slate-700 w-20 shrink-0">Date</span>
<span className="text-[8px] text-slate-700 w-8 shrink-0 text-right">Pat.</span>
<span className="text-[8px] text-slate-700 w-8 shrink-0 text-right">Log.</span>
<span className="text-[8px] text-slate-700 w-8 shrink-0 text-right">Clos</span>
<span className="text-[8px] text-slate-700 flex-1 truncate">Régime</span>
</div>
{runs.length > 0 ? (
<div className="space-y-2.5 flex-1 overflow-hidden">
{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 (
<div key={r.run_id ?? i} className="flex items-center gap-1">
<span className="text-[9px] text-slate-400 font-mono w-20 shrink-0">{dateStr}</span>
<span className={clsx('text-[9px] font-mono font-bold w-8 shrink-0 text-right', pat > 0 ? 'text-blue-400' : 'text-slate-600')}>
+{pat}
</span>
<span className={clsx('text-[9px] font-mono w-8 shrink-0 text-right', tStats.logged > 0 ? 'text-emerald-400' : 'text-slate-600')}>
{tStats.logged}
</span>
<span className={clsx('text-[9px] font-mono w-8 shrink-0 text-right', tStats.closed > 0 ? 'text-slate-400' : 'text-slate-600')}>
{tStats.closed}
</span>
<span className="text-[8px] text-slate-600 flex-1 truncate">{regime}</span>
<span className={clsx('w-1.5 h-1.5 rounded-full shrink-0', ok ? 'bg-emerald-500' : 'bg-slate-700')} />
</div>
)
})}
</div>
) : (
<div className="text-[10px] text-slate-600 mt-1">Aucun cycle enregistré</div>
)}
@@ -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<string, any> = {}
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 (
<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>
<span className="section-title mb-0 text-[10px]">📥 Derniers trades loggés</span>
<ArrowUpRight className="w-3 h-3 text-slate-600" />
</div>
{rows.length > 0 ? (
<div className="space-y-2 mt-1">
{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 ? (
<div className="space-y-2.5 mt-1">
{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 (
<div key={i}>
{logged ? (
<>
{/* Trade row — primary */}
<div className="flex items-center gap-1.5 text-[10px]">
<span className="shrink-0">{isBear ? '🐻' : '🐂'}</span>
<span className="font-mono text-slate-200 font-bold w-10 truncate">{t.underlying ?? '—'}</span>
<span className="text-slate-400 truncate flex-1">{t.strategy ?? ''}</span>
{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>
)}
{score !== null && (
<span className={clsx('font-mono shrink-0 font-bold', scoreColor(score))}>{score}</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>
{/* Pattern name — secondary */}
<div className="text-[8px] text-slate-600 italic truncate mt-0.5 ml-4">{p.name}</div>
</>
) : (
<>
{/* 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 (
<>
<div className="flex items-center gap-1.5 text-[10px]">
<span className="text-slate-600 shrink-0">{isBearS ? '🐻' : '🐂'}</span>
<span className={clsx('font-mono font-bold shrink-0', underlying ? 'text-slate-400' : 'text-slate-700')}>
{underlying ?? '—'}
</span>
<span className="text-slate-600 truncate flex-1">{strategy ?? 'trade suggéré'}</span>
{evVal !== null && (
<span className="font-mono text-[9px] text-slate-600">
EV{evVal >= 0 ? '+' : ''}{evVal.toFixed(2)}
</span>
)}
{score !== null && (
<span className={clsx('font-mono shrink-0 font-bold', scoreColor(score))}>{score}</span>
)}
<span className="text-[8px] text-slate-700 shrink-0">non loggé</span>
</div>
<div className="text-[8px] text-slate-700 italic truncate mt-0.5 ml-4">{p.name}</div>
</>
)
})()}
</>
<div className="flex items-center gap-1.5 text-[10px]">
<span className="shrink-0">{isBear ? '🐻' : '🐂'}</span>
<span className="font-mono text-slate-200 font-bold w-10 truncate shrink-0">{t.underlying ?? '—'}</span>
<span className="text-slate-500 truncate flex-1 text-[9px]">{t.strategy ?? ''}</span>
{score !== null && (
<span className={clsx('font-mono font-bold shrink-0 text-[9px]', scoreColor(score))}>{score}</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>
) : (
<span className="text-slate-700 shrink-0 text-[9px]">en cours</span>
)}
</div>
{entryDate && (
<div className="text-[8px] text-slate-700 mt-0.5 ml-4 font-mono">{entryDate} · {t.pattern_name ? <span className="italic">{t.pattern_name}</span> : ''}</div>
)}
</div>
)
})}
</div>
) : (
<div className="text-[10px] text-slate-600 mt-1">
{lastCycle ? 'Aucun pattern ajouté ce cycle' : 'En attente du prochain cycle'}
</div>
<div className="text-[10px] text-slate-600 mt-1">Aucun trade loggé</div>
)}
</Link>
)
})()}
{/* 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 (
<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]"> Pattern du cycle</span>
<span className="section-title mb-0 text-[10px]"> Derniers Patterns ajoutés</span>
<ArrowUpRight className="w-3 h-3 text-slate-600" />
</div>
{cyclePatterns.length > 0 ? (
<div className="space-y-1.5 mt-1">
{cyclePatterns.map((p, i) => {
{recent.length > 0 ? (
<div className="space-y-2.5 mt-1">
{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 (
<div key={p.id} className="flex items-center gap-1.5">
<span className="text-[10px] text-slate-700 w-3 shrink-0 font-mono">{i + 1}</span>
<div className="flex-1 min-w-0">
<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 key={p.id}>
<div className="flex items-center gap-1.5">
<span className="text-[9px] text-slate-700 w-3 shrink-0 font-mono">{i + 1}</span>
<div className="flex-1 min-w-0">
<div className="text-[10px] text-slate-300 truncate leading-tight">{p.name}</div>
</div>
{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 className="text-[8px] text-slate-700 mt-0.5 ml-4 font-mono">
{ticker && <span className="text-slate-500">{ticker}</span>}
{ticker && dateStr && ' · '}
{dateStr}
</div>
{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">
{lastCycle ? 'Aucun pattern ajouté ce cycle' : 'Aucun scoré — lancer le scoring IA'}
</div>
<div className="text-[10px] text-slate-600 mt-1">Aucun pattern enregistré</div>
)}
</Link>
)