feat: instrument model

This commit is contained in:
OpenSquared
2026-07-03 15:34:48 +02:00
parent 243375ff07
commit 2d8cccec07
3 changed files with 582 additions and 67 deletions

View File

@@ -40,6 +40,10 @@ class WhatIfBody(BaseModel):
start_date: Optional[str] = None
class NodeConfigBody(BaseModel):
macro_key: Optional[str] = None # "" to clear, None = no-op
class CalibrateBody(BaseModel):
ref_date: Optional[str] = None
@@ -535,6 +539,128 @@ def calibrate_intercept(
conn.close()
@router.patch("/{instrument}/nodes/{node_id}")
def update_node_config(
instrument: str,
node_id: str,
body: NodeConfigBody,
) -> Dict[str, Any]:
"""Met à jour la configuration d'un nœud (macro_key) dans le graph_json persisté."""
import json as _json
from services.database import get_conn
conn = get_conn()
try:
inst = instrument.upper()
row = conn.execute(
"SELECT graph_json FROM instrument_models WHERE instrument=?", (inst,)
).fetchone()
if not row:
raise HTTPException(status_code=404, detail=f"Instrument {inst} introuvable")
graph_def = _json.loads(row["graph_json"])
found = False
for node in graph_def.get("nodes", []):
if node["id"] == node_id:
if body.macro_key is not None:
if body.macro_key == "":
node.pop("macro_key", None) # clear
else:
node["macro_key"] = body.macro_key
found = True
break
if not found:
raise HTTPException(status_code=404, detail=f"Nœud {node_id} introuvable")
conn.execute(
"UPDATE instrument_models SET graph_json=?, updated_at=datetime('now') WHERE instrument=?",
(_json.dumps(graph_def), inst)
)
conn.commit()
return {"ok": True, "node_id": node_id, "macro_key": body.macro_key}
finally:
conn.close()
@router.get("/{instrument}/macro-guidance")
def get_macro_guidance(instrument: str) -> List[Dict[str, Any]]:
"""
Retourne l'état courant et le prochain forecast pour chaque nœud input_manual
avec un macro_key configuré.
"""
import json as _json
from services.database import get_conn
from services.instrument_models import build_macro_node_timeline, FF_MACRO_KEYS, _parse_ff_num
from datetime import datetime, timedelta, date as date_type
conn = get_conn()
try:
inst = instrument.upper()
row = conn.execute(
"SELECT graph_json FROM instrument_models WHERE instrument=?", (inst,)
).fetchone()
if not row:
return []
graph_def = _json.loads(row["graph_json"])
today = datetime.utcnow().date()
result = []
for node in graph_def.get("nodes", []):
if node.get("node_type") != "input_manual":
continue
macro_key = node.get("macro_key")
if not macro_key:
continue
meta = FF_MACRO_KEYS.get(macro_key, {})
currency = meta.get("currency", "")
patterns = meta.get("names", [])
# Current interpolated value
date_from = today - timedelta(days=90)
date_to = today + timedelta(days=180)
tl = build_macro_node_timeline(conn, macro_key, date_from, date_to)
current_v = tl.get(str(today))
# Next scheduled event from ff_calendar
next_event = None
if currency and patterns:
try:
ff_rows = conn.execute(
"""SELECT event_date, event_name, forecast_value, previous_value
FROM ff_calendar
WHERE currency=? AND event_date > ?
ORDER BY event_date ASC LIMIT 20""",
(currency, str(today))
).fetchall()
for r in ff_rows:
if any(p in r["event_name"].lower() for p in patterns):
ev_date = date_type.fromisoformat(r["event_date"])
forecast_v = _parse_ff_num(r.get("forecast_value") or r.get("previous_value"))
next_event = {
"date": r["event_date"],
"name": r["event_name"],
"forecast": forecast_v,
"days_until": (ev_date - today).days,
}
break
except Exception:
pass
result.append({
"node_id": node["id"],
"node_label": node.get("label", node["id"]),
"macro_key": macro_key,
"unit": node.get("unit", ""),
"current_value": round(current_v, 4) if current_v is not None else None,
"next_event": next_event,
})
return result
finally:
conn.close()
@router.get("/{instrument}")
def get_instrument_model(
instrument: str,