feat: instrument analysis
This commit is contained in:
@@ -2,7 +2,7 @@ import { useState, useEffect, useCallback, useMemo, useRef } from 'react'
|
|||||||
import { useParams, useNavigate } from 'react-router-dom'
|
import { useParams, useNavigate } from 'react-router-dom'
|
||||||
import {
|
import {
|
||||||
Sparkles, RefreshCw, ChevronDown, TrendingUp, TrendingDown,
|
Sparkles, RefreshCw, ChevronDown, TrendingUp, TrendingDown,
|
||||||
Minus, BarChart2, Clock, Calendar, AlertCircle, Pencil, Save, X, Plus, Trash2,
|
Minus, BarChart2, Clock, Calendar, AlertCircle,
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import clsx from 'clsx'
|
import clsx from 'clsx'
|
||||||
@@ -12,13 +12,15 @@ const api = axios.create({ baseURL: '/api' })
|
|||||||
|
|
||||||
// ── Types ─────────────────────────────────────────────────────────────────────
|
// ── Types ─────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
interface Driver {
|
interface CausalTemplate {
|
||||||
key: string; label: string; weight: number; keywords: string[]; type?: string
|
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 {
|
interface InstrumentConfig {
|
||||||
id: string; name: string; yf_ticker: string; category: string; currency: string
|
id: string; name: string; yf_ticker: string; category: string; currency: string
|
||||||
description: string; drivers: Driver[]
|
description: string
|
||||||
regime_labels: string[]
|
regime_labels: string[]
|
||||||
chart: { ma_periods: number[]; show_volume: boolean }
|
chart: { ma_periods: number[]; show_volume: boolean }
|
||||||
correlation_instruments: string[]
|
correlation_instruments: string[]
|
||||||
@@ -134,18 +136,41 @@ function isActiveAt(ev: SnapshotEvent, selectedDate: string | null): boolean {
|
|||||||
return diffDays <= 30
|
return diffDays <= 30
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Driver type config ────────────────────────────────────────────────────────
|
// ── Causal graph config ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
const DRIVER_TYPES = ['event_calendar', 'report', 'geopolitical', 'fundamental', 'sentiment', 'technical'] as const
|
// Maps snapshot event category → causal template category
|
||||||
type DriverType = typeof DRIVER_TYPES[number]
|
const EVENT_TO_CAUSAL_CAT: Record<string, string> = {
|
||||||
|
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<string, { short: string; tw: string }> = {
|
// Maps instrument dashboard category → causal lab instrument keys
|
||||||
event_calendar: { short: 'Cal.', tw: 'text-amber-400 bg-amber-900/30 border-amber-700/30' },
|
const CAT_TO_CAUSAL_INST: Record<string, string[]> = {
|
||||||
report: { short: 'Rep.', tw: 'text-blue-400 bg-blue-900/30 border-blue-700/30' },
|
equity_index: ['SP500'],
|
||||||
geopolitical: { short: 'Geo.', tw: 'text-red-400 bg-red-900/30 border-red-700/30' },
|
equity_intl: ['SP500'],
|
||||||
fundamental: { short: 'Fund.', tw: 'text-emerald-400 bg-emerald-900/30 border-emerald-700/30' },
|
metal: ['XAUUSD'],
|
||||||
sentiment: { short: 'Sent.', tw: 'text-violet-400 bg-violet-900/30 border-violet-700/30' },
|
energy: ['BRENT'],
|
||||||
technical: { short: 'Tech.', tw: 'text-cyan-400 bg-cyan-900/30 border-cyan-700/30' },
|
fx: ['EURUSD'],
|
||||||
|
bond: ['US10Y', 'EU10Y'],
|
||||||
|
credit: ['US10Y'],
|
||||||
|
volatility: ['SP500'],
|
||||||
|
stock: ['SP500'],
|
||||||
|
crypto: [],
|
||||||
|
}
|
||||||
|
|
||||||
|
const CAUSAL_CAT_TW: Record<string, string> = {
|
||||||
|
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 ───────────────────────────────────────────────
|
// ── 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<Driver[]>(() => 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 (
|
|
||||||
<div className="rounded-xl border border-blue-700/40 bg-dark-800/80 p-4 space-y-4">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<span className="text-sm font-semibold text-white">Éditer les drivers — {instrumentId}</span>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<button onClick={addDriver}
|
|
||||||
className="flex items-center gap-1 text-xs px-2.5 py-1 bg-blue-800/30 hover:bg-blue-800/50 text-blue-300 border border-blue-700/40 rounded-lg transition-colors">
|
|
||||||
<Plus className="w-3 h-3" /> Ajouter
|
|
||||||
</button>
|
|
||||||
<button onClick={save} disabled={saving}
|
|
||||||
className="flex items-center gap-1.5 text-xs px-3 py-1 bg-emerald-800/30 hover:bg-emerald-800/50 text-emerald-300 border border-emerald-700/40 rounded-lg transition-colors disabled:opacity-40">
|
|
||||||
{saving ? <RefreshCw className="w-3 h-3 animate-spin" /> : <Save className="w-3 h-3" />}
|
|
||||||
{saving ? 'Sauvegarde...' : 'Sauvegarder'}
|
|
||||||
</button>
|
|
||||||
<button onClick={onClose} className="p-1 text-slate-500 hover:text-white transition-colors">
|
|
||||||
<X className="w-4 h-4" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{error && <div className="text-xs text-red-400 bg-red-900/20 border border-red-700/30 rounded-lg px-3 py-1.5">{error}</div>}
|
|
||||||
|
|
||||||
<div className="space-y-2 max-h-[420px] overflow-y-auto pr-1">
|
|
||||||
{local.map((d, i) => (
|
|
||||||
<div key={i} className="rounded-lg border border-slate-700/30 bg-dark-700/40 p-3 space-y-2">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<input
|
|
||||||
className="flex-1 text-xs bg-dark-600 border border-slate-700/40 rounded px-2 py-1 text-white placeholder-slate-600"
|
|
||||||
placeholder="Label"
|
|
||||||
value={d.label}
|
|
||||||
onChange={e => updateDriver(i, 'label', e.target.value)}
|
|
||||||
/>
|
|
||||||
{/* Type selector */}
|
|
||||||
<select
|
|
||||||
className="text-xs bg-dark-600 border border-slate-700/40 rounded px-1.5 py-1 text-slate-300"
|
|
||||||
value={d.type ?? 'fundamental'}
|
|
||||||
onChange={e => updateDriver(i, 'type', e.target.value)}
|
|
||||||
>
|
|
||||||
{DRIVER_TYPES.map(t => (
|
|
||||||
<option key={t} value={t}>{DRIVER_TYPE_CFG[t]?.short ?? t}</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
<div className="flex items-center gap-1.5">
|
|
||||||
<span className="text-xs text-slate-500">Poids</span>
|
|
||||||
<input
|
|
||||||
type="number" min="0" max="1" step="0.05"
|
|
||||||
className="w-16 text-xs bg-dark-600 border border-slate-700/40 rounded px-2 py-1 text-white"
|
|
||||||
value={d.weight}
|
|
||||||
onChange={e => updateDriver(i, 'weight', parseFloat(e.target.value))}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<button onClick={() => removeDriver(i)} className="p-1 text-slate-600 hover:text-red-400 transition-colors">
|
|
||||||
<Trash2 className="w-3.5 h-3.5" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label className="text-xs text-slate-600 mb-0.5 block">Mots-clés (séparés par virgule)</label>
|
|
||||||
<input
|
|
||||||
className="w-full text-xs bg-dark-600 border border-slate-700/40 rounded px-2 py-1 text-slate-300 placeholder-slate-600"
|
|
||||||
placeholder="Fed, FOMC, pivot, QE, taux directeur..."
|
|
||||||
value={(d.keywords ?? []).join(', ')}
|
|
||||||
onChange={e => updateKeywords(i, e.target.value)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Macro gauge helpers ───────────────────────────────────────────────────────
|
// ── Macro gauge helpers ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
const SCENARIO_META: Record<string, { label: string; emoji: string; color: string }> = {
|
const SCENARIO_META: Record<string, { label: string; emoji: string; color: string }> = {
|
||||||
@@ -849,45 +758,89 @@ function EventTimeline({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── DriversValidation ─────────────────────────────────────────────────────────
|
// ── EventGraphCards ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
function DriversValidation({
|
function EventGraphCards({
|
||||||
drivers, events, selectedDate,
|
events, templates, selectedDate, instrumentCategory,
|
||||||
}: {
|
}: {
|
||||||
drivers: Driver[]
|
|
||||||
events: SnapshotEvent[]
|
events: SnapshotEvent[]
|
||||||
|
templates: CausalTemplate[]
|
||||||
selectedDate: string | null
|
selectedDate: string | null
|
||||||
|
instrumentCategory: string
|
||||||
}) {
|
}) {
|
||||||
if (!drivers.length) {
|
const causalInsts = CAT_TO_CAUSAL_INST[instrumentCategory] ?? []
|
||||||
return <div className="text-xs text-slate-600 italic py-2">Aucun driver configuré</div>
|
|
||||||
|
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 (
|
||||||
|
<div className="text-xs text-slate-600 italic py-3">
|
||||||
|
Aucun graphe causal associé aux événements de cette période
|
||||||
|
</div>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="grid grid-cols-2 gap-2 sm:grid-cols-3">
|
<div className="grid grid-cols-2 gap-2 sm:grid-cols-3">
|
||||||
{drivers.map(d => {
|
{relevant.map(t => {
|
||||||
const matches = events.filter(ev => ev.category === d.type || ev.category === d.key)
|
const eventActive = activeCats.has(t.category)
|
||||||
const active = matches.some(ev => isActiveAt(ev, selectedDate))
|
const instrumentTouched = causalInsts.some(ci => t.instruments.includes(ci))
|
||||||
const typeCfg = DRIVER_TYPE_CFG[d.type ?? '']
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={d.key}
|
key={t.id}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'rounded-lg border p-2.5 transition-colors',
|
'rounded-lg border p-2.5 transition-colors',
|
||||||
active ? 'border-emerald-600/50 bg-emerald-900/20' : 'border-slate-700/30 bg-dark-700/40'
|
eventActive && instrumentTouched
|
||||||
|
? 'border-emerald-600/50 bg-emerald-900/20'
|
||||||
|
: eventActive
|
||||||
|
? 'border-amber-600/40 bg-amber-900/10'
|
||||||
|
: 'border-slate-700/30 bg-dark-700/40'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="flex items-start justify-between gap-1 mb-1.5">
|
<div className="flex items-start justify-between gap-1 mb-1.5">
|
||||||
<span className={clsx('text-xs font-semibold leading-tight', active ? 'text-emerald-300' : 'text-slate-400')}>
|
<span className={clsx(
|
||||||
{d.label}
|
'text-xs font-semibold leading-tight',
|
||||||
|
eventActive && instrumentTouched ? 'text-emerald-300'
|
||||||
|
: eventActive ? 'text-amber-300'
|
||||||
|
: 'text-slate-500'
|
||||||
|
)}>
|
||||||
|
{t.name}
|
||||||
</span>
|
</span>
|
||||||
{typeCfg && (
|
<span className={clsx(
|
||||||
<span className={clsx('shrink-0 text-[10px] px-1 py-0.5 rounded border', typeCfg.tw)}>
|
'shrink-0 text-[9px] px-1 py-0.5 rounded border',
|
||||||
{typeCfg.short}
|
CAUSAL_CAT_TW[t.category] ?? 'text-slate-500 border-slate-700/30'
|
||||||
</span>
|
)}>
|
||||||
)}
|
{t.category}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-wrap gap-1 mt-1 mb-1.5">
|
||||||
|
{t.instruments.map(inst => (
|
||||||
|
<span key={inst} className={clsx(
|
||||||
|
'text-[9px] px-1 py-0 rounded border',
|
||||||
|
causalInsts.includes(inst)
|
||||||
|
? 'text-emerald-400 bg-emerald-900/30 border-emerald-700/40'
|
||||||
|
: 'text-slate-600 border-slate-700/30'
|
||||||
|
)}>{inst}</span>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<span className="text-[10px] text-slate-600">poids {d.weight.toFixed(2)}</span>
|
<span className={clsx('text-[9px]', eventActive ? 'text-amber-500' : 'text-slate-700')}>
|
||||||
<span className={clsx('w-2 h-2 rounded-full', active ? 'bg-emerald-400' : 'bg-slate-600')} />
|
{eventActive ? '● actif' : '○ période'}
|
||||||
|
</span>
|
||||||
|
<span className={clsx(
|
||||||
|
'w-2 h-2 rounded-full',
|
||||||
|
eventActive && instrumentTouched ? 'bg-emerald-400'
|
||||||
|
: eventActive ? 'bg-amber-500'
|
||||||
|
: 'bg-slate-700'
|
||||||
|
)} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
@@ -899,17 +852,24 @@ function DriversValidation({
|
|||||||
// ── ExplanationScore ──────────────────────────────────────────────────────────
|
// ── ExplanationScore ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
function ExplanationScore({
|
function ExplanationScore({
|
||||||
drivers, events, selectedDate,
|
events, templates, selectedDate, instrumentCategory,
|
||||||
}: {
|
}: {
|
||||||
drivers: Driver[]
|
|
||||||
events: SnapshotEvent[]
|
events: SnapshotEvent[]
|
||||||
|
templates: CausalTemplate[]
|
||||||
selectedDate: string | null
|
selectedDate: string | null
|
||||||
|
instrumentCategory: string
|
||||||
}) {
|
}) {
|
||||||
const totalWeight = drivers.reduce((s, d) => s + (d.weight || 0), 0)
|
const causalInsts = CAT_TO_CAUSAL_INST[instrumentCategory] ?? []
|
||||||
const activeWeight = drivers
|
const allCats = new Set(events.map(ev => EVENT_TO_CAUSAL_CAT[ev.category]).filter(Boolean))
|
||||||
.filter(d => events.filter(ev => ev.category === d.type || ev.category === d.key).some(ev => isActiveAt(ev, selectedDate)))
|
const activeCats = new Set(
|
||||||
.reduce((s, d) => s + (d.weight || 0), 0)
|
events.filter(ev => isActiveAt(ev, selectedDate))
|
||||||
const score = totalWeight > 0 ? (activeWeight / totalWeight) * 100 : 0
|
.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 verdict = score >= 60 ? 'Bien expliqué' : score >= 30 ? 'Partiellement' : 'Peu lisible'
|
||||||
const badgeCls = score >= 60
|
const badgeCls = score >= 60
|
||||||
? 'text-emerald-400 bg-emerald-900/30 border-emerald-700/40'
|
? 'text-emerald-400 bg-emerald-900/30 border-emerald-700/40'
|
||||||
@@ -927,6 +887,11 @@ function ExplanationScore({
|
|||||||
<span className={clsx('text-xs font-semibold px-2 py-1 rounded-lg border whitespace-nowrap', badgeCls)}>
|
<span className={clsx('text-xs font-semibold px-2 py-1 rounded-lg border whitespace-nowrap', badgeCls)}>
|
||||||
{Math.round(score)}% — {verdict}
|
{Math.round(score)}% — {verdict}
|
||||||
</span>
|
</span>
|
||||||
|
{periodTmpl.length > 0 && (
|
||||||
|
<span className="text-[10px] text-slate-600 whitespace-nowrap">
|
||||||
|
{touchedActive.length}/{periodTmpl.length} graphes
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -949,15 +914,15 @@ export default function InstrumentDashboard() {
|
|||||||
const [loadingNarr, setLoadingNarr] = useState(false)
|
const [loadingNarr, setLoadingNarr] = useState(false)
|
||||||
const [selectorOpen, setSelectorOpen] = useState(false)
|
const [selectorOpen, setSelectorOpen] = useState(false)
|
||||||
const [selectedDate, setSelectedDate] = useState<string | null>(null)
|
const [selectedDate, setSelectedDate] = useState<string | null>(null)
|
||||||
const [editDrivers, setEditDrivers] = useState(false)
|
const [tabUnder, setTabUnder] = useState<'counters' | 'analyse'>('counters')
|
||||||
const [tabUnder, setTabUnder] = useState<'counters' | 'analyse'>('counters')
|
const [templates, setTemplates] = useState<CausalTemplate[]>([])
|
||||||
const [localDrivers, setLocalDrivers] = useState<Driver[] | null>(null)
|
const [macroAtDate, setMacroAtDate] = useState<MacroGaugeSnap | null>(null)
|
||||||
const [macroAtDate, setMacroAtDate] = useState<MacroGaugeSnap | null>(null)
|
|
||||||
|
|
||||||
const instrumentId = id.toUpperCase()
|
const instrumentId = id.toUpperCase()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
api.get('/instruments').then(r => setInstruments(r.data)).catch(() => {})
|
api.get('/instruments').then(r => setInstruments(r.data)).catch(() => {})
|
||||||
|
api.get('/causal-lab/templates').then(r => setTemplates(r.data)).catch(() => {})
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -965,8 +930,6 @@ export default function InstrumentDashboard() {
|
|||||||
setSnapshot(null)
|
setSnapshot(null)
|
||||||
setNarrative('')
|
setNarrative('')
|
||||||
setSelectedDate(null)
|
setSelectedDate(null)
|
||||||
setLocalDrivers(null)
|
|
||||||
setEditDrivers(false)
|
|
||||||
setMacroAtDate(null)
|
setMacroAtDate(null)
|
||||||
api.get(`/instruments/${instrumentId}/snapshot?period=${period}`)
|
api.get(`/instruments/${instrumentId}/snapshot?period=${period}`)
|
||||||
.then(r => {
|
.then(r => {
|
||||||
@@ -1075,8 +1038,6 @@ export default function InstrumentDashboard() {
|
|||||||
const dateLabel = effectiveDate ? fmtDateFR(effectiveDate) : '—'
|
const dateLabel = effectiveDate ? fmtDateFR(effectiveDate) : '—'
|
||||||
const displayPrice = dateTrend?.current_price ?? snapshot?.current_price
|
const displayPrice = dateTrend?.current_price ?? snapshot?.current_price
|
||||||
|
|
||||||
const activeDrivers = localDrivers ?? snapshot?.instrument?.drivers ?? []
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-6 max-w-screen-xl mx-auto space-y-4">
|
<div className="p-6 max-w-screen-xl mx-auto space-y-4">
|
||||||
|
|
||||||
@@ -1137,18 +1098,6 @@ export default function InstrumentDashboard() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="ml-auto flex items-center gap-2">
|
<div className="ml-auto flex items-center gap-2">
|
||||||
{snapshot && (
|
|
||||||
<button onClick={() => setEditDrivers(e => !e)}
|
|
||||||
className={clsx('flex items-center gap-1.5 text-xs px-3 py-1.5 border rounded-lg transition-colors',
|
|
||||||
editDrivers
|
|
||||||
? 'bg-blue-800/40 text-blue-300 border-blue-700/40'
|
|
||||||
: 'bg-dark-800 text-slate-400 border-slate-700/40 hover:text-white hover:bg-dark-700'
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<Pencil className="w-3 h-3" />
|
|
||||||
Drivers
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
<div className="flex items-center gap-1 bg-dark-800 border border-slate-700/40 rounded-xl p-1">
|
<div className="flex items-center gap-1 bg-dark-800 border border-slate-700/40 rounded-xl p-1">
|
||||||
{PERIODS.map(p => (
|
{PERIODS.map(p => (
|
||||||
<button key={p.key} onClick={() => setPeriod(p.key)}
|
<button key={p.key} onClick={() => setPeriod(p.key)}
|
||||||
@@ -1261,38 +1210,33 @@ export default function InstrumentDashboard() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Zone basse — validation des drivers */}
|
{/* Zone basse — graphes causaux liés aux événements */}
|
||||||
<div className="rounded-xl border border-slate-700/40 bg-dark-800/60 p-4">
|
<div className="rounded-xl border border-slate-700/40 bg-dark-800/60 p-4">
|
||||||
<div className="flex items-center gap-2 mb-3">
|
<div className="flex items-center gap-2 mb-3">
|
||||||
<BarChart2 className="w-4 h-4 text-violet-400" />
|
<BarChart2 className="w-4 h-4 text-violet-400" />
|
||||||
<span className="text-xs font-semibold text-slate-400 uppercase tracking-wide">Validation des drivers</span>
|
<span className="text-xs font-semibold text-slate-400 uppercase tracking-wide">Graphes causaux</span>
|
||||||
<span className="text-xs text-slate-600 ml-auto">{fmtDateFR(effectiveDate)}</span>
|
<span className="text-xs text-slate-600 ml-auto">
|
||||||
|
vert = actif + touche l'instrument · ambre = actif · gris = période seulement
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<DriversValidation
|
<EventGraphCards
|
||||||
drivers={activeDrivers}
|
|
||||||
events={snapshot.events}
|
events={snapshot.events}
|
||||||
|
templates={templates}
|
||||||
selectedDate={effectiveDate}
|
selectedDate={effectiveDate}
|
||||||
|
instrumentCategory={selected?.category ?? ''}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Note globale */}
|
{/* Note globale */}
|
||||||
<ExplanationScore
|
<ExplanationScore
|
||||||
drivers={activeDrivers}
|
|
||||||
events={snapshot.events}
|
events={snapshot.events}
|
||||||
|
templates={templates}
|
||||||
selectedDate={effectiveDate}
|
selectedDate={effectiveDate}
|
||||||
|
instrumentCategory={selected?.category ?? ''}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{editDrivers && (
|
|
||||||
<DriversPanel
|
|
||||||
instrumentId={instrumentId}
|
|
||||||
drivers={activeDrivers}
|
|
||||||
onSave={saved => { setLocalDrivers(saved); setEditDrivers(false) }}
|
|
||||||
onClose={() => setEditDrivers(false)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<NarrativeCard
|
<NarrativeCard
|
||||||
narrative={narrative}
|
narrative={narrative}
|
||||||
loading={loadingNarr}
|
loading={loadingNarr}
|
||||||
|
|||||||
Reference in New Issue
Block a user