feat: desk ia

This commit is contained in:
OpenSquared
2026-06-29 20:21:52 +02:00
parent 551ebc3ef4
commit f4a55a8009
2 changed files with 160 additions and 5 deletions

View File

@@ -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:

View File

@@ -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