feat: instrument analysis
This commit is contained in:
@@ -100,7 +100,7 @@ function KeepAlivePage({ path, component: Component }: { path: string; component
|
|||||||
|
|
||||||
// Registers the instrument tab on navigation (renders nothing itself)
|
// Registers the instrument tab on navigation (renders nothing itself)
|
||||||
function InstrumentRoute() {
|
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()
|
const { openInstrument } = useTabs()
|
||||||
useEffect(() => { openInstrument(id.toUpperCase()) }, [id, openInstrument])
|
useEffect(() => { openInstrument(id.toUpperCase()) }, [id, openInstrument])
|
||||||
return null
|
return null
|
||||||
@@ -129,7 +129,7 @@ function NormalRoutes() {
|
|||||||
return (
|
return (
|
||||||
<div className={isInstrument ? 'hidden' : 'flex-1 overflow-auto'}>
|
<div className={isInstrument ? 'hidden' : 'flex-1 overflow-auto'}>
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/instruments" element={<Navigate to={`/instruments/${localStorage.getItem('last_instrument') || 'EURUSD'}`} replace />} />
|
<Route path="/instruments" element={<Navigate to={`/instruments/${localStorage.getItem('last_instrument') || 'EURUSD=X'}`} replace />} />
|
||||||
<Route path="/instruments/:id" element={<InstrumentRoute />} />
|
<Route path="/instruments/:id" element={<InstrumentRoute />} />
|
||||||
{/* Legacy redirects */}
|
{/* Legacy redirects */}
|
||||||
<Route path="/impact" element={<Navigate to="/market-events" replace />} />
|
<Route path="/impact" element={<Navigate to="/market-events" replace />} />
|
||||||
|
|||||||
@@ -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 {
|
interface TabsCtx {
|
||||||
kept: ReadonlySet<string>
|
kept: ReadonlySet<string>
|
||||||
|
|||||||
@@ -968,10 +968,9 @@ function CausalFrise({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 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 = ev.id != null ? (causalScores[ev.id] ?? null) : null
|
||||||
const s = inst ? comprehensionScore(ev, tmpl, inst) : null
|
|
||||||
if (s == null) return null
|
if (s == null) return null
|
||||||
const barCls = s >= 70 ? 'bg-emerald-500' : s >= 40 ? 'bg-amber-500' : 'bg-red-500'
|
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'
|
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),
|
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 (debugRows.length > 0) debugRows[debugRows.length - 1].score = s
|
||||||
if (s != null) scored.push(s)
|
if (s != null) scored.push(s)
|
||||||
}
|
}
|
||||||
@@ -1206,7 +1205,7 @@ const PERIODS = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
export default function InstrumentDashboard({ instrumentIdProp, isVisible }: { instrumentIdProp?: string; isVisible?: boolean } = {}) {
|
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 navigate = useNavigate()
|
||||||
const [period, setPeriod] = useState('1y')
|
const [period, setPeriod] = useState('1y')
|
||||||
const [chartStyle, setChartStyle] = useState<'candles' | 'line'>('candles')
|
const [chartStyle, setChartStyle] = useState<'candles' | 'line'>('candles')
|
||||||
@@ -1220,6 +1219,7 @@ export default function InstrumentDashboard({ instrumentIdProp, isVisible }: { i
|
|||||||
const [selectedDate, setSelectedDate] = useState<string | null>(null)
|
const [selectedDate, setSelectedDate] = useState<string | null>(null)
|
||||||
const [tabUnder, setTabUnder] = useState<'counters' | 'analyse'>('counters')
|
const [tabUnder, setTabUnder] = useState<'counters' | 'analyse'>('counters')
|
||||||
const [templates, setTemplates] = useState<CausalTemplate[]>([])
|
const [templates, setTemplates] = useState<CausalTemplate[]>([])
|
||||||
|
const [causalScores, setCausalScores] = useState<Record<number, number | null>>({}) // eventId → activation_score*100
|
||||||
const [macroAtDate, setMacroAtDate] = useState<MacroGaugeSnap | null>(null)
|
const [macroAtDate, setMacroAtDate] = useState<MacroGaugeSnap | null>(null)
|
||||||
const [theoryCurve, setTheoryCurve] = useState<TheoPoint[] | null>(null)
|
const [theoryCurve, setTheoryCurve] = useState<TheoPoint[] | null>(null)
|
||||||
const [loadingTheory, setLoadingTheory] = useState(false)
|
const [loadingTheory, setLoadingTheory] = useState(false)
|
||||||
@@ -1264,6 +1264,20 @@ export default function InstrumentDashboard({ instrumentIdProp, isVisible }: { i
|
|||||||
.catch(() => {})
|
.catch(() => {})
|
||||||
}, [instrumentId, period])
|
}, [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(() => {
|
useEffect(() => {
|
||||||
setSnapshot(null)
|
setSnapshot(null)
|
||||||
setNarrative('')
|
setNarrative('')
|
||||||
@@ -1276,6 +1290,11 @@ export default function InstrumentDashboard({ instrumentIdProp, isVisible }: { i
|
|||||||
fetchSnapshot()
|
fetchSnapshot()
|
||||||
}, [instrumentId, period])
|
}, [instrumentId, period])
|
||||||
|
|
||||||
|
// Refresh causal scores whenever the snapshot events change
|
||||||
|
useEffect(() => {
|
||||||
|
if (snapshot?.events?.length) refreshCausalScores(snapshot.events)
|
||||||
|
}, [snapshot?.events, refreshCausalScores])
|
||||||
|
|
||||||
const loadNarrative = useCallback(() => {
|
const loadNarrative = useCallback(() => {
|
||||||
setLoadingNarr(true)
|
setLoadingNarr(true)
|
||||||
api.post(`/instruments/${instrumentId}/narrative`)
|
api.post(`/instruments/${instrumentId}/narrative`)
|
||||||
|
|||||||
Reference in New Issue
Block a user