feat: macro regime + 30 historical events + regime confidence fix
- Add macro_regime (goldilocks/stagflation/recession/etc.) to every instrument snapshot via get_macro_gauges() + score_macro_scenarios() - RegimeCard now shows global macro cycle section (emoji + label + top-3 scenarios) above technical signals - Fix _detect_regime() confidence: capped at 85% max; add late-bull (dist_MA200 > 20%) and correction-in-bull (MA50 > MA200 but momentum < -3%) detection so regime no longer locks at 100% - Add macro_events_bootstrap.py with 30 curated historical events (FOMC 2022-2025, CPI surprises, Ukraine/Hamas/Iran geopolitics, BOJ pivots, Bitcoin ETF, Liberation Day tariffs, SVB crisis, etc.) - POST /api/timeline/bootstrap-macro endpoint (idempotent, deduplicates by name) - Fix event date filter in _get_relevant_events(): overlap logic instead of start-only filter — events extending into the chart window are now included - EventTimelineStrip: add "Signaux Techniques" fallback row for events not matched by any driver keyword (MA crossovers are now always visible) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -44,11 +44,22 @@ interface TrendMetrics {
|
||||
current_price: number; high_52w: number; low_52w: number
|
||||
}
|
||||
|
||||
interface MacroRegime {
|
||||
dominant: string
|
||||
label: string
|
||||
color: string
|
||||
emoji: string
|
||||
scores: Record<string, number>
|
||||
ranked: string[]
|
||||
asset_bias: Record<string, any>
|
||||
}
|
||||
|
||||
interface Snapshot {
|
||||
instrument: InstrumentConfig
|
||||
price_data: PriceCandle[]
|
||||
indicators: Record<string, LinePoint[]>
|
||||
regime: { current: string; confidence: number; scores: Record<string, number>; signals: RegimeSignals }
|
||||
macro_regime: MacroRegime
|
||||
trend: TrendMetrics
|
||||
events: SnapshotEvent[]
|
||||
current_price: number; change_pct: number; change_abs: number; period: string
|
||||
@@ -103,12 +114,31 @@ function pctN(a: number | undefined, b: number | undefined): number {
|
||||
return ((a - b) / b) * 100
|
||||
}
|
||||
|
||||
// ── Macro regime colour mapping ───────────────────────────────────────────────
|
||||
|
||||
const MACRO_COLOR_MAP: Record<string, string> = {
|
||||
goldilocks: 'emerald',
|
||||
desinflation: 'cyan',
|
||||
soft_landing: 'blue',
|
||||
reflation: 'amber',
|
||||
stagflation: 'orange',
|
||||
inflation_shock: 'red',
|
||||
recession: 'red',
|
||||
crise_liquidite: 'red',
|
||||
incertain: 'slate',
|
||||
}
|
||||
|
||||
function macroRegimeColor(dominant: string): string {
|
||||
return MACRO_COLOR_MAP[dominant] ?? 'slate'
|
||||
}
|
||||
|
||||
// ── RegimeCard (métriques de signaux) ─────────────────────────────────────────
|
||||
|
||||
function RegimeCard({
|
||||
regime, signalsAt, dateLabel,
|
||||
regime, macroRegime, signalsAt, dateLabel,
|
||||
}: {
|
||||
regime: Snapshot['regime']
|
||||
macroRegime: MacroRegime | null
|
||||
signalsAt: RegimeSignals | null
|
||||
dateLabel: string
|
||||
}) {
|
||||
@@ -121,6 +151,8 @@ function RegimeCard({
|
||||
orange: 'border-orange-700/40 bg-orange-950/30',
|
||||
slate: 'border-slate-700/40 bg-slate-900/30',
|
||||
blue: 'border-blue-700/40 bg-blue-950/30',
|
||||
cyan: 'border-cyan-700/40 bg-cyan-950/30',
|
||||
amber: 'border-amber-700/40 bg-amber-950/30',
|
||||
}
|
||||
|
||||
// 6 signal metrics displayed as a compact grid
|
||||
@@ -163,20 +195,55 @@ 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`)} />
|
||||
<span className="text-xs font-semibold text-slate-400 uppercase tracking-wide">Régime Détecté</span>
|
||||
<span className="text-xs font-semibold text-slate-400 uppercase tracking-wide">Régime</span>
|
||||
</div>
|
||||
<span className="text-xs text-slate-600">{dateLabel}</span>
|
||||
</div>
|
||||
|
||||
{/* Regime label + confidence */}
|
||||
{/* 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">
|
||||
<span className="text-xs text-slate-500 uppercase tracking-wide">Cycle macro global</span>
|
||||
<span className="text-xs text-slate-600">Fed cycle</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-base">{macroRegime.emoji || '🌍'}</span>
|
||||
<span className={clsx('text-sm font-bold', `text-${macroCol}-300`)}>{macroRegime.label || macroRegime.dominant}</span>
|
||||
</div>
|
||||
{top3Macro.length > 0 && (
|
||||
<div className="flex flex-wrap gap-1 pt-0.5">
|
||||
{top3Macro.map((key, i) => {
|
||||
const score = macroRegime.scores?.[key]
|
||||
const c = macroRegimeColor(key)
|
||||
return (
|
||||
<span key={key} className={clsx(
|
||||
'text-xs px-1.5 py-0.5 rounded border',
|
||||
i === 0 ? `text-${c}-300 bg-${c}-900/30 border-${c}-700/40` : 'text-slate-500 bg-dark-800/40 border-slate-700/30'
|
||||
)}>
|
||||
{key} {score !== undefined ? Math.round(score) + '%' : ''}
|
||||
</span>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Technical regime label + confidence */}
|
||||
<div className={clsx('rounded-lg px-3 py-2', `bg-${col}-900/20`)}>
|
||||
<div className={clsx('text-base font-bold leading-tight', `text-${col}-300`)}>{regime.current}</div>
|
||||
<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>
|
||||
<div className="flex items-center gap-2 mt-1">
|
||||
<div className="flex-1 h-1.5 bg-slate-800 rounded-full overflow-hidden">
|
||||
<div className={clsx('h-full rounded-full', `bg-${col}-500`)}
|
||||
@@ -409,6 +476,13 @@ function EventTimelineStrip({
|
||||
// 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))
|
||||
|
||||
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>
|
||||
@@ -453,6 +527,34 @@ function EventTimelineStrip({
|
||||
</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>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
@@ -834,6 +936,7 @@ export default function InstrumentDashboard() {
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<RegimeCard
|
||||
regime={snapshot.regime}
|
||||
macroRegime={snapshot.macro_regime ?? null}
|
||||
signalsAt={dateSignals}
|
||||
dateLabel={dateLabel}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user