feat: instrument model
This commit is contained in:
@@ -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", []),
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -73,6 +73,7 @@ interface ModelState {
|
||||
output_node: string
|
||||
regime: RegimeInfo
|
||||
event_details: Record<string, EventDetail[]>
|
||||
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<string, EventDetail[]>
|
||||
colLabels?: string[]
|
||||
}) {
|
||||
const wrapperRef = useRef<HTMLDivElement>(null)
|
||||
const nodeRefs = useRef<Map<string, HTMLDivElement>>(new Map())
|
||||
@@ -561,8 +563,26 @@ function DagView({ nodes, instrument, onEdit, eventDetails }: {
|
||||
const [hoveredId, setHoveredId] = useState<string | null>(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<string, ModelNode>()
|
||||
@@ -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 (
|
||||
<g key={e.key} opacity={opacity}>
|
||||
<path d={curve} fill="none" stroke={color} strokeWidth={sw}/>
|
||||
{/* Arrowhead */}
|
||||
<polygon
|
||||
points={`${e.x2},${e.y2} ${e.x2-5},${e.y2-2.5} ${e.x2-5},${e.y2+2.5}`}
|
||||
fill={color} fillOpacity={0.8}
|
||||
fill={color} fillOpacity={0.9}
|
||||
/>
|
||||
{/* Weight label */}
|
||||
{showWeight && (
|
||||
<text
|
||||
x={mx} y={my - 5}
|
||||
fill={color} fontSize="8" fontFamily="monospace"
|
||||
textAnchor="middle" dominantBaseline="middle"
|
||||
style={{ userSelect: 'none' }}
|
||||
>
|
||||
<text x={mx} y={my - 5} fill={color} fontSize="8" fontFamily="monospace"
|
||||
textAnchor="middle" dominantBaseline="middle" style={{ userSelect:'none' }}>
|
||||
{wLabel}
|
||||
</text>
|
||||
)}
|
||||
@@ -696,14 +702,53 @@ function DagView({ nodes, instrument, onEdit, eventDetails }: {
|
||||
})}
|
||||
</svg>
|
||||
|
||||
{/* Grille — z:10 */}
|
||||
<div className="grid grid-cols-4 gap-x-4 min-w-[820px]" style={{ position: 'relative', zIndex: 10 }}>
|
||||
{COL_LABELS.map((lbl, ci) => (
|
||||
{/* Chocs CT (événements cachés du DAG) */}
|
||||
{hiddenEvents.length > 0 && (
|
||||
<div className="flex flex-wrap gap-1.5 mb-3 pb-2 border-b border-slate-700/20">
|
||||
<span className="text-xs text-slate-600 uppercase tracking-wider self-center mr-1">Chocs CT</span>
|
||||
{hiddenEvents.map(ev => {
|
||||
const v = ev.pip_contribution
|
||||
const hasVal = v !== 0 || ev.baseline_value
|
||||
return (
|
||||
<div
|
||||
key={ev.id}
|
||||
className={clsx(
|
||||
'px-2 py-1 rounded border text-xs cursor-pointer transition-colors',
|
||||
hasVal
|
||||
? 'border-sky-700/50 bg-sky-900/20 text-sky-300'
|
||||
: 'border-slate-700/30 bg-dark-800/30 text-slate-600',
|
||||
)}
|
||||
onClick={() => onEdit(ev)}
|
||||
title={ev.description}
|
||||
>
|
||||
<span className="font-medium">{ev.label}</span>
|
||||
{hasVal && (
|
||||
<span className={clsx('ml-1 font-mono font-bold', pipColor(v))}>
|
||||
{v > 0 ? '+' : ''}{fmt(v)}p
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Grille dynamique — z:10 */}
|
||||
<div
|
||||
className="grid gap-x-3"
|
||||
style={{
|
||||
gridTemplateColumns: `repeat(${cols.length}, minmax(130px, 1fr))`,
|
||||
minWidth: `${cols.length * 145}px`,
|
||||
position: 'relative',
|
||||
zIndex: 10,
|
||||
}}
|
||||
>
|
||||
{effectiveColLabels.slice(0, cols.length).map((lbl, ci) => (
|
||||
<div key={ci}>
|
||||
<div className="text-xs font-semibold text-slate-500 uppercase tracking-wider mb-2 pb-1 border-b border-slate-700/30">
|
||||
{lbl} <span className="font-normal normal-case text-slate-600">({cols[ci].length})</span>
|
||||
<div className="text-xs font-semibold text-slate-500 uppercase tracking-wider mb-2 pb-1 border-b border-slate-700/30 truncate">
|
||||
{lbl} <span className="font-normal normal-case text-slate-600">({cols[ci]?.length ?? 0})</span>
|
||||
</div>
|
||||
{cols[ci].map(n => (
|
||||
{(cols[ci] ?? []).map(n => (
|
||||
<div
|
||||
key={n.id}
|
||||
ref={el => { 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 */}
|
||||
<div className="flex items-center gap-3 mt-3 pt-3 border-t border-slate-700/20 flex-wrap text-xs">
|
||||
<div className="flex items-center gap-1 text-sky-400">
|
||||
<span className="w-4 h-px bg-sky-400 inline-block"/> Events → couches
|
||||
</div>
|
||||
<div className="flex items-center gap-1 text-violet-400">
|
||||
<span className="w-4 h-px bg-violet-400 inline-block"/> Manuel → couches <span className="text-slate-600 ml-0.5">(w: au survol)</span>
|
||||
<span className="w-4 h-px bg-violet-400 inline-block"/> Macro → dérivé <span className="text-slate-600 ml-0.5">(w: survol)</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1 text-amber-400">
|
||||
<span className="w-4 h-px bg-amber-400 inline-block"/> Couches → output <span className="text-slate-600 ml-0.5">(w: toujours)</span>
|
||||
<span className="w-4 h-px bg-amber-400 inline-block"/> Dérivé → agrégat <span className="text-slate-600 ml-0.5">(w: visible)</span>
|
||||
</div>
|
||||
<div className="ml-auto flex items-center gap-1 text-slate-500">
|
||||
<Edit3 className="w-3 h-3"/> Cliquer nœud pour éditer
|
||||
<Edit3 className="w-3 h-3"/> Cliquer nœud pour éditer · <span className="text-sky-400">Chocs CT</span> = chocs court terme additifs
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -2442,7 +2484,7 @@ export default function InstrumentModels() {
|
||||
{/* Main content */}
|
||||
{state && (
|
||||
<div className="rounded-xl border border-slate-700/40 bg-dark-800/60 p-4">
|
||||
{view === 'dag' && <DagView nodes={state.nodes} instrument={instrument} onEdit={setEditNode} eventDetails={state.event_details || {}}/>}
|
||||
{view === 'dag' && <DagView nodes={state.nodes} instrument={instrument} onEdit={setEditNode} eventDetails={state.event_details || {}} colLabels={state.col_labels}/>}
|
||||
{view === 'table' && <TableView nodes={state.nodes} onEdit={setEditNode}/>}
|
||||
{view === 'timeline' && <TimelineView instrument={instrument}/>}
|
||||
{view === 'calibration' && <CalibrationView instrument={instrument} eventDetails={state.event_details || {}}/>}
|
||||
|
||||
Reference in New Issue
Block a user