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