feat: instrument analysis
This commit is contained in:
@@ -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),
|
||||
|
||||
Reference in New Issue
Block a user