From 3bcecdab09b0fbd8ff90c2e6ccc3a2e3b38714bd Mon Sep 17 00:00:00 2001 From: OpenSquared Date: Sun, 28 Jun 2026 23:43:52 +0200 Subject: [PATCH] feat: instrument analysis --- backend/services/instrument_service.py | 15 +++++++++++---- frontend/src/pages/InstrumentDashboard.tsx | 21 +++++++++++---------- 2 files changed, 22 insertions(+), 14 deletions(-) diff --git a/backend/services/instrument_service.py b/backend/services/instrument_service.py index 5df533e..9505a73 100644 --- a/backend/services/instrument_service.py +++ b/backend/services/instrument_service.py @@ -521,10 +521,12 @@ def _get_relevant_events( config: Dict, from_date: Optional[str] = None, to_date: Optional[str] = None, + instrument_id: Optional[str] = None, ) -> List[Dict]: """ Filter market_events DB rows relevant to the instrument, by date range and keyword/asset match. - Returns max 15 events sorted by start_date descending. + Also includes events that have a causal analysis for this instrument (regardless of keywords). + Returns max 30 events sorted by start_date asc. """ try: from services.database import get_conn @@ -549,6 +551,7 @@ def _get_relevant_events( keywords = [kw.lower() for kw in config.get("event_keywords", [])] related = [ra.lower() for ra in config.get("related_assets", [])] + inst_upper = (instrument_id or "").upper() filtered = [] for ev in all_events: @@ -580,9 +583,13 @@ def _get_relevant_events( ev_assets = (ev.get("affected_assets") or "").lower() keyword_hit = any(kw in ev_name or kw in ev_desc for kw in keywords) - asset_hit = any(ra in ev_assets for ra in related) + asset_hit = any(ra in ev_assets for ra in related) - if keyword_hit or asset_hit: + # Always include events that have a causal analysis for this instrument + analyzed = ev.get("analyzed_instruments") or "" + analysis_hit = inst_upper and inst_upper in [i.strip().upper() for i in analyzed.split(",") if i.strip()] + + if keyword_hit or asset_hit or analysis_hit: filtered.append({ "id": ev.get("id"), "template_id": ev.get("template_id"), @@ -669,7 +676,7 @@ async def get_snapshot( try: chart_start = price_data[0]["time"] if price_data else None chart_end = price_data[-1]["time"] if price_data else None - events = _get_relevant_events(config, from_date=chart_start, to_date=chart_end) + events = _get_relevant_events(config, from_date=chart_start, to_date=chart_end, instrument_id=instrument_id) except Exception as e: logger.warning(f"[instrument_service] Event filtering error: {e}") events = [] diff --git a/frontend/src/pages/InstrumentDashboard.tsx b/frontend/src/pages/InstrumentDashboard.tsx index 5aa116b..a157e2e 100644 --- a/frontend/src/pages/InstrumentDashboard.tsx +++ b/frontend/src/pages/InstrumentDashboard.tsx @@ -170,13 +170,14 @@ const CAT_TO_CAUSAL_INST: Record = { 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', +// Keyed by market_event.category — same palette as the ★ stars on the chart +const EV_CAT_TW: Record = { + event_calendar: 'text-amber-400 border-amber-700/40 bg-amber-900/20', + geopolitical: 'text-red-400 border-red-700/40 bg-red-900/20', + fundamental: 'text-emerald-400 border-emerald-700/40 bg-emerald-900/20', + report: 'text-blue-400 border-blue-700/40 bg-blue-900/20', + sentiment: 'text-violet-400 border-violet-700/40 bg-violet-900/20', + technical: 'text-cyan-400 border-cyan-700/40 bg-cyan-900/20', } // ── Macro regime colour mapping ─────────────────────────────────────────────── @@ -852,7 +853,7 @@ function CausalFrise({ {/* Chips */} {placed.map(({ ev, tmpl, x1, w, lane, active }) => { - const catTw = CAUSAL_CAT_TW[tmpl.category] ?? 'text-slate-400 border-slate-700/30 bg-slate-800/40' + const catTw = EV_CAT_TW[ev.category] ?? 'text-slate-400 border-slate-700/30 bg-slate-800/40' const chipY = lane * FRISE_LANE_H + FRISE_CHIP_PAD const isOpen = activeChip?.ev.id === ev.id && activeChip?.tmpl.id === tmpl.id const charsFit = Math.floor((w - 18) / 5.5) @@ -905,8 +906,8 @@ function CausalFrise({
{tmpl.name} {tmpl.category} + EV_CAT_TW[ev.category] ?? 'text-slate-500 border-slate-700/30' + )}>{ev.category}
{ev.title}