From f4a55a800947ecdada617cc4816d2673ec67cd95 Mon Sep 17 00:00:00 2001 From: OpenSquared Date: Mon, 29 Jun 2026 20:21:52 +0200 Subject: [PATCH] feat: desk ia --- backend/routers/causal_lab.py | 142 ++++++++++++++++++++++ backend/services/market_event_detector.py | 23 +++- 2 files changed, 160 insertions(+), 5 deletions(-) diff --git a/backend/routers/causal_lab.py b/backend/routers/causal_lab.py index e869fd8..e4a7d37 100644 --- a/backend/routers/causal_lab.py +++ b/backend/routers/causal_lab.py @@ -1125,6 +1125,146 @@ 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. + """ + try: + from services.database import get_conn + from services.causal_graphs import get_template, evaluate_graph + + conn = get_conn() + tmpl = get_template(conn, template_id) + if not tmpl: + conn.close() + logger.warning(f"[auto_analysis] template #{template_id} introuvable") + return False + + graph = tmpl["graph_json"] + inputs = {} + mapping = graph.get("input_mapping", {}) + edate_str = event["start_date"][:10] + + for input_id, cfg in mapping.items(): + src = cfg.get("source", "") + key = cfg.get("key", "") + field = cfg.get("field", "close") + + if src == "economic_events" and key: + row_e = conn.execute( + """SELECT actual_value, forecast_value FROM economic_events + WHERE series_id = ? AND event_date <= ? AND actual_value IS NOT NULL + ORDER BY event_date DESC LIMIT 1""", + (key, edate_str), + ).fetchone() + if row_e: + if field == "surprise" and row_e["forecast_value"] is not None: + inputs[input_id] = round(float(row_e["actual_value"]) - float(row_e["forecast_value"]), 4) + else: + inputs[input_id] = float(row_e["actual_value"]) + + elif src == "ff_calendar" and key: + row_f = conn.execute( + """SELECT actual_value, forecast_value FROM ff_calendar + WHERE event_name = ? AND event_date <= ? AND actual_value IS NOT NULL + ORDER BY event_date DESC LIMIT 1""", + (key, edate_str), + ).fetchone() + if row_f and row_f["actual_value"]: + try: + actual = float(row_f["actual_value"]) + if field == "surprise" and row_f["forecast_value"]: + inputs[input_id] = round(actual - float(row_f["forecast_value"]), 4) + else: + inputs[input_id] = actual + except (ValueError, TypeError): + pass + + elif src == "surprise_bps" and event.get("surprise_pct") is not None: + raw = float(event["surprise_pct"]) + node_range = cfg.get("range") + if node_range and len(node_range) == 2: + lo, hi = float(node_range[0]), float(node_range[1]) + inputs[input_id] = round(max(lo, min(hi, raw)), 4) + else: + inputs[input_id] = round(raw, 4) + + elif src == "surprise" and event.get("surprise_pct") is not None: + raw = float(event["surprise_pct"]) + node_range = cfg.get("range") + if node_range and len(node_range) == 2: + lo, hi = float(node_range[0]), float(node_range[1]) + bound = max(abs(lo), abs(hi)) + if bound <= 10: + inputs[input_id] = round(max(lo, min(hi, raw / 100 * bound)), 4) + else: + inputs[input_id] = round(max(lo, min(hi, raw)), 4) + else: + inputs[input_id] = round(raw / 100, 4) + + elif src == "impact_score_scaled" and event.get("impact_score") is not None: + inputs[input_id] = float(event["impact_score"]) / 10.0 + + elif src == "impact_score_pct" and event.get("impact_score") is not None: + inputs[input_id] = float(event["impact_score"]) + + elif src == "actual_value" and event.get("actual_value") is not None: + inputs[input_id] = float(event["actual_value"]) + + node_values = evaluate_graph(graph, inputs, {}) + + instruments = list(set(tmpl.get("instruments", []))) or ["EURUSD"] + all_instruments_csv = ",".join(sorted(instruments)) or "EURUSD" + analyzed_at = datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%SZ") + event_id = event["id"] + + logger.info( + f"[auto_analysis] event #{event_id} → tmpl #{template_id} | " + f"inputs={inputs} | instruments={all_instruments_csv}" + ) + + existing = conn.execute( + "SELECT id FROM causal_event_analyses WHERE market_event_id=? AND template_id=?", + (event_id, template_id), + ).fetchone() + + if existing: + conn.execute(""" + UPDATE causal_event_analyses + SET instrument=?, inputs_json=?, override_params=?, prediction_json=?, + actual_json=?, activation_score=?, drift_json=?, analyzed_at=? + 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, + event_id, template_id, + )) + else: + conn.execute(""" + INSERT INTO causal_event_analyses + (market_event_id, template_id, instrument, inputs_json, + override_params, prediction_json, actual_json, + activation_score, drift_json, analyzed_at) + VALUES (?,?,?,?,?,?,?,?,?,?) + """, ( + event_id, template_id, all_instruments_csv, + json.dumps(inputs), json.dumps({}), + json.dumps(node_values), json.dumps({}), + None, json.dumps({}), analyzed_at, + )) + conn.commit() + conn.close() + logger.info(f"[auto_analysis] causal_event_analyses upserted → event #{event_id}, tmpl #{template_id}") + return True + + except Exception as e: + logger.error(f"[auto_analysis] event #{event.get('id')}: {e}") + return False + + def auto_assign_template(event_id: int) -> dict: """ Auto-assign or auto-create a causal template for a market event. @@ -1162,6 +1302,7 @@ def auto_assign_template(event_id: int) -> dict: conn.commit() row = conn.execute("SELECT name FROM causal_graph_templates WHERE id = ?", (tmpl_id,)).fetchone() conn.close() + _run_auto_analysis(event, tmpl_id) return {"template_id": tmpl_id, "action": "assigned", "name": row["name"] if row else "", "confidence": confidence} # Step 2 — no good match → generate a new template @@ -1220,6 +1361,7 @@ Règles : coef intermédiaire ∈ [-5,5] ; coef output en pips, |val| ∈ [30,20 conn2.close() logger.info(f"[auto_template] Created template #{new_id} '{spec.get('name')}' for event #{event_id}") + _run_auto_analysis(event, new_id) return {"template_id": new_id, "action": "created", "name": spec.get("name", "Template IA"), "confidence": confidence} except Exception as e: diff --git a/backend/services/market_event_detector.py b/backend/services/market_event_detector.py index 6282004..4088c74 100644 --- a/backend/services/market_event_detector.py +++ b/backend/services/market_event_detector.py @@ -1658,17 +1658,30 @@ def check_new_market_events( # Auto-assign or auto-create causal templates for eco events if "eco" in sources and bool(eco_cfg.get("auto_template", False)): eco_events = results.get("eco", []) + logger.info(f"[check_events/auto_template] auto_template=ON, {len(eco_events)} new eco events to process") if eco_events: try: from routers.causal_lab import auto_assign_template for ev in eco_events: - eid = ev.get("event_id") + eid = ev.get("event_id") + ename = ev.get("name", "?") + logger.info(f"[check_events/auto_template] processing event #{eid} '{ename}'") if eid: res = auto_assign_template(eid) - action = res.get("action", "error") - name = res.get("name", "") - logger.info(f"[check_events/auto_template] event #{eid} → {action}: '{name}'") + if "error" in res: + logger.warning(f"[check_events/auto_template] event #{eid} error: {res['error']}") + else: + action = res.get("action", "?") + name = res.get("name", "") + tmpl_id = res.get("template_id") + confidence = res.get("confidence", 0) + logger.info( + f"[check_events/auto_template] event #{eid} → {action}: " + f"'{name}' (tmpl #{tmpl_id}, conf={confidence:.2f})" + ) except Exception as e: - logger.warning(f"[check_events/auto_template] failed: {e}") + logger.warning(f"[check_events/auto_template] failed: {e}", exc_info=True) + else: + logger.info("[check_events/auto_template] no new eco events this cycle") return results