feat: macro gauge DB + regime triggers + date-aware instrument snapshot
DB: - New table macro_gauge_snapshots (daily snapshot of all 28+ gauges + dominant + scores) - save_macro_gauge_snapshot / get_macro_gauge_snapshot_at / get_macro_gauge_history - Auto-save once per calendar day on every macro-regime fetch (not just force=True) API: - GET /api/market/macro-gauges/at?date=YYYY-MM-DD — nearest snapshot ≤ date - GET /api/market/macro-gauges/history?days=N Detector (_check_macro_gauges in Eco Desk): - Regime transition events (goldilocks→stagflation etc.) with severity scoring - Yield curve inversion / désinversion (slope_10y3m sign change) - DXY shock (% change over lookback window) - Credit stress (HYG drop threshold) - Gold/Copper ratio regime crossings InstrumentDashboard: - macroAtDate state: fetches /api/market/macro-gauges/at when crosshair date ≠ last date - RegimeCard uses historical macro regime when on a past date - MacroGaugePanel: full breakdown of all gauges by bloc (liquidité, crédit, énergie...) visible only when on a historical date — shows value + change_pct + regime scores bar AIDesks: added fundamental + sentiment to AIDesk type Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -24,7 +24,7 @@ interface SignalDef {
|
||||
interface AIDesk {
|
||||
id?: number
|
||||
name: string
|
||||
type: 'news' | 'technical' | 'eco' | 'report'
|
||||
type: 'news' | 'fundamental' | 'technical' | 'eco' | 'report' | 'sentiment'
|
||||
active: boolean
|
||||
system_prompt: string
|
||||
instruments: string[]
|
||||
|
||||
@@ -56,6 +56,18 @@ interface MacroRegime {
|
||||
asset_bias: Record<string, any>
|
||||
}
|
||||
|
||||
interface GaugeValue {
|
||||
id: string; label: string; value: number | null; change_pct: number | null
|
||||
unit: string; bloc: string; note?: string
|
||||
}
|
||||
|
||||
interface MacroGaugeSnap {
|
||||
snapshot_date: string
|
||||
dominant: string
|
||||
regime_scores: Record<string, number>
|
||||
gauges: Record<string, GaugeValue>
|
||||
}
|
||||
|
||||
interface Snapshot {
|
||||
instrument: InstrumentConfig
|
||||
price_data: PriceCandle[]
|
||||
@@ -621,6 +633,118 @@ function DriversPanel({ instrumentId, drivers, onSave, onClose }: {
|
||||
)
|
||||
}
|
||||
|
||||
// ── Macro gauge helpers ───────────────────────────────────────────────────────
|
||||
|
||||
const SCENARIO_META: Record<string, { label: string; emoji: string; color: string }> = {
|
||||
goldilocks: { label: 'Goldilocks', emoji: '🟢', color: '#10b981' },
|
||||
desinflation: { label: 'Désinflation', emoji: '🔵', color: '#3b82f6' },
|
||||
soft_landing: { label: 'Soft Landing', emoji: '🔷', color: '#06b6d4' },
|
||||
reflation: { label: 'Reflation', emoji: '🟠', color: '#f97316' },
|
||||
stagflation: { label: 'Stagflation', emoji: '🟡', color: '#f59e0b' },
|
||||
inflation_shock: { label: 'Choc Inflationniste', emoji: '🔥', color: '#dc2626' },
|
||||
recession: { label: 'Récession', emoji: '🔴', color: '#ef4444' },
|
||||
crise_liquidite: { label: 'Crise de liquidité', emoji: '🟣', color: '#7c3aed' },
|
||||
incertain: { label: 'Incertain', emoji: '⬜', color: '#64748b' },
|
||||
}
|
||||
|
||||
function snapToMacroRegime(snap: MacroGaugeSnap): MacroRegime {
|
||||
const scores = snap.regime_scores ?? {}
|
||||
const meta = SCENARIO_META[snap.dominant] ?? SCENARIO_META.incertain
|
||||
const ranked = Object.entries(scores)
|
||||
.sort(([, a], [, b]) => b - a)
|
||||
.map(([k]) => k)
|
||||
return {
|
||||
dominant: snap.dominant,
|
||||
label: meta.label,
|
||||
color: meta.color,
|
||||
emoji: meta.emoji,
|
||||
scores,
|
||||
ranked,
|
||||
asset_bias: {},
|
||||
}
|
||||
}
|
||||
|
||||
const BLOC_LABELS: Record<string, string> = {
|
||||
liquidite: 'Liquidité / Taux',
|
||||
credit: 'Crédit / Vol',
|
||||
energie: 'Énergie',
|
||||
metaux: 'Métaux',
|
||||
croissance: 'Croissance US',
|
||||
secteurs: 'Secteurs',
|
||||
volatilite: 'Volatilité surf.',
|
||||
global: 'Global / EM',
|
||||
forex_ro: 'Forex Risk-Off',
|
||||
derive: 'Dérivés',
|
||||
}
|
||||
|
||||
function MacroGaugePanel({ snap, dateLabel }: { snap: MacroGaugeSnap; dateLabel: string }) {
|
||||
const byBloc: Record<string, GaugeValue[]> = {}
|
||||
for (const g of Object.values(snap.gauges)) {
|
||||
if (g.value === null && g.change_pct === null) continue
|
||||
const b = g.bloc ?? 'derive'
|
||||
if (!byBloc[b]) byBloc[b] = []
|
||||
byBloc[b].push(g)
|
||||
}
|
||||
const meta = SCENARIO_META[snap.dominant] ?? SCENARIO_META.incertain
|
||||
|
||||
return (
|
||||
<div className="bg-dark-800/60 rounded-xl border border-slate-700/30 p-4 space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-base">{meta.emoji}</span>
|
||||
<span className="text-sm font-semibold text-white">{meta.label}</span>
|
||||
<span className="text-xs text-slate-500">— contexte macro au {dateLabel}</span>
|
||||
</div>
|
||||
<span className="text-xs text-slate-600">{snap.snapshot_date}</span>
|
||||
</div>
|
||||
|
||||
{/* Regime scores mini bar */}
|
||||
<div className="flex gap-1 h-1.5">
|
||||
{Object.entries(snap.regime_scores)
|
||||
.sort(([,a],[,b]) => b - a)
|
||||
.slice(0, 6)
|
||||
.map(([k, v]) => (
|
||||
<div key={k} title={`${SCENARIO_META[k]?.label ?? k}: ${(v*100).toFixed(0)}%`}
|
||||
style={{ width: `${v*100}%`, background: SCENARIO_META[k]?.color ?? '#64748b' }}
|
||||
className="rounded-full transition-all"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Gauges by bloc */}
|
||||
<div className="grid grid-cols-2 gap-x-6 gap-y-3">
|
||||
{Object.entries(byBloc).map(([bloc, gauges]) => (
|
||||
<div key={bloc}>
|
||||
<div className="text-xs text-slate-600 uppercase tracking-wide mb-1">{BLOC_LABELS[bloc] ?? bloc}</div>
|
||||
<div className="space-y-0.5">
|
||||
{gauges.map(g => (
|
||||
<div key={g.id} className="flex items-center justify-between text-xs">
|
||||
<span className="text-slate-400 truncate max-w-[120px]" title={g.label}>{g.label}</span>
|
||||
<div className="flex items-center gap-2">
|
||||
{g.value !== null && (
|
||||
<span className="text-white font-mono">
|
||||
{g.unit === '%' ? g.value.toFixed(2) + '%'
|
||||
: g.unit === 'pts' ? g.value.toFixed(1)
|
||||
: g.unit === 'ratio' ? g.value.toFixed(3)
|
||||
: g.value.toFixed(2)}
|
||||
</span>
|
||||
)}
|
||||
{g.change_pct !== null && g.change_pct !== undefined && (
|
||||
<span className={clsx('font-mono', g.change_pct >= 0 ? 'text-emerald-400' : 'text-red-400')}>
|
||||
{g.change_pct >= 0 ? '+' : ''}{g.change_pct.toFixed(1)}%
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// ── Main page ─────────────────────────────────────────────────────────────────
|
||||
|
||||
const PERIODS = [
|
||||
@@ -641,6 +765,7 @@ export default function InstrumentDashboard() {
|
||||
const [selectedDate, setSelectedDate] = useState<string | null>(null)
|
||||
const [editDrivers, setEditDrivers] = useState(false)
|
||||
const [localDrivers, setLocalDrivers] = useState<Driver[] | null>(null)
|
||||
const [macroAtDate, setMacroAtDate] = useState<MacroGaugeSnap | null>(null)
|
||||
|
||||
const instrumentId = id.toUpperCase()
|
||||
|
||||
@@ -655,6 +780,7 @@ export default function InstrumentDashboard() {
|
||||
setSelectedDate(null)
|
||||
setLocalDrivers(null)
|
||||
setEditDrivers(false)
|
||||
setMacroAtDate(null)
|
||||
api.get(`/instruments/${instrumentId}/snapshot?period=${period}`)
|
||||
.then(r => {
|
||||
setSnapshot(r.data)
|
||||
@@ -691,6 +817,16 @@ export default function InstrumentDashboard() {
|
||||
return { priceMap, indMap, sortedDates, dateIndex }
|
||||
}, [snapshot])
|
||||
|
||||
// Fetch macro gauge context when crosshair date changes (after sortedDates is available)
|
||||
useEffect(() => {
|
||||
if (!selectedDate || !sortedDates.length) return
|
||||
const isLast = selectedDate === sortedDates[sortedDates.length - 1]
|
||||
if (isLast) { setMacroAtDate(null); return }
|
||||
api.get(`/market/macro-gauges/at?date=${selectedDate}`)
|
||||
.then(r => { if (r.data?.snapshot_date) setMacroAtDate(r.data) })
|
||||
.catch(() => {})
|
||||
}, [selectedDate, sortedDates])
|
||||
|
||||
const effectiveDate = useMemo(() => {
|
||||
if (selectedDate && dateIndex[selectedDate] !== undefined) return selectedDate
|
||||
return sortedDates[sortedDates.length - 1] ?? null
|
||||
@@ -882,7 +1018,7 @@ export default function InstrumentDashboard() {
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<RegimeCard
|
||||
regime={snapshot.regime}
|
||||
macroRegime={snapshot.macro_regime ?? null}
|
||||
macroRegime={macroAtDate ? snapToMacroRegime(macroAtDate) : (snapshot.macro_regime ?? null)}
|
||||
signalsAt={dateSignals}
|
||||
dateLabel={dateLabel}
|
||||
/>
|
||||
@@ -896,6 +1032,11 @@ export default function InstrumentDashboard() {
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Macro gauge detail panel — shown when on a historical date */}
|
||||
{macroAtDate && (
|
||||
<MacroGaugePanel snap={macroAtDate} dateLabel={dateLabel} />
|
||||
)}
|
||||
|
||||
{editDrivers && (
|
||||
<DriversPanel
|
||||
instrumentId={instrumentId}
|
||||
|
||||
Reference in New Issue
Block a user