feat: instrument analysis

This commit is contained in:
OpenSquared
2026-06-28 23:43:52 +02:00
parent 3d7b3e47c9
commit 3bcecdab09
2 changed files with 22 additions and 14 deletions

View File

@@ -521,10 +521,12 @@ def _get_relevant_events(
config: Dict, config: Dict,
from_date: Optional[str] = None, from_date: Optional[str] = None,
to_date: Optional[str] = None, to_date: Optional[str] = None,
instrument_id: Optional[str] = None,
) -> List[Dict]: ) -> List[Dict]:
""" """
Filter market_events DB rows relevant to the instrument, by date range and keyword/asset match. 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: try:
from services.database import get_conn from services.database import get_conn
@@ -549,6 +551,7 @@ def _get_relevant_events(
keywords = [kw.lower() for kw in config.get("event_keywords", [])] keywords = [kw.lower() for kw in config.get("event_keywords", [])]
related = [ra.lower() for ra in config.get("related_assets", [])] related = [ra.lower() for ra in config.get("related_assets", [])]
inst_upper = (instrument_id or "").upper()
filtered = [] filtered = []
for ev in all_events: for ev in all_events:
@@ -580,9 +583,13 @@ def _get_relevant_events(
ev_assets = (ev.get("affected_assets") or "").lower() ev_assets = (ev.get("affected_assets") or "").lower()
keyword_hit = any(kw in ev_name or kw in ev_desc for kw in keywords) 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({ filtered.append({
"id": ev.get("id"), "id": ev.get("id"),
"template_id": ev.get("template_id"), "template_id": ev.get("template_id"),
@@ -669,7 +676,7 @@ async def get_snapshot(
try: try:
chart_start = price_data[0]["time"] if price_data else None chart_start = price_data[0]["time"] if price_data else None
chart_end = price_data[-1]["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: except Exception as e:
logger.warning(f"[instrument_service] Event filtering error: {e}") logger.warning(f"[instrument_service] Event filtering error: {e}")
events = [] events = []

View File

@@ -170,13 +170,14 @@ const CAT_TO_CAUSAL_INST: Record<string, string[]> = {
crypto: [], crypto: [],
} }
const CAUSAL_CAT_TW: Record<string, string> = { // Keyed by market_event.category — same palette as the ★ stars on the chart
macro_us: 'text-blue-400 border-blue-700/30 bg-blue-900/20', const EV_CAT_TW: Record<string, string> = {
macro_eu: 'text-cyan-400 border-cyan-700/30 bg-cyan-900/20', event_calendar: 'text-amber-400 border-amber-700/40 bg-amber-900/20',
geopolitical: 'text-red-400 border-red-700/30 bg-red-900/20', geopolitical: 'text-red-400 border-red-700/40 bg-red-900/20',
report: 'text-amber-400 border-amber-700/30 bg-amber-900/20', fundamental: 'text-emerald-400 border-emerald-700/40 bg-emerald-900/20',
sentiment: 'text-violet-400 border-violet-700/30 bg-violet-900/20', report: 'text-blue-400 border-blue-700/40 bg-blue-900/20',
commodity: 'text-orange-400 border-orange-700/30 bg-orange-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 ─────────────────────────────────────────────── // ── Macro regime colour mapping ───────────────────────────────────────────────
@@ -852,7 +853,7 @@ function CausalFrise({
{/* Chips */} {/* Chips */}
{placed.map(({ ev, tmpl, x1, w, lane, active }) => { {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 chipY = lane * FRISE_LANE_H + FRISE_CHIP_PAD
const isOpen = activeChip?.ev.id === ev.id && activeChip?.tmpl.id === tmpl.id const isOpen = activeChip?.ev.id === ev.id && activeChip?.tmpl.id === tmpl.id
const charsFit = Math.floor((w - 18) / 5.5) const charsFit = Math.floor((w - 18) / 5.5)
@@ -905,8 +906,8 @@ function CausalFrise({
<div className="flex items-start gap-2 mb-1.5 pr-4"> <div className="flex items-start gap-2 mb-1.5 pr-4">
<span className="flex-1 text-xs font-semibold text-slate-200 leading-tight line-clamp-2">{tmpl.name}</span> <span className="flex-1 text-xs font-semibold text-slate-200 leading-tight line-clamp-2">{tmpl.name}</span>
<span className={clsx('text-[9px] px-1 py-0.5 rounded border shrink-0', <span className={clsx('text-[9px] px-1 py-0.5 rounded border shrink-0',
CAUSAL_CAT_TW[tmpl.category] ?? 'text-slate-500 border-slate-700/30' EV_CAT_TW[ev.category] ?? 'text-slate-500 border-slate-700/30'
)}>{tmpl.category}</span> )}>{ev.category}</span>
</div> </div>
<div className="text-[10px] text-slate-400 truncate mb-1.5">{ev.title}</div> <div className="text-[10px] text-slate-400 truncate mb-1.5">{ev.title}</div>