From 5e819b5b67db374542fd5d58846db9b63d7bc044 Mon Sep 17 00:00:00 2001 From: OpenSquared Date: Mon, 29 Jun 2026 20:44:03 +0200 Subject: [PATCH] feat: instrument analysis --- backend/services/instrument_service.py | 12 +- frontend/src/pages/InstrumentDashboard.tsx | 125 ++++++++++++++++----- 2 files changed, 103 insertions(+), 34 deletions(-) diff --git a/backend/services/instrument_service.py b/backend/services/instrument_service.py index cbb35c6..dd56e87 100644 --- a/backend/services/instrument_service.py +++ b/backend/services/instrument_service.py @@ -543,9 +543,11 @@ def _get_relevant_events( SELECT e.*, (SELECT GROUP_CONCAT(DISTINCT a.instrument) FROM causal_event_analyses a - WHERE a.market_event_id = e.id) AS analyzed_instruments, - (SELECT a.template_id FROM causal_event_analyses a WHERE a.market_event_id = e.id ORDER BY a.id DESC LIMIT 1) AS cea_template_id, - (SELECT a.id FROM causal_event_analyses a WHERE a.market_event_id = e.id ORDER BY a.id DESC LIMIT 1) AS analysis_id + WHERE a.market_event_id = e.id) AS analyzed_instruments, + (SELECT a.template_id FROM causal_event_analyses a WHERE a.market_event_id = e.id ORDER BY a.id DESC LIMIT 1) AS cea_template_id, + (SELECT a.id FROM causal_event_analyses a WHERE a.market_event_id = e.id ORDER BY a.id DESC LIMIT 1) AS analysis_id, + (SELECT a.prediction_json FROM causal_event_analyses a WHERE a.market_event_id = e.id ORDER BY a.id DESC LIMIT 1) AS cea_prediction_json, + (SELECT a.actual_json FROM causal_event_analyses a WHERE a.market_event_id = e.id ORDER BY a.id DESC LIMIT 1) AS cea_actual_json FROM market_events e ORDER BY e.start_date DESC """).fetchall() @@ -553,7 +555,9 @@ def _get_relevant_events( # Use cea_template_id (from causal_event_analyses) explicitly to avoid # any collision with an e.* column named template_id all_events = [ - {**dict(r), "template_id": r["cea_template_id"]} + {**dict(r), "template_id": r["cea_template_id"], + "prediction_json": r["cea_prediction_json"], + "actual_json": r["cea_actual_json"]} for r in rows ] except Exception as e: diff --git a/frontend/src/pages/InstrumentDashboard.tsx b/frontend/src/pages/InstrumentDashboard.tsx index a157e2e..77b08f1 100644 --- a/frontend/src/pages/InstrumentDashboard.tsx +++ b/frontend/src/pages/InstrumentDashboard.tsx @@ -18,7 +18,10 @@ const NO_CHART_EVENTS: never[] = [] 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[] } + graph_json: { + nodes: { id: string; type: string; label: string; instrument?: string }[] + edges: { from: string; to: string; lag_days?: number; lag_min?: number; sign?: string }[] + } } interface InstrumentConfig { @@ -39,6 +42,8 @@ interface SnapshotEvent { category: string; sub_type?: string; description: string; impact_score: number expected_value?: string | null; actual_value?: string | null surprise_pct?: number | null; unit?: string | null; absorption_pct?: number | null + prediction_json?: string | null // JSON: {node_id: pips} from causal_event_analyses + actual_json?: string | null // JSON: {instrument: pips} from causal_event_analyses } interface RegimeSignals { @@ -690,6 +695,38 @@ function makeTdToX(priceData: PriceCandle[], width: number): (d: string) => numb return (d: string) => (snap(d) / (N - 1)) * width } +// ── Comprehension scoring ───────────────────────────────────────────────────── + +function templateAbsorptionDays(tmpl: CausalTemplate): number { + const maxLag = Math.max(0, ...tmpl.graph_json.edges.map(e => e.lag_days || 0)) + return maxLag > 0 ? maxLag : 30 +} + +/** Score 0-100 measuring how well the graph predicted actual pips for an instrument. */ +function comprehensionScore(ev: SnapshotEvent, tmpl: CausalTemplate, instrument: string): number | null { + if (!ev.prediction_json || !ev.actual_json) return null + try { + const preds: Record = JSON.parse(ev.prediction_json) + const actuals: Record = JSON.parse(ev.actual_json) + const actual = actuals[instrument] ?? actuals[instrument.toUpperCase()] ?? actuals[instrument.toLowerCase()] + if (actual == null) return null + + // Find the market_asset node for this instrument + const outputNode = tmpl.graph_json.nodes.find(n => + n.type === 'market_asset' && n.instrument?.toUpperCase() === instrument.toUpperCase() + ) + const predicted = outputNode ? (preds[outputNode.id] ?? null) : null + if (predicted == null) return null + if (predicted === 0 && actual === 0) return 100 + if (predicted === 0) return 40 + + const sameDir = predicted * actual > 0 + if (!sameDir) return 0 + const ratio = Math.min(Math.abs(actual), Math.abs(predicted)) / Math.max(Math.abs(actual), Math.abs(predicted)) + return Math.round(50 + ratio * 50) + } catch { return null } +} + // ── CausalFrise ─────────────────────────────────────────────────────────────── const FRISE_LANE_H = 24 // px par lane @@ -784,8 +821,9 @@ function CausalFrise({ .map(ev => { const tmpl = templates.find(t => t.id === ev.template_id) if (!tmpl) return null + const absorptionDays = templateAbsorptionDays(tmpl) const endDate = ev.end_date ?? (() => { - const d = new Date(ev.date); d.setDate(d.getDate() + 30) + const d = new Date(ev.date); d.setDate(d.getDate() + absorptionDays) return d.toISOString().slice(0, 10) })() const x1 = tdToX(ev.date) @@ -912,10 +950,10 @@ function CausalFrise({
{ev.title}
-
+
- {fmtDateFR(ev.date)} → {ev.end_date ? fmtDateFR(ev.end_date) : '+30j'} + {fmtDateFR(ev.date)} → {ev.end_date ? fmtDateFR(ev.end_date) : `+${templateAbsorptionDays(tmpl)}j`} {ev.impact_score != null && ( ★ {ev.impact_score.toFixed(1)} @@ -927,6 +965,26 @@ function CausalFrise({ )}
+ {/* Per-graph comprehension score */} + {(() => { + const inst = causalInsts.find(ci => tmpl.instruments.includes(ci)) ?? causalInsts[0] + const s = inst ? comprehensionScore(ev, tmpl, inst) : 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' + return ( +
+
+ Précision prédiction + {s}% +
+
+
+
+
+ ) + })()} +