diff --git a/backend/services/instrument_models.py b/backend/services/instrument_models.py
index 36438dd..4eac947 100644
--- a/backend/services/instrument_models.py
+++ b/backend/services/instrument_models.py
@@ -758,8 +758,10 @@ def _build_inputs(
ov = overrides.get(nid)
if ntype == "input_event":
- # Events sont déjà en pips → pas de saturation (déjà non-linéaire via lifecycle)
- inputs[nid] = float(ov["value"]) if ov else ev_by_cat.get(node.get("event_category", ""), 0.0)
+ # Baseline (niveau structurel user) + surprise event (lifecycle) → additif
+ base = float(ov["value"]) if ov else 0.0
+ events = ev_by_cat.get(node.get("event_category", ""), 0.0)
+ inputs[nid] = base + events
elif ntype == "input_manual":
coeff = float(node.get("coefficient_to_pips", 1.0))
@@ -953,9 +955,13 @@ def get_model_state(conn, instrument: str, at_date: Optional[str] = None) -> Opt
st["pip_contribution"] = val
if ntype == "input_event":
- cat = node.get("event_category", "")
- st["source"] = "manual" if ov else ("events" if val != 0.0 else "neutral")
- st["raw_value"] = ov["value"] if ov else round(ev_by_cat.get(cat, 0.0), 1)
+ cat = node.get("event_category", "")
+ event_surprise = round(ev_by_cat.get(cat, 0.0), 1)
+ baseline = float(ov["value"]) if ov else 0.0
+ st["raw_value"] = baseline
+ st["baseline_value"] = baseline
+ st["event_surprise"] = event_surprise
+ st["source"] = "manual" if ov else ("events" if event_surprise != 0.0 else "neutral")
if ov:
st["override_note"] = ov.get("note", "")
st["override_set_at"] = ov.get("set_at", "")
diff --git a/frontend/src/pages/InstrumentModels.tsx b/frontend/src/pages/InstrumentModels.tsx
index e9197b6..75324d1 100644
--- a/frontend/src/pages/InstrumentModels.tsx
+++ b/frontend/src/pages/InstrumentModels.tsx
@@ -41,6 +41,9 @@ interface ModelNode {
pip_saturated?: number
saturation_pct?: number
regime_weight?: number
+ // Phase 3 — event nodes: baseline (structurel) + surprise (événement)
+ baseline_value?: number
+ event_surprise?: number
}
interface RegimeInfo {
@@ -347,7 +350,7 @@ function NodeEditModal({ node, instrument, onClose, onSaved }: {
Valeur ({node.unit})
{node.node_type === 'input_manual' && coeff !== 0 &&
× {coeff} = {fmt(preview)} pips}
- {node.node_type === 'input_event' && (pips directs)}
+ {node.node_type === 'input_event' && niveau structurel (les events s'y ajoutent)}
setVal(e.target.value)}
@@ -444,22 +447,35 @@ function NodeCard({ node, onEdit, eventDetails }: {
)}
) : node.node_type === 'input_event' ? (() => {
- const cat = node.event_category || ''
- const evList = eventDetails?.[cat] || []
- const maxLf = evList.length > 0 ? Math.max(...evList.map(e => e.lifecycle_factor)) : 0
- const barW = Math.round(maxLf * 100)
+ const cat = node.event_category || ''
+ const evList = eventDetails?.[cat] || []
+ const maxLf = evList.length > 0 ? Math.max(...evList.map(e => e.lifecycle_factor)) : 0
+ const barW = Math.round(maxLf * 100)
+ const baseline = node.baseline_value ?? 0
+ const surprise = node.event_surprise ?? 0
+ const hasBaseline = baseline !== 0
return (
-
- {fmt(v)} pips
- {evList.length > 0 && (
- {evList.length} ev
- )}
-
+ {hasBaseline ? (
+
+
+ base:{fmt(baseline)}p
+ {surprise !== 0 && (
+ surp:{fmt(surprise)}p
+ )}
+
+
={fmt(v)}p
+
+ ) : (
+
+ {fmt(v)} pips
+ {evList.length > 0 && {evList.length} ev}
+
+ )}
{evList.length > 0 && (
-
@@ -498,38 +514,174 @@ function NodeCard({ node, onEdit, eventDetails }: {
)
}
+interface EdgeDraw {
+ x1: number; y1: number; x2: number; y2: number
+ coeff: number; fromId: string; toId: string; key: string
+ fromType: NodeType
+}
+
function DagView({ nodes, instrument, onEdit, eventDetails }: {
nodes: ModelNode[]; instrument: string; onEdit: (n: ModelNode) => void
eventDetails: Record
}) {
+ const wrapperRef = useRef(null)
+ const nodeRefs = useRef