import { useState, useEffect } from 'react' import { Link } from 'react-router-dom' import axios from 'axios' import clsx from 'clsx' import { Globe, BarChart2, Activity, Calendar, ArrowRight, Clock, TrendingUp, TrendingDown, Minus, } from 'lucide-react' const api = axios.create({ baseURL: '/api' }) function todayStr() { return new Date().toISOString().split('T')[0] } function fmtShort(d: string) { return new Date(d).toLocaleDateString('fr-FR', { day: '2-digit', month: 'short' }) } function daysSince(ref: string, start: string) { return Math.floor((new Date(ref).getTime() - new Date(start).getTime()) / 86_400_000) } function offsetDate(d: string, days: number) { const dt = new Date(d); dt.setDate(dt.getDate() + days) return dt.toISOString().split('T')[0] } // ── Types ────────────────────────────────────────────────────────────────────── interface MarketEvent { id: number; name: string; start_date: string; end_date: string | null level: string; category: string; description: string; market_impact: string affected_assets: string; impact_score: number } interface DayContext { ref_date: string events: { long: MarketEvent | null; medium: MarketEvent | null; short: MarketEvent | null } has_commentary: boolean long_commentary: string; medium_commentary: string; short_commentary: string } interface Quote { symbol: string; price: number; change_pct: number; name?: string } interface MacroRegime { scenarios: { dominant: string; scores: Record } gauges: Record } interface CalEvent { title: string; date: string; importance: number; currency?: string } // ── Config ───────────────────────────────────────────────────────────────────── const LEVEL = { long: { label: 'Long Terme', badge: 'L', ring: 'border-violet-700/50', bg: 'bg-violet-950/20', header: 'bg-violet-950/40 border-violet-700/40', accent: 'text-violet-400', dot: 'bg-violet-500', badgeCls: 'bg-violet-800 text-violet-200', }, medium: { label: 'Moyen Terme', badge: 'M', ring: 'border-blue-700/50', bg: 'bg-blue-950/20', header: 'bg-blue-950/40 border-blue-700/40', accent: 'text-blue-400', dot: 'bg-blue-500', badgeCls: 'bg-blue-800 text-blue-200', }, short: { label: 'Court Terme', badge: 'C', ring: 'border-emerald-700/50', bg: 'bg-emerald-950/20', header: 'bg-emerald-950/40 border-emerald-700/40', accent: 'text-emerald-400', dot: 'bg-emerald-500', badgeCls: 'bg-emerald-800 text-emerald-200', }, } as const const REGIME_LABELS: Record = { risk_on: 'Risk On', risk_off: 'Risk Off', stagflation: 'Stagflation', goldilocks: 'Goldilocks', recession: 'Récession', reflation: 'Reflation', soft_landing: 'Soft Landing', desinflation: 'Désinflation', incertain: 'Incertain', } // Assets relevant to each temporal level — fallback to standard set const LEVEL_ASSETS: Record = { long: ['SPY', 'GLD', 'USO', 'TLT'], medium: ['SPY', 'USO', 'GLD', 'UUP'], short: ['QQQ', 'TLT', 'UUP', 'GLD'], } // Try to map affected_assets field to quote symbols we have function relevantAssets(ev: MarketEvent | null, level: string): string[] { if (!ev) return LEVEL_ASSETS[level] ?? [] try { const raw: string[] = JSON.parse(ev.affected_assets || '[]') const MAP: Record = { SPX: 'SPY', SPY: 'SPY', QQQ: 'QQQ', NVDA: 'NVDA', GC: 'GLD', Gold: 'GLD', GLD: 'GLD', CL: 'USO', Oil: 'USO', USO: 'USO', NG: 'USO', TLT: 'TLT', USD: 'UUP', UUP: 'UUP', VIX: 'VIX', HYG: 'HYG', } const mapped = raw.map(a => MAP[a]).filter(Boolean) as string[] const unique = [...new Set(mapped)].slice(0, 4) return unique.length >= 2 ? unique : LEVEL_ASSETS[level] ?? [] } catch { return LEVEL_ASSETS[level] ?? [] } } // ── Cells ────────────────────────────────────────────────────────────────────── function AbsorptionBar({ pct }: { pct: number | null | undefined }) { if (pct === null || pct === undefined) return null const color = pct >= 80 ? 'bg-red-500' : pct >= 40 ? 'bg-yellow-500' : 'bg-emerald-500' const label = pct >= 80 ? 'Fully priced' : pct >= 40 ? 'Partial' : 'Not priced' return (
Absorption marché = 80 ? 'text-red-400' : pct >= 40 ? 'text-yellow-400' : 'text-emerald-400')}> {pct.toFixed(0)}% — {label}
) } function CellGeo({ ev, level, refDate, }: { ev: MarketEvent | null; level: keyof typeof LEVEL; refDate: string }) { const cfg = LEVEL[level] if (!ev) return (
Aucun événement {cfg.label.toLowerCase()} actif
) const days = daysSince(refDate, ev.start_date) const absorption = (ev as any).absorption_pct as number | null return (
{ev.name}
{ev.start_date} J+{days} {!ev.end_date && en cours}

{ev.description}

{ev.market_impact && (

{ev.market_impact}

)}
) } // MA period per temporal level const MA_PERIOD: Record = { long: 100, medium: 20, short: 10 } function CellMarkets({ ev, level, quotes, maData, }: { ev: MarketEvent | null; level: string; quotes: Record | null maData: Record }) { // Use event's relevant_indicators if configured let configuredInds: { symbol: string; indicator: string; label: string }[] = [] if (ev) { try { configuredInds = JSON.parse((ev as any).relevant_indicators || '[]') } catch { /**/ } } const assets = configuredInds.length >= 2 ? configuredInds.map(i => i.symbol).slice(0, 4) : relevantAssets(ev, level) const maPeriod = MA_PERIOD[level] ?? 20 const maLabel = `MA${maPeriod}` return (
{/* MA context label */}
{configuredInds.length >= 2 ? 'Indicateurs configurés' : `Prix spot + ${maLabel}`}
{assets.map(sym => { const q = quotes?.[sym] if (!q) return (
{sym}
) const up = q.change_pct > 0.1 const dn = q.change_pct < -0.1 const ma = maData[sym] const maVsPct = ma && q.price ? ((q.price - ma) / ma * 100) : null const maUp = (maVsPct ?? 0) > 0 return (
{q.name || sym}
{q.price?.toFixed(q.price > 100 ? 1 : 2)} {up ? : dn ? : } {q.change_pct > 0 ? '+' : ''}{q.change_pct?.toFixed(2)}%
{ma !== undefined && ma !== null && (
{maLabel} {ma.toFixed(ma > 100 ? 1 : 2)} {maVsPct !== null ? `${maUp ? '+' : ''}${maVsPct.toFixed(1)}%` : ''}
)}
) })}
) } function CellMacro({ level, macro, }: { level: keyof typeof LEVEL; macro: MacroRegime | null }) { const cfg = LEVEL[level] const dominant = macro?.scenarios?.dominant ?? 'incertain' const dominantLabel = REGIME_LABELS[dominant] ?? dominant const scores = macro?.scenarios?.scores ?? {} const maxScore = Math.max(...Object.values(scores).map(Number), 1) const top3 = Object.entries(scores) .sort((a, b) => Number(b[1]) - Number(a[1])) .slice(0, 3) // Key gauge per level const gaugeKeys = { long: ['vix', 'us10y'], medium: ['dxy', 'credit_spread'], short: ['vix', 'dxy'], }[level] ?? ['vix', 'dxy'] return (
{dominantLabel}
{top3.map(([name, score]) => { const pct = Math.min(100, Math.round((Number(score) / maxScore) * 100)) const label = REGIME_LABELS[name] ?? name return (
{label}
{pct}%
) })}
{gaugeKeys.map(k => { const g = macro?.gauges?.[k] if (!g?.value) return null const up = (g.change_pct ?? 0) > 0 return (
{g.label} {g.value?.toFixed(g.value > 10 ? 1 : 2)}
) })}
) } // Calendar events filtered by EXCLUSIVE temporal horizon ranges function calForLevel(events: CalEvent[], level: string, refDate: string): CalEvent[] { // Exclusive ranges so each level shows different events: // short = 0-7 days ahead // medium = 8-30 days ahead // long = 31-90 days ahead const ranges: Record = { short: [0, 7], medium: [8, 30], long: [31, 90], } const [from, to] = ranges[level] ?? [0, 30] return events .filter(e => { const d = e.date return d >= offsetDate(refDate, from) && d <= offsetDate(refDate, to) }) .sort((a, b) => a.date.localeCompare(b.date)) .slice(0, 4) } function CellCalendar({ level, events, refDate, }: { level: keyof typeof LEVEL; events: CalEvent[] | null; refDate: string }) { const cfg = LEVEL[level] const horizon = { long: '3 mois', medium: '1 mois', short: '7 jours' }[level] const items = calForLevel(events ?? [], level, refDate) return (
Horizon {horizon}
{items.length === 0 && (
Aucun événement
)} {items.map((ev, i) => { const imp = ev.importance ?? 1 const impColor = imp >= 3 ? 'text-red-400' : imp >= 2 ? 'text-yellow-400' : 'text-slate-500' return (
{fmtShort(ev.date)} {ev.currency && [{ev.currency}]} {ev.title}
{[1, 2, 3].map(n => (
= 3 ? 'bg-red-500' : imp >= 2 ? 'bg-yellow-500' : 'bg-slate-500') : 'bg-slate-700' )} /> ))}
) })}
) } // ── Temporal Row ─────────────────────────────────────────────────────────────── function TemporalRow({ level, ev, quotes, macro, calEvents, refDate, maData, }: { level: keyof typeof LEVEL ev: MarketEvent | null quotes: Record | null macro: MacroRegime | null calEvents: CalEvent[] | null refDate: string maData: Record }) { const cfg = LEVEL[level] return (
{/* Row header */}
{cfg.badge} {cfg.label} {ev && ( <> · {ev.name} J+{daysSince(refDate, ev.start_date)} depuis {ev.start_date} )} {!ev && ( Aucun événement actif )}
{/* Row body: 4 columns */}
{/* Pilier 1: Géopolitique */}
Géopolitique
{/* Pilier 2: Prix & Marchés */}
Prix & Marchés
{/* Pilier 3: Régimes Macro */}
Régimes Macro
{/* Pilier 4: Calendrier */}
Calendrier
) } // ── Main ─────────────────────────────────────────────────────────────────────── // Compute simple moving average from array of closes (most recent last) function computeMA(closes: number[], period: number): number | null { if (closes.length < period) return null const slice = closes.slice(-period) return slice.reduce((a, b) => a + b, 0) / period } export default function ExternalSnapshot() { const [refDate, setRefDate] = useState(todayStr()) const [ctx, setCtx] = useState(null) const [quotes, setQuotes] = useState | null>(null) const [macro, setMacro] = useState(null) const [calEvents, setCalEvents] = useState(null) // maData[level][symbol] = MA value const [maData, setMaData] = useState>>({ long: {}, medium: {}, short: {}, }) useEffect(() => { api.get(`/timeline/day/${refDate}`) .then(r => setCtx(r.data)) .catch(() => setCtx(null)) }, [refDate]) useEffect(() => { api.get('/market/quotes').then(r => { const flat: Record = {} for (const arr of Object.values(r.data as Record)) for (const q of arr) flat[q.symbol] = q setQuotes(flat) }).catch(() => {}) api.get('/market/macro-regime').then(r => setMacro(r.data)).catch(() => {}) api.get('/geo/calendar').then(r => setCalEvents(r.data)).catch(() => {}) // Fetch MA data for key symbols (200 days history) const KEY_SYMS = ['SPY', 'GLD', 'USO', 'TLT', 'UUP', 'QQQ'] const MA_PERIODS = { long: 100, medium: 20, short: 10 } Promise.allSettled( KEY_SYMS.map(sym => api.get(`/market/history/${sym}`, { params: { days: 200 } }) .then(r => { const candles: { close: number }[] = Array.isArray(r.data) ? r.data : [] const closes = candles.map(c => c.close).filter(Boolean) const result: Record> = { long: {}, medium: {}, short: {}, } for (const [level, period] of Object.entries(MA_PERIODS)) { result[level][sym] = computeMA(closes, period as number) } return result }) ) ).then(results => { const merged: Record> = { long: {}, medium: {}, short: {} } for (const res of results) { if (res.status === 'fulfilled') { for (const level of ['long', 'medium', 'short']) { Object.assign(merged[level], res.value[level]) } } } setMaData(merged) }) }, []) const navigate = (days: number) => { const next = offsetDate(refDate, days) if (next <= todayStr()) setRefDate(next) } const evLong = ctx?.events?.long ?? null const evMedium = ctx?.events?.medium ?? null const evShort = ctx?.events?.short ?? null return (
{/* Header */}

Snapshot Externe

3 temporalités × 4 piliers — vision situationnelle complète

setRefDate(e.target.value)} className="bg-dark-800 border border-slate-700/40 rounded-lg px-3 py-1.5 text-sm text-white focus:outline-none focus:border-slate-500" />
{/* Column headers */}
Géopolitique
Prix & Marchés
Régimes Macro
Calendrier
{/* 3 temporal rows */} {/* Footer links */}
Voir aussi : {[ ['/timeline', 'Timeline'], ['/specialist-desks','COT & Curves'], ['/geo', 'Geo Radar'], ['/institutional', 'Rapports'], ].map(([to, label]) => ( {label} ))}
) }