From 9778c74ae3cd46b67c21bfd3fce5949e58a7156c Mon Sep 17 00:00:00 2001 From: OpenSquared Date: Fri, 3 Jul 2026 22:16:02 +0200 Subject: [PATCH] feat: instrument model --- backend/services/instrument_models.py | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/backend/services/instrument_models.py b/backend/services/instrument_models.py index 8ecf8c0..e455c98 100644 --- a/backend/services/instrument_models.py +++ b/backend/services/instrument_models.py @@ -1639,6 +1639,22 @@ def simulate_timeline( vals_struct = evaluate_graph(gj_struct, inputs_struct) static_structural = round(float(vals_struct.get(output_id, 0.0)), 1) + # Unités par nœud input_manual — pour corriger les valeurs brutes de ff_calendar + # (NFP: raw=228000 → attendu=228 K; PMI: raw=54.7 → attendu=4.7 pts écart/50) + macro_node_units: dict[str, str] = { + n["id"]: n.get("unit", "") + for n in graph_def.get("nodes", []) + if n.get("node_type") == "input_manual" + } + + def _unit_correct(v: float, unit: str) -> float: + """Convertit les valeurs brutes ff_calendar en valeur attendue par le nœud.""" + if unit == "K" and abs(v) >= 1_000: + return round(v / 1_000, 1) + if unit == "pts" and v > 10: + return round(v - 50.0, 1) + return v + # ── Auto-anchor : offset pour que la synthétique parte du prix réel à date_from ── # Calculé une seule fois sur le prix réel à date_from (ou le plus proche disponible). # Cet offset compense l'écart de calibration sans changer la dynamique du modèle. @@ -1660,6 +1676,7 @@ def simulate_timeline( # Valeur la plus proche avant date_from v = next((tl[d] for d in sorted(tl) if d <= str(date_from)), next(iter(tl.values()), None)) if v is not None: + v = _unit_correct(v, macro_node_units.get(node_id, "")) anchor_overrides[node_id] = {"value": v, "note": "anchor", "set_at": ""} gj_a = _graph_json_for_eval(graph_def, {}) in_a = _build_inputs(graph_def, anchor_overrides, {}, saturation=True) @@ -1677,7 +1694,7 @@ def simulate_timeline( for node_id, tl in macro_node_timelines.items(): v0 = tl.get(str(date_from)) if v0 is not None: - macro_last[node_id] = v0 + macro_last[node_id] = _unit_correct(v0, macro_node_units.get(node_id, "")) # Catégories d'events gérées par les nœuds input_event du graphe # Les catégories sans nœud correspondant sont appliquées comme choc direct sur l'output. @@ -1733,6 +1750,7 @@ def simulate_timeline( for node_id, tl in macro_node_timelines.items(): v = tl.get(cur_str) if v is not None: + v = _unit_correct(v, macro_node_units.get(node_id, "")) macro_last[node_id] = v else: v = macro_last.get(node_id) # gap-fill (weekend)