feat: eco calendar bootstrap + driver types + timeline cursor
Backend: - eco_calendar_bootstrap.py: 75 historical events 2020-2026 (FOMC, NFP, CPI, GDP, ISM, BOJ, ECB, BOE) with expected_value/actual_value/ surprise_pct/unit/absorption_pct fields - database.py: 5 new columns on market_events (expected_value, actual_value, surprise_pct, unit, sub_type) + updated save_market_event - timeline.py: POST /api/timeline/bootstrap-eco endpoint - instrument_service.py: _get_relevant_events now returns eco fields - instruments.json: type field on all 90 drivers across 20 instruments (event_calendar | report | geopolitical | fundamental | sentiment | technical) Frontend (InstrumentDashboard): - EventTimelineStrip: vertical cursor line tracking crosshair selectedDate - EventsCard: active-event highlight at hovered date + expected/actual/ surprise_pct display + absorption progress bar - DriversPanel: type selector dropdown per driver - DriverTypeBadge: colored micro-badge on driver labels in strip Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -13,7 +13,7 @@ const api = axios.create({ baseURL: '/api' })
|
||||
// ── Types ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
interface Driver {
|
||||
key: string; label: string; weight: number; keywords: string[]
|
||||
key: string; label: string; weight: number; keywords: string[]; type?: string
|
||||
}
|
||||
|
||||
interface InstrumentConfig {
|
||||
@@ -29,7 +29,9 @@ interface LinePoint { time: string; value: number }
|
||||
|
||||
interface SnapshotEvent {
|
||||
date: string; end_date: string | null; title: string; level: string
|
||||
category: string; description: string; impact_score: number
|
||||
category: string; sub_type?: string; description: string; impact_score: number
|
||||
expected_value?: string | null; actual_value?: string | null
|
||||
surprise_pct?: number | null; unit?: string | null; absorption_pct?: number | null
|
||||
}
|
||||
|
||||
interface RegimeSignals {
|
||||
@@ -114,6 +116,32 @@ function pctN(a: number | undefined, b: number | undefined): number {
|
||||
return ((a - b) / b) * 100
|
||||
}
|
||||
|
||||
// ── Driver type config ────────────────────────────────────────────────────────
|
||||
|
||||
const DRIVER_TYPES = ['event_calendar', 'report', 'geopolitical', 'fundamental', 'sentiment', 'technical'] as const
|
||||
type DriverType = typeof DRIVER_TYPES[number]
|
||||
|
||||
const DRIVER_TYPE_CFG: Record<string, { short: string; tw: string }> = {
|
||||
event_calendar: { short: 'Cal.', tw: 'text-amber-400 bg-amber-900/30 border-amber-700/30' },
|
||||
report: { short: 'Rep.', tw: 'text-blue-400 bg-blue-900/30 border-blue-700/30' },
|
||||
geopolitical: { short: 'Geo.', tw: 'text-red-400 bg-red-900/30 border-red-700/30' },
|
||||
fundamental: { short: 'Fund.', tw: 'text-emerald-400 bg-emerald-900/30 border-emerald-700/30' },
|
||||
sentiment: { short: 'Sent.', tw: 'text-violet-400 bg-violet-900/30 border-violet-700/30' },
|
||||
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> = {
|
||||
@@ -132,7 +160,7 @@ function macroRegimeColor(dominant: string): string {
|
||||
return MACRO_COLOR_MAP[dominant] ?? 'slate'
|
||||
}
|
||||
|
||||
// ── RegimeCard (métriques de signaux) ─────────────────────────────────────────
|
||||
// ── RegimeCard ────────────────────────────────────────────────────────────────
|
||||
|
||||
function RegimeCard({
|
||||
regime, macroRegime, signalsAt, dateLabel,
|
||||
@@ -155,7 +183,6 @@ function RegimeCard({
|
||||
amber: 'border-amber-700/40 bg-amber-950/30',
|
||||
}
|
||||
|
||||
// 6 signal metrics displayed as a compact grid
|
||||
const ma50Color = signals.ma50_above_ma200 ? 'text-emerald-400' : 'text-red-400'
|
||||
const volColor = (signals.vol_ratio_pct ?? 0) > 130 ? 'text-orange-400' : (signals.vol_ratio_pct ?? 0) < 70 ? 'text-cyan-400' : 'text-slate-300'
|
||||
|
||||
@@ -166,21 +193,9 @@ function RegimeCard({
|
||||
sub: signals.ma50_above_ma200 === true ? '↑ Golden cross' : signals.ma50_above_ma200 === false ? '↓ Death cross' : '',
|
||||
color: ma50Color,
|
||||
},
|
||||
{
|
||||
label: 'Slope MA50 (10j)',
|
||||
value: fmt(signals.ma50_slope_pct) + '%',
|
||||
color: pctColor(signals.ma50_slope_pct),
|
||||
},
|
||||
{
|
||||
label: 'Slope MA200 (10j)',
|
||||
value: fmt(signals.ma200_slope_pct) + '%',
|
||||
color: pctColor(signals.ma200_slope_pct),
|
||||
},
|
||||
{
|
||||
label: 'Momentum 20j',
|
||||
value: fmt(signals.momentum_20d_pct) + '%',
|
||||
color: pctColor(signals.momentum_20d_pct),
|
||||
},
|
||||
{ label: 'Slope MA50 (10j)', value: fmt(signals.ma50_slope_pct) + '%', color: pctColor(signals.ma50_slope_pct) },
|
||||
{ label: 'Slope MA200 (10j)', value: fmt(signals.ma200_slope_pct) + '%', color: pctColor(signals.ma200_slope_pct) },
|
||||
{ label: 'Momentum 20j', value: fmt(signals.momentum_20d_pct) + '%', color: pctColor(signals.momentum_20d_pct) },
|
||||
{
|
||||
label: 'Distance MA200',
|
||||
value: fmt(signals.dist_ma200_pct) + '%',
|
||||
@@ -195,13 +210,11 @@ function RegimeCard({
|
||||
},
|
||||
]
|
||||
|
||||
// Macro regime display
|
||||
const macroCol = macroRegime ? macroRegimeColor(macroRegime.dominant) : 'slate'
|
||||
const top3Macro = macroRegime?.ranked?.slice(0, 3) ?? []
|
||||
|
||||
return (
|
||||
<div className={clsx('rounded-xl border p-4 space-y-3', borderMap[col])}>
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<BarChart2 className={clsx('w-4 h-4', `text-${col}-400`)} />
|
||||
@@ -210,7 +223,6 @@ function RegimeCard({
|
||||
<span className="text-xs text-slate-600">{dateLabel}</span>
|
||||
</div>
|
||||
|
||||
{/* Macro regime section (global cycle) */}
|
||||
{macroRegime && macroRegime.dominant !== 'incertain' && (
|
||||
<div className="rounded-lg border border-slate-700/30 bg-dark-700/40 px-3 py-2 space-y-1.5">
|
||||
<div className="flex items-center justify-between">
|
||||
@@ -240,7 +252,6 @@ function RegimeCard({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Technical regime label + confidence */}
|
||||
<div className={clsx('rounded-lg px-3 py-2', `bg-${col}-900/20`)}>
|
||||
<div className="text-xs text-slate-500 mb-0.5">Régime technique</div>
|
||||
<div className={clsx('text-sm font-bold leading-tight', `text-${col}-300`)}>{regime.current}</div>
|
||||
@@ -253,7 +264,6 @@ function RegimeCard({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 6 signal metrics — 2 column grid */}
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
{metrics.map(m => (
|
||||
<div key={m.label} className="rounded-lg bg-dark-700/50 px-2.5 py-2">
|
||||
@@ -364,13 +374,32 @@ function TrendCard({ trend, dateLabel }: { trend: TrendMetrics; dateLabel: strin
|
||||
|
||||
// ── EventsCard ────────────────────────────────────────────────────────────────
|
||||
|
||||
function EventsCard({ events }: { events: SnapshotEvent[] }) {
|
||||
function EventsCard({ events, selectedDate }: { events: SnapshotEvent[]; selectedDate: string | null }) {
|
||||
const navigate = useNavigate()
|
||||
const LEVEL_C: Record<string, string> = {
|
||||
long: 'text-violet-400 bg-violet-900/30 border-violet-700/30',
|
||||
medium: 'text-blue-400 bg-blue-900/30 border-blue-700/30',
|
||||
short: 'text-emerald-400 bg-emerald-900/30 border-emerald-700/30',
|
||||
}
|
||||
|
||||
function isActiveAt(ev: SnapshotEvent): boolean {
|
||||
if (!selectedDate) return false
|
||||
const evStart = ev.date
|
||||
const evEnd = ev.end_date
|
||||
if (evStart > selectedDate) return false
|
||||
if (evEnd) return evEnd >= selectedDate
|
||||
// point event: active if within 30 days after
|
||||
const diffDays = (new Date(selectedDate).getTime() - new Date(evStart).getTime()) / 86400000
|
||||
return diffDays <= 30
|
||||
}
|
||||
|
||||
function surpriseColor(v: number | null | undefined): string {
|
||||
if (v === null || v === undefined) return 'text-slate-400'
|
||||
if (v > 5) return 'text-emerald-400'
|
||||
if (v < -5) return 'text-red-400'
|
||||
return 'text-slate-400'
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="rounded-xl border border-slate-700/40 bg-dark-800/60 p-4 space-y-3">
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -381,25 +410,75 @@ function EventsCard({ events }: { events: SnapshotEvent[] }) {
|
||||
<div className="text-xs text-slate-600 italic py-2">Aucun événement lié trouvé</div>
|
||||
) : (
|
||||
<div className="space-y-2 max-h-[380px] overflow-y-auto pr-1">
|
||||
{events.map((ev, i) => (
|
||||
<div key={i}
|
||||
className="rounded-lg border border-slate-700/30 bg-dark-700/40 p-2 cursor-pointer hover:bg-dark-700/70 transition-colors"
|
||||
onClick={() => navigate(`/timeline?date=${ev.date}`)}
|
||||
>
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<span className="text-xs font-medium text-slate-300 leading-tight">{ev.title}</span>
|
||||
<span className={clsx('shrink-0 text-xs px-1.5 py-0.5 rounded border', LEVEL_C[ev.level] ?? 'text-slate-400 bg-slate-800 border-slate-700/30')}>
|
||||
{ev.level === 'long' ? 'LT' : ev.level === 'medium' ? 'MT' : 'CT'}
|
||||
</span>
|
||||
{events.map((ev, i) => {
|
||||
const active = isActiveAt(ev)
|
||||
return (
|
||||
<div key={i}
|
||||
className={clsx(
|
||||
'rounded-lg border p-2 cursor-pointer transition-colors',
|
||||
active
|
||||
? 'border-amber-600/50 bg-amber-900/20 hover:bg-amber-900/30'
|
||||
: 'border-slate-700/30 bg-dark-700/40 hover:bg-dark-700/70'
|
||||
)}
|
||||
onClick={() => navigate(`/timeline?date=${ev.date}`)}
|
||||
>
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<div className="flex items-center gap-1.5 min-w-0">
|
||||
{active && <span className="shrink-0 w-1.5 h-1.5 rounded-full bg-amber-400 mt-0.5" />}
|
||||
<span className="text-xs font-medium text-slate-300 leading-tight truncate">{ev.title}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1 shrink-0">
|
||||
{ev.sub_type && (
|
||||
<span className="text-xs px-1 py-0 rounded bg-slate-700/50 text-slate-400 border border-slate-600/30"
|
||||
style={{ fontSize: 8, paddingTop: 1, paddingBottom: 1 }}>
|
||||
{ev.sub_type}
|
||||
</span>
|
||||
)}
|
||||
<span className={clsx('text-xs px-1.5 py-0.5 rounded border', LEVEL_C[ev.level] ?? 'text-slate-400 bg-slate-800 border-slate-700/30')}>
|
||||
{ev.level === 'long' ? 'LT' : ev.level === 'medium' ? 'MT' : 'CT'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Eco calendar fields */}
|
||||
{(ev.expected_value || ev.actual_value) && (
|
||||
<div className="flex items-center gap-3 mt-1.5 text-xs">
|
||||
{ev.expected_value && (
|
||||
<span className="text-slate-500">Att. <span className="text-slate-300">{ev.expected_value}</span></span>
|
||||
)}
|
||||
{ev.actual_value && (
|
||||
<span className="text-slate-500">Réel <span className="font-semibold text-white">{ev.actual_value}</span></span>
|
||||
)}
|
||||
{ev.surprise_pct !== null && ev.surprise_pct !== undefined && (
|
||||
<span className={clsx('font-semibold', surpriseColor(ev.surprise_pct))}>
|
||||
{ev.surprise_pct > 0 ? '+' : ''}{ev.surprise_pct.toFixed(0)}%
|
||||
{ev.surprise_pct > 5 ? ' ↑' : ev.surprise_pct < -5 ? ' ↓' : ''}
|
||||
</span>
|
||||
)}
|
||||
{ev.unit && <span className="text-slate-600">{ev.unit}</span>}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{ev.absorption_pct !== null && ev.absorption_pct !== undefined && (
|
||||
<div className="mt-1.5">
|
||||
<div className="flex items-center justify-between text-xs text-slate-600 mb-0.5">
|
||||
<span>Absorption marché</span><span>{ev.absorption_pct}%</span>
|
||||
</div>
|
||||
<div className="h-1 bg-slate-800 rounded-full overflow-hidden">
|
||||
<div className="h-full bg-amber-600/60 rounded-full" style={{ width: `${ev.absorption_pct}%` }} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex items-center gap-2 mt-1 text-xs text-slate-500">
|
||||
<Clock className="w-3 h-3" />
|
||||
{fmtDateFR(ev.date)}{ev.end_date ? ` → ${fmtDateFR(ev.end_date)}` : ''}
|
||||
{ev.impact_score > 0.7 && <AlertCircle className="w-3 h-3 text-orange-400" />}
|
||||
</div>
|
||||
{ev.description && <p className="text-xs text-slate-600 mt-1 line-clamp-1">{ev.description}</p>}
|
||||
</div>
|
||||
<div className="flex items-center gap-2 mt-1 text-xs text-slate-500">
|
||||
<Clock className="w-3 h-3" />
|
||||
{ev.date}{ev.end_date ? ` → ${ev.end_date}` : ''}
|
||||
{ev.impact_score > 0.7 && <AlertCircle className="w-3 h-3 text-orange-400" />}
|
||||
</div>
|
||||
{ev.description && <p className="text-xs text-slate-600 mt-1 line-clamp-1">{ev.description}</p>}
|
||||
</div>
|
||||
))}
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -455,9 +534,9 @@ function eventMatchesDriver(ev: SnapshotEvent, keywords: string[]): boolean {
|
||||
}
|
||||
|
||||
function EventTimelineStrip({
|
||||
events, priceData, drivers,
|
||||
events, priceData, drivers, selectedDate,
|
||||
}: {
|
||||
events: SnapshotEvent[]; priceData: PriceCandle[]; drivers: Driver[]
|
||||
events: SnapshotEvent[]; priceData: PriceCandle[]; drivers: Driver[]; selectedDate: string | null
|
||||
}) {
|
||||
if (priceData.length < 2) return null
|
||||
|
||||
@@ -468,21 +547,21 @@ function EventTimelineStrip({
|
||||
|
||||
function leftPct(d: string) { return Math.max(0, Math.min(99, ((dateToMs(d) - chartStart) / totalMs) * 100)) }
|
||||
function widthPct(start: string, end: string | null): number {
|
||||
// Default width = 3% of period if no end date
|
||||
const endMs = end ? dateToMs(end) : dateToMs(start) + totalMs * 0.03
|
||||
return Math.min(100, Math.max(0.4, ((endMs - dateToMs(start)) / totalMs) * 100))
|
||||
}
|
||||
|
||||
// Top 4 drivers by weight
|
||||
const topDrivers = [...drivers].sort((a, b) => b.weight - a.weight).slice(0, 4)
|
||||
|
||||
// Events matched by ANY driver (for fallback row)
|
||||
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>
|
||||
@@ -492,13 +571,13 @@ function EventTimelineStrip({
|
||||
const matchedEvs = events.filter(ev => eventMatchesDriver(ev, driver.keywords ?? []))
|
||||
return (
|
||||
<div key={driver.key} className="flex items-center gap-2 h-6">
|
||||
<span className={clsx('text-xs font-semibold shrink-0 truncate w-28', cols.label)}
|
||||
style={{ fontSize: 10 }}
|
||||
title={driver.label}
|
||||
>
|
||||
{driver.label}
|
||||
</span>
|
||||
{/* paddingRight aligns with chart right price scale (~60px) */}
|
||||
<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)
|
||||
@@ -523,6 +602,13 @@ function EventTimelineStrip({
|
||||
<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>
|
||||
)
|
||||
@@ -552,6 +638,12 @@ function EventTimelineStrip({
|
||||
</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>
|
||||
)}
|
||||
@@ -582,7 +674,10 @@ function DriversPanel({ instrumentId, drivers, onSave, onClose }: {
|
||||
}
|
||||
|
||||
function addDriver() {
|
||||
setLocal(prev => [...prev, { key: `driver_${Date.now()}`, label: 'Nouveau driver', weight: 0.5, keywords: [] }])
|
||||
setLocal(prev => [...prev, {
|
||||
key: `driver_${Date.now()}`, label: 'Nouveau driver',
|
||||
weight: 0.5, keywords: [], type: 'fundamental',
|
||||
}])
|
||||
}
|
||||
|
||||
function removeDriver(i: number) {
|
||||
@@ -634,6 +729,16 @@ function DriversPanel({ instrumentId, drivers, onSave, onClose }: {
|
||||
value={d.label}
|
||||
onChange={e => updateDriver(i, 'label', e.target.value)}
|
||||
/>
|
||||
{/* Type selector */}
|
||||
<select
|
||||
className="text-xs bg-dark-600 border border-slate-700/40 rounded px-1.5 py-1 text-slate-300"
|
||||
value={d.type ?? 'fundamental'}
|
||||
onChange={e => updateDriver(i, 'type', e.target.value)}
|
||||
>
|
||||
{DRIVER_TYPES.map(t => (
|
||||
<option key={t} value={t}>{DRIVER_TYPE_CFG[t]?.short ?? t}</option>
|
||||
))}
|
||||
</select>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="text-xs text-slate-500">Poids</span>
|
||||
<input
|
||||
@@ -682,7 +787,6 @@ export default function InstrumentDashboard() {
|
||||
const [selectorOpen, setSelectorOpen] = useState(false)
|
||||
const [selectedDate, setSelectedDate] = useState<string | null>(null)
|
||||
const [editDrivers, setEditDrivers] = useState(false)
|
||||
// Local drivers (updated optimistically after save)
|
||||
const [localDrivers, setLocalDrivers] = useState<Driver[] | null>(null)
|
||||
|
||||
const instrumentId = id.toUpperCase()
|
||||
@@ -720,7 +824,6 @@ export default function InstrumentDashboard() {
|
||||
if (date) setSelectedDate(date)
|
||||
}, [])
|
||||
|
||||
// ── Lookup maps ───────────────────────────────────────────────────────────
|
||||
const { priceMap, indMap, sortedDates, dateIndex } = useMemo(() => {
|
||||
if (!snapshot) return { priceMap: {} as Record<string, PriceCandle>, indMap: {} as Record<string, Record<string, number>>, sortedDates: [] as string[], dateIndex: {} as Record<string, number> }
|
||||
const priceMap: Record<string, PriceCandle> = {}
|
||||
@@ -786,7 +889,6 @@ export default function InstrumentDashboard() {
|
||||
}
|
||||
}, [effectiveDate, priceMap, indMap, sortedDates, dateIndex, snapshot])
|
||||
|
||||
// ── UI ────────────────────────────────────────────────────────────────────
|
||||
const grouped = CATEGORY_ORDER.map(cat => ({
|
||||
cat, label: CATEGORY_LABELS[cat] ?? cat,
|
||||
items: instruments.filter(i => i.category === cat),
|
||||
@@ -797,7 +899,6 @@ export default function InstrumentDashboard() {
|
||||
const dateLabel = effectiveDate ? fmtDateFR(effectiveDate) : '—'
|
||||
const displayPrice = dateTrend?.current_price ?? snapshot?.current_price
|
||||
|
||||
// Drivers to use (local override after edit, else from snapshot)
|
||||
const activeDrivers = localDrivers ?? snapshot?.instrument?.drivers ?? []
|
||||
|
||||
return (
|
||||
@@ -860,7 +961,6 @@ export default function InstrumentDashboard() {
|
||||
)}
|
||||
|
||||
<div className="ml-auto flex items-center gap-2">
|
||||
{/* Drivers edit button */}
|
||||
{snapshot && (
|
||||
<button onClick={() => setEditDrivers(e => !e)}
|
||||
className={clsx('flex items-center gap-1.5 text-xs px-3 py-1.5 border rounded-lg transition-colors',
|
||||
@@ -873,7 +973,6 @@ export default function InstrumentDashboard() {
|
||||
Drivers
|
||||
</button>
|
||||
)}
|
||||
{/* Period selector */}
|
||||
<div className="flex items-center gap-1 bg-dark-800 border border-slate-700/40 rounded-xl p-1">
|
||||
{PERIODS.map(p => (
|
||||
<button key={p.key} onClick={() => setPeriod(p.key)}
|
||||
@@ -916,6 +1015,7 @@ export default function InstrumentDashboard() {
|
||||
events={snapshot.events}
|
||||
priceData={snapshot.price_data}
|
||||
drivers={activeDrivers}
|
||||
selectedDate={effectiveDate}
|
||||
/>
|
||||
|
||||
{/* Date badge */}
|
||||
@@ -944,10 +1044,12 @@ export default function InstrumentDashboard() {
|
||||
trend={dateTrend ?? snapshot.trend}
|
||||
dateLabel={dateLabel}
|
||||
/>
|
||||
<EventsCard events={snapshot.events} />
|
||||
<EventsCard
|
||||
events={snapshot.events}
|
||||
selectedDate={effectiveDate}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Drivers edit panel */}
|
||||
{editDrivers && (
|
||||
<DriversPanel
|
||||
instrumentId={instrumentId}
|
||||
@@ -957,7 +1059,6 @@ export default function InstrumentDashboard() {
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* AI Narrative */}
|
||||
<NarrativeCard
|
||||
narrative={narrative}
|
||||
loading={loadingNarr}
|
||||
|
||||
Reference in New Issue
Block a user