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:
@@ -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 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 (
|
||||
<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>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user