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:
@@ -1090,6 +1090,25 @@ def get_status() -> Dict[str, Any]:
|
|||||||
recent = get_cycle_runs(limit=1)
|
recent = get_cycle_runs(limit=1)
|
||||||
last = recent[0] if recent else None
|
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 {
|
return {
|
||||||
**_current_status,
|
**_current_status,
|
||||||
"enabled": enabled,
|
"enabled": enabled,
|
||||||
|
|||||||
@@ -32,6 +32,12 @@ const ASSET_COLORS: Record<string, string> = {
|
|||||||
rates: '#64748b', unknown: '#334155',
|
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 }) {
|
function ViewToggle({ value, onChange }: { value: 'simulated' | 'portfolio'; onChange: (v: 'simulated' | 'portfolio') => void }) {
|
||||||
const click = (v: 'simulated' | 'portfolio') => (e: React.MouseEvent) => {
|
const click = (v: 'simulated' | 'portfolio') => (e: React.MouseEvent) => {
|
||||||
e.preventDefault(); e.stopPropagation(); onChange(v)
|
e.preventDefault(); e.stopPropagation(); onChange(v)
|
||||||
@@ -113,7 +119,9 @@ export default function Dashboard() {
|
|||||||
const dom = sc.dominant ?? 'incertain'
|
const dom = sc.dominant ?? 'incertain'
|
||||||
const m = sc.meta?.[dom] ?? { label: dom, color: '#94a3b8', emoji: '?' }
|
const m = sc.meta?.[dom] ?? { label: dom, color: '#94a3b8', emoji: '?' }
|
||||||
const assetBias: Record<string, string> = sc.asset_bias?.[dom] ?? {}
|
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])
|
}, [macroData])
|
||||||
|
|
||||||
const gauge = riskScore ? riskGauge(riskScore.score) : null
|
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 }))
|
? Object.entries(riskScore.breakdown).map(([k, v]) => ({ subject: k.replace('_', ' '), score: v }))
|
||||||
: []
|
: []
|
||||||
|
|
||||||
// Last cycle run_id for filtering trades
|
// Cycle trades: use scoring_run_id (which differs from cycle run_id)
|
||||||
const lastRunId: string | null = (cycleStatusData as any)?.last_cycle?.run_id ?? null
|
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 mtmTrades: any[] = (tradeMtmData as any)?.trades ?? []
|
||||||
const cycleTradesAll = lastRunId ? mtmTrades.filter((t: any) => t.run_id === lastRunId) : []
|
const cycleTrades = scoringRunId ? mtmTrades.filter((t: any) => t.run_id === scoringRunId) : []
|
||||||
const cycleTrades = cycleTradesAll.length > 0 ? cycleTradesAll : mtmTrades.slice(0, 4)
|
|
||||||
|
|
||||||
// Top scored patterns
|
// Patterns from the last cycle only (filter by created_at >= cycle started_at)
|
||||||
const topScoredPatterns = useMemo(() =>
|
const cyclePatterns = useMemo(() => {
|
||||||
[...allPatterns]
|
const started = lastCycle?.started_at ?? null
|
||||||
.filter(p => scoreMap[p.id]?.score != null)
|
if (!started) return []
|
||||||
.sort((a, b) => (scoreMap[b.id]?.score ?? 0) - (scoreMap[a.id]?.score ?? 0))
|
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)
|
.slice(0, 4)
|
||||||
, [allPatterns, scoreMap])
|
}, [allPatterns, scoreMap, lastCycle])
|
||||||
|
|
||||||
// Top impactful news
|
// Top impactful news
|
||||||
const topNews = useMemo(() =>
|
const topNews = useMemo(() =>
|
||||||
@@ -302,6 +312,11 @@ export default function Dashboard() {
|
|||||||
: null
|
: null
|
||||||
const winners = withPnl.filter((t: any) => t.pnl_pct > 0).length
|
const winners = withPnl.filter((t: any) => t.pnl_pct > 0).length
|
||||||
const losers = 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 targetHit = trades.filter((t: any) => t.alert_type === 'target_reached').length
|
||||||
const stopHit = trades.filter((t: any) => t.alert_type === 'stop_loss').length
|
const stopHit = trades.filter((t: any) => t.alert_type === 'stop_loss').length
|
||||||
|
|
||||||
@@ -321,23 +336,49 @@ export default function Dashboard() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{pnlView === 'simulated' ? (
|
{pnlView === 'simulated' ? (
|
||||||
<div className="flex items-end justify-between mt-1">
|
<>
|
||||||
<div>
|
<div className="flex items-end justify-between mt-1">
|
||||||
<div className={clsx('text-2xl font-bold font-mono',
|
<div>
|
||||||
avgPnl === null ? 'text-slate-500' : avgPnl >= 0 ? 'text-emerald-400' : 'text-red-400')}>
|
<div className={clsx('text-2xl font-bold font-mono',
|
||||||
{avgPnl !== null ? `${avgPnl >= 0 ? '+' : ''}${avgPnl.toFixed(1)}%` : '—'}
|
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>
|
||||||
<div className="text-[10px] text-slate-500 mt-1">
|
<div className="text-right space-y-0.5">
|
||||||
{trades.length} trades · <span className="text-emerald-500">{winners}✓</span>{' '}
|
{targetHit > 0 && <div className="text-[10px] text-emerald-400">🎯 {targetHit} cible{targetHit > 1 ? 's' : ''}</div>}
|
||||||
<span className="text-red-400">{losers}✗</span>
|
{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>
|
</div>
|
||||||
<div className="text-right space-y-0.5">
|
<div className="mt-2 pt-2 border-t border-slate-700/30 flex justify-between gap-2">
|
||||||
{targetHit > 0 && <div className="text-[10px] text-emerald-400">🎯 {targetHit} cible{targetHit > 1 ? 's' : ''}</div>}
|
<div className="text-center">
|
||||||
{stopHit > 0 && <div className="text-[10px] text-red-400">⛔ {stopHit} stop</div>}
|
<div className="text-[10px] text-slate-600">Ouvert</div>
|
||||||
<div className="text-[10px] text-slate-600">{withPnl.length} pricés</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>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex items-end justify-between mt-1">
|
<div className="flex items-end justify-between mt-1">
|
||||||
<div>
|
<div>
|
||||||
@@ -375,7 +416,6 @@ export default function Dashboard() {
|
|||||||
.sort(([, a], [, b]) => (b as any).pct - (a as any).pct)
|
.sort(([, a], [, b]) => (b as any).pct - (a as any).pct)
|
||||||
.slice(0, 5)
|
.slice(0, 5)
|
||||||
|
|
||||||
const pf = portfolio as any
|
|
||||||
return (
|
return (
|
||||||
<Link to="/risk" className="card block hover:border-slate-600/60 transition-all cursor-pointer">
|
<Link to="/risk" className="card block hover:border-slate-600/60 transition-all cursor-pointer">
|
||||||
<div className="flex items-center justify-between mb-1">
|
<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')}>
|
<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'}
|
{alertCount > 0 ? `${alertCount} alerte${alertCount > 1 ? 's' : ''}` : 'OK'}
|
||||||
</div>
|
</div>
|
||||||
{/* Allocation bars */}
|
|
||||||
{sortedClasses.length > 0 && (
|
{sortedClasses.length > 0 && (
|
||||||
<div className="mt-2 space-y-1">
|
<div className="mt-2 space-y-1">
|
||||||
{sortedClasses.map(([cls, data]: [string, any]) => (
|
{sortedClasses.map(([cls, data]: [string, any]) => (
|
||||||
@@ -425,7 +464,7 @@ export default function Dashboard() {
|
|||||||
|
|
||||||
{/* Dernier Cycle — bilan complet */}
|
{/* Dernier Cycle — bilan complet */}
|
||||||
{(() => {
|
{(() => {
|
||||||
const last = (cycleStatusData as any)?.last_cycle
|
const last = lastCycle
|
||||||
const ts = last?.ts
|
const ts = last?.ts
|
||||||
? new Date(last.ts.endsWith('Z') ? last.ts : last.ts + 'Z')
|
? new Date(last.ts.endsWith('Z') ? last.ts : last.ts + 'Z')
|
||||||
: null
|
: null
|
||||||
@@ -436,9 +475,11 @@ export default function Dashboard() {
|
|||||||
: `${Math.round(elapsed / 1440)}j`
|
: `${Math.round(elapsed / 1440)}j`
|
||||||
const patternsAdded: number = last?.patterns_added ?? 0
|
const patternsAdded: number = last?.patterns_added ?? 0
|
||||||
const patternsScored: number = last?.patterns_scored ?? 0
|
const patternsScored: number = last?.patterns_scored ?? 0
|
||||||
const tradesLogged: number = lastRunId
|
const tradesLogged: number = cycleTrades.length
|
||||||
? mtmTrades.filter((t: any) => t.run_id === lastRunId).length
|
const tradesClosed = cycleTrades.filter((t: any) => t.status === 'closed').length
|
||||||
: 0
|
const commentary = last?.commentary
|
||||||
|
? (() => { try { return typeof last.commentary === 'string' ? JSON.parse(last.commentary) : last.commentary } catch { return null } })()
|
||||||
|
: null
|
||||||
return (
|
return (
|
||||||
<Link to="/journal" className="card block hover:border-slate-600/60 transition-all cursor-pointer">
|
<Link to="/journal" className="card block hover:border-slate-600/60 transition-all cursor-pointer">
|
||||||
<div className="flex items-center justify-between mb-1">
|
<div className="flex items-center justify-between mb-1">
|
||||||
@@ -447,20 +488,31 @@ export default function Dashboard() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="text-xl font-bold text-blue-400 mt-0.5 font-mono">{elapsedStr}</div>
|
<div className="text-xl font-bold text-blue-400 mt-0.5 font-mono">{elapsedStr}</div>
|
||||||
{last ? (
|
{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="mt-1.5 grid grid-cols-4 gap-1 text-center">
|
||||||
<div className="text-sm font-bold text-slate-200">+{patternsAdded}</div>
|
<div className="bg-dark-700/60 rounded px-1 py-1">
|
||||||
<div className="text-[9px] text-slate-600 leading-tight">patterns</div>
|
<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>
|
||||||
<div className="bg-dark-700/60 rounded px-1 py-1">
|
{commentary?.commentary && (
|
||||||
<div className="text-sm font-bold text-emerald-400">{patternsScored}</div>
|
<div className="mt-1.5 text-[9px] text-slate-500 line-clamp-2 italic leading-tight">
|
||||||
<div className="text-[9px] text-slate-600 leading-tight">scorés</div>
|
{commentary.commentary.slice(0, 100)}…
|
||||||
</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>
|
|
||||||
) : (
|
) : (
|
||||||
<div className="text-[10px] text-slate-600 mt-1">Aucun cycle enregistré</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 */}
|
{/* Régime Macro */}
|
||||||
{(() => {
|
{(() => {
|
||||||
const dom = macroInfo?.dominant
|
const dom = macroInfo?.dominant
|
||||||
const colorClass = dom === 'growth' ? 'text-emerald-400'
|
const colorClass = dom === 'growth' || dom === 'goldilocks' || dom === 'soft_landing' ? 'text-emerald-400'
|
||||||
: dom === 'stagflation' || dom === 'recession' ? 'text-red-400'
|
: dom === 'stagflation' || dom === 'recession' || dom === 'crise_liquidite' ? 'text-red-400'
|
||||||
: dom === 'deflation' ? 'text-blue-300'
|
: dom === 'deflation' || dom === 'desinflation' ? 'text-blue-300'
|
||||||
|
: dom === 'inflation_shock' ? 'text-orange-400'
|
||||||
: 'text-slate-400'
|
: 'text-slate-400'
|
||||||
|
const ranked = macroInfo?.ranked ?? []
|
||||||
return (
|
return (
|
||||||
<Link to="/macro" className="card block hover:border-slate-600/60 transition-all cursor-pointer">
|
<Link to="/macro" className="card block hover:border-slate-600/60 transition-all cursor-pointer">
|
||||||
<div className="flex items-center justify-between mb-1">
|
<div className="flex items-center justify-between mb-1">
|
||||||
<span className="section-title mb-0 text-[10px]">🌐 Régime Macro</span>
|
<span className="section-title mb-0 text-[10px]">🌐 Régime Macro</span>
|
||||||
<ArrowUpRight className="w-3 h-3 text-slate-600" />
|
<ArrowUpRight className="w-3 h-3 text-slate-600" />
|
||||||
</div>
|
</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}` : '—'}
|
{macroInfo ? `${macroInfo.emoji} ${macroInfo.label}` : '—'}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-[10px] text-slate-500 mt-1 line-clamp-1">
|
{ranked.length > 0 && (
|
||||||
{macroInfo
|
<div className="mt-1.5 space-y-1">
|
||||||
? Object.entries(macroInfo.assetBias).slice(0, 2).map(([k, v]) => `${k}→${v}`).join(' · ')
|
{ranked.slice(0, 4).map(([key, score]: [string, number]) => {
|
||||||
: 'Chargement...'}
|
const maxScore = ranked[0]?.[1] ?? 1
|
||||||
</div>
|
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>
|
</Link>
|
||||||
)
|
)
|
||||||
})()}
|
})()}
|
||||||
@@ -526,28 +600,28 @@ export default function Dashboard() {
|
|||||||
|
|
||||||
{/* Trades du dernier cycle — détaillé */}
|
{/* Trades du dernier cycle — détaillé */}
|
||||||
{(() => {
|
{(() => {
|
||||||
const last = (cycleStatusData as any)?.last_cycle
|
const patternsAdded: number = lastCycle?.patterns_added ?? 0
|
||||||
const added: number = last?.patterns_added ?? 0
|
|
||||||
return (
|
return (
|
||||||
<Link to="/journal" className="card block hover:border-slate-600/60 transition-all cursor-pointer">
|
<Link to="/journal" className="card block hover:border-slate-600/60 transition-all cursor-pointer">
|
||||||
<div className="flex items-center justify-between mb-1">
|
<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]">📥 Trades du cycle</span>
|
||||||
<ArrowUpRight className="w-3 h-3 text-slate-600" />
|
<ArrowUpRight className="w-3 h-3 text-slate-600" />
|
||||||
</div>
|
</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 ? (
|
{cycleTrades.length > 0 ? (
|
||||||
<div className="space-y-1 mt-1.5">
|
<div className="space-y-1 mt-1.5">
|
||||||
{cycleTrades.slice(0, 3).map((t: any, i) => {
|
{cycleTrades.slice(0, 3).map((t: any, i) => {
|
||||||
const pnl: number | null = t.pnl_pct ?? null
|
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 (
|
return (
|
||||||
<div key={i} className="flex items-center gap-1.5 text-[10px]">
|
<div key={i} className="flex items-center gap-1.5 text-[10px]">
|
||||||
<span className="shrink-0">{isBear ? '🐻' : '🐂'}</span>
|
<span className="shrink-0">{isBear ? '🐻' : '🐂'}</span>
|
||||||
<span className="font-mono text-slate-300 font-semibold w-10 truncate">{t.underlying ?? '—'}</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>
|
<span className="text-slate-600 truncate flex-1">{t.strategy ?? ''}</span>
|
||||||
{pnl !== null && (
|
{ev !== null && (
|
||||||
<span className={clsx('font-mono font-bold shrink-0', pnl >= 0 ? 'text-emerald-400' : 'text-red-400')}>
|
<span className={clsx('font-mono shrink-0 text-[9px]', ev >= 0.5 ? 'text-emerald-400' : ev >= 0 ? 'text-yellow-400' : 'text-red-400')}>
|
||||||
{pnl >= 0 ? '+' : ''}{pnl.toFixed(1)}%
|
EV{ev >= 0 ? '+' : ''}{ev.toFixed(2)}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{t.score_at_entry != null && (
|
{t.score_at_entry != null && (
|
||||||
@@ -555,28 +629,35 @@ export default function Dashboard() {
|
|||||||
{t.score_at_entry}
|
{t.score_at_entry}
|
||||||
</span>
|
</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>
|
</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>
|
</Link>
|
||||||
)
|
)
|
||||||
})()}
|
})()}
|
||||||
|
|
||||||
{/* Top patterns scorés du cycle */}
|
{/* Patterns du dernier cycle */}
|
||||||
{(() => {
|
{(() => {
|
||||||
return (
|
return (
|
||||||
<Link to="/patterns" className="card block hover:border-slate-600/60 transition-all cursor-pointer">
|
<Link to="/patterns" className="card block hover:border-slate-600/60 transition-all cursor-pointer">
|
||||||
<div className="flex items-center justify-between mb-1">
|
<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" />
|
<ArrowUpRight className="w-3 h-3 text-slate-600" />
|
||||||
</div>
|
</div>
|
||||||
{topScoredPatterns.length > 0 ? (
|
{cyclePatterns.length > 0 ? (
|
||||||
<div className="space-y-1.5 mt-1">
|
<div className="space-y-1.5 mt-1">
|
||||||
{topScoredPatterns.map((p, i) => {
|
{cyclePatterns.map((p, i) => {
|
||||||
const sp = scoreMap[p.id]
|
const sp = scoreMap[p.id]
|
||||||
const score = sp?.score ?? null
|
const score = sp?.score ?? null
|
||||||
const ticker = sp?.recommended_trade?.underlying ?? 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>
|
<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>}
|
{ticker && <div className="text-[9px] text-slate-600 font-mono">{ticker}</div>}
|
||||||
</div>
|
</div>
|
||||||
{score !== null && (
|
{score !== null ? (
|
||||||
<span className={clsx('text-sm font-bold font-mono shrink-0', scoreColor(score))}>{score}</span>
|
<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>
|
</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>
|
</Link>
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user