feat: instrument analysis

This commit is contained in:
OpenSquared
2026-06-28 16:36:28 +02:00
parent 0693d41d91
commit e4e17330b4
6 changed files with 715 additions and 649 deletions

View File

@@ -329,11 +329,15 @@ class InstantiateRequest(BaseModel):
class AnalyzeRequest(BaseModel):
market_event_id: int
template_id: int
instrument: str = "EURUSD"
instrument: str = "" # vide = analyser tous les instruments du template
inputs: dict = {}
coef_overrides: dict = {}
class CreateFromEventRequest(BaseModel):
market_event_id: int
# ── Endpoints ─────────────────────────────────────────────────────────────────
@router.get("/api/causal-lab/debug")
@@ -697,7 +701,12 @@ Règles :
Retourne UNIQUEMENT un JSON : {{ "node_id": valeur_numerique, ... }}
Inclure uniquement les nœuds listés ci-dessus."""
client = _openai_client()
from services.database import get_config as _get_cfg
import openai as _openai
_key = _get_cfg("openai_api_key") or ""
if not _key:
raise HTTPException(400, "Clé OpenAI manquante dans la configuration")
client = _openai.OpenAI(api_key=_key)
resp = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": prompt}],
@@ -864,8 +873,13 @@ def analyze_event(body: AnalyzeRequest):
default=0,
)
# Prix réels
instruments = list({body.instrument} | set(tmpl.get("instruments", [body.instrument])))
# Instruments : utiliser tous ceux du template si aucun spécifié
if body.instrument:
instruments = list({body.instrument} | set(tmpl.get("instruments", [body.instrument])))
else:
instruments = list(set(tmpl.get("instruments", []))) or ["EURUSD"]
primary_inst = body.instrument or (instruments[0] if instruments else "EURUSD")
prices = _fetch_prices(event["start_date"], instruments)
edate = event["start_date"][:10]
@@ -893,16 +907,18 @@ def analyze_event(body: AnalyzeRequest):
"event": event,
"template_id": body.template_id,
"template_name": tmpl["name"],
"instrument": body.instrument,
"instrument": primary_inst,
"instruments": instruments,
"inputs": inputs,
"node_values": node_values,
"actual_moves": actual_moves,
"yields": yields,
"activation": activation,
"drift": drift_by_inst.get(body.instrument, {}),
"drift": drift_by_inst.get(primary_inst, {}),
"prices_mode": prices.get("mode", "none"),
"effective_lag_min": effective_lag,
"analyzed_at": analyzed_at,
"graph_json": graph, # structure complète pour visualisation frontend
}
# Persistance
@@ -915,13 +931,13 @@ def analyze_event(body: AnalyzeRequest):
""", (
body.market_event_id,
body.template_id,
body.instrument,
primary_inst,
json.dumps(inputs),
json.dumps(body.coef_overrides),
json.dumps(node_values),
json.dumps(actual_moves),
activation.get("score"),
json.dumps(drift_by_inst.get(body.instrument, {})),
json.dumps(drift_by_inst.get(primary_inst, {})),
analyzed_at,
))
conn.commit()
@@ -929,11 +945,11 @@ def analyze_event(body: AnalyzeRequest):
# Calibration
update_calibration(conn, body.template_id, {
"activation_score": activation.get("score"),
"instrument": body.instrument,
"pred_pips": node_values.get(body.instrument.lower(), 0) or
"instrument": primary_inst,
"pred_pips": node_values.get(primary_inst.lower(), 0) or
next((v for k, v in node_values.items()
if body.instrument.lower() in k.lower()), 0),
"actual_pips": actual_moves.get(body.instrument),
if primary_inst.lower() in k.lower()), 0),
"actual_pips": actual_moves.get(primary_inst),
"analyzed_at": analyzed_at,
})
conn.close()
@@ -947,6 +963,194 @@ def analyze_event(body: AnalyzeRequest):
raise HTTPException(500, str(e))
def _build_graph_json_from_spec(spec: dict) -> dict:
"""Construit un graph_json complet depuis le spec simplifié généré par GPT-4o."""
input_node = spec.get("input_node", {})
intermediates = spec.get("intermediate_nodes", [])
outputs = spec.get("output_nodes", [])
nodes: list = []
edges: list = []
coefficients: dict = {}
input_mapping: dict = {}
# Nœud d'entrée — centré en haut
inp_id = input_node.get("id", "surprise")
nodes.append({
"id": inp_id, "label": input_node.get("label", "Surprise"),
"type": "input", "x": 200, "y": 50,
"unit": input_node.get("unit", "% vs consensus"),
})
input_mapping[inp_id] = {
"source": "surprise",
"unit": input_node.get("unit", "% vs consensus"),
"description": input_node.get("label", "Surprise"),
}
# Nœuds intermédiaires — étalés horizontalement au milieu
n_inter = max(len(intermediates), 1)
x_step_i = max(160, 340 // n_inter)
for i, inter in enumerate(intermediates):
x = 200 - (len(intermediates) - 1) * x_step_i // 2 + i * x_step_i
inter_id = inter["id"]
coef_name = inter.get("coef_name", f"coef_{inter_id}")
coef_val = float(inter.get("coef_value", 2.0))
coefficients[coef_name] = {
"value": coef_val, "calibrated": False,
"description": inter.get("coef_desc", coef_name),
}
nodes.append({
"id": inter_id, "label": inter.get("label", inter_id),
"type": "intermediate", "x": x, "y": 175,
"formula": f"{inp_id} * {coef_name}",
})
edges.append({"from": inp_id, "to": inter_id,
"sign": "positive" if coef_val >= 0 else "negative", "style": "solid"})
# Nœuds de sortie — étalés horizontalement en bas
inter_ids = [n["id"] for n in intermediates]
n_out = max(len(outputs), 1)
x_step_o = max(140, 320 // n_out)
for i, out in enumerate(outputs):
x = 200 - (len(outputs) - 1) * x_step_o // 2 + i * x_step_o
out_id = out["id"]
coef_name = out.get("coef_name", f"coef_{out_id}")
coef_val = float(out.get("coef_value", -100.0))
coefficients[coef_name] = {
"value": coef_val, "calibrated": False,
"description": out.get("coef_desc", coef_name),
}
src = out.get("source_intermediate", "")
from_id = src if src in inter_ids else (intermediates[0]["id"] if intermediates else inp_id)
nodes.append({
"id": out_id, "label": out.get("label", out_id),
"type": "market_asset", "x": x, "y": 310,
"unit": "pips", "instrument": out.get("instrument", "EURUSD"),
"formula": f"{from_id} * {coef_name}",
})
edges.append({"from": from_id, "to": out_id,
"sign": "positive" if coef_val >= 0 else "negative", "style": "solid"})
instruments = list({out.get("instrument", "EURUSD") for out in outputs})
return {
"nodes": nodes, "edges": edges,
"coefficients": coefficients, "input_mapping": input_mapping,
"instruments": instruments,
}
@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."""
try:
from services.database import get_conn, get_config
from services.causal_graphs import init_tables
import openai
key = get_config("openai_api_key") or ""
if not key:
raise HTTPException(400, "Clé OpenAI manquante dans la configuration")
conn = get_conn()
init_tables(conn)
ev_row = conn.execute(
"SELECT * FROM market_events WHERE id = ?", (body.market_event_id,)
).fetchone()
if not ev_row:
conn.close()
raise HTTPException(404, "Événement introuvable")
event = dict(ev_row)
prompt = f"""Tu es un analyste financier spécialisé en graphes causaux macro.
Crée un graphe causal pour l'événement suivant.
Événement :
- Nom : {event.get('name')}
- Catégorie : {event.get('category')} / {event.get('sub_type', '')}
- Description : {(event.get('description') or '')[:400]}
- Réel : {event.get('actual_value')} | Attendu : {event.get('expected_value')} | Surprise % : {event.get('surprise_pct')}
Retourne UNIQUEMENT ce JSON (sans commentaire ni markdown) :
{{
"name": "<nom court, ex: 'CPI US — Taux/USD'>",
"category": "macro_us",
"description": "<1 phrase décrivant le mécanisme>",
"input_node": {{
"id": "<snake_case>",
"label": "<label court>",
"unit": "<unité>"
}},
"intermediate_nodes": [
{{
"id": "<snake_case>",
"label": "<label>",
"coef_name": "<coef_snake_case>",
"coef_value": <float [-5,5]>,
"coef_desc": "<description>"
}}
],
"output_nodes": [
{{
"id": "<instr_lower>_pip",
"label": "<INSTRUMENT>",
"instrument": "<EURUSD|XAUUSD|SP500|BRENT|US10Y>",
"source_intermediate": "<id_nœud_intermédiaire>",
"coef_name": "<coef_snake_case>",
"coef_value": <pips/unité, valeur absolue 30-200, signe selon impact>,
"coef_desc": "<description>"
}}
]
}}
Règles : coef intermédiaire ∈ [-5,5] ; coef output en pips, |val| ∈ [30,200].
Surprise CPI US supérieur → taux ↑ → USD ↑ → EURUSD ↓ (coef négatif), XAU ↓, SP500 ↓."""
client = openai.OpenAI(api_key=key)
resp = client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": prompt}],
response_format={"type": "json_object"},
temperature=0.3,
max_tokens=900,
)
spec = json.loads(resp.choices[0].message.content or "{}")
graph_json = _build_graph_json_from_spec(spec)
category = spec.get("category", "macro_us")
instruments = graph_json.get("instruments", ["EURUSD"])
cur = conn.execute("""
INSERT INTO causal_graph_templates
(name, category, sub_type, description, instruments, graph_json, heuristic_ver)
VALUES (?, ?, ?, ?, ?, ?, 1)
""", (
spec.get("name", event.get("name", "Template IA")),
category,
event.get("sub_type", ""),
spec.get("description", ""),
json.dumps(instruments),
json.dumps(graph_json),
))
new_id = cur.lastrowid
conn.commit()
conn.close()
return {
"id": new_id,
"name": spec.get("name", "Template IA"),
"category": category,
"instruments": instruments,
"graph_json": graph_json,
}
except HTTPException:
raise
except Exception as e:
logger.error(f"[causal_lab] create-from-event: {e}")
raise HTTPException(500, str(e))
@router.get("/api/causal-lab/analyses")
def list_analyses(
template_id: int = Query(0),