diff --git a/frontend/src/pages/InstrumentDashboard.tsx b/frontend/src/pages/InstrumentDashboard.tsx index bdadeb4..ed44211 100644 --- a/frontend/src/pages/InstrumentDashboard.tsx +++ b/frontend/src/pages/InstrumentDashboard.tsx @@ -2,7 +2,7 @@ import { useState, useEffect, useCallback, useMemo, useRef } from 'react' import { useParams, useNavigate } from 'react-router-dom' import { Sparkles, RefreshCw, ChevronDown, TrendingUp, TrendingDown, - Minus, BarChart2, Clock, Calendar, AlertCircle, Pencil, Save, X, Plus, Trash2, + Minus, BarChart2, Clock, Calendar, AlertCircle, } from 'lucide-react' import axios from 'axios' import clsx from 'clsx' @@ -12,13 +12,15 @@ const api = axios.create({ baseURL: '/api' }) // ── Types ───────────────────────────────────────────────────────────────────── -interface Driver { - key: string; label: string; weight: number; keywords: string[]; type?: string +interface CausalTemplate { + id: number; name: string; category: string; sub_type: string + instruments: string[]; description: string + graph_json: { nodes: { id: string; type: string; label: string; instrument?: string }[]; edges: any[] } } interface InstrumentConfig { id: string; name: string; yf_ticker: string; category: string; currency: string - description: string; drivers: Driver[] + description: string regime_labels: string[] chart: { ma_periods: number[]; show_volume: boolean } correlation_instruments: string[] @@ -134,18 +136,41 @@ function isActiveAt(ev: SnapshotEvent, selectedDate: string | null): boolean { return diffDays <= 30 } -// ── Driver type config ──────────────────────────────────────────────────────── +// ── Causal graph config ─────────────────────────────────────────────────────── -const DRIVER_TYPES = ['event_calendar', 'report', 'geopolitical', 'fundamental', 'sentiment', 'technical'] as const -type DriverType = typeof DRIVER_TYPES[number] +// Maps snapshot event category → causal template category +const EVENT_TO_CAUSAL_CAT: Record = { + macro_us: 'macro_us', + macro_eu: 'macro_eu', + geopolitical: 'geopolitical', + report: 'report', + sentiment: 'sentiment', + commodity: 'commodity', + fundamental: 'macro_us', + event_calendar: 'macro_us', +} -const DRIVER_TYPE_CFG: Record = { - event_calendar: { short: 'Cal.', tw: 'text-amber-400 bg-amber-900/30 border-amber-700/30' }, - report: { short: 'Rep.', tw: 'text-blue-400 bg-blue-900/30 border-blue-700/30' }, - geopolitical: { short: 'Geo.', tw: 'text-red-400 bg-red-900/30 border-red-700/30' }, - fundamental: { short: 'Fund.', tw: 'text-emerald-400 bg-emerald-900/30 border-emerald-700/30' }, - sentiment: { short: 'Sent.', tw: 'text-violet-400 bg-violet-900/30 border-violet-700/30' }, - technical: { short: 'Tech.', tw: 'text-cyan-400 bg-cyan-900/30 border-cyan-700/30' }, +// Maps instrument dashboard category → causal lab instrument keys +const CAT_TO_CAUSAL_INST: Record = { + equity_index: ['SP500'], + equity_intl: ['SP500'], + metal: ['XAUUSD'], + energy: ['BRENT'], + fx: ['EURUSD'], + bond: ['US10Y', 'EU10Y'], + credit: ['US10Y'], + volatility: ['SP500'], + stock: ['SP500'], + crypto: [], +} + +const CAUSAL_CAT_TW: Record = { + macro_us: 'text-blue-400 border-blue-700/30 bg-blue-900/20', + macro_eu: 'text-cyan-400 border-cyan-700/30 bg-cyan-900/20', + geopolitical: 'text-red-400 border-red-700/30 bg-red-900/20', + report: 'text-amber-400 border-amber-700/30 bg-amber-900/20', + sentiment: 'text-violet-400 border-violet-700/30 bg-violet-900/20', + commodity: 'text-orange-400 border-orange-700/30 bg-orange-900/20', } // ── Macro regime colour mapping ─────────────────────────────────────────────── @@ -525,122 +550,6 @@ function NarrativeCard({ narrative, loading, onLoad, instrument }: { ) } -// ── DriversPanel — édition inline ──────────────────────────────────────────── - -function DriversPanel({ instrumentId, drivers, onSave, onClose }: { - instrumentId: string - drivers: Driver[] - onSave: (drivers: Driver[]) => void - onClose: () => void -}) { - const [local, setLocal] = useState(() => drivers.map(d => ({ ...d, keywords: [...(d.keywords ?? [])] }))) - const [saving, setSaving] = useState(false) - const [error, setError] = useState('') - - function updateDriver(i: number, field: keyof Driver, val: any) { - setLocal(prev => prev.map((d, idx) => idx === i ? { ...d, [field]: val } : d)) - } - - function updateKeywords(i: number, raw: string) { - const kws = raw.split(',').map(s => s.trim()).filter(Boolean) - updateDriver(i, 'keywords', kws) - } - - function addDriver() { - setLocal(prev => [...prev, { - key: `driver_${Date.now()}`, label: 'Nouveau driver', - weight: 0.5, keywords: [], type: 'fundamental', - }]) - } - - function removeDriver(i: number) { - setLocal(prev => prev.filter((_, idx) => idx !== i)) - } - - async function save() { - setSaving(true); setError('') - try { - await api.put(`/instruments/${instrumentId}/drivers`, { drivers: local }) - onSave(local) - onClose() - } catch (e: any) { - setError(e?.response?.data?.detail ?? 'Erreur de sauvegarde') - } finally { - setSaving(false) - } - } - - return ( -
-
- Éditer les drivers — {instrumentId} -
- - - -
-
- - {error &&
{error}
} - -
- {local.map((d, i) => ( -
-
- updateDriver(i, 'label', e.target.value)} - /> - {/* Type selector */} - -
- Poids - updateDriver(i, 'weight', parseFloat(e.target.value))} - /> -
- -
-
- - updateKeywords(i, e.target.value)} - /> -
-
- ))} -
-
- ) -} - // ── Macro gauge helpers ─────────────────────────────────────────────────────── const SCENARIO_META: Record = { @@ -849,45 +758,89 @@ function EventTimeline({ ) } -// ── DriversValidation ───────────────────────────────────────────────────────── +// ── EventGraphCards ─────────────────────────────────────────────────────────── -function DriversValidation({ - drivers, events, selectedDate, +function EventGraphCards({ + events, templates, selectedDate, instrumentCategory, }: { - drivers: Driver[] events: SnapshotEvent[] + templates: CausalTemplate[] selectedDate: string | null + instrumentCategory: string }) { - if (!drivers.length) { - return
Aucun driver configuré
+ const causalInsts = CAT_TO_CAUSAL_INST[instrumentCategory] ?? [] + + const allCats = new Set( + events.map(ev => EVENT_TO_CAUSAL_CAT[ev.category]).filter(Boolean) + ) + const activeCats = new Set( + events.filter(ev => isActiveAt(ev, selectedDate)) + .map(ev => EVENT_TO_CAUSAL_CAT[ev.category]).filter(Boolean) + ) + + const relevant = templates.filter(t => allCats.has(t.category)) + + if (!relevant.length) { + return ( +
+ Aucun graphe causal associé aux événements de cette période +
+ ) } + return (
- {drivers.map(d => { - const matches = events.filter(ev => ev.category === d.type || ev.category === d.key) - const active = matches.some(ev => isActiveAt(ev, selectedDate)) - const typeCfg = DRIVER_TYPE_CFG[d.type ?? ''] + {relevant.map(t => { + const eventActive = activeCats.has(t.category) + const instrumentTouched = causalInsts.some(ci => t.instruments.includes(ci)) return (
- - {d.label} + + {t.name} - {typeCfg && ( - - {typeCfg.short} - - )} + + {t.category} + +
+
+ {t.instruments.map(inst => ( + {inst} + ))}
- poids {d.weight.toFixed(2)} - + + {eventActive ? '● actif' : '○ période'} + +
) @@ -899,17 +852,24 @@ function DriversValidation({ // ── ExplanationScore ────────────────────────────────────────────────────────── function ExplanationScore({ - drivers, events, selectedDate, + events, templates, selectedDate, instrumentCategory, }: { - drivers: Driver[] events: SnapshotEvent[] + templates: CausalTemplate[] selectedDate: string | null + instrumentCategory: string }) { - const totalWeight = drivers.reduce((s, d) => s + (d.weight || 0), 0) - const activeWeight = drivers - .filter(d => events.filter(ev => ev.category === d.type || ev.category === d.key).some(ev => isActiveAt(ev, selectedDate))) - .reduce((s, d) => s + (d.weight || 0), 0) - const score = totalWeight > 0 ? (activeWeight / totalWeight) * 100 : 0 + const causalInsts = CAT_TO_CAUSAL_INST[instrumentCategory] ?? [] + const allCats = new Set(events.map(ev => EVENT_TO_CAUSAL_CAT[ev.category]).filter(Boolean)) + const activeCats = new Set( + events.filter(ev => isActiveAt(ev, selectedDate)) + .map(ev => EVENT_TO_CAUSAL_CAT[ev.category]).filter(Boolean) + ) + const periodTmpl = templates.filter(t => allCats.has(t.category)) + const touchedActive = templates.filter(t => + activeCats.has(t.category) && causalInsts.some(ci => t.instruments.includes(ci)) + ) + const score = periodTmpl.length > 0 ? (touchedActive.length / periodTmpl.length) * 100 : 0 const verdict = score >= 60 ? 'Bien expliqué' : score >= 30 ? 'Partiellement' : 'Peu lisible' const badgeCls = score >= 60 ? 'text-emerald-400 bg-emerald-900/30 border-emerald-700/40' @@ -927,6 +887,11 @@ function ExplanationScore({ {Math.round(score)}% — {verdict} + {periodTmpl.length > 0 && ( + + {touchedActive.length}/{periodTmpl.length} graphes + + )}
) } @@ -949,15 +914,15 @@ export default function InstrumentDashboard() { const [loadingNarr, setLoadingNarr] = useState(false) const [selectorOpen, setSelectorOpen] = useState(false) const [selectedDate, setSelectedDate] = useState(null) - const [editDrivers, setEditDrivers] = useState(false) - const [tabUnder, setTabUnder] = useState<'counters' | 'analyse'>('counters') - const [localDrivers, setLocalDrivers] = useState(null) - const [macroAtDate, setMacroAtDate] = useState(null) + const [tabUnder, setTabUnder] = useState<'counters' | 'analyse'>('counters') + const [templates, setTemplates] = useState([]) + const [macroAtDate, setMacroAtDate] = useState(null) const instrumentId = id.toUpperCase() useEffect(() => { api.get('/instruments').then(r => setInstruments(r.data)).catch(() => {}) + api.get('/causal-lab/templates').then(r => setTemplates(r.data)).catch(() => {}) }, []) useEffect(() => { @@ -965,8 +930,6 @@ export default function InstrumentDashboard() { setSnapshot(null) setNarrative('') setSelectedDate(null) - setLocalDrivers(null) - setEditDrivers(false) setMacroAtDate(null) api.get(`/instruments/${instrumentId}/snapshot?period=${period}`) .then(r => { @@ -1075,8 +1038,6 @@ export default function InstrumentDashboard() { const dateLabel = effectiveDate ? fmtDateFR(effectiveDate) : '—' const displayPrice = dateTrend?.current_price ?? snapshot?.current_price - const activeDrivers = localDrivers ?? snapshot?.instrument?.drivers ?? [] - return (
@@ -1137,18 +1098,6 @@ export default function InstrumentDashboard() { )}
- {snapshot && ( - - )}
{PERIODS.map(p => (
- {/* Zone basse — validation des drivers */} + {/* Zone basse — graphes causaux liés aux événements */}
- Validation des drivers - {fmtDateFR(effectiveDate)} + Graphes causaux + + vert = actif + touche l'instrument · ambre = actif · gris = période seulement +
-
{/* Note globale */}
)} - {editDrivers && ( - { setLocalDrivers(saved); setEditDrivers(false) }} - onClose={() => setEditDrivers(false)} - /> - )} -