From ff551deecdb6114273c0221c1ad33f4ca23b15e7 Mon Sep 17 00:00:00 2001 From: OpenSquared Date: Fri, 3 Jul 2026 17:20:22 +0200 Subject: [PATCH] feat: instrument model --- backend/services/instrument_models.py | 144 ++++++++++++++++------ frontend/src/pages/InstrumentModels.tsx | 152 +++++++++++++++--------- 2 files changed, 203 insertions(+), 93 deletions(-) diff --git a/backend/services/instrument_models.py b/backend/services/instrument_models.py index caf544a..e183a7f 100644 --- a/backend/services/instrument_models.py +++ b/backend/services/instrument_models.py @@ -529,49 +529,115 @@ INSTRUMENT_MODELS: dict[str, dict] = { # ══════════════════════════════════════════════════════════════════════════════ "EURUSD": { "name": "EUR/USD", - "description": "EUR/USD — Graphe causal macro-natif : noeuds = variables macro réelles auto-synchronisées depuis FF Calendar", + "description": "Graphe causal pondéré 7 couches : macro fondamentaux → hawkishness → dynamiques marché → taux & demande → positionnement → flux → prix", "output_node": "eurusd", "price_intercept": 1.10, "pip_to_price": 0.0001, "yf_ticker": "EURUSD=X", + "col_labels": ["Variables Macro", "Politique Monétaire", "Dynamiques Marché", "Taux & Demande", "Positionnement", "Order Flow", "Output"], "nodes": [ - # ── Layer 0a : chocs événementiels (surprises court terme) ─────────────── - {"id":"in_cb", "label":"Choc Banques Centrales", "node_type":"input_event","category":"central_bank", "unit":"pips","display_col":0,"event_category":"central_bank", "description":"Surprises Fed/BCE : décisions inattendues, guidance hawkish/dovish, minutes."}, - {"id":"in_geo", "label":"Choc Géopolitique", "node_type":"input_event","category":"geopolitical", "unit":"pips","display_col":0,"event_category":"geopolitical", "description":"Conflits, sanctions, tensions → flight to USD."}, - {"id":"in_trade", "label":"Choc Commercial/Tarifs", "node_type":"input_event","category":"trade_policy", "unit":"pips","display_col":0,"event_category":"trade_policy", "description":"Tarifs US-UE, représailles → USD/EUR volatilité."}, - {"id":"in_credit","label":"Stress Crédit", "node_type":"input_event","category":"credit_stress","unit":"pips","display_col":0,"event_category":"credit_stress", "description":"Stress bancaire, spreads → USD safe haven."}, - # ── Layer 0b : politique monétaire (taux directeurs absolus) ───────────── - # macro_key = id → auto-sync depuis ff_calendar sans mapping manuel - {"id":"fed_rate","label":"Taux Fed (%)", "node_type":"input_manual","category":"monetary","unit":"%","coefficient_to_pips":-30, "macro_key":"fed_rate", "display_col":1,"description":"Taux directeur Fed en %. Hausse → USD fort → pair ↓. Auto-sync depuis FF Calendar."}, - {"id":"ecb_rate","label":"Taux BCE (%)", "node_type":"input_manual","category":"monetary","unit":"%","coefficient_to_pips":+30, "macro_key":"ecb_rate", "display_col":1,"description":"Taux directeur BCE en %. Hausse → EUR fort → pair ↑. Auto-sync depuis FF Calendar."}, - # ── Layer 0c : inflation ───────────────────────────────────────────────── - {"id":"us_cpi", "label":"CPI US YoY (%)", "node_type":"input_manual","category":"inflation","unit":"%","coefficient_to_pips":-10, "macro_key":"us_cpi_yoy","display_col":1,"description":"Inflation US YoY. Hausse → anticipations Fed hawkish → USD ↑ → pair ↓."}, - {"id":"eu_cpi", "label":"HICP Eurozone (%)", "node_type":"input_manual","category":"inflation","unit":"%","coefficient_to_pips":+10, "macro_key":"eu_cpi_yoy","display_col":1,"description":"Inflation EU YoY. Hausse → BCE hawkish → EUR ↑ → pair ↑."}, - # ── Layer 0d : croissance ───────────────────────────────────────────────── - {"id":"us_gdp", "label":"GDP US QoQ (%)", "node_type":"input_manual","category":"growth","unit":"%","coefficient_to_pips":-15, "macro_key":"us_gdp", "display_col":1,"description":"Croissance US trimestrielle. Surperformance → USD ↑ → pair ↓."}, - {"id":"eu_gdp", "label":"GDP Eurozone QoQ (%)","node_type":"input_manual","category":"growth","unit":"%","coefficient_to_pips":+15, "macro_key":"eu_gdp", "display_col":1,"description":"Croissance EU trimestrielle. Surperformance → EUR ↑ → pair ↑."}, - # ── Layer 0e : emploi & activité ───────────────────────────────────────── - {"id":"us_nfp", "label":"NFP US (K/mois)", "node_type":"input_manual","category":"labor","unit":"K","coefficient_to_pips":-0.05, "macro_key":"us_nfp", "display_col":1,"description":"Emplois non-agricoles US en K. 150K= neutre. Plus → USD ↑ → pair ↓."}, - {"id":"eu_pmi", "label":"PMI EU (écart/50)", "node_type":"input_manual","category":"activity","unit":"pts","coefficient_to_pips":+2.5,"macro_key":"eu_pmi", "display_col":1,"description":"PMI Composite Eurozone MOINS 50 (+4 = PMI=54, expansion → EUR ↑)."}, - {"id":"us_pmi", "label":"PMI US (écart/50)", "node_type":"input_manual","category":"activity","unit":"pts","coefficient_to_pips":-2.5,"macro_key":"us_pmi", "display_col":1,"description":"PMI ISM US MOINS 50 (+3 = PMI=53, expansion → USD ↑ → pair ↓)."}, - # ── Layer 0f : sentiment & risque (pas de macro_key — saisi ou events) ─── - {"id":"vix", "label":"VIX (niveau)", "node_type":"input_manual","category":"risk","unit":"pts","coefficient_to_pips":-1.5, "display_col":1,"description":"Volatilité equity US. Spike → safe haven USD → pair ↓."}, - {"id":"us_equity","label":"S&P 500 momentum", "node_type":"input_manual","category":"risk","unit":"score","coefficient_to_pips":+0.3, "display_col":1,"description":"Risk-on US. Hausse → appétit risque → EUR ↑. Score -5/+5."}, - {"id":"eu_fragm","label":"Fragmentation EU", "node_type":"input_manual","category":"political","unit":"score","coefficient_to_pips":-0.5, "display_col":1,"description":"Risque fragmentation zone euro, stress BTP/Bund. Score 0-5 → EUR ↓."}, - # ── Layer 1 : domaines synthèse ─────────────────────────────────────────── - {"id":"layer_monetary","label":"▶ Différentiel Monétaire","node_type":"intermediate","category":"intermediate","unit":"pips","display_col":2, - "formula":"in_cb + fed_rate + ecb_rate + us_cpi + eu_cpi", - "description":"Taux directeurs + inflation → différentiel de politique monétaire Fed/BCE."}, - {"id":"layer_growth", "label":"▶ Différentiel Croissance","node_type":"intermediate","category":"intermediate","unit":"pips","display_col":2, - "formula":"us_gdp + eu_gdp + us_nfp + eu_pmi + us_pmi", - "description":"Croissance relative US vs EU. Positif = EU surperform → EUR ↑."}, - {"id":"layer_risk", "label":"▶ Sentiment & Risque", "node_type":"intermediate","category":"intermediate","unit":"pips","display_col":2, - "formula":"in_geo + in_trade + in_credit + vix + us_equity + eu_fragm", - "description":"Risque géopolitique, sentiment, appétit risque → impact EUR/USD."}, - # ── Layer 2 : output ────────────────────────────────────────────────────── - {"id":"eurusd","label":"EUR/USD — Biais Net","node_type":"output","category":"output","unit":"pips","display_col":3, - "formula":"layer_monetary + layer_growth + layer_risk", - "description":"Biais net EUR/USD. Positif = haussier EUR. Divisé en 3 piliers : monétaire, croissance, risque."}, + # ── Chocs CT : événements court terme ───────────────────────────────────── + # display_col:-1 → cachés dans le DAG visuel, additifs dans la formule output + {"id":"in_cb", "label":"Choc Banques Centrales","node_type":"input_event","category":"central_bank","unit":"pips","display_col":-1,"event_category":"central_bank","description":"Surprise Fed/BCE : décision inattendue, guidance. Choc CT additionné à l'output."}, + {"id":"in_geo", "label":"Choc Géopolitique", "node_type":"input_event","category":"geopolitical","unit":"pips","display_col":-1,"event_category":"geopolitical","description":"Conflits, sanctions → flight to USD. Choc CT."}, + {"id":"in_trade", "label":"Choc Commercial", "node_type":"input_event","category":"trade_policy","unit":"pips","display_col":-1,"event_category":"trade_policy","description":"Tarifs US-UE. Choc CT."}, + {"id":"in_credit","label":"Stress Crédit", "node_type":"input_event","category":"credit_stress","unit":"pips","display_col":-1,"event_category":"credit_stress","description":"Stress bancaire → safe haven USD. Choc CT."}, + + # ── Layer 0 (col 0) : Variables macro fondamentales ─────────────────────── + # Toutes auto-synchronisées via FF Calendar (macro_key) + # Coefficients calibrés : au neutre (all=0) output=0, range typique ±200 pips + {"id":"fed_rate", "label":"Taux Fed (%)", "node_type":"input_manual","category":"monetary","unit":"%","coefficient_to_pips":-60, "macro_key":"fed_rate", "display_col":0,"description":"Taux directeur Fed en %. Hausse → USD fort → EURUSD ↓. Dominant driver."}, + {"id":"ecb_rate", "label":"Taux BCE (%)", "node_type":"input_manual","category":"monetary","unit":"%","coefficient_to_pips":+60, "macro_key":"ecb_rate", "display_col":0,"description":"Taux directeur BCE en %. Hausse → EUR fort → EURUSD ↑."}, + {"id":"us_cpi", "label":"CPI US YoY (%)", "node_type":"input_manual","category":"inflation","unit":"%","coefficient_to_pips":-20,"macro_key":"us_cpi_yoy","display_col":0,"description":"Inflation US annuelle. Hausse → Fed hawkish → USD ↑."}, + {"id":"eu_cpi", "label":"HICP Eurozone (%)", "node_type":"input_manual","category":"inflation","unit":"%","coefficient_to_pips":+20,"macro_key":"eu_cpi_yoy","display_col":0,"description":"Inflation EU annuelle. Hausse → BCE hawkish → EUR ↑."}, + {"id":"us_growth","label":"GDP US QoQ (%)", "node_type":"input_manual","category":"growth","unit":"%","coefficient_to_pips":-30, "macro_key":"us_gdp", "display_col":0,"description":"Croissance US trimestrielle. Surperformance → USD dominant."}, + {"id":"eu_growth","label":"GDP Eurozone QoQ (%)","node_type":"input_manual","category":"growth","unit":"%","coefficient_to_pips":+30, "macro_key":"eu_gdp", "display_col":0,"description":"Croissance EU trimestrielle. Surperformance → EUR dominant."}, + {"id":"us_nfp", "label":"NFP US (K/mois)", "node_type":"input_manual","category":"labor","unit":"K","coefficient_to_pips":-0.08,"macro_key":"us_nfp", "display_col":0,"description":"Emplois non-agricoles. Fort (>200K) → USD hawkish."}, + {"id":"eu_pmi", "label":"PMI EU (écart/50)", "node_type":"input_manual","category":"activity","unit":"pts","coefficient_to_pips":+5,"macro_key":"eu_pmi", "display_col":0,"description":"PMI EU écart/50. Positif = expansion EU → EUR achetée."}, + {"id":"us_pmi", "label":"PMI US (écart/50)", "node_type":"input_manual","category":"activity","unit":"pts","coefficient_to_pips":-5,"macro_key":"us_pmi", "display_col":0,"description":"PMI US écart/50. Positif = expansion US → USD fort."}, + + # ── Layer 1 (col 1) : Politique monétaire ──────────────────────────────── + # rate_lvl : différentiel de taux directs (driver dominant) + # ecb_hawk / fed_hawk : pression hawkish perçue (données macro → anticipations futures) + {"id":"rate_lvl", "label":"Niveaux Taux (BCE−Fed)", + "node_type":"intermediate","category":"monetary","unit":"pips","display_col":1, + "formula":"ecb_rate + fed_rate", + "description":"Différentiel de taux directeurs brut BCE−Fed (en pips). Driver dominant du pair."}, + {"id":"ecb_hawk", "label":"BCE Hawkishness", + "node_type":"intermediate","category":"monetary","unit":"pips","display_col":1, + "formula":"0.55*eu_cpi + 0.30*eu_growth + 0.15*eu_pmi", + "description":"Pression hawkish BCE perçue : inflation + croissance + activité EU → anticipations taux CE."}, + {"id":"fed_hawk", "label":"Fed Hawkishness", + "node_type":"intermediate","category":"monetary","unit":"pips","display_col":1, + "formula":"0.55*us_cpi + 0.30*us_growth + 0.12*us_nfp + 0.03*us_pmi", + "description":"Pression hawkish Fed perçue : inflation + croissance + emploi → anticipations taux US."}, + + # ── Layer 2 (col 2) : Dynamiques de marché ─────────────────────────────── + # rate_diff : différentiel de hawkishness perçue (secondaire au rate_lvl) + # eu_equities / us_equities : proxies croissance/risque equity + # risk_on : appétit risque global + {"id":"rate_diff", "label":"Différentiel Hawkishness", + "node_type":"intermediate","category":"monetary","unit":"pips","display_col":2, + "formula":"ecb_hawk + fed_hawk", + "description":"Différentiel BCE−Fed de hawkishness. Positif = BCE perçue plus hawkish que Fed."}, + {"id":"eu_equities","label":"EU Equities", + "node_type":"intermediate","category":"risk","unit":"pips","display_col":2, + "formula":"0.60*eu_growth + 0.40*eu_pmi", + "description":"Proxy performance equity EU. Croissance + activité → flux entrants zone euro."}, + {"id":"us_equities","label":"US Equities", + "node_type":"intermediate","category":"risk","unit":"pips","display_col":2, + "formula":"0.60*us_growth + 0.40*us_pmi", + "description":"Proxy performance equity US. Expansion US → USD résilient, risk-on."}, + {"id":"risk_on", "label":"Risk-On Global", + "node_type":"intermediate","category":"risk","unit":"pips","display_col":2, + "formula":"0.50*eu_equities + 0.30*us_equities + 0.20*eu_growth", + "description":"Appétit risque global. Risk-on → carry → EUR achetée vs USD."}, + + # ── Layer 3 (col 3) : Taux longs & Demande de change ───────────────────── + {"id":"us10y", "label":"US10Y Signal", + "node_type":"intermediate","category":"rates","unit":"pips","display_col":3, + "formula":"0.50*fed_hawk + 0.30*us_growth + 0.20*us_cpi", + "description":"Signal rendement US 10Y. Hausse → USD attractif → EURUSD ↓."}, + {"id":"carry", "label":"Carry Trade EUR", + "node_type":"intermediate","category":"positioning","unit":"pips","display_col":3, + "formula":"0.65*rate_diff + 0.35*risk_on", + "description":"Attractivité carry EUR/USD. Positif = taux EU > taux US + marché calme → EUR long."}, + {"id":"eur_demand","label":"EUR Demand", + "node_type":"intermediate","category":"flow","unit":"pips","display_col":3, + "formula":"0.55*eu_equities + 0.30*eu_growth + 0.15*eu_pmi", + "description":"Demande structurelle EUR : flux investissement zone euro, capitaux entrants."}, + {"id":"usd_demand","label":"USD Demand", + "node_type":"intermediate","category":"flow","unit":"pips","display_col":3, + "formula":"0.55*us_equities + 0.30*us_growth + 0.15*us_nfp", + "description":"Demande structurelle USD. Forte = EURUSD ↓. (Valeurs déjà en signe négatif.)"}, + + # ── Layer 4 (col 4) : Positionnement & Microstructure ──────────────────── + {"id":"cftc_usd_long","label":"CFTC USD Long", + "node_type":"intermediate","category":"positioning","unit":"pips","display_col":4, + "formula":"0.50*fed_hawk + 0.30*us10y + 0.20*rate_diff", + "description":"Positionnement spéculatif long USD (proxy CFTC). Négatif = longs USD → USD déjà saturé."}, + {"id":"dealer_gamma","label":"Dealer Gamma", + "node_type":"intermediate","category":"positioning","unit":"pips","display_col":4, + "formula":"0.60*risk_on + 0.40*carry", + "description":"Position gamma dealers. Positif = marché calme, options équilibrées → tends à stabiliser."}, + {"id":"liquidity", "label":"Liquidité EUR/USD", + "node_type":"intermediate","category":"flow","unit":"pips","display_col":4, + "formula":"0.50*eur_demand + 0.30*carry + 0.20*dealer_gamma", + "description":"Liquidité côté EUR. Profondeur acheteurs EUR institutionnels."}, + + # ── Layer 5 (col 5) : Order Flow synthétique ────────────────────────────── + {"id":"order_flow","label":"Order Flow Net", + "node_type":"intermediate","category":"flow","unit":"pips","display_col":5, + "formula":"0.35*eur_demand + 0.25*liquidity + 0.20*carry + 0.12*cftc_usd_long + 0.08*dealer_gamma", + "description":"Flux d'ordres net EUR/USD. Positif = pression acheteuse EUR. Driver direct du prix court terme."}, + + # ── Output (col 6) ──────────────────────────────────────────────────────── + # Formule = contribution pondérée cross-couches (pas de double-comptage car nœuds distincts) + # + chocs CT additifs (in_cb, in_geo, in_trade, in_credit) + {"id":"eurusd", "label":"EUR/USD — Biais Net", + "node_type":"output","category":"output","unit":"pips","display_col":6, + "formula":"0.40*rate_lvl + 0.30*rate_diff + 0.20*order_flow + 0.06*eur_demand + 0.04*usd_demand + in_cb + in_geo + in_trade + in_credit", + "description":"Biais fondamental net EUR/USD en pips. Positif = haussier EUR. Dominé par différentiel taux + hawkishness + flux."}, ] }, @@ -1002,6 +1068,7 @@ def seed_instrument_models(conn): "description": model["description"], "output_node": model["output_node"], "nodes": model["nodes"], + "col_labels": model.get("col_labels", []), }) conn.execute(""" INSERT INTO instrument_models (instrument, graph_json) @@ -1355,6 +1422,7 @@ def get_model_state(conn, instrument: str, at_date: Optional[str] = None) -> Opt "output_node": output_id, "regime": regime_info, "event_details": event_details, + "col_labels": graph_def.get("col_labels", []), } diff --git a/frontend/src/pages/InstrumentModels.tsx b/frontend/src/pages/InstrumentModels.tsx index 230cdd0..4bd3f40 100644 --- a/frontend/src/pages/InstrumentModels.tsx +++ b/frontend/src/pages/InstrumentModels.tsx @@ -73,6 +73,7 @@ interface ModelState { output_node: string regime: RegimeInfo event_details: Record + col_labels?: string[] } interface ActiveEvent { @@ -463,7 +464,7 @@ function NodeEditModal({ node, instrument, onClose, onSaved }: { // ── DAG View ────────────────────────────────────────────────────────────────── -const COL_LABELS = ['Events actifs', 'Variables manuelles', 'Couches intermédiaires', 'Résultat net'] +const DEFAULT_COL_LABELS = ['Events actifs', 'Variables manuelles', 'Couches intermédiaires', 'Résultat net'] function NodeCard({ node, onEdit, eventDetails }: { node: ModelNode; onEdit: (n: ModelNode) => void @@ -551,9 +552,10 @@ interface EdgeDraw { fromType: NodeType } -function DagView({ nodes, instrument, onEdit, eventDetails }: { +function DagView({ nodes, instrument, onEdit, eventDetails, colLabels }: { nodes: ModelNode[]; instrument: string; onEdit: (n: ModelNode) => void eventDetails: Record + colLabels?: string[] }) { const wrapperRef = useRef(null) const nodeRefs = useRef>(new Map()) @@ -561,8 +563,26 @@ function DagView({ nodes, instrument, onEdit, eventDetails }: { const [hoveredId, setHoveredId] = useState(null) const [svgH, setSvgH] = useState(600) - const cols: ModelNode[][] = [[], [], [], []] - for (const n of nodes) cols[Math.min(n.display_col ?? 0, 3)].push(n) + // Separate hidden nodes (display_col < 0 = event chocs) from visible nodes + const hiddenEvents = useMemo(() => nodes.filter(n => (n.display_col ?? 0) < 0), [nodes]) + const visibleNodes = useMemo(() => nodes.filter(n => (n.display_col ?? 0) >= 0), [nodes]) + + // Build dynamic columns from visible nodes + const maxCol = useMemo(() => + visibleNodes.reduce((m, n) => Math.max(m, n.display_col ?? 0), 0), [visibleNodes]) + const cols = useMemo(() => { + const c: ModelNode[][] = Array.from({ length: maxCol + 1 }, () => []) + for (const n of visibleNodes) c[n.display_col ?? 0].push(n) + return c + }, [visibleNodes, maxCol]) + + const effectiveColLabels = useMemo(() => { + if (colLabels && colLabels.length >= cols.length) return colLabels + // Fall back: fill with defaults then generic labels + const base = [...DEFAULT_COL_LABELS] + while (base.length <= maxCol) base.push(`Couche ${base.length}`) + return base + }, [colLabels, cols.length, maxCol]) const nodeMap = useMemo(() => { const m = new Map() @@ -575,8 +595,9 @@ function DagView({ nodes, instrument, onEdit, eventDetails }: { for (const node of nodes) { if (!node.formula) continue for (const term of node.formula.split('+')) { - const t = term.trim() - const m1 = t.match(/^([\d.+-]+)\s*\*\s*(\w+)$/) + const t = term.trim() + // Match: optional_sign digit.digit * identifier (e.g. "0.55*ecb_rate" or "0.04*usd_demand") + const m1 = t.match(/^(-?[\d.]+)\s*\*\s*(\w+)$/) if (m1) { result.push({ from: m1[2], to: node.id, coeff: parseFloat(m1[1]) }) } else if (/^\w+$/.test(t)) { @@ -605,17 +626,8 @@ function DagView({ nodes, instrument, onEdit, eventDetails }: { const fromNode = nodeMap.get(conn.from) const fromType = fromNode?.node_type ?? 'input_event' - // Weight to display on edge: - // • input_manual → intermediate: coefficient_to_pips of the source node - // • intermediate → output: formula coefficient (regime-adjusted) - // • input_event → *: nothing (already in pips) - let displayWeight: number | null = null - if (fromType === 'input_manual') { - const c = fromNode?.coefficient_to_pips ?? 1 - if (c !== 0) displayWeight = c - } else if (fromType === 'intermediate' && conn.coeff !== 1.0) { - displayWeight = conn.coeff - } + // Weight to display on edge = formula coefficient (always, if not 1.0) + const displayWeight: number | null = conn.coeff !== 1.0 ? conn.coeff : null edges.push({ x1: fr.right - rect0.left + sL, @@ -642,9 +654,9 @@ function DagView({ nodes, instrument, onEdit, eventDetails }: { }, [recompute]) function edgeColor(e: EdgeDraw): string { - if (e.fromType === 'input_event') return '#38bdf8' if (e.fromType === 'input_manual') return '#a78bfa' - return '#f59e0b' + if (e.fromType === 'intermediate') return '#f59e0b' + return '#38bdf8' } return ( @@ -656,38 +668,32 @@ function DagView({ nodes, instrument, onEdit, eventDetails }: { aria-hidden="true" > {svgEdges.map(e => { - const isHov = hoveredId === e.fromId || hoveredId === e.toId - const isToOutput = e.fromType === 'intermediate' - // Output edges always visible; input edges ghosted unless hovered - const opacity = isHov ? 0.90 : isToOutput ? 0.60 : 0.18 - const color = edgeColor(e) - const sw = isHov ? 2 : isToOutput ? 1.5 : 1 - const dx = Math.min(52, (e.x2 - e.x1) * 0.38) - const curve = `M ${e.x1} ${e.y1} C ${e.x1+dx} ${e.y1}, ${e.x2-dx} ${e.y2}, ${e.x2} ${e.y2}` - const mx = (e.x1 + e.x2) / 2 - const my = (e.y1 + e.y2) / 2 - // Show weight: always on output edges, on hover for input edges - const showWeight = e.displayWeight !== null && (isToOutput || isHov) - const wLabel = e.displayWeight !== null - ? `w:${Math.abs(e.displayWeight) >= 10 ? Math.round(e.displayWeight) : e.displayWeight.toFixed(1)}` + const isHov = hoveredId === e.fromId || hoveredId === e.toId + const isLeafEdge = e.fromType === 'input_manual' + const color = edgeColor(e) + const sw = isHov ? 2.5 : isLeafEdge ? 1 : 1.5 + const opacity = isHov ? 0.95 : isLeafEdge ? 0.25 : 0.65 + const dx = Math.min(60, (e.x2 - e.x1) * 0.35) + const curve = `M ${e.x1} ${e.y1} C ${e.x1+dx} ${e.y1}, ${e.x2-dx} ${e.y2}, ${e.x2} ${e.y2}` + const mx = (e.x1 + e.x2) / 2 + const my = (e.y1 + e.y2) / 2 + const showWeight = e.displayWeight !== null && (!isLeafEdge || isHov) + const wLabel = e.displayWeight !== null + ? (Math.abs(e.displayWeight) >= 10 + ? `${e.displayWeight > 0 ? '' : '−'}${Math.round(Math.abs(e.displayWeight))}` + : `${e.displayWeight.toFixed(2)}`) : '' return ( - {/* Arrowhead */} - {/* Weight label */} {showWeight && ( - + {wLabel} )} @@ -696,14 +702,53 @@ function DagView({ nodes, instrument, onEdit, eventDetails }: { })} - {/* Grille — z:10 */} -
- {COL_LABELS.map((lbl, ci) => ( + {/* Chocs CT (événements cachés du DAG) */} + {hiddenEvents.length > 0 && ( +
+ Chocs CT + {hiddenEvents.map(ev => { + const v = ev.pip_contribution + const hasVal = v !== 0 || ev.baseline_value + return ( +
onEdit(ev)} + title={ev.description} + > + {ev.label} + {hasVal && ( + + {v > 0 ? '+' : ''}{fmt(v)}p + + )} +
+ ) + })} +
+ )} + + {/* Grille dynamique — z:10 */} +
+ {effectiveColLabels.slice(0, cols.length).map((lbl, ci) => (
-
- {lbl} ({cols[ci].length}) +
+ {lbl} ({cols[ci]?.length ?? 0})
- {cols[ci].map(n => ( + {(cols[ci] ?? []).map(n => (
{ if (el) nodeRefs.current.set(n.id, el); else nodeRefs.current.delete(n.id) }} @@ -719,17 +764,14 @@ function DagView({ nodes, instrument, onEdit, eventDetails }: { {/* Légende */}
-
- Events → couches -
- Manuel → couches (w: au survol) + Macro → dérivé (w: survol)
- Couches → output (w: toujours) + Dérivé → agrégat (w: visible)
- Cliquer nœud pour éditer + Cliquer nœud pour éditer · Chocs CT = chocs court terme additifs
@@ -2442,7 +2484,7 @@ export default function InstrumentModels() { {/* Main content */} {state && (
- {view === 'dag' && } + {view === 'dag' && } {view === 'table' && } {view === 'timeline' && } {view === 'calibration' && }