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