From b223f50f8b49d9c885007b2a57ddb61f9b7090b6 Mon Sep 17 00:00:00 2001 From: OpenSquared Date: Mon, 29 Jun 2026 21:16:29 +0200 Subject: [PATCH] feat: market event --- backend/routers/causal_lab.py | 79 +++++++++++++++++++--- frontend/src/pages/InstrumentDashboard.tsx | 28 ++++++++ 2 files changed, 98 insertions(+), 9 deletions(-) diff --git a/backend/routers/causal_lab.py b/backend/routers/causal_lab.py index e4a7d37..780e9d7 100644 --- a/backend/routers/causal_lab.py +++ b/backend/routers/causal_lab.py @@ -1127,9 +1127,9 @@ def _build_graph_json_from_spec(spec: dict) -> dict: def _run_auto_analysis(event: dict, template_id: int) -> bool: """ - Lightweight causal analysis for the auto_template pipeline. - Builds inputs from event fields (no yfinance), evaluates the graph, - and upserts into causal_event_analyses so the instrument frise can read it. + Causal analysis for the auto_template pipeline. + Builds inputs from event fields, evaluates the graph, fetches actual prices, + and upserts into causal_event_analyses so the instrument frise can score it. """ try: from services.database import get_conn @@ -1220,9 +1220,27 @@ def _run_auto_analysis(event: dict, template_id: int) -> bool: analyzed_at = datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%SZ") event_id = event["id"] + # Effective lag from template edges (for price window and chip width) + edges = graph.get("edges", []) + effective_lag_days = max((e.get("lag_days", 0) or 0 for e in edges), default=0) + + # Fetch actual prices to compute actual pips for scoring + actual_moves: dict = {} + drift_by_inst: dict = {} + try: + prices = _fetch_prices(event["start_date"], instruments, lag_days=effective_lag_days) + for inst in instruments: + drift = _drift_metrics(prices, event["start_date"], inst, + lag_min=0, lag_days=effective_lag_days) + drift_by_inst[inst] = drift + if drift.get("post_pips") is not None: + actual_moves[inst] = drift["post_pips"] + except Exception as _pe: + logger.warning(f"[auto_analysis] price fetch failed for event #{event_id}: {_pe}") + logger.info( f"[auto_analysis] event #{event_id} → tmpl #{template_id} | " - f"inputs={inputs} | instruments={all_instruments_csv}" + f"inputs={inputs} | instruments={all_instruments_csv} | actual_moves={actual_moves}" ) existing = conn.execute( @@ -1238,8 +1256,8 @@ def _run_auto_analysis(event: dict, template_id: int) -> bool: WHERE market_event_id=? AND template_id=? """, ( all_instruments_csv, json.dumps(inputs), json.dumps({}), - json.dumps(node_values), json.dumps({}), None, - json.dumps({}), analyzed_at, + json.dumps(node_values), json.dumps(actual_moves), None, + json.dumps(drift_by_inst), analyzed_at, event_id, template_id, )) else: @@ -1252,12 +1270,12 @@ def _run_auto_analysis(event: dict, template_id: int) -> bool: """, ( event_id, template_id, all_instruments_csv, json.dumps(inputs), json.dumps({}), - json.dumps(node_values), json.dumps({}), - None, json.dumps({}), analyzed_at, + json.dumps(node_values), json.dumps(actual_moves), + None, json.dumps(drift_by_inst), analyzed_at, )) conn.commit() conn.close() - logger.info(f"[auto_analysis] causal_event_analyses upserted → event #{event_id}, tmpl #{template_id}") + logger.info(f"[auto_analysis] upserted event #{event_id}, tmpl #{template_id}, actual_moves={actual_moves}") return True except Exception as e: @@ -1369,6 +1387,49 @@ Règles : coef intermédiaire ∈ [-5,5] ; coef output en pips, |val| ∈ [30,20 return {"error": str(e)} +@router.post("/api/causal-lab/auto-analyze/refresh") +def refresh_auto_analyses(): + """ + Re-run _run_auto_analysis for all causal_event_analyses rows with empty actual_json. + Fills in real price data so comprehension scores can be computed. + """ + try: + from services.database import get_conn + conn = get_conn() + rows = conn.execute(""" + SELECT a.id AS cea_id, a.market_event_id, a.template_id, + a.actual_json + FROM causal_event_analyses a + WHERE a.actual_json IS NULL OR a.actual_json = '{}' OR a.actual_json = 'null' + """).fetchall() + conn.close() + + refreshed, failed = 0, 0 + for row in rows: + try: + conn2 = get_conn() + ev_row = conn2.execute( + "SELECT * FROM market_events WHERE id = ?", (row["market_event_id"],) + ).fetchone() + conn2.close() + if not ev_row: + continue + ok = _run_auto_analysis(dict(ev_row), row["template_id"]) + if ok: + refreshed += 1 + else: + failed += 1 + except Exception as _e: + logger.warning(f"[refresh_auto_analyses] cea#{row['cea_id']}: {_e}") + failed += 1 + + logger.info(f"[refresh_auto_analyses] done: {refreshed} refreshed, {failed} failed") + return {"refreshed": refreshed, "failed": failed, "total": len(rows)} + except Exception as e: + logger.error(f"[refresh_auto_analyses] {e}") + raise HTTPException(500, str(e)) + + @router.post("/api/causal-lab/create-from-event") def create_template_from_event(body: CreateFromEventRequest): """GPT-4o génère et enregistre un template causal adapté à l'événement.""" diff --git a/frontend/src/pages/InstrumentDashboard.tsx b/frontend/src/pages/InstrumentDashboard.tsx index 77b08f1..456498f 100644 --- a/frontend/src/pages/InstrumentDashboard.tsx +++ b/frontend/src/pages/InstrumentDashboard.tsx @@ -1020,6 +1020,9 @@ function ExplanationScore({ templates: CausalTemplate[] causalInsts: string[] }) { + const [refreshing, setRefreshing] = useState(false) + const [refreshDone, setRefreshDone] = useState(false) + // For each linked event, compute the comprehension score and collect scored ones const scored: number[] = [] const totalLinked: number[] = [] @@ -1051,6 +1054,19 @@ function ExplanationScore({ const barCls = globalScore == null ? 'bg-slate-600' : globalScore >= 70 ? 'bg-emerald-500' : globalScore >= 40 ? 'bg-amber-500' : 'bg-red-500' + const handleRefresh = async () => { + setRefreshing(true) + try { + await api.post('/causal-lab/auto-analyze/refresh') + setRefreshDone(true) + setTimeout(() => setRefreshDone(false), 3000) + } finally { + setRefreshing(false) + } + } + + const needsRefresh = totalLinked.length > 0 && scored.length === 0 && !refreshDone + return (
Note globale @@ -1063,6 +1079,18 @@ function ExplanationScore({ {scored.length}/{totalLinked.length} graphes + {needsRefresh && ( + + )} + {refreshDone && ( + ✓ Rechargez la page + )}
) }