refactor: align market_events categories with driver types + event stars on chart

- database.py: idempotent migration — calendar→event_calendar, geopolitique→geopolitical,
  macro events classified by name pattern (FOMC/CPI/BOJ/OPEC+→event_calendar,
  NVIDIA→report, rest→fundamental)
- eco_calendar_bootstrap.py: calendar→event_calendar (75 events)
- macro_events_bootstrap.py: all 30 events now carry aligned category + sub_type
  (FOMC×9, CPI×3, BOJ×3, OPEC+×2 → event_calendar ; war/geo×6 → geopolitical ;
   ChatGPT/BTC/PBOC → fundamental ; NVIDIA×2 → report)
- InstrumentDashboard: replace EventTimelineStrip (horizontal bars below chart)
  with EventStarsStrip (★ icons above chart, sized by impact_score, colored by category)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
OpenSquared
2026-06-25 17:01:07 +02:00
parent a68896cf67
commit 51a5531454
4 changed files with 168 additions and 232 deletions

View File

@@ -130,18 +130,6 @@ const DRIVER_TYPE_CFG: Record<string, { short: string; tw: string }> = {
technical: { short: 'Tech.', tw: 'text-cyan-400 bg-cyan-900/30 border-cyan-700/30' },
}
function DriverTypeBadge({ type }: { type?: string }) {
if (!type) return null
const cfg = DRIVER_TYPE_CFG[type]
if (!cfg) return null
return (
<span className={clsx('text-xs px-1 py-0 rounded border leading-none', cfg.tw)}
style={{ fontSize: 8, paddingTop: 1, paddingBottom: 1 }}>
{cfg.short}
</span>
)
}
// ── Macro regime colour mapping ───────────────────────────────────────────────
const MACRO_COLOR_MAP: Record<string, string> = {
@@ -519,135 +507,59 @@ function NarrativeCard({ narrative, loading, onLoad, instrument }: {
)
}
// ── EventTimelineStrip — by drivers ──────────────────────────────────────────
// ── EventStarsStrip — étoiles au-dessus du graphe ────────────────────────────
const DRIVER_PALETTE = [
{ bg: 'bg-violet-800/60', border: 'border-violet-700/60', text: 'text-violet-100', label: 'text-violet-400' },
{ bg: 'bg-blue-800/60', border: 'border-blue-700/60', text: 'text-blue-100', label: 'text-blue-400' },
{ bg: 'bg-emerald-800/60', border: 'border-emerald-700/60', text: 'text-emerald-100', label: 'text-emerald-400' },
{ bg: 'bg-amber-800/60', border: 'border-amber-700/60', text: 'text-amber-100', label: 'text-amber-400' },
]
function eventMatchesDriver(ev: SnapshotEvent, keywords: string[]): boolean {
const text = `${ev.title} ${ev.description ?? ''} ${ev.category ?? ''}`.toLowerCase()
return keywords.some(kw => text.includes(kw.toLowerCase()))
const CAT_STAR_COLORS: Record<string, string> = {
event_calendar: '#f59e0b', // amber
geopolitical: '#ef4444', // red
fundamental: '#10b981', // emerald
report: '#3b82f6', // blue
sentiment: '#8b5cf6', // violet
technical: '#06b6d4', // cyan
}
function EventTimelineStrip({
events, priceData, drivers, selectedDate,
function EventStarsStrip({
events, priceData,
}: {
events: SnapshotEvent[]; priceData: PriceCandle[]; drivers: Driver[]; selectedDate: string | null
events: SnapshotEvent[]
priceData: PriceCandle[]
}) {
if (priceData.length < 2) return null
if (priceData.length < 2 || events.length === 0) return null
const chartStart = dateToMs(priceData[0].time)
const chartEnd = dateToMs(priceData[priceData.length - 1].time)
const totalMs = chartEnd - chartStart
if (totalMs <= 0) return null
function leftPct(d: string) { return Math.max(0, Math.min(99, ((dateToMs(d) - chartStart) / totalMs) * 100)) }
function widthPct(start: string, end: string | null): number {
const endMs = end ? dateToMs(end) : dateToMs(start) + totalMs * 0.03
return Math.min(100, Math.max(0.4, ((endMs - dateToMs(start)) / totalMs) * 100))
function leftPct(d: string) {
return Math.max(0.5, Math.min(98.5, ((dateToMs(d) - chartStart) / totalMs) * 100))
}
const topDrivers = [...drivers].sort((a, b) => b.weight - a.weight).slice(0, 4)
const allKeywords = topDrivers.flatMap(d => d.keywords ?? [])
const matchedAnyIdxs = new Set(
events.map((ev, i) => eventMatchesDriver(ev, allKeywords) ? i : -1).filter(i => i >= 0)
)
const unmatchedEvs = events.filter((_ev, i) => !matchedAnyIdxs.has(i))
// Cursor position for the selected date
const cursorLeft = selectedDate ? leftPct(selectedDate) : null
return (
<div className="bg-dark-900/40 rounded-xl border border-slate-700/40 px-4 py-3">
<div className="text-xs text-slate-500 uppercase tracking-wide mb-2.5">Événements par driver</div>
<div className="space-y-1.5">
{topDrivers.map((driver, di) => {
const cols = DRIVER_PALETTE[di % DRIVER_PALETTE.length]
const matchedEvs = events.filter(ev => eventMatchesDriver(ev, driver.keywords ?? []))
return (
<div key={driver.key} className="flex items-center gap-2 h-6">
<div className="flex items-center gap-1 shrink-0 w-28">
<span className={clsx('text-xs font-semibold truncate', cols.label)}
style={{ fontSize: 10 }} title={driver.label}>
{driver.label}
</span>
<DriverTypeBadge type={driver.type} />
</div>
<div className="relative flex-1 h-full" style={{ paddingRight: 62 }}>
{matchedEvs.map((ev, i) => {
const left = leftPct(ev.date)
const width = widthPct(ev.date, ev.end_date)
return (
<div key={i}
title={`${ev.title}\n${ev.date}${ev.end_date ? ' → ' + ev.end_date : ''}\n${ev.description ?? ''}`}
className={clsx('absolute top-0 h-full rounded border overflow-hidden', cols.bg, cols.border)}
style={{ left: `${left}%`, width: `${width}%` }}
>
{width > 3 && (
<span className={clsx('absolute inset-0 flex items-center px-1 truncate pointer-events-none', cols.text)}
style={{ fontSize: 9 }}>
{ev.title}
</span>
)}
</div>
)
})}
{matchedEvs.length === 0 && (
<div className="absolute inset-0 flex items-center">
<div className="w-full border-t border-dashed border-slate-700/30" />
</div>
)}
{/* Vertical cursor for selected date */}
{cursorLeft !== null && (
<div
className="absolute top-0 bottom-0 w-px bg-white/50 z-20 pointer-events-none"
style={{ left: `${cursorLeft}%` }}
/>
)}
</div>
</div>
)
})}
{/* Fallback row: events not matched by any driver keyword */}
{unmatchedEvs.length > 0 && (
<div className="flex items-center gap-2 h-6">
<span className="text-xs font-semibold shrink-0 truncate w-28 text-slate-500"
style={{ fontSize: 10 }}>Signaux tech.</span>
<div className="relative flex-1 h-full" style={{ paddingRight: 62 }}>
{unmatchedEvs.map((ev, i) => {
const left = leftPct(ev.date)
const width = widthPct(ev.date, ev.end_date)
return (
<div key={i}
title={`${ev.title}\n${ev.date}${ev.end_date ? ' → ' + ev.end_date : ''}\n${ev.description ?? ''}`}
className="absolute top-0 h-full rounded border overflow-hidden bg-slate-700/40 border-slate-600/40"
style={{ left: `${left}%`, width: `${width}%` }}
>
{width > 3 && (
<span className="absolute inset-0 flex items-center px-1 truncate pointer-events-none text-slate-400"
style={{ fontSize: 9 }}>
{ev.title}
</span>
)}
</div>
)
})}
{cursorLeft !== null && (
<div
className="absolute top-0 bottom-0 w-px bg-white/50 z-20 pointer-events-none"
style={{ left: `${cursorLeft}%` }}
/>
)}
</div>
</div>
)}
</div>
<div className="relative" style={{ height: 28, paddingRight: 62 }}>
{events.map((ev, i) => {
const left = leftPct(ev.date)
const color = CAT_STAR_COLORS[ev.category] ?? '#94a3b8'
const score = ev.impact_score ?? 0.5
const size = score >= 0.8 ? 16 : score >= 0.6 ? 13 : 11
return (
<span
key={i}
className="absolute -translate-x-1/2 select-none cursor-default transition-transform hover:scale-150"
style={{
left: `${left}%`,
bottom: 2,
fontSize: size,
color,
lineHeight: 1,
filter: `drop-shadow(0 0 3px ${color}60)`,
}}
title={`${ev.title}\n${ev.date}${ev.end_date ? ' → ' + ev.end_date : ''}\n${ev.description ?? ''}`}
>
</span>
)
})}
</div>
)
}
@@ -1003,6 +915,11 @@ export default function InstrumentDashboard() {
{/* ── Content ── */}
{!loading && snapshot && (
<>
<EventStarsStrip
events={snapshot.events}
priceData={snapshot.price_data}
/>
<InstrumentChart
priceData={snapshot.price_data}
indicators={snapshot.indicators}
@@ -1011,13 +928,6 @@ export default function InstrumentDashboard() {
onDateHover={handleDateHover}
/>
<EventTimelineStrip
events={snapshot.events}
priceData={snapshot.price_data}
drivers={activeDrivers}
selectedDate={effectiveDate}
/>
{/* Date badge */}
<div className="flex items-center gap-2">
<div className={clsx(