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:
OpenSquared
2026-06-26 09:34:35 +02:00
parent a12d7a1ef3
commit 85eb864584
5 changed files with 513 additions and 5 deletions

View File

@@ -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[]

View File

@@ -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}