import { useState, useEffect } from 'react' import { useMacroRegime, useAiStatus } from '../hooks/useApi' import { RefreshCw, Activity, ChevronDown, ChevronUp, Brain } from 'lucide-react' import clsx from 'clsx' import axios from 'axios' const api = axios.create({ baseURL: '/api' }) interface MacroGauge { id: string; label: string; ticker?: string | null value: number | null; change_pct: number | null unit: string; bloc: string; note?: string | null } const SCENARIO_ORDER = [ 'goldilocks', 'desinflation', 'soft_landing', 'reflation', 'stagflation', 'inflation_shock', 'recession', 'crise_liquidite', ] as const const BLOC_INFO: Record = { liquidite: { label: 'Global Liquidity', emoji: 'πŸ’§', description: 'Market fuel β€” DXY↓ = loose global conditions; yield curve = recession/recovery signal' }, credit: { label: 'Credit & Stress', emoji: '⚑', description: 'Systemic thermometer β€” HYG (HY, bonds lead equities), LQD (IG), VIX' }, energie: { label: 'Energy', emoji: 'β›½', description: 'Primary inflation driver β€” Brent↑↑ = stagflation/shock; Brent↓ = disinflationary' }, metaux: { label: 'Strategic Metals', emoji: 'πŸ₯‡', description: 'Copper = "Dr Copper" (global growth); Gold = haven/real rates; Gold/Cu ratio = fear vs expansion bias' }, croissance: { label: 'Global Growth', emoji: 'πŸ“Š', description: 'Real cycle β€” S&P, Russell 2000 (breadth risk-on), XLI (ISM mfg proxy), Baltic Dry (secondary)' }, derive: { label: 'Derived Indicators', emoji: 'πŸ”—', description: 'Slope 10Y–3M (recession) Β· Gold/Cu (fear vs expansion) Β· S&P vs 200d Β· Russell vs S&P (breadth)' }, } const ASSET_BIAS_COLORS: Record = { 'bullish+': '#10b981', 'bullish': '#34d399', 'neutral': '#64748b', 'bearish': '#f97316', 'bearish+': '#ef4444', 'defensive': '#f59e0b', } const ASSET_BIAS_LABELS: Record = { 'bullish+': 'β˜…β˜… Very Favorable', 'bullish': 'β˜… Favorable', 'neutral': 'β†’ Neutral', 'bearish': 'βœ— Unfavorable', 'bearish+': 'βœ—βœ— Against', 'defensive': '⚠ Defensive', } const ASSET_CLASS_LABELS: Record = { energy: 'Energy β›½', metals: 'Metals πŸ₯‡', agriculture: 'Agriculture 🌾', indices: 'Indices πŸ“Š', equities: 'Equities πŸ“ˆ', forex: 'Forex πŸ’±', } function GaugeRow({ g }: { g: MacroGauge }) { const val = g.value const chg = g.change_pct const up = chg != null && chg > 0 const dn = chg != null && chg < 0 const fmt = (v: number) => { if (v > 10000) return `${(v / 1000).toFixed(1)}k` if (v > 1000) return v.toFixed(0) if (v > 100) return v.toFixed(2) return v.toFixed(3) } return (
{g.label} {g.ticker && {g.ticker}}
{val != null ? ( {fmt(val)}{g.unit} ) : ( N/A )} {chg != null && ( {up ? '↑' : dn ? '↓' : 'β†’'} {Math.abs(chg).toFixed(2)}% )} {g.note && ( {g.note} )}
) } function BlocCard({ id, gauges }: { id: string; gauges: MacroGauge[] }) { const [open, setOpen] = useState(true) const info = BLOC_INFO[id] ?? { label: id, emoji: 'πŸ“Œ', description: '' } if (!gauges.length) return null return (
{open && (
{gauges.map(g => )}
)}
) } export default function MacroRegime() { const { data, isLoading, refetch, isFetching, forceRefetch } = useMacroRegime() const { data: aiStatus } = useAiStatus() const [savedNarration, setSavedNarration] = useState<{ text: string; at: string } | null>(null) const [loadingNarration, setLoadingNarration] = useState(false) useEffect(() => { try { const raw = localStorage.getItem('geo-options:macro-narration') if (raw) setSavedNarration(JSON.parse(raw)) } catch {} }, []) const gauges: Record = data?.gauges ?? {} const scenarios = data?.scenarios ?? {} const dominant: string = scenarios.dominant ?? 'incertain' const scores: Record = scenarios.scores ?? {} const meta: Record = scenarios.meta ?? {} const ranked: [string, number][] = scenarios.ranked ?? [] const reasons: Record = scenarios.reasons ?? {} const assetBias: Record> = scenarios.asset_bias ?? {} const historyDays: number = scenarios.history_days ?? 0 const regimeStability: number = scenarios.regime_stability ?? 0 const dominantMeta = meta[dominant] ?? { label: dominant, color: '#94a3b8', emoji: '?' } const dominantBias: Record = assetBias[dominant] ?? {} // Group gauges by bloc const byBloc: Record = {} for (const g of Object.values(gauges)) { if (!byBloc[g.bloc]) byBloc[g.bloc] = [] byBloc[g.bloc].push(g) } // Add derived gauges into their own bloc const derivedGauges = ([gauges.slope_10y3m, gauges.gold_copper_ratio, gauges.spx_vs_200d] as (MacroGauge | undefined)[]) .filter((g): g is MacroGauge => !!g) if (derivedGauges.length) byBloc['derive'] = derivedGauges const handleAiNarration = async () => { setLoadingNarration(true) try { const freshData = await forceRefetch() const res = await api.post('/ai/macro-narration', { macro_regime: freshData }) const text = res.data.narration ?? res.data.text ?? JSON.stringify(res.data) const entry = { text, at: new Date().toISOString() } setSavedNarration(entry) localStorage.setItem('geo-options:macro-narration', JSON.stringify(entry)) } catch { const entry = { text: 'Error during generation β€” please check your OpenAI key.', at: new Date().toISOString() } setSavedNarration(entry) } finally { setLoadingNarration(false) } } return (
{/* Header */}

Macro Regime

30 institutional gauges Β· Regime detection Β· Scenario compatibility

{data?.fetched_at && ( {data.cached ? `cache ${data.cache_age_sec}s` : 'live'} Β· {data.fetched_at.slice(11, 16)} UTC )}
{isLoading ? (
Loading macro gauges...
) : ( <> {/* Dominant scenario banner */} {dominant !== 'incertain' && (
{dominantMeta.emoji}
Dominant Scenario
{dominantMeta.label}
{(reasons[dominant] ?? []).map((r, i) => ( {r} ))}
{scores[dominant]}%
regime score
{regimeStability > 0 && (
stable {regimeStability}d
)} {historyDays > 0 && (
{historyDays}d history
)}
)}
{/* Left: Gauge blocs */}
Gauges by bloc ({Object.values(gauges).length} indicators)
{['liquidite', 'credit', 'energie', 'metaux', 'croissance', 'derive'].map(bloc => ( ))}
{/* Right: Scenarios + asset compatibility */}
{/* Scenario ranking */}
Scenario Ranking
{ranked.map(([key, score]) => { const m = meta[key] ?? { label: key, color: '#94a3b8', emoji: '?' } const isDom = key === dominant const rl = reasons[key] ?? [] return (
{m.emoji} {m.label} {score}%
{isDom && rl.length > 0 && (
{rl.slice(0, 3).map((r, i) => (
β€Ί {r}
))}
)}
) })}
{/* Asset class compatibility for dominant scenario */} {dominant !== 'incertain' && Object.keys(dominantBias).length > 0 && (
Asset Classes Β· {dominantMeta.emoji} {dominantMeta.label}
{Object.entries(dominantBias).map(([cls, bias]) => { const col = ASSET_BIAS_COLORS[bias] ?? '#64748b' const lbl = ASSET_BIAS_LABELS[bias] ?? bias return (
{ASSET_CLASS_LABELS[cls] ?? cls} {lbl}
) })}
)} {/* AI narration block */}
AI Analysis
{savedNarration?.at && ( {new Date(savedNarration.at).toLocaleDateString('fr-FR', { day: '2-digit', month: 'short', hour: '2-digit', minute: '2-digit', })} )}
{savedNarration ? (

{savedNarration.text}

) : (

GPT-4o can interpret the current regime, identify inconsistencies between gauges and suggest trading biases.

)} {aiStatus?.enabled ? ( ) : (
OpenAI not configured
)}
{/* Reading order reminder */}
Reading order (5 min)
1. Liquidity β†’ Fed balance sheet, DXY, real rates
2. Credit β†’ HY spreads, VIX, MOVE
3. Energy β†’ Brent, natural gas
4. Metals β†’ Gold/Copper ratio
5. Synthesis β†’ Dominant scenario
)}
) }