feat: instrument model

This commit is contained in:
OpenSquared
2026-07-03 17:39:44 +02:00
parent ff551deecd
commit 716d8fa56c
3 changed files with 405 additions and 10 deletions

View File

@@ -148,6 +148,43 @@ def apply_gauge_suggestions(
conn.close()
@router.get("/{instrument}/macro-sync")
def get_macro_sync(instrument: str) -> Dict[str, Any]:
"""Retourne les valeurs actuelles + forecasts FF Calendar pour tous les nœuds macro-liés."""
from services.database import get_conn
from services.instrument_models import get_macro_sync_items
conn = get_conn()
try:
items = get_macro_sync_items(conn, instrument)
return {"instrument": instrument.upper(), "items": items, "count": len(items)}
finally:
conn.close()
class MacroSyncApplyBody(BaseModel):
items: list[dict] # [{node_id, value, note}]
@router.post("/{instrument}/macro-sync/apply")
def apply_macro_sync(instrument: str, body: MacroSyncApplyBody) -> Dict[str, Any]:
"""Applique les valeurs macro (actuelles ou forecasts) comme node overrides."""
from services.database import get_conn
from services.instrument_models import set_node_override
conn = get_conn()
try:
inst = instrument.upper()
saved = []
for item in body.items:
nid = item.get("node_id", "")
value = item.get("value")
note = item.get("note", "[Sync Marché FF Calendar]")
if nid and value is not None:
set_node_override(conn, inst, nid, float(value), note)
saved.append(nid)
return {"ok": True, "instrument": inst, "saved": saved, "count": len(saved)}
finally:
conn.close()
@router.delete("/{instrument}/overrides")
def clear_all_overrides(instrument: str) -> Dict[str, Any]:
"""Supprime TOUTES les overrides d'un instrument (reset à zéro)."""

View File

@@ -315,6 +315,132 @@ def build_node_combined_timeline(
return result
# ── Macro sync — lecture directe ff_calendar → overrides ──────────────────────
def get_macro_sync_items(conn, instrument: str) -> list[dict]:
"""
Pour chaque nœud avec macro_key dans le graphe, retourne :
- actual_value : dernière valeur publiée (passé)
- actual_date : date de cette publication
- forecast_value: forecast du prochain événement (futur)
- forecast_date / next_event_name / days_until
- current_override : valeur déjà settée en DB (ou None)
Utilisé par le bouton "Sync Marché" pour pré-remplir le graphe.
"""
inst_upper = instrument.upper()
row = conn.execute(
"SELECT graph_json FROM instrument_models WHERE instrument=?", (inst_upper,)
).fetchone()
if not row:
return []
graph_def = json.loads(row["graph_json"])
today = date_type.today()
today_str = str(today)
q_from = str(today - timedelta(days=730))
q_to = str(today + timedelta(days=180))
overrides = {r["node_id"]: float(r["value"]) for r in conn.execute(
"SELECT node_id, value FROM instrument_node_overrides WHERE instrument=?", (inst_upper,)
).fetchall()}
items = []
for node in graph_def.get("nodes", []):
mk = node.get("macro_key")
if not mk or node.get("node_type") != "input_manual":
continue
mk_info = FF_MACRO_KEYS.get(mk)
if not mk_info:
continue
currency = mk_info["currency"]
patterns = mk_info["names"]
try:
rows = conn.execute(
"""SELECT event_date, event_name, actual_value, forecast_value, previous_value
FROM ff_calendar
WHERE currency=? AND event_date>=? AND event_date<=?
ORDER BY event_date ASC""",
(currency, q_from, q_to)
).fetchall()
except Exception:
rows = []
matched = [
dict(r) for r in rows
if any(p in str(r["event_name"]).lower() for p in patterns)
]
# Deduplicate by date
seen: set[str] = set()
deduped = []
for ev in matched:
if ev["event_date"] not in seen:
seen.add(ev["event_date"])
deduped.append(ev)
# Latest actual (most recent past event with actual_value)
past = [e for e in deduped if e["event_date"] <= today_str]
actual_val: Optional[float] = None
actual_date: Optional[str] = None
for ev in reversed(past):
v = _parse_ff_num(ev.get("actual_value"))
if v is not None:
actual_val = v
actual_date = ev["event_date"]
break
# Next forecast (first future event)
future = [e for e in deduped if e["event_date"] > today_str]
forecast_val: Optional[float] = None
forecast_date: Optional[str] = None
next_event_name: Optional[str] = None
days_until: Optional[int] = None
if future:
nxt = future[0]
forecast_val = (
_parse_ff_num(nxt.get("forecast_value"))
or _parse_ff_num(nxt.get("previous_value"))
)
forecast_date = nxt["event_date"]
next_event_name = nxt["event_name"]
try:
days_until = (date_type.fromisoformat(forecast_date) - today).days
except Exception:
days_until = None
# Unit-aware post-conversion
# The FF Calendar stores NFP in absolute (e.g. 228000), node expects K (228)
# PMI stored as absolute value (54.7), node expects deviation from 50 (4.7)
unit = node.get("unit", "")
def _convert(v: Optional[float]) -> Optional[float]:
if v is None:
return None
if unit == "K" and abs(v) >= 1_000:
return round(v / 1_000, 1)
if unit == "pts" and v > 10: # PMI > 10 → is absolute, subtract 50
return round(v - 50.0, 1)
return round(v, 4)
items.append({
"node_id": node["id"],
"node_label": node["label"],
"macro_key": mk,
"unit": unit,
"coefficient_to_pips": node.get("coefficient_to_pips", 1),
"actual_value": _convert(actual_val),
"actual_date": actual_date,
"forecast_value": _convert(forecast_val),
"forecast_date": forecast_date,
"next_event_name": next_event_name,
"days_until_forecast": days_until,
"current_override": overrides.get(node["id"]),
})
return items
# ── Scenario CRUD ──────────────────────────────────────────────────────────────
def get_node_scenarios(conn, instrument: str, node_id: Optional[str] = None) -> list[dict]: