From ecb9b74b204bb1c7f2c6eca1fd41ed01934f0c8b Mon Sep 17 00:00:00 2001 From: OpenSquared Date: Tue, 30 Jun 2026 16:50:26 +0200 Subject: [PATCH] feat: instrument analysis --- frontend/src/App.tsx | 4 +-- frontend/src/context/TabsContext.tsx | 5 +++- frontend/src/pages/InstrumentDashboard.tsx | 29 ++++++++++++++++++---- 3 files changed, 30 insertions(+), 8 deletions(-) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 894c03b..4cd6caa 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -100,7 +100,7 @@ function KeepAlivePage({ path, component: Component }: { path: string; component // Registers the instrument tab on navigation (renders nothing itself) function InstrumentRoute() { - const { id = localStorage.getItem('last_instrument') || 'EURUSD' } = useParams<{ id: string }>() + const { id = localStorage.getItem('last_instrument') || 'EURUSD=X' } = useParams<{ id: string }>() const { openInstrument } = useTabs() useEffect(() => { openInstrument(id.toUpperCase()) }, [id, openInstrument]) return null @@ -129,7 +129,7 @@ function NormalRoutes() { return (
- } /> + } /> } /> {/* Legacy redirects */} } /> diff --git a/frontend/src/context/TabsContext.tsx b/frontend/src/context/TabsContext.tsx index cca7e3e..c9b5e17 100644 --- a/frontend/src/context/TabsContext.tsx +++ b/frontend/src/context/TabsContext.tsx @@ -1,4 +1,7 @@ -import { createContext, useContext, useCallback, useState, ReactNode } from 'react' +import { createContext, useContext, useCallback, useState, useEffect, ReactNode } from 'react' + +// Instruments that exist in instruments.json — used to validate localStorage +const VALID_INSTRUMENT_SUFFIX = /^[A-Z0-9]+=?[A-Z0-9-]*$/ interface TabsCtx { kept: ReadonlySet diff --git a/frontend/src/pages/InstrumentDashboard.tsx b/frontend/src/pages/InstrumentDashboard.tsx index 6dcf4bc..88e8875 100644 --- a/frontend/src/pages/InstrumentDashboard.tsx +++ b/frontend/src/pages/InstrumentDashboard.tsx @@ -968,10 +968,9 @@ function CausalFrise({ )}
- {/* Per-graph comprehension score */} + {/* Précision — source directe depuis /causal-lab/analyses (même valeur que MarketEvents) */} {(() => { - const inst = causalInsts.find(ci => tmpl.instruments.includes(ci)) ?? causalInsts[0] - const s = inst ? comprehensionScore(ev, tmpl, inst) : null + const s = ev.id != null ? (causalScores[ev.id] ?? null) : null if (s == null) return null const barCls = s >= 70 ? 'bg-emerald-500' : s >= 40 ? 'bg-amber-500' : 'bg-red-500' const txtCls = s >= 70 ? 'text-emerald-400' : s >= 40 ? 'text-amber-400' : 'text-red-400' @@ -1072,7 +1071,7 @@ function ExplanationScore({ rawPred: (ev.prediction_json ?? 'null').slice(0, 30), }) - const s = comprehensionScore(ev, tmpl, inst) + const s = ev.id != null ? (causalScores[ev.id] ?? null) : null if (debugRows.length > 0) debugRows[debugRows.length - 1].score = s if (s != null) scored.push(s) } @@ -1206,7 +1205,7 @@ const PERIODS = [ ] export default function InstrumentDashboard({ instrumentIdProp, isVisible }: { instrumentIdProp?: string; isVisible?: boolean } = {}) { - const { id: paramId = localStorage.getItem('last_instrument') || 'EURUSD' } = useParams<{ id: string }>() + const { id: paramId = localStorage.getItem('last_instrument') || 'EURUSD=X' } = useParams<{ id: string }>() const navigate = useNavigate() const [period, setPeriod] = useState('1y') const [chartStyle, setChartStyle] = useState<'candles' | 'line'>('candles') @@ -1220,6 +1219,7 @@ export default function InstrumentDashboard({ instrumentIdProp, isVisible }: { i const [selectedDate, setSelectedDate] = useState(null) const [tabUnder, setTabUnder] = useState<'counters' | 'analyse'>('counters') const [templates, setTemplates] = useState([]) + const [causalScores, setCausalScores] = useState>({}) // eventId → activation_score*100 const [macroAtDate, setMacroAtDate] = useState(null) const [theoryCurve, setTheoryCurve] = useState(null) const [loadingTheory, setLoadingTheory] = useState(false) @@ -1264,6 +1264,20 @@ export default function InstrumentDashboard({ instrumentIdProp, isVisible }: { i .catch(() => {}) }, [instrumentId, period]) + // Fetch causal scores directly from the analyses API (not via snapshot) so the popup + // always shows the DB-stored activation_score, independent of snapshot staleness + const refreshCausalScores = useCallback((events: SnapshotEvent[]) => { + const ids = events.map(e => e.id).filter(Boolean) as number[] + if (!ids.length) return + Promise.all(ids.map(id => + api.get(`/causal-lab/analyses?market_event_id=${id}&limit=1`) + .then(r => ({ id, score: r.data[0]?.activation_score ?? null })) + .catch(() => ({ id, score: null })) + )).then(results => { + setCausalScores(Object.fromEntries(results.map(r => [r.id, r.score != null ? Math.round(r.score * 100) : null]))) + }) + }, []) + useEffect(() => { setSnapshot(null) setNarrative('') @@ -1276,6 +1290,11 @@ export default function InstrumentDashboard({ instrumentIdProp, isVisible }: { i fetchSnapshot() }, [instrumentId, period]) + // Refresh causal scores whenever the snapshot events change + useEffect(() => { + if (snapshot?.events?.length) refreshCausalScores(snapshot.events) + }, [snapshot?.events, refreshCausalScores]) + const loadNarrative = useCallback(() => { setLoadingNarr(true) api.post(`/instruments/${instrumentId}/narrative`)