From 7f7c343c9e496b30461255b7dfc519892a7fcf98 Mon Sep 17 00:00:00 2001 From: OpenSquared Date: Thu, 2 Jul 2026 22:34:16 +0200 Subject: [PATCH] feat: instrument model --- backend/routers/instrument_models.py | 39 +- backend/services/instrument_models.py | 1032 +++++++++++--------- frontend/src/pages/InstrumentModels.tsx | 1152 +++++++++++++---------- 3 files changed, 1260 insertions(+), 963 deletions(-) diff --git a/backend/routers/instrument_models.py b/backend/routers/instrument_models.py index 4ebd2e7..b2030ae 100644 --- a/backend/routers/instrument_models.py +++ b/backend/routers/instrument_models.py @@ -15,7 +15,6 @@ class OverrideBody(BaseModel): @router.get("", response_model=List[Dict[str, Any]]) def list_instrument_models(): - """Liste tous les modèles (métadonnées, sans valeurs calculées).""" from services.database import get_conn from services.instrument_models import INSTRUMENT_MODELS conn = get_conn() @@ -27,15 +26,17 @@ def list_instrument_models(): for r in rows: inst = r["instrument"] meta = INSTRUMENT_MODELS.get(inst, {}) - node_counts = {"structural": 0, "event_driven": 0, "output": 0} + counts: dict[str, int] = {} for n in meta.get("nodes", []): - node_counts[n.get("type", "structural")] = node_counts.get(n.get("type", "structural"), 0) + 1 + t = n.get("node_type", "unknown") + counts[t] = counts.get(t, 0) + 1 result.append({ "instrument": inst, "name": meta.get("name", inst), "description": meta.get("description", ""), - "n_structural": node_counts["structural"], - "n_event_driven": node_counts["event_driven"], + "n_input_event": counts.get("input_event", 0), + "n_input_manual": counts.get("input_manual", 0), + "n_intermediate": counts.get("intermediate", 0), "updated_at": r["updated_at"], }) return result @@ -43,12 +44,30 @@ def list_instrument_models(): conn.close() +@router.get("/{instrument}/timeline") +def get_instrument_timeline( + instrument: str, + period: str = Query("1y", description="5d|1mo|3mo|6mo|1y|2y"), +) -> List[Dict[str, Any]]: + """Simulation jour par jour de tous les nœuds du modèle sur la période.""" + from services.database import get_conn + from services.instrument_models import simulate_timeline + conn = get_conn() + try: + data = simulate_timeline(conn, instrument.upper(), period) + if not data: + raise HTTPException(status_code=404, detail=f"Modèle introuvable pour {instrument.upper()}") + return data + finally: + conn.close() + + @router.get("/{instrument}") def get_instrument_model( instrument: str, at_date: Optional[str] = Query(None, description="YYYY-MM-DD (défaut: aujourd'hui)"), ) -> Dict[str, Any]: - """Graphe complet avec valeurs courantes des nœuds.""" + """Graphe complet avec valeurs courantes des nœuds (toutes couches).""" from services.database import get_conn from services.instrument_models import get_model_state conn = get_conn() @@ -63,7 +82,7 @@ def get_instrument_model( @router.put("/{instrument}/nodes/{node_id}/override") def set_override(instrument: str, node_id: str, body: OverrideBody) -> Dict[str, Any]: - """Définit ou met à jour la valeur manuelle d'un nœud structurel.""" + """Définit ou met à jour la valeur manuelle d'un nœud.""" from services.database import get_conn from services.instrument_models import set_node_override conn = get_conn() @@ -76,7 +95,7 @@ def set_override(instrument: str, node_id: str, body: OverrideBody) -> Dict[str, @router.delete("/{instrument}/nodes/{node_id}/override") def clear_override(instrument: str, node_id: str) -> Dict[str, Any]: - """Supprime l'override manuel d'un nœud (retour à valeur neutre / events).""" + """Supprime l'override manuel d'un nœud (retour neutre/events).""" from services.database import get_conn from services.instrument_models import clear_node_override conn = get_conn() @@ -87,8 +106,8 @@ def clear_override(instrument: str, node_id: str) -> Dict[str, Any]: conn.close() -@router.get("/{instrument}/nodes/{node_id}/overrides") -def get_node_overrides(instrument: str) -> List[Dict[str, Any]]: +@router.get("/{instrument}/nodes/overrides") +def get_all_overrides(instrument: str) -> List[Dict[str, Any]]: """Toutes les overrides manuelles pour un instrument.""" from services.database import get_conn conn = get_conn() diff --git a/backend/services/instrument_models.py b/backend/services/instrument_models.py index 9a95308..77f721d 100644 --- a/backend/services/instrument_models.py +++ b/backend/services/instrument_models.py @@ -1,21 +1,23 @@ """ -Instrument Models — graphe causal exhaustif par instrument. +Instrument Models — Phase 1 : propagation en chaîne réelle. -Deux types de nœuds : - - structural : facteurs éditables manuellement (carry, taux réels, croissance…) - - event_driven: pressions auto depuis causal_event_analyses, groupées par catégorie template - - output : nœud résultat (somme en pips) +Architecture DAG (3 couches) : + Layer 0 — Inputs : + - input_event : valeur auto depuis causal_event_analyses (par catégorie template) + - input_manual : valeur utilisateur (unité native → pips via coefficient_to_pips) + Layer 1 — Intermediate : formules agrégeant les inputs par domaine + Layer 2 — Output : formule sommant les nœuds intermédiaires -Valeur d'un nœud structural = override utilisateur × coefficient → pips -Valeur d'un nœud event_driven = sum(pips × decay) depuis les analyses actives -Output = sum(tous les nœuds en pips) +Évaluation : evaluate_graph() de causal_graphs.py (formula-based DAG). +Lifecycle : montée (rise_days) → plateau → décroissance (absorption_days). +Timeline : simulation jour par jour via simulate_timeline(). """ import json import math from datetime import datetime, timedelta, date as date_type from typing import Optional -# ── Helpers ──────────────────────────────────────────────────────────────────── +# ── Lifecycle & decay ────────────────────────────────────────────────────────── def _decay(days: int, absorption: int, dtype: str) -> float: if days < 0: return 0.0 @@ -25,425 +27,398 @@ def _decay(days: int, absorption: int, dtype: str) -> float: return math.exp(-lam * days) -# ── Comprehensive node definitions ───────────────────────────────────────────── -# coefficient (structural only) : native_unit × coefficient = pips impact +def _lifecycle(days: int, rise: int, plateau: int, absorption: int, dtype: str) -> float: + """Montée linéaire → plateau → décroissance (exp/linear/step).""" + if days < 0: return 0.0 + if days < rise: return days / max(rise, 1) + if days < rise + plateau: return 1.0 + return _decay(days - rise - plateau, absorption, dtype) + + +# ── Category labels ──────────────────────────────────────────────────────────── +CAT_LABELS: dict[str, str] = { + "monetary":"Monétaire","inflation":"Inflation","macro":"Macro/Croissance", + "political":"Politique","flows":"Flux & Réserves","positioning":"Positionnement", + "sentiment":"Sentiment & Risque","credit":"Crédit","earnings":"Bénéfices", + "valuation":"Valorisation","tech":"Technologie","commodity":"Commodités", + "supply":"Offre","output":"Résultat","intermediate":"Couche intermédiaire", + # event categories + "central_bank":"Banques Centrales","monetary_shock":"Surprise Macro", + "geopolitical":"Géopolitique","trade_policy":"Commerce / Tarifs", + "growth_shock":"Choc Croissance","credit_stress":"Stress Crédit", + "technical":"Technique","sentiment":"Sentiment", + "positioning":"Positionnement","unclassified":"Non Classifié", +} + +# ── Helper to build formula from list of node IDs ────────────────────────────── +def _sum_formula(*node_ids: str) -> str: + return " + ".join(node_ids) + + +# ── Built-in instrument model definitions ────────────────────────────────────── +# Each model defines: +# nodes : list of node dicts +# output_node : id of the output node +# +# Node schema: +# id, label, node_type (input_event|input_manual|intermediate|output), +# category, unit (for manual), coefficient_to_pips (for manual), +# event_category (for event nodes), formula (for intermediate/output), +# description, display_col (0=inputs-event, 1=inputs-manual, 2=intermediate, 3=output) +# +# All intermediate/output values are in pips. +# For input_manual : value_in_native_unit * coefficient_to_pips = pips injected into graph. INSTRUMENT_MODELS: dict[str, dict] = { -# ═══════════════════════════════════════════════════════════════════════════════ +# ══════════════════════════════════════════════════════════════════════════════ "EURUSD": { - "name": "EUR/USD", "output_node": "eurusd", - "description": "Taux de change Euro / Dollar — pair G10 la plus liquide au monde", + "name": "EUR/USD", + "description": "Taux de change Euro/Dollar — modèle causal 3 couches avec 4 domaines d'influence", + "output_node": "eurusd", "nodes": [ - # ── Monétaire ────────────────────────────────────────────────────────────── - {"id":"rate_diff_ois_2y","label":"Spread OIS 2Y USD-EUR","type":"structural","category":"monetary","unit":"bps","coefficient":1.5, - "description":"Différentiel taux swap OIS 2 ans USD vs EUR. Principal déterminant court terme. Positif = USD plus rémunérateur → EUR/USD ↓"}, - {"id":"fed_path_12m","label":"Anticipation Fed 12m","type":"structural","category":"monetary","unit":"bps","coefficient":0.8, - "description":"Variation cumulée taux Fed attendue sur 12m (négatif = cuts → USD ↓ → EUR/USD ↑)"}, - {"id":"ecb_path_12m","label":"Anticipation BCE 12m","type":"structural","category":"monetary","unit":"bps","coefficient":-0.8, - "description":"Variation cumulée taux BCE attendue sur 12m (négatif = cuts → EUR ↓ → EUR/USD ↓)"}, - {"id":"us_real_rate","label":"Taux réel US 10Y (TIPS)","type":"structural","category":"monetary","unit":"bps","coefficient":-0.5, - "description":"Rendement TIPS 10Y. Hausse = USD attractif pour capitaux → EUR/USD ↓"}, - {"id":"eu_real_rate","label":"Taux réel EU 10Y","type":"structural","category":"monetary","unit":"bps","coefficient":0.5, - "description":"Taux réel zone euro 10Y. Hausse = EUR attractif → EUR/USD ↑"}, - {"id":"carry_attractiveness","label":"Attractivité carry EUR/USD","type":"structural","category":"monetary","unit":"score","coefficient":0.6, - "description":"Score synthétique carry trade EUR vs USD (+= EUR avantageux, -=USD avantageux)"}, - # ── Macro ────────────────────────────────────────────────────────────────── - {"id":"us_growth_advantage","label":"Avantage croissance US/EU","type":"structural","category":"macro","unit":"pts","coefficient":-15.0, - "description":"Différentiel PIB US - EU (annualisé). US surperformance → USD fort → pair ↓"}, - {"id":"us_labor_market","label":"Marché emploi US","type":"structural","category":"macro","unit":"score","coefficient":-0.4, - "description":"Score santé marché emploi US (NFP, chômage, salaires). Fort = USD ↑"}, - {"id":"eu_pmi_composite","label":"PMI composite Eurozone","type":"structural","category":"macro","unit":"pts","coefficient":0.3, - "description":"PMI composite zone euro (>50 = expansion). Hausse = EUR ↑"}, - # ── Inflation ────────────────────────────────────────────────────────────── - {"id":"us_cpi_yoy","label":"CPI US YoY","type":"structural","category":"inflation","unit":"%","coefficient":-0.8, - "description":"Inflation américaine. Hausse surprise → Fed plus hawkish → USD ↑ → pair ↓"}, - {"id":"eu_cpi_yoy","label":"HICP Eurozone YoY","type":"structural","category":"inflation","unit":"%","coefficient":0.8, - "description":"Inflation zone euro. Hausse surprise → BCE plus hawkish → EUR ↑"}, - # ── Géopolitique & Politique ──────────────────────────────────────────────── - {"id":"eu_fragmentation_risk","label":"Risque fragmentation UE","type":"structural","category":"political","unit":"score","coefficient":-0.5, - "description":"Risque politique/fragmentation zone euro (0=stable, 100=crise). Hausse → EUR ↓"}, - {"id":"us_political_risk","label":"Incertitude politique US","type":"structural","category":"political","unit":"score","coefficient":0.3, - "description":"Incertitude politique américaine (debt ceiling, shutdown, élections). Hausse → USD ↓"}, - {"id":"energy_price_impact","label":"Prix énergie (termes échanges EU)","type":"structural","category":"macro","unit":"$/bbl","coefficient":-0.25, - "description":"Prix pétrole/gaz. Hausse élargit déficit commercial EU → EUR ↓ structurellement"}, - # ── Sentiment & Risque ───────────────────────────────────────────────────── - {"id":"risk_appetite","label":"Appétit risque mondial","type":"structural","category":"sentiment","unit":"score","coefficient":0.5, - "description":"Score risk-on/off global (+100=risk-on max). Risk-on → sorties USD → EUR/USD ↑"}, - {"id":"vix_level","label":"Niveau VIX","type":"structural","category":"sentiment","unit":"pts","coefficient":-0.4, - "description":"VIX. Spike → flight to USD safe haven → EUR/USD ↓"}, - # ── Positionnement & Flux ───────────────────────────────────────────────── - {"id":"cftc_eur_net","label":"Positions nettes EUR (CoT)","type":"structural","category":"positioning","unit":"k contrats","coefficient":0.1, - "description":"Positions spéculatives nettes EUR sur CME (CoT). Long extrême → risque retournement."}, - {"id":"dollar_reserve_demand","label":"Demande réserves USD","type":"structural","category":"flows","unit":"score","coefficient":-0.4, - "description":"Demande globale de réserves en USD (score). Fort = USD structurellement demandé → pair ↓"}, - # ── Event-driven (auto depuis causal_event_analyses) ──────────────────────── - {"id":"ev_central_bank","label":"Pression Banques Centrales","type":"event_driven","category":"central_bank","unit":"pips", - "description":"Contributions cumulées des événements banques centrales actifs (décisions, minutes, guidance). Décroissance exp."}, - {"id":"ev_macro_surprise","label":"Surprise Données Macro","type":"event_driven","category":"monetary_shock","unit":"pips", - "description":"Surprises publications économiques (CPI, NFP, PIB, PMI, ventes détail). Décroissance rapide ~12j."}, - {"id":"ev_geopolitical","label":"Risque Géopolitique","type":"event_driven","category":"geopolitical","unit":"pips", - "description":"Chocs géopolitiques actifs (conflits, sanctions, tensions). Décroissance linéaire ~21j."}, - {"id":"ev_trade_policy","label":"Choc Commercial / Tarifs","type":"event_driven","category":"trade_policy","unit":"pips", - "description":"Annonces commerciales (tarifs, accords, menaces). Décroissance lente."}, - {"id":"ev_growth_shock","label":"Choc de Croissance","type":"event_driven","category":"growth_shock","unit":"pips", - "description":"Chocs perspectives croissance (récession, rebond inattendu)."}, - {"id":"ev_commodity","label":"Choc Commodités","type":"event_driven","category":"commodity","unit":"pips", - "description":"Chocs matières premières (pétrole, gaz, métaux) impactant USD ou EUR."}, - {"id":"ev_credit_stress","label":"Stress Crédit / Liquidité","type":"event_driven","category":"credit_stress","unit":"pips", - "description":"Événements stress crédit/liquidité (banking stress, spreads IG/HY)."}, - {"id":"ev_sentiment","label":"Sentiment & Positionnement","type":"event_driven","category":"sentiment","unit":"pips", - "description":"Changements sentiment et flux positionnement institutionnel."}, - {"id":"ev_technical","label":"Momentum Technique","type":"event_driven","category":"technical","unit":"pips", - "description":"Signaux techniques (cassures, croisements MA, niveaux clés)."}, - # ── Output ───────────────────────────────────────────────────────────────── - {"id":"eurusd","label":"EUR/USD Impact Net","type":"output","category":"output","unit":"pips", - "description":"Pression nette = Σ(structurels × coefficient) + Σ(event-driven en pips)"}, + # ── Layer 0a : event inputs ─────────────────────────────────────────────── + {"id":"in_cb", "label":"Banques Centrales", "node_type":"input_event", "category":"central_bank", "unit":"pips","display_col":0,"description":"Décisions Fed/BCE, minutes, forward guidance. Décroissance exp ~14j.","event_category":"central_bank"}, + {"id":"in_macro", "label":"Surprises Macro (données)","node_type":"input_event","category":"monetary_shock","unit":"pips","display_col":0,"description":"CPI, NFP, PIB, PMI US/EU. Décroissance rapide ~12j.","event_category":"monetary_shock"}, + {"id":"in_geo", "label":"Risque Géopolitique", "node_type":"input_event", "category":"geopolitical", "unit":"pips","display_col":0,"description":"Conflits, sanctions, tensions. Décroissance linéaire ~21j.","event_category":"geopolitical"}, + {"id":"in_trade", "label":"Choc Commercial/Tarifs", "node_type":"input_event", "category":"trade_policy", "unit":"pips","display_col":0,"description":"Tarifs US-UE, accords commerciaux.","event_category":"trade_policy"}, + {"id":"in_growth", "label":"Choc Croissance", "node_type":"input_event", "category":"growth_shock", "unit":"pips","display_col":0,"description":"Chocs perspectives croissance.","event_category":"growth_shock"}, + {"id":"in_credit", "label":"Stress Crédit/Liquidité", "node_type":"input_event", "category":"credit_stress", "unit":"pips","display_col":0,"description":"Banking stress, spreads IG/HY.","event_category":"credit_stress"}, + {"id":"in_technical", "label":"Momentum Technique", "node_type":"input_event", "category":"technical", "unit":"pips","display_col":0,"description":"Cassures niveaux clés, tendances.","event_category":"technical"}, + {"id":"in_commodity", "label":"Choc Commodités", "node_type":"input_event", "category":"commodity", "unit":"pips","display_col":0,"description":"Pétrole, gaz, métaux → inflation EU.","event_category":"commodity"}, + {"id":"in_sentiment", "label":"Sentiment/Positionnement","node_type":"input_event", "category":"sentiment", "unit":"pips","display_col":0,"description":"Flux institutionnels, risk-on/off.","event_category":"sentiment"}, + # ── Layer 0b : manual structural inputs ────────────────────────────────── + {"id":"m_rate_diff_2y", "label":"Spread OIS 2Y USD-EUR", "node_type":"input_manual","category":"monetary", "unit":"bps", "coefficient_to_pips": 1.5, "display_col":1,"description":"Principal driver court terme. Positif = USD plus rémunérateur → pair ↓"}, + {"id":"m_fed_path", "label":"Anticipation Fed 12m", "node_type":"input_manual","category":"monetary", "unit":"bps", "coefficient_to_pips": 0.8, "display_col":1,"description":"Cuts attendus → USD ↓ → pair ↑. Hikes → USD ↑ → pair ↓"}, + {"id":"m_ecb_path", "label":"Anticipation BCE 12m", "node_type":"input_manual","category":"monetary", "unit":"bps", "coefficient_to_pips":-0.8, "display_col":1,"description":"Cuts BCE → EUR ↓. Hikes → EUR ↑"}, + {"id":"m_us_real_rate", "label":"Taux réel US 10Y (TIPS)", "node_type":"input_manual","category":"monetary", "unit":"bps", "coefficient_to_pips":-0.5, "display_col":1,"description":"Hausse → USD attractif → pair ↓"}, + {"id":"m_eu_real_rate", "label":"Taux réel EU 10Y", "node_type":"input_manual","category":"monetary", "unit":"bps", "coefficient_to_pips": 0.5, "display_col":1,"description":"Hausse → EUR attractif → pair ↑"}, + {"id":"m_carry", "label":"Score carry EUR/USD", "node_type":"input_manual","category":"monetary", "unit":"score", "coefficient_to_pips": 0.6, "display_col":1,"description":"Score attractivité carry (+= EUR avantageux)"}, + {"id":"m_us_growth_adv", "label":"Avantage croissance US vs EU","node_type":"input_manual","category":"macro", "unit":"pts%", "coefficient_to_pips":-15.0,"display_col":1,"description":"Différentiel PIB US-EU. US surperform → USD ↑ → pair ↓"}, + {"id":"m_eu_pmi", "label":"PMI composite Eurozone", "node_type":"input_manual","category":"macro", "unit":"pts", "coefficient_to_pips": 0.3, "display_col":1,"description":"Expansion EU → EUR ↑"}, + {"id":"m_energy_price", "label":"Prix énergie ($/bbl delta)", "node_type":"input_manual","category":"macro", "unit":"$/bbl", "coefficient_to_pips":-0.25,"display_col":1,"description":"Énergie chère → déficit commercial EU → EUR ↓"}, + {"id":"m_us_cpi", "label":"CPI US YoY", "node_type":"input_manual","category":"inflation", "unit":"%", "coefficient_to_pips":-0.8, "display_col":1,"description":"Inflation US → Fed hawkish → USD ↑ → pair ↓"}, + {"id":"m_eu_cpi", "label":"HICP Eurozone YoY", "node_type":"input_manual","category":"inflation", "unit":"%", "coefficient_to_pips": 0.8, "display_col":1,"description":"Inflation EU → BCE hawkish → EUR ↑"}, + {"id":"m_risk_appetite", "label":"Appétit risque mondial", "node_type":"input_manual","category":"sentiment", "unit":"score", "coefficient_to_pips": 0.5, "display_col":1,"description":"Risk-on → sorties USD → pair ↑"}, + {"id":"m_vix", "label":"Niveau VIX", "node_type":"input_manual","category":"sentiment", "unit":"pts", "coefficient_to_pips":-0.4, "display_col":1,"description":"VIX spike → flight to USD → pair ↓"}, + {"id":"m_eu_fragmentation","label":"Risque fragmentation EU", "node_type":"input_manual","category":"political", "unit":"score", "coefficient_to_pips":-0.5, "display_col":1,"description":"Risque politique EU → EUR ↓"}, + {"id":"m_us_political", "label":"Incertitude politique US", "node_type":"input_manual","category":"political", "unit":"score", "coefficient_to_pips": 0.3, "display_col":1,"description":"Incertitude US → USD ↓ → pair ↑"}, + {"id":"m_cftc_eur", "label":"Positions nettes EUR (CoT)", "node_type":"input_manual","category":"positioning","unit":"k lots","coefficient_to_pips": 0.1, "display_col":1,"description":"Net long EUR CFTC. Extrême → risque retournement"}, + {"id":"m_dollar_reserve", "label":"Demande réserves USD", "node_type":"input_manual","category":"flows", "unit":"score", "coefficient_to_pips":-0.4, "display_col":1,"description":"Demande réserves → USD structurellement fort → pair ↓"}, + # ── Layer 1 : intermediate (4 domaines) ────────────────────────────────── + {"id":"layer_monetary", "label":"▶ Pression Monétaire & Taux", "node_type":"intermediate","category":"intermediate","unit":"pips","display_col":2, + "formula":"in_cb + in_macro + m_rate_diff_2y + m_fed_path + m_ecb_path + m_us_real_rate + m_eu_real_rate + m_carry", + "description":"Domaine monétaire : taux directeurs, OIS, anticipations Fed/BCE, carry. Driver n°1 de l'EURUSD."}, + {"id":"layer_growth", "label":"▶ Pression Macro & Croissance", "node_type":"intermediate","category":"intermediate","unit":"pips","display_col":2, + "formula":"in_growth + in_commodity + m_us_growth_adv + m_eu_pmi + m_energy_price + m_us_cpi + m_eu_cpi", + "description":"Domaine macro : croissance relative, inflation, énergie. Impact via anticipations BC."}, + {"id":"layer_risk", "label":"▶ Pression Risque & Géopolitique", "node_type":"intermediate","category":"intermediate","unit":"pips","display_col":2, + "formula":"in_geo + in_credit + in_sentiment + m_risk_appetite + m_vix + m_eu_fragmentation", + "description":"Domaine risque : géopolitique, stress crédit, aversion au risque (USD safe haven)."}, + {"id":"layer_positioning","label":"▶ Pression Positionnement & Flux", "node_type":"intermediate","category":"intermediate","unit":"pips","display_col":2, + "formula":"in_trade + in_technical + m_cftc_eur + m_dollar_reserve + m_us_political", + "description":"Domaine positionnement : flux spéculatifs, technicalités, réserves, politique."}, + # ── Layer 2 : output ────────────────────────────────────────────────────── + {"id":"eurusd","label":"EUR/USD — Impact Net","node_type":"output","category":"output","unit":"pips","display_col":3, + "formula":"layer_monetary + layer_growth + layer_risk + layer_positioning", + "description":"Pression nette cumulée = Σ(4 domaines). Positif = biais haussier EUR/USD."}, ] }, -# ═══════════════════════════════════════════════════════════════════════════════ +# ══════════════════════════════════════════════════════════════════════════════ "USDJPY": { "name": "USD/JPY", "output_node": "usdjpy", - "description": "Taux de change Dollar / Yen — pair carry & safe haven par excellence", + "description": "Carry & safe haven — yield diff 10Y + BoJ + risk appetite", "nodes": [ - {"id":"yield_diff_10y","label":"Différentiel rendement 10Y US-JP","type":"structural","category":"monetary","unit":"bps","coefficient":1.2, - "description":"Spread rendement UST10Y - JGB10Y. Principal moteur USD/JPY. Hausse = USD/JPY ↑"}, - {"id":"fed_path_12m","label":"Anticipation Fed 12m","type":"structural","category":"monetary","unit":"bps","coefficient":0.6, - "description":"Variation cumulée taux Fed 12m. Hausse = USD ↑ → USD/JPY ↑"}, - {"id":"boj_policy_stance","label":"Biais BoJ (hawkish/dovish)","type":"structural","category":"monetary","unit":"score","coefficient":-8.0, - "description":"Score posture BoJ (+= hawkish). Hawkish BoJ → JPY apprécie → USD/JPY ↓"}, - {"id":"jgb_yield_10y","label":"Rendement JGB 10Y","type":"structural","category":"monetary","unit":"bps","coefficient":-0.8, - "description":"Rendement JGB 10Y. Hausse = JPY attractif → USD/JPY ↓"}, - {"id":"us_real_rate","label":"Taux réel US 10Y","type":"structural","category":"monetary","unit":"bps","coefficient":0.6, - "description":"TIPS 10Y. Hausse = USD attractif vs actifs risqués → USD/JPY ↑"}, - {"id":"risk_appetite","label":"Appétit risque mondial","type":"structural","category":"sentiment","unit":"score","coefficient":-1.2, - "description":"Score risk-on. Risk-off → fuite vers JPY safe haven → USD/JPY ↓"}, - {"id":"vix_level","label":"Niveau VIX","type":"structural","category":"sentiment","unit":"pts","coefficient":-0.8, - "description":"VIX. Spike → demande JPY refuge → USD/JPY ↓"}, - {"id":"carry_trade_momentum","label":"Momentum carry USD/JPY","type":"structural","category":"positioning","unit":"score","coefficient":0.4, - "description":"Force du carry trade. Positif = flux acheteurs USD/JPY (emprunter JPY, investir USD)."}, - {"id":"cftc_jpy_net_short","label":"Positions nettes JPY short (CoT)","type":"structural","category":"positioning","unit":"k contrats","coefficient":0.15, - "description":"Positions short JPY spéculatifs CoT CFTC. Extrême = risque short squeeze → USD/JPY ↓ brutal."}, - {"id":"mof_intervention_risk","label":"Risque intervention MoF","type":"structural","category":"political","unit":"score","coefficient":-0.8, - "description":"Probabilité intervention verbale/physique du Trésor japonais (0=aucune, 100=imminente). Hausse → USD/JPY ↓ préventif."}, - {"id":"japan_current_account","label":"Balance courante Japon","type":"structural","category":"flows","unit":"Mds¥","coefficient":-0.05, - "description":"Excédent courant japonais. Large surplus = rapatriements YEN → USD/JPY ↓ structurel."}, - {"id":"us_japan_trade_tension","label":"Tensions commerciales US-Japon","type":"structural","category":"political","unit":"score","coefficient":0.3, - "description":"Tensions bilatérales US-Japon (tarifs, pressions). Hausse → USD/JPY incertain."}, - {"id":"ev_central_bank","label":"Pression Banques Centrales","type":"event_driven","category":"central_bank","unit":"pips","description":"Fed, BoJ decisions/minutes actifs."}, - {"id":"ev_macro_surprise","label":"Surprise Macro","type":"event_driven","category":"monetary_shock","unit":"pips","description":"NFP, CPI US, Tankan, CPI Japon surprises."}, - {"id":"ev_geopolitical","label":"Risque Géopolitique","type":"event_driven","category":"geopolitical","unit":"pips","description":"Tensions NK, conflits régionaux → JPY safe haven."}, - {"id":"ev_trade_policy","label":"Choc Commercial","type":"event_driven","category":"trade_policy","unit":"pips","description":"Tarifs US-Japon, accords commerciaux."}, - {"id":"ev_growth_shock","label":"Choc Croissance","type":"event_driven","category":"growth_shock","unit":"pips","description":"Chocs PIB/récession → risk-off → JPY."}, - {"id":"ev_credit_stress","label":"Stress Crédit","type":"event_driven","category":"credit_stress","unit":"pips","description":"Stress bancaire → flight to JPY."}, - {"id":"ev_sentiment","label":"Sentiment & Flux","type":"event_driven","category":"sentiment","unit":"pips","description":"Positionnement spéculatif, flux risk-on/off."}, - {"id":"ev_technical","label":"Momentum Technique","type":"event_driven","category":"technical","unit":"pips","description":"Niveaux clés, tendances USD/JPY."}, - {"id":"usdjpy","label":"USD/JPY Impact Net","type":"output","category":"output","unit":"pips","description":"Pression nette cumulée USD/JPY"}, + {"id":"in_cb", "label":"Banques Centrales", "node_type":"input_event","category":"central_bank", "unit":"pips","display_col":0,"event_category":"central_bank","description":"Fed/BoJ décisions."}, + {"id":"in_macro", "label":"Surprises Macro", "node_type":"input_event","category":"monetary_shock","unit":"pips","display_col":0,"event_category":"monetary_shock","description":"NFP, CPI US, Tankan, CPI Japon."}, + {"id":"in_geo", "label":"Risque Géopolitique", "node_type":"input_event","category":"geopolitical", "unit":"pips","display_col":0,"event_category":"geopolitical","description":"NK tensions → JPY safe haven demandé."}, + {"id":"in_trade", "label":"Choc Commercial", "node_type":"input_event","category":"trade_policy", "unit":"pips","display_col":0,"event_category":"trade_policy","description":"Tarifs US-Japon."}, + {"id":"in_growth", "label":"Choc Croissance", "node_type":"input_event","category":"growth_shock", "unit":"pips","display_col":0,"event_category":"growth_shock","description":"Récession → risk-off → JPY."}, + {"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 → JPY refuge."}, + {"id":"in_sentiment", "label":"Sentiment/Flux", "node_type":"input_event","category":"sentiment", "unit":"pips","display_col":0,"event_category":"sentiment","description":"Risk-on/off."}, + {"id":"in_technical", "label":"Momentum Technique", "node_type":"input_event","category":"technical", "unit":"pips","display_col":0,"event_category":"technical","description":"Niveaux clés USD/JPY."}, + {"id":"m_yield_diff", "label":"Diff. rendement 10Y US-JP","node_type":"input_manual","category":"monetary","unit":"bps","coefficient_to_pips": 1.2,"display_col":1,"description":"Principal driver. Hausse → USD/JPY ↑"}, + {"id":"m_fed_path", "label":"Anticipation Fed 12m", "node_type":"input_manual","category":"monetary","unit":"bps","coefficient_to_pips": 0.6,"display_col":1,"description":"Fed hike → USD/JPY ↑"}, + {"id":"m_boj_stance", "label":"Biais BoJ (score)", "node_type":"input_manual","category":"monetary","unit":"score","coefficient_to_pips":-8.0,"display_col":1,"description":"Hawkish → JPY ↑ → USD/JPY ↓"}, + {"id":"m_jgb_10y", "label":"Rendement JGB 10Y", "node_type":"input_manual","category":"monetary","unit":"bps","coefficient_to_pips":-0.8,"display_col":1,"description":"JGB yield ↑ → JPY attractif → paire ↓"}, + {"id":"m_us_real_rate","label":"Taux réel US 10Y", "node_type":"input_manual","category":"monetary","unit":"bps","coefficient_to_pips": 0.6,"display_col":1,"description":"Taux réel US ↑ → USD ↑ vs JPY"}, + {"id":"m_risk_appetite","label":"Appétit risque", "node_type":"input_manual","category":"sentiment","unit":"score","coefficient_to_pips":-1.2,"display_col":1,"description":"Risk-off → JPY safe haven → paire ↓"}, + {"id":"m_vix", "label":"Niveau VIX", "node_type":"input_manual","category":"sentiment","unit":"pts","coefficient_to_pips":-0.8,"display_col":1,"description":"VIX spike → JPY refuge → paire ↓"}, + {"id":"m_carry_momentum","label":"Momentum carry", "node_type":"input_manual","category":"positioning","unit":"score","coefficient_to_pips": 0.4,"display_col":1,"description":"Carry actif → acheteurs USD/JPY"}, + {"id":"m_mof_risk", "label":"Risque intervention MoF","node_type":"input_manual","category":"political","unit":"score","coefficient_to_pips":-0.8,"display_col":1,"description":"Probabilité intervention MoF. Hausse → paire ↓ préventif"}, + {"id":"m_cftc_jpy", "label":"Net short JPY (CoT)", "node_type":"input_manual","category":"positioning","unit":"k lots","coefficient_to_pips": 0.15,"display_col":1,"description":"Extreme short JPY → risque short squeeze → paire ↓"}, + {"id":"layer_rates", "label":"▶ Différentiel Taux/Carry","node_type":"intermediate","category":"intermediate","unit":"pips","display_col":2, + "formula":"in_cb + in_macro + m_yield_diff + m_fed_path + m_boj_stance + m_jgb_10y + m_us_real_rate + m_carry_momentum", + "description":"Yield differential, Fed vs BoJ, carry trade momentum."}, + {"id":"layer_risk", "label":"▶ Risque & Safe Haven JPY","node_type":"intermediate","category":"intermediate","unit":"pips","display_col":2, + "formula":"in_geo + in_credit + in_sentiment + m_risk_appetite + m_vix", + "description":"JPY comme refuge : risk-off, géopolitique, stress crédit."}, + {"id":"layer_policy", "label":"▶ Politique & Positionnement","node_type":"intermediate","category":"intermediate","unit":"pips","display_col":2, + "formula":"in_trade + in_technical + in_growth + m_mof_risk + m_cftc_jpy", + "description":"Intervention MoF, tarifs US-Japon, technique, CFTC."}, + {"id":"usdjpy","label":"USD/JPY — Impact Net","node_type":"output","category":"output","unit":"pips","display_col":3, + "formula":"layer_rates + layer_risk + layer_policy","description":"Pression nette cumulée USD/JPY"}, ] }, -# ═══════════════════════════════════════════════════════════════════════════════ +# ══════════════════════════════════════════════════════════════════════════════ "XAUUSD": { "name": "XAU/USD (Or)", "output_node": "xauusd", - "description": "Or vs Dollar — actif refuge, couverture inflation et géopolitique", + "description": "Or/Dollar — taux réels, dollar, géopolitique, banques centrales", "nodes": [ - {"id":"us_real_rate_10y","label":"Taux réel US 10Y (TIPS)","type":"structural","category":"monetary","unit":"bps","coefficient":-2.5, - "description":"PLUS IMPORTANT pour l'or. Taux réel US négatif/en baisse → or ↑. Chaque -10bps ≈ +25 pips or."}, - {"id":"dxy_level","label":"Indice Dollar (DXY)","type":"structural","category":"monetary","unit":"pts","coefficient":-3.0, - "description":"Niveau DXY. Or libellé en USD → DXY ↑ = or ↓ mécaniquement (corrélation ~-0.75)."}, - {"id":"inflation_breakeven_10y","label":"Breakeven inflation 10Y US","type":"structural","category":"inflation","unit":"bps","coefficient":1.5, - "description":"Anticipations inflation 10Y US (TIPS). Hausse = valeur réelle USD ↓ → or demandé comme couverture."}, - {"id":"fed_path_12m","label":"Anticipation Fed 12m","type":"structural","category":"monetary","unit":"bps","coefficient":-1.0, - "description":"Variation taux Fed attendue. Cuts attendus → taux réels ↓ → or ↑."}, - {"id":"global_uncertainty_score","label":"Indice incertitude mondiale","type":"structural","category":"sentiment","unit":"score","coefficient":0.3, - "description":"Score incertitude géopolitique/économique globale. Hausse → demande refuge or."}, - {"id":"vix_level","label":"Niveau VIX","type":"structural","category":"sentiment","unit":"pts","coefficient":0.4, - "description":"VIX. Spike → or comme couverture risque extrême."}, - {"id":"central_bank_gold_buying","label":"Achats CB (or)","type":"structural","category":"flows","unit":"tonnes/mois","coefficient":2.0, - "description":"Achats nets d'or par les banques centrales mondiales. Driver structurel majeur depuis 2022."}, - {"id":"etf_gold_flows","label":"Flux ETF or (GLD, IAU)","type":"structural","category":"flows","unit":"tonnes","coefficient":1.5, - "description":"Flux net dans les ETF adossés à l'or. Entrées → demande physique → or ↑."}, - {"id":"cftc_gold_net_long","label":"Positions nettes or (CoT)","type":"structural","category":"positioning","unit":"k contrats","coefficient":0.15, - "description":"Positions spéculatives nettes or COMEX. Extrême long → risque liquidation → or ↓."}, - {"id":"us_fiscal_deficit","label":"Déficit fiscal US (%PIB)","type":"structural","category":"macro","unit":"%","coefficient":0.5, - "description":"Inquiétudes sur la trajectoire fiscale US → doutes sur USD → or refuge."}, - {"id":"gold_mine_cost","label":"Coût d'extraction (AISC)","type":"structural","category":"supply","unit":"$/oz","coefficient":0.05, - "description":"All-in sustaining cost mines d'or. Support fondamental pour le prix."}, - {"id":"india_china_demand","label":"Demande physique Inde/Chine","type":"structural","category":"flows","unit":"score","coefficient":0.4, - "description":"Demande physique joaillerie/investment Inde et Chine. Saisonnalité (Diwali, Nouvel An chinois)."}, - {"id":"ev_central_bank","label":"Pression Banques Centrales","type":"event_driven","category":"central_bank","unit":"pips","description":"Décisions Fed/BoE/BCE → impact taux réels → or."}, - {"id":"ev_macro_surprise","label":"Surprise Macro","type":"event_driven","category":"monetary_shock","unit":"pips","description":"CPI/PCE surprises, NFP → réévaluation taux réels → or."}, - {"id":"ev_geopolitical","label":"Risque Géopolitique","type":"event_driven","category":"geopolitical","unit":"pips","description":"Conflits, tensions → demande refuge or maximale."}, - {"id":"ev_trade_policy","label":"Choc Commercial","type":"event_driven","category":"trade_policy","unit":"pips","description":"Tarifs/guerres commerciales → incertitude → or."}, - {"id":"ev_growth_shock","label":"Choc Croissance","type":"event_driven","category":"growth_shock","unit":"pips","description":"Récession → easing monétaire attendu → or ↑."}, - {"id":"ev_credit_stress","label":"Stress Crédit / Systémique","type":"event_driven","category":"credit_stress","unit":"pips","description":"Crises bancaires, stress liquidité → or refuge absolu."}, - {"id":"ev_commodity","label":"Choc Commodités","type":"event_driven","category":"commodity","unit":"pips","description":"Chocs matières premières (inflation des inputs)."}, - {"id":"ev_sentiment","label":"Sentiment & Flux","type":"event_driven","category":"sentiment","unit":"pips","description":"Sentiment risk-off, flux vers or."}, - {"id":"ev_technical","label":"Momentum Technique","type":"event_driven","category":"technical","unit":"pips","description":"Cassures ATH, niveaux clés or."}, - {"id":"xauusd","label":"XAU/USD Impact Net","type":"output","category":"output","unit":"pips","description":"Pression nette cumulée sur or/USD"}, + {"id":"in_cb", "label":"Banques Centrales", "node_type":"input_event","category":"central_bank", "unit":"pips","display_col":0,"event_category":"central_bank","description":"Fed (taux réels) → or."}, + {"id":"in_macro", "label":"Surprises Macro", "node_type":"input_event","category":"monetary_shock","unit":"pips","display_col":0,"event_category":"monetary_shock","description":"CPI, PCE → anticipations taux réels → or."}, + {"id":"in_geo", "label":"Risque Géopolitique", "node_type":"input_event","category":"geopolitical", "unit":"pips","display_col":0,"event_category":"geopolitical","description":"Conflits → refuge or maximal."}, + {"id":"in_credit", "label":"Stress Crédit/Systémique","node_type":"input_event","category":"credit_stress","unit":"pips","display_col":0,"event_category":"credit_stress","description":"Crises bancaires → or refuge absolu."}, + {"id":"in_growth", "label":"Choc Croissance", "node_type":"input_event","category":"growth_shock", "unit":"pips","display_col":0,"event_category":"growth_shock","description":"Récession → easing → or ↑."}, + {"id":"in_trade", "label":"Choc Commercial", "node_type":"input_event","category":"trade_policy", "unit":"pips","display_col":0,"event_category":"trade_policy","description":"Incertitude → or refuge."}, + {"id":"in_commodity","label":"Choc Commodités", "node_type":"input_event","category":"commodity", "unit":"pips","display_col":0,"event_category":"commodity","description":"Inflation inputs (énergie)."}, + {"id":"in_sentiment","label":"Sentiment/Flux", "node_type":"input_event","category":"sentiment", "unit":"pips","display_col":0,"event_category":"sentiment","description":"Risk-off flows vers or."}, + {"id":"in_technical","label":"Momentum Technique", "node_type":"input_event","category":"technical", "unit":"pips","display_col":0,"event_category":"technical","description":"Cassures ATH, niveaux or."}, + {"id":"m_us_real_rate","label":"Taux réel US 10Y (TIPS)","node_type":"input_manual","category":"monetary","unit":"bps","coefficient_to_pips":-2.5,"display_col":1,"description":"DRIVER PRINCIPAL. Chaque -10bps ≈ +25 pips or."}, + {"id":"m_dxy", "label":"Indice Dollar (DXY)", "node_type":"input_manual","category":"monetary","unit":"pts","coefficient_to_pips":-3.0,"display_col":1,"description":"Or libellé USD → DXY ↑ = or ↓ mécaniquement."}, + {"id":"m_fed_path", "label":"Anticipation Fed 12m","node_type":"input_manual","category":"monetary","unit":"bps","coefficient_to_pips":-1.0,"display_col":1,"description":"Cuts attendus → taux réels ↓ → or ↑."}, + {"id":"m_breakeven", "label":"Breakeven inflation 10Y","node_type":"input_manual","category":"inflation","unit":"bps","coefficient_to_pips": 1.5,"display_col":1,"description":"Anticipations inflation → or comme couverture ↑."}, + {"id":"m_vix", "label":"Niveau VIX", "node_type":"input_manual","category":"sentiment","unit":"pts","coefficient_to_pips": 0.4,"display_col":1,"description":"VIX spike → or refuge."}, + {"id":"m_cb_buying", "label":"Achats CB (tonnes/mois)","node_type":"input_manual","category":"flows","unit":"t","coefficient_to_pips": 2.0,"display_col":1,"description":"Achats banques centrales. Driver structurel majeur depuis 2022."}, + {"id":"m_etf_flows", "label":"Flux ETF or (GLD, IAU)","node_type":"input_manual","category":"flows","unit":"tonnes","coefficient_to_pips": 1.5,"display_col":1,"description":"Entrées ETF → demande physique → or ↑."}, + {"id":"m_cftc_gold", "label":"Net long or (CoT)", "node_type":"input_manual","category":"positioning","unit":"k lots","coefficient_to_pips": 0.15,"display_col":1,"description":"Extrême long → risque liquidation."}, + {"id":"m_fiscal_risk","label":"Risque fiscal US (score)","node_type":"input_manual","category":"macro","unit":"score","coefficient_to_pips": 0.5,"display_col":1,"description":"Déficit/dette US → doutes USD → or refuge."}, + {"id":"m_india_china","label":"Demande physique Inde/Chine","node_type":"input_manual","category":"flows","unit":"score","coefficient_to_pips": 0.4,"display_col":1,"description":"Joaillerie, investment. Saisonnalité."}, + {"id":"layer_rates","label":"▶ Taux Réels & Dollar","node_type":"intermediate","category":"intermediate","unit":"pips","display_col":2, + "formula":"in_cb + in_macro + m_us_real_rate + m_dxy + m_fed_path + m_breakeven", + "description":"Taux réels US = driver fondamental or. DXY = mécanisme de transmission."}, + {"id":"layer_demand","label":"▶ Demande & Flux", "node_type":"intermediate","category":"intermediate","unit":"pips","display_col":2, + "formula":"m_cb_buying + m_etf_flows + m_cftc_gold + m_india_china + m_fiscal_risk", + "description":"Acheteurs physiques et financiers : CB, ETF, CFTC, Asie."}, + {"id":"layer_refuge","label":"▶ Valeur Refuge", "node_type":"intermediate","category":"intermediate","unit":"pips","display_col":2, + "formula":"in_geo + in_credit + in_growth + in_trade + in_sentiment + m_vix", + "description":"Or comme couverture : géopolitique, stress systémique, récession."}, + {"id":"layer_technical","label":"▶ Technique & Momentum","node_type":"intermediate","category":"intermediate","unit":"pips","display_col":2, + "formula":"in_technical + in_commodity", + "description":"Signaux techniques et commodités (coûts extraction)."}, + {"id":"xauusd","label":"XAU/USD — Impact Net","node_type":"output","category":"output","unit":"pips","display_col":3, + "formula":"layer_rates + layer_demand + layer_refuge + layer_technical","description":"Pression nette cumulée or/USD"}, ] }, -# ═══════════════════════════════════════════════════════════════════════════════ +# ══════════════════════════════════════════════════════════════════════════════ "SP500": { "name": "S&P 500", "output_node": "sp500", - "description": "Indice actions US large cap — baromètre risque global", + "description": "Indice actions US — taux, bénéfices, risque, liquidités", "nodes": [ - {"id":"fed_path_12m","label":"Anticipation Fed 12m","type":"structural","category":"monetary","unit":"bps","coefficient":-0.6, - "description":"Cuts attendus → taux discount ↓ → PE expansion → SP500 ↑. Chaque -25bps ≈ +15 pts SP500."}, - {"id":"us_real_rate_10y","label":"Taux réel US 10Y","type":"structural","category":"monetary","unit":"bps","coefficient":-1.5, - "description":"Taux réel = taux d'actualisation des bénéfices futurs. Hausse → PE compression → SP500 ↓."}, - {"id":"eps_growth_12m","label":"Croissance BPA attendue 12m","type":"structural","category":"earnings","unit":"%","coefficient":8.0, - "description":"Révisions croissance bénéfices S&P 500. +1% révision haussière ≈ +8 pts SP500."}, - {"id":"eps_revision_ratio","label":"Ratio révisions BPA","type":"structural","category":"earnings","unit":"ratio","coefficient":2.0, - "description":"Ratio révisions haussières/baissières (>1 = majorité haussière). Fort driver momentum."}, - {"id":"pe_expansion","label":"Expansion multiple PE","type":"structural","category":"valuation","unit":"x","coefficient":12.0, - "description":"Variation multiple PE du S&P 500. +1x PE ≈ +12 pts SP500 (base ~4500)."}, - {"id":"financial_conditions","label":"Indice conditions financières","type":"structural","category":"monetary","unit":"score","coefficient":1.5, - "description":"Chicago Fed NFCI ou Goldman GSFCI. Desserrement = SP500 ↑."}, - {"id":"credit_spread_ig","label":"Spread crédit IG (bps)","type":"structural","category":"credit","unit":"bps","coefficient":-0.8, - "description":"Spread obligations IG. Hausse = conditions crédit durcissent → SP500 ↓."}, - {"id":"credit_spread_hy","label":"Spread crédit HY (bps)","type":"structural","category":"credit","unit":"bps","coefficient":-0.5, - "description":"Spread high yield. Baromètre stress financier. Spike → SP500 ↓."}, - {"id":"buyback_volume","label":"Volume rachats actions","type":"structural","category":"flows","unit":"Mds$/sem","coefficient":0.5, - "description":"Flux de rachats d'actions S&P 500 en cours. Support technique majeur."}, - {"id":"retail_flow","label":"Flux retail (FOMO)","type":"structural","category":"flows","unit":"score","coefficient":0.3, - "description":"Score flux acheteurs retail (ETF, options call). Momentum-driven."}, - {"id":"consumer_confidence","label":"Confiance consommateur US","type":"structural","category":"macro","unit":"pts","coefficient":0.3, - "description":"Conference Board ou Michigan. Signal dépenses → bénéfices → SP500."}, - {"id":"us_gdp_growth","label":"Croissance PIB US","type":"structural","category":"macro","unit":"%","coefficient":5.0, - "description":"Croissance réelle US annualisée. Surprise haussière → SP500 ↑."}, - {"id":"vix_level","label":"Niveau VIX","type":"structural","category":"sentiment","unit":"pts","coefficient":-0.8, - "description":"VIX. Spike → risk-off → SP500 ↓. Normalisation VIX → SP500 ↑."}, - {"id":"geopolitical_risk_premium","label":"Prime risque géopolitique","type":"structural","category":"political","unit":"score","coefficient":-0.5, - "description":"Tensions géopolitiques majeures → incertitude → prime de risque → SP500 ↓."}, - {"id":"ev_central_bank","label":"Pression Banques Centrales","type":"event_driven","category":"central_bank","unit":"pips","description":"Fed pivot, pause, hawkish surprise → SP500 directement."}, - {"id":"ev_macro_surprise","label":"Surprise Macro","type":"event_driven","category":"monetary_shock","unit":"pips","description":"NFP, CPI, PIB, ISM surprises."}, - {"id":"ev_geopolitical","label":"Risque Géopolitique","type":"event_driven","category":"geopolitical","unit":"pips","description":"Conflits, sanctions → risk-off SP500."}, - {"id":"ev_trade_policy","label":"Choc Commercial","type":"event_driven","category":"trade_policy","unit":"pips","description":"Tarifs → chaînes supply, marges bénéficiaires SP500."}, - {"id":"ev_growth_shock","label":"Choc Croissance","type":"event_driven","category":"growth_shock","unit":"pips","description":"Récession/reprise choc."}, - {"id":"ev_credit_stress","label":"Stress Crédit","type":"event_driven","category":"credit_stress","unit":"pips","description":"Banking stress → SP500 ↓ brutal."}, - {"id":"ev_commodity","label":"Choc Commodités","type":"event_driven","category":"commodity","unit":"pips","description":"Choc énergie → marges → SP500."}, - {"id":"ev_sentiment","label":"Sentiment & Positionnement","type":"event_driven","category":"sentiment","unit":"pips","description":"Flux institutionnels, CTA positioning."}, - {"id":"ev_technical","label":"Momentum Technique","type":"event_driven","category":"technical","unit":"pips","description":"Niveaux clés SP500, MA200, cassures."}, - {"id":"sp500","label":"S&P 500 Impact Net","type":"output","category":"output","unit":"pips","description":"Pression nette cumulée S&P 500"}, + {"id":"in_cb", "label":"Banques Centrales", "node_type":"input_event","category":"central_bank","unit":"pips","display_col":0,"event_category":"central_bank","description":"Fed pivot/hike → SP500 directement."}, + {"id":"in_macro", "label":"Surprises Macro", "node_type":"input_event","category":"monetary_shock","unit":"pips","display_col":0,"event_category":"monetary_shock","description":"NFP, CPI, PIB US."}, + {"id":"in_geo", "label":"Risque Géopolitique", "node_type":"input_event","category":"geopolitical","unit":"pips","display_col":0,"event_category":"geopolitical","description":"Risk-off → SP500 vendu."}, + {"id":"in_trade", "label":"Choc Commercial", "node_type":"input_event","category":"trade_policy","unit":"pips","display_col":0,"event_category":"trade_policy","description":"Tarifs → marges bénéficiaires SP500."}, + {"id":"in_growth", "label":"Choc Croissance", "node_type":"input_event","category":"growth_shock","unit":"pips","display_col":0,"event_category":"growth_shock","description":"Récession → SP500 ↓ massif."}, + {"id":"in_credit", "label":"Stress Crédit", "node_type":"input_event","category":"credit_stress","unit":"pips","display_col":0,"event_category":"credit_stress","description":"Banking stress → SP500 ↓ brutal."}, + {"id":"in_commodity","label":"Choc Commodités", "node_type":"input_event","category":"commodity","unit":"pips","display_col":0,"event_category":"commodity","description":"Énergie → marges opérationnelles."}, + {"id":"in_sentiment","label":"Sentiment/Positionnement","node_type":"input_event","category":"sentiment","unit":"pips","display_col":0,"event_category":"sentiment","description":"CTA, hedge fund flows."}, + {"id":"in_technical","label":"Momentum Technique", "node_type":"input_event","category":"technical","unit":"pips","display_col":0,"event_category":"technical","description":"MA200, cassures."}, + {"id":"m_fed_path", "label":"Anticipation Fed 12m", "node_type":"input_manual","category":"monetary","unit":"bps","coefficient_to_pips":-0.6,"display_col":1,"description":"Cuts → taux discount ↓ → PE expansion → SP500 ↑"}, + {"id":"m_real_rate","label":"Taux réel US 10Y", "node_type":"input_manual","category":"monetary","unit":"bps","coefficient_to_pips":-1.5,"display_col":1,"description":"Taux réel = taux d'actualisation. Hausse → PE compression → SP500 ↓"}, + {"id":"m_fin_cond", "label":"Conditions financières", "node_type":"input_manual","category":"monetary","unit":"score","coefficient_to_pips": 1.5,"display_col":1,"description":"Desserrement → SP500 ↑"}, + {"id":"m_eps_growth","label":"Croissance BPA attendue","node_type":"input_manual","category":"earnings","unit":"%","coefficient_to_pips": 8.0,"display_col":1,"description":"+1% BPA revision ≈ +8 pts SP500"}, + {"id":"m_eps_revision","label":"Ratio révisions BPA","node_type":"input_manual","category":"earnings","unit":"ratio","coefficient_to_pips": 2.0,"display_col":1,"description":"Ratio haussier/baissier. Fort driver momentum."}, + {"id":"m_pe", "label":"Multiple PE forward", "node_type":"input_manual","category":"valuation","unit":"x","coefficient_to_pips":12.0,"display_col":1,"description":"+1x PE ≈ +12 pts SP500"}, + {"id":"m_ig_spread","label":"Spread crédit IG (bps)","node_type":"input_manual","category":"credit","unit":"bps","coefficient_to_pips":-0.8,"display_col":1,"description":"Spread IG hausse → conditions crédit durcissent → SP500 ↓"}, + {"id":"m_buybacks", "label":"Volume rachats actions","node_type":"input_manual","category":"flows","unit":"Mds$/sem","coefficient_to_pips": 0.5,"display_col":1,"description":"Flux rachats = support technique majeur"}, + {"id":"m_vix", "label":"Niveau VIX", "node_type":"input_manual","category":"sentiment","unit":"pts","coefficient_to_pips":-0.8,"display_col":1,"description":"VIX spike → risk-off → SP500 ↓"}, + {"id":"m_geo_risk_prem","label":"Prime risque géopolitique","node_type":"input_manual","category":"political","unit":"score","coefficient_to_pips":-0.5,"display_col":1,"description":"Tensions géopolitiques → incertitude → SP500 ↓"}, + {"id":"m_gdp", "label":"Croissance PIB US", "node_type":"input_manual","category":"macro","unit":"%","coefficient_to_pips": 5.0,"display_col":1,"description":"Surprise haussière → SP500 ↑"}, + {"id":"layer_monetary","label":"▶ Politique Monétaire","node_type":"intermediate","category":"intermediate","unit":"pips","display_col":2, + "formula":"in_cb + in_macro + m_fed_path + m_real_rate + m_fin_cond", + "description":"Fed, taux réels, conditions financières — transmission directe sur PE."}, + {"id":"layer_earnings","label":"▶ Bénéfices & Valorisation","node_type":"intermediate","category":"intermediate","unit":"pips","display_col":2, + "formula":"m_eps_growth + m_eps_revision + m_pe + m_gdp", + "description":"BPA, révisions, multiple. Fondamental long terme."}, + {"id":"layer_risk_credit","label":"▶ Risque & Crédit","node_type":"intermediate","category":"intermediate","unit":"pips","display_col":2, + "formula":"in_geo + in_credit + in_growth + in_trade + in_commodity + m_ig_spread + m_vix + m_geo_risk_prem", + "description":"Risque systémique, géopolitique, crédit — driver de la prime de risque."}, + {"id":"layer_flows","label":"▶ Flux & Positionnement","node_type":"intermediate","category":"intermediate","unit":"pips","display_col":2, + "formula":"in_sentiment + in_technical + m_buybacks", + "description":"Flux institutionnels, retail, rachats, momentum."}, + {"id":"sp500","label":"S&P 500 — Impact Net","node_type":"output","category":"output","unit":"pips","display_col":3, + "formula":"layer_monetary + layer_earnings + layer_risk_credit + layer_flows","description":"Pression nette cumulée S&P 500"}, ] }, -# ═══════════════════════════════════════════════════════════════════════════════ +# ══════════════════════════════════════════════════════════════════════════════ "TLT": { "name": "TLT (US Long Bonds)", "output_node": "tlt", - "description": "ETF obligations US 20Y+ — duration longue, sensible aux taux et récession", + "description": "ETF obligations US 20Y+ — duration, inflation, récession, supply", "nodes": [ - {"id":"fed_terminal_rate","label":"Taux terminal Fed anticipé","type":"structural","category":"monetary","unit":"bps","coefficient":-0.4, - "description":"Taux Fed terminal pricé par les marchés. Hausse → taux longs up → TLT ↓ (duration ~18)."}, - {"id":"us_10y_yield","label":"Rendement UST 10Y","type":"structural","category":"monetary","unit":"bps","coefficient":-0.3, - "description":"Rendement 10Y direct. Chaque +1bps ≈ -$0.18 sur $100 TLT (duration modifiée ~18)."}, - {"id":"us_30y_yield","label":"Rendement UST 30Y","type":"structural","category":"monetary","unit":"bps","coefficient":-0.25, - "description":"Rendement 30Y. TLT détient principalement des 20-30Y."}, - {"id":"inflation_breakeven_10y","label":"Breakeven inflation 10Y","type":"structural","category":"inflation","unit":"bps","coefficient":-0.2, - "description":"Anticipations inflation 10Y. Hausse → taux nominaux ↑ → TLT ↓."}, - {"id":"recession_probability","label":"Probabilité récession 12m","type":"structural","category":"macro","unit":"%","coefficient":0.3, - "description":"Probabilité récession 12m (modèle yield curve ou Fed NY). Hausse → flight to safety → TLT ↑."}, - {"id":"fed_qt_pace","label":"Rythme QT Fed (Mds$/mois)","type":"structural","category":"monetary","unit":"Mds$","coefficient":-0.1, - "description":"Réduction bilan Fed (quantitative tightening). Plus vite = pression vendeur sur Treasuries → TLT ↓."}, - {"id":"us_fiscal_deficit","label":"Déficit fiscal US (%PIB)","type":"structural","category":"macro","unit":"%","coefficient":-0.4, - "description":"Déficit croissant → supply massive de Treasuries → pression vendeuse → TLT ↓."}, - {"id":"term_premium","label":"Prime de terme (ACM)","type":"structural","category":"monetary","unit":"bps","coefficient":-0.3, - "description":"Prime de terme (Adrian-Crump-Moench). Hausse = extra rendement exigé → TLT ↓."}, - {"id":"foreign_treasury_demand","label":"Demande étrangère Treasuries","type":"structural","category":"flows","unit":"Mds$","coefficient":0.05, - "description":"Achats Treasuries par banques centrales étrangères (Chine, Japon). Fort = TLT ↑."}, - {"id":"yield_curve_slope","label":"Pente courbe 2Y-10Y","type":"structural","category":"monetary","unit":"bps","coefficient":0.1, - "description":"Steepening (2Y-10Y ↑) = TLT ↑ relatif (marché price fin de hausse Fed)."}, - {"id":"vix_level","label":"Niveau VIX","type":"structural","category":"sentiment","unit":"pts","coefficient":0.5, - "description":"VIX spike → flight to quality Treasuries → TLT ↑ (sauf si crise stagflationniste)."}, - {"id":"sp500_correlation","label":"Corrélation SP500 (inverse)","type":"structural","category":"sentiment","unit":"score","coefficient":-0.3, - "description":"En régime normal : SP500 ↑ → TLT ↓ (rotation risque). Score + = corrélation positive anormale."}, - {"id":"ev_central_bank","label":"Pression Banques Centrales","type":"event_driven","category":"central_bank","unit":"pips","description":"FOMC décisions, minutes → impact direct TLT."}, - {"id":"ev_macro_surprise","label":"Surprise Macro","type":"event_driven","category":"monetary_shock","unit":"pips","description":"CPI, PCE, NFP → réévaluation taux → TLT."}, - {"id":"ev_geopolitical","label":"Risque Géopolitique","type":"event_driven","category":"geopolitical","unit":"pips","description":"Crises → flight to safety Treasuries."}, - {"id":"ev_growth_shock","label":"Choc Croissance","type":"event_driven","category":"growth_shock","unit":"pips","description":"Récession → TLT ↑ massif."}, - {"id":"ev_credit_stress","label":"Stress Crédit","type":"event_driven","category":"credit_stress","unit":"pips","description":"Banking stress → fuite vers Treasuries."}, - {"id":"ev_trade_policy","label":"Choc Commercial","type":"event_driven","category":"trade_policy","unit":"pips","description":"Tarifs → incertitude → Treasuries demandés ou vendus."}, - {"id":"ev_sentiment","label":"Sentiment & Flux","type":"event_driven","category":"sentiment","unit":"pips","description":"Flux institutionnels bonds."}, - {"id":"ev_technical","label":"Momentum Technique","type":"event_driven","category":"technical","unit":"pips","description":"Niveaux clés TLT, moyennes mobiles."}, - {"id":"tlt","label":"TLT Impact Net","type":"output","category":"output","unit":"pips","description":"Pression nette cumulée TLT"}, + {"id":"in_cb", "label":"Banques Centrales","node_type":"input_event","category":"central_bank","unit":"pips","display_col":0,"event_category":"central_bank","description":"FOMC décisions/minutes → impact direct TLT."}, + {"id":"in_macro", "label":"Surprises Macro", "node_type":"input_event","category":"monetary_shock","unit":"pips","display_col":0,"event_category":"monetary_shock","description":"CPI, PCE, NFP → réévaluation taux."}, + {"id":"in_geo", "label":"Géopolitique", "node_type":"input_event","category":"geopolitical","unit":"pips","display_col":0,"event_category":"geopolitical","description":"Crises → flight to safety Treasuries."}, + {"id":"in_growth", "label":"Choc Croissance", "node_type":"input_event","category":"growth_shock","unit":"pips","display_col":0,"event_category":"growth_shock","description":"Récession → TLT ↑ massif."}, + {"id":"in_credit", "label":"Stress Crédit", "node_type":"input_event","category":"credit_stress","unit":"pips","display_col":0,"event_category":"credit_stress","description":"Banking stress → Treasuries demandés."}, + {"id":"in_trade", "label":"Choc Commercial", "node_type":"input_event","category":"trade_policy","unit":"pips","display_col":0,"event_category":"trade_policy","description":"Tarifs → incertitude → Treasuries."}, + {"id":"in_technical","label":"Technique", "node_type":"input_event","category":"technical","unit":"pips","display_col":0,"event_category":"technical","description":"Niveaux TLT, tendances."}, + {"id":"in_sentiment","label":"Sentiment/Flux", "node_type":"input_event","category":"sentiment","unit":"pips","display_col":0,"event_category":"sentiment","description":"Flux obligataires institutionnels."}, + {"id":"m_terminal_rate","label":"Taux terminal Fed","node_type":"input_manual","category":"monetary","unit":"bps","coefficient_to_pips":-0.4,"display_col":1,"description":"Taux terminal ↑ → taux longs ↑ → TLT ↓ (duration ~18)"}, + {"id":"m_us_10y", "label":"Rendement UST 10Y","node_type":"input_manual","category":"monetary","unit":"bps","coefficient_to_pips":-0.3,"display_col":1,"description":"Chaque +1bps ≈ -$0.18 sur $100 TLT"}, + {"id":"m_us_30y", "label":"Rendement UST 30Y","node_type":"input_manual","category":"monetary","unit":"bps","coefficient_to_pips":-0.25,"display_col":1,"description":"TLT détient principalement des 20-30Y"}, + {"id":"m_breakeven","label":"Breakeven inflation 10Y","node_type":"input_manual","category":"inflation","unit":"bps","coefficient_to_pips":-0.2,"display_col":1,"description":"Inflation anticipée ↑ → taux nominaux ↑ → TLT ↓"}, + {"id":"m_recession_prob","label":"Probabilité récession 12m","node_type":"input_manual","category":"macro","unit":"%","coefficient_to_pips": 0.3,"display_col":1,"description":"Récession → flight to bonds → TLT ↑"}, + {"id":"m_qt_pace", "label":"Rythme QT Fed (Mds$/mois)","node_type":"input_manual","category":"monetary","unit":"Mds$","coefficient_to_pips":-0.1,"display_col":1,"description":"QT = pression vendeuse sur Treasuries → TLT ↓"}, + {"id":"m_deficit", "label":"Déficit fiscal US (%PIB)","node_type":"input_manual","category":"macro","unit":"%","coefficient_to_pips":-0.4,"display_col":1,"description":"Déficit → supply massive → pression vendeuse → TLT ↓"}, + {"id":"m_term_prem","label":"Prime de terme (ACM)","node_type":"input_manual","category":"monetary","unit":"bps","coefficient_to_pips":-0.3,"display_col":1,"description":"Prime de terme ↑ = extra rendement exigé → TLT ↓"}, + {"id":"m_foreign_demand","label":"Demande étrangère Treasuries","node_type":"input_manual","category":"flows","unit":"Mds$","coefficient_to_pips": 0.05,"display_col":1,"description":"Achats CB étrangères (Chine, Japon) → TLT ↑"}, + {"id":"m_vix", "label":"Niveau VIX", "node_type":"input_manual","category":"sentiment","unit":"pts","coefficient_to_pips": 0.5,"display_col":1,"description":"VIX spike → flight to quality → TLT ↑"}, + {"id":"layer_rates","label":"▶ Taux & Politique Monétaire","node_type":"intermediate","category":"intermediate","unit":"pips","display_col":2, + "formula":"in_cb + in_macro + m_terminal_rate + m_us_10y + m_us_30y + m_breakeven + m_qt_pace + m_term_prem", + "description":"Taux directeurs, duration, QT, inflation anticipée."}, + {"id":"layer_supply","label":"▶ Supply & Demande Treasuries","node_type":"intermediate","category":"intermediate","unit":"pips","display_col":2, + "formula":"m_deficit + m_foreign_demand", + "description":"Émission nette, achats étrangers — équilibre offre/demande marché obligataire."}, + {"id":"layer_macro","label":"▶ Macro & Récession","node_type":"intermediate","category":"intermediate","unit":"pips","display_col":2, + "formula":"in_growth + in_credit + in_trade + m_recession_prob", + "description":"Risque récession → flight to safety → TLT ↑."}, + {"id":"layer_risk","label":"▶ Risque & Refuge","node_type":"intermediate","category":"intermediate","unit":"pips","display_col":2, + "formula":"in_geo + in_sentiment + in_technical + m_vix", + "description":"Géopolitique, sentiment, VIX → demande refuge Treasuries."}, + {"id":"tlt","label":"TLT — Impact Net","node_type":"output","category":"output","unit":"pips","display_col":3, + "formula":"layer_rates + layer_supply + layer_macro + layer_risk","description":"Pression nette cumulée TLT"}, ] }, -# ═══════════════════════════════════════════════════════════════════════════════ +# ══════════════════════════════════════════════════════════════════════════════ "GBPUSD": { "name": "GBP/USD", "output_node": "gbpusd", - "description": "Taux de change Livre sterling / Dollar — sensible aux données UK et BoE", + "description": "Livre sterling/Dollar — BoE, données UK, risque politique", "nodes": [ - {"id":"boe_path_12m","label":"Anticipation BoE 12m","type":"structural","category":"monetary","unit":"bps","coefficient":0.7, - "description":"Variation cumulée taux BoE attendue 12m. Hikes → GBP ↑ → GBP/USD ↑."}, - {"id":"fed_path_12m","label":"Anticipation Fed 12m","type":"structural","category":"monetary","unit":"bps","coefficient":-0.7, - "description":"Variation cumulée taux Fed attendue 12m. Hikes → USD ↑ → GBP/USD ↓."}, - {"id":"uk_us_rate_differential","label":"Différentiel taux BoE-Fed","type":"structural","category":"monetary","unit":"bps","coefficient":1.0, - "description":"Spread taux directeurs BoE vs Fed. Principal driver EUR/USD."}, - {"id":"uk_cpi_yoy","label":"CPI UK YoY","type":"structural","category":"inflation","unit":"%","coefficient":0.7, - "description":"Inflation UK. Persistance → BoE forcé de rester hawkish → GBP ↑."}, - {"id":"uk_gdp_growth","label":"Croissance PIB UK","type":"structural","category":"macro","unit":"%","coefficient":3.0, - "description":"PIB UK annualisé. Surprise haussière → GBP ↑."}, - {"id":"uk_pmi_composite","label":"PMI composite UK","type":"structural","category":"macro","unit":"pts","coefficient":0.25, - "description":"PMI composite UK. >50 = expansion → GBP ↑."}, - {"id":"uk_labor_market","label":"Marché emploi UK","type":"structural","category":"macro","unit":"score","coefficient":0.4, - "description":"Score santé emploi UK (chômage, salaires, emplois). Fort = BoE hawkish → GBP ↑."}, - {"id":"uk_political_risk","label":"Risque politique UK","type":"structural","category":"political","unit":"score","coefficient":-0.4, - "description":"Incertitude politique UK (snap election, budget, Brexit séquelles). Hausse → GBP ↓."}, - {"id":"uk_current_account","label":"Balance courante UK (%PIB)","type":"structural","category":"flows","unit":"%","coefficient":0.3, - "description":"Déficit courant UK chronique. Amélioration → moins de pression vendeuse GBP."}, - {"id":"risk_appetite","label":"Appétit risque mondial","type":"structural","category":"sentiment","unit":"score","coefficient":0.4, - "description":"Risk-on → GBP comme devise cyclique/risquée apprécie vs USD."}, - {"id":"cftc_gbp_net","label":"Positions nettes GBP (CoT)","type":"structural","category":"positioning","unit":"k contrats","coefficient":0.08, - "description":"Positions spéculatives nettes GBP CME CoT."}, - {"id":"ev_central_bank","label":"Pression Banques Centrales","type":"event_driven","category":"central_bank","unit":"pips","description":"BoE, Fed décisions/minutes."}, - {"id":"ev_macro_surprise","label":"Surprise Macro","type":"event_driven","category":"monetary_shock","unit":"pips","description":"CPI UK/US, NFP, GDP UK surprises."}, - {"id":"ev_geopolitical","label":"Risque Géopolitique","type":"event_driven","category":"geopolitical","unit":"pips","description":"Conflits → risk-off → GBP vendu."}, - {"id":"ev_trade_policy","label":"Choc Commercial","type":"event_driven","category":"trade_policy","unit":"pips","description":"Tarifs US → UK exposé."}, - {"id":"ev_growth_shock","label":"Choc Croissance","type":"event_driven","category":"growth_shock","unit":"pips","description":"Récession UK/US."}, - {"id":"ev_credit_stress","label":"Stress Crédit","type":"event_driven","category":"credit_stress","unit":"pips","description":"Stress Gilts, banking UK."}, - {"id":"ev_sentiment","label":"Sentiment & Flux","type":"event_driven","category":"sentiment","unit":"pips","description":"Positionnement GBP."}, - {"id":"ev_technical","label":"Momentum Technique","type":"event_driven","category":"technical","unit":"pips","description":"Niveaux clés cable."}, - {"id":"gbpusd","label":"GBP/USD Impact Net","type":"output","category":"output","unit":"pips","description":"Pression nette cumulée GBP/USD"}, + {"id":"in_cb", "label":"Banques Centrales","node_type":"input_event","category":"central_bank","unit":"pips","display_col":0,"event_category":"central_bank","description":"BoE, Fed."}, + {"id":"in_macro", "label":"Surprises Macro", "node_type":"input_event","category":"monetary_shock","unit":"pips","display_col":0,"event_category":"monetary_shock","description":"CPI UK/US, NFP, GDP UK."}, + {"id":"in_geo", "label":"Géopolitique", "node_type":"input_event","category":"geopolitical","unit":"pips","display_col":0,"event_category":"geopolitical","description":"GBP comme devise cyclique, vendue en risk-off."}, + {"id":"in_trade", "label":"Choc Commercial", "node_type":"input_event","category":"trade_policy","unit":"pips","display_col":0,"event_category":"trade_policy","description":"Tarifs US → UK exposé."}, + {"id":"in_growth", "label":"Choc Croissance", "node_type":"input_event","category":"growth_shock","unit":"pips","display_col":0,"event_category":"growth_shock","description":"Récession UK."}, + {"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 Gilts, banking UK."}, + {"id":"in_technical","label":"Technique", "node_type":"input_event","category":"technical","unit":"pips","display_col":0,"event_category":"technical","description":"Niveaux clés cable."}, + {"id":"in_sentiment","label":"Sentiment/Flux", "node_type":"input_event","category":"sentiment","unit":"pips","display_col":0,"event_category":"sentiment","description":"Positionnement GBP."}, + {"id":"m_boe_path", "label":"Anticipation BoE 12m","node_type":"input_manual","category":"monetary","unit":"bps","coefficient_to_pips": 0.7,"display_col":1,"description":"Hikes BoE → GBP ↑"}, + {"id":"m_fed_path", "label":"Anticipation Fed 12m","node_type":"input_manual","category":"monetary","unit":"bps","coefficient_to_pips":-0.7,"display_col":1,"description":"Hikes Fed → USD ↑ → paire ↓"}, + {"id":"m_uk_cpi", "label":"CPI UK YoY", "node_type":"input_manual","category":"inflation","unit":"%","coefficient_to_pips": 0.7,"display_col":1,"description":"Persistance inflation → BoE hawkish → GBP ↑"}, + {"id":"m_uk_gdp", "label":"Croissance PIB UK", "node_type":"input_manual","category":"macro","unit":"%","coefficient_to_pips": 3.0,"display_col":1,"description":"Surprise PIB UK → GBP ↑"}, + {"id":"m_uk_pmi", "label":"PMI composite UK", "node_type":"input_manual","category":"macro","unit":"pts","coefficient_to_pips": 0.25,"display_col":1,"description":">50 = expansion UK → GBP ↑"}, + {"id":"m_uk_pol_risk","label":"Risque politique UK","node_type":"input_manual","category":"political","unit":"score","coefficient_to_pips":-0.4,"display_col":1,"description":"Incertitude UK → GBP ↓"}, + {"id":"m_risk_appetite","label":"Appétit risque","node_type":"input_manual","category":"sentiment","unit":"score","coefficient_to_pips": 0.4,"display_col":1,"description":"Risk-on → GBP comme devise cyclique ↑"}, + {"id":"m_cftc_gbp", "label":"Positions nettes GBP","node_type":"input_manual","category":"positioning","unit":"k lots","coefficient_to_pips": 0.08,"display_col":1,"description":"Net long GBP CoT."}, + {"id":"layer_monetary","label":"▶ Différentiel Monétaire","node_type":"intermediate","category":"intermediate","unit":"pips","display_col":2, + "formula":"in_cb + in_macro + m_boe_path + m_fed_path + m_uk_cpi", + "description":"BoE vs Fed, inflation UK — driver principal GBP/USD."}, + {"id":"layer_uk_macro","label":"▶ Fondamentaux UK","node_type":"intermediate","category":"intermediate","unit":"pips","display_col":2, + "formula":"in_growth + m_uk_gdp + m_uk_pmi + m_uk_pol_risk", + "description":"Croissance, PMI, risque politique UK."}, + {"id":"layer_global","label":"▶ Risque & Flux Globaux","node_type":"intermediate","category":"intermediate","unit":"pips","display_col":2, + "formula":"in_geo + in_credit + in_trade + in_technical + in_sentiment + m_risk_appetite + m_cftc_gbp", + "description":"GBP exposé aux chocs globaux (devise cyclique)."}, + {"id":"gbpusd","label":"GBP/USD — Impact Net","node_type":"output","category":"output","unit":"pips","display_col":3, + "formula":"layer_monetary + layer_uk_macro + layer_global","description":"Pression nette cumulée GBP/USD"}, ] }, -# ═══════════════════════════════════════════════════════════════════════════════ +# ══════════════════════════════════════════════════════════════════════════════ "EEM": { "name": "EEM (Marchés Émergents)", "output_node": "eem", - "description": "ETF actions marchés émergents — sensible au dollar, Chine et commodités", + "description": "ETF EM — dollar, Chine, commodités, risk appetite", "nodes": [ - {"id":"dxy_inverse","label":"Indice Dollar (DXY) — impact inverse","type":"structural","category":"monetary","unit":"pts","coefficient":-0.8, - "description":"USD fort → pression sur dettes EM en USD, sorties capitaux EM → EEM ↓. DXY ↑ = EEM ↓."}, - {"id":"us_real_rate","label":"Taux réel US 10Y","type":"structural","category":"monetary","unit":"bps","coefficient":-0.6, - "description":"Taux réel US élevé → capitaux retournent aux US → sorties EM → EEM ↓."}, - {"id":"fed_path_12m","label":"Anticipation Fed 12m","type":"structural","category":"monetary","unit":"bps","coefficient":-0.5, - "description":"Cuts Fed → dollar faible + taux attractifs EM → capitaux entrent EM → EEM ↑."}, - {"id":"china_pmi","label":"PMI manufacturier Chine","type":"structural","category":"macro","unit":"pts","coefficient":0.6, - "description":"PMI Caixin/NBS Chine. Expansion = croissance EM → EEM ↑ (Chine ~28% de l'indice)."}, - {"id":"china_growth_stimulus","label":"Stimulus croissance Chine","type":"structural","category":"macro","unit":"score","coefficient":0.8, - "description":"Score stimulus PBOC/gouvernement. Annonces majeures → EEM spike haussier."}, - {"id":"commodity_complex","label":"Indice commodités (export EM)","type":"structural","category":"commodity","unit":"score","coefficient":0.4, - "description":"Commodités elevés → exportateurs EM (Brésil, Russie, Afrique du Sud) profitent → EEM ↑."}, - {"id":"em_credit_spread","label":"Spread crédit souverain EM","type":"structural","category":"credit","unit":"bps","coefficient":-0.5, - "description":"EMBI spread. Hausse = stress EM → sorties → EEM ↓."}, - {"id":"em_capital_flows","label":"Flux capitaux EM nets","type":"structural","category":"flows","unit":"Mds$","coefficient":0.3, - "description":"IIF flux nets vers EM. Entrées soutenues = EEM ↑ structurel."}, - {"id":"risk_appetite","label":"Appétit risque mondial","type":"structural","category":"sentiment","unit":"score","coefficient":0.6, - "description":"Risk-on → recherche de rendement EM → EEM ↑."}, - {"id":"vix_level","label":"Niveau VIX","type":"structural","category":"sentiment","unit":"pts","coefficient":-0.5, - "description":"VIX spike → sorties EM (flight to quality) → EEM ↓."}, - {"id":"us_china_tension","label":"Tensions US-Chine","type":"structural","category":"political","unit":"score","coefficient":-0.5, - "description":"Tensions US-Chine (tarifs, sanctions, tech). Hausse → risque EM → EEM ↓."}, - {"id":"ev_central_bank","label":"Pression Banques Centrales","type":"event_driven","category":"central_bank","unit":"pips","description":"Fed pivot, PBOC mesures."}, - {"id":"ev_macro_surprise","label":"Surprise Macro","type":"event_driven","category":"monetary_shock","unit":"pips","description":"Données Chine, US macro."}, - {"id":"ev_geopolitical","label":"Risque Géopolitique","type":"event_driven","category":"geopolitical","unit":"pips","description":"Tensions régionales EM."}, - {"id":"ev_trade_policy","label":"Choc Commercial","type":"event_driven","category":"trade_policy","unit":"pips","description":"Tarifs US-Chine, sanctions."}, - {"id":"ev_growth_shock","label":"Choc Croissance","type":"event_driven","category":"growth_shock","unit":"pips","description":"Récession US/Chine → EEM ↓."}, - {"id":"ev_commodity","label":"Choc Commodités","type":"event_driven","category":"commodity","unit":"pips","description":"Chocs matières premières → exportateurs EM."}, - {"id":"ev_credit_stress","label":"Stress Crédit EM","type":"event_driven","category":"credit_stress","unit":"pips","description":"Stress souverain EM, crise devises EM."}, - {"id":"ev_sentiment","label":"Sentiment & Flux","type":"event_driven","category":"sentiment","unit":"pips","description":"Flux ETF EM, risk-on/off."}, - {"id":"ev_technical","label":"Momentum Technique","type":"event_driven","category":"technical","unit":"pips","description":"Niveaux EEM, tendances."}, - {"id":"eem","label":"EEM Impact Net","type":"output","category":"output","unit":"pips","description":"Pression nette cumulée EEM"}, + {"id":"in_cb", "label":"Banques Centrales","node_type":"input_event","category":"central_bank","unit":"pips","display_col":0,"event_category":"central_bank","description":"Fed pivot → EM bénéficient du dollar faible."}, + {"id":"in_macro", "label":"Surprises Macro", "node_type":"input_event","category":"monetary_shock","unit":"pips","display_col":0,"event_category":"monetary_shock","description":"Données Chine, US macro."}, + {"id":"in_geo", "label":"Géopolitique", "node_type":"input_event","category":"geopolitical","unit":"pips","display_col":0,"event_category":"geopolitical","description":"Tensions régionales EM."}, + {"id":"in_trade", "label":"Choc Commercial", "node_type":"input_event","category":"trade_policy","unit":"pips","display_col":0,"event_category":"trade_policy","description":"Tarifs US-Chine, sanctions."}, + {"id":"in_growth", "label":"Choc Croissance", "node_type":"input_event","category":"growth_shock","unit":"pips","display_col":0,"event_category":"growth_shock","description":"Récession US/Chine → EEM ↓."}, + {"id":"in_credit", "label":"Stress Crédit EM", "node_type":"input_event","category":"credit_stress","unit":"pips","display_col":0,"event_category":"credit_stress","description":"Stress souverain EM, crise devises."}, + {"id":"in_commodity","label":"Choc Commodités", "node_type":"input_event","category":"commodity","unit":"pips","display_col":0,"event_category":"commodity","description":"Exportateurs EM profitent des commodités élevées."}, + {"id":"in_sentiment","label":"Sentiment/Flux", "node_type":"input_event","category":"sentiment","unit":"pips","display_col":0,"event_category":"sentiment","description":"Flux ETF EM, risk-on/off."}, + {"id":"in_technical","label":"Technique", "node_type":"input_event","category":"technical","unit":"pips","display_col":0,"event_category":"technical","description":"Niveaux EEM."}, + {"id":"m_dxy_inv", "label":"Dollar (DXY) — impact inverse","node_type":"input_manual","category":"monetary","unit":"pts","coefficient_to_pips":-0.8,"display_col":1,"description":"USD fort → pression dettes EM → EEM ↓. DXY ↑ = EEM ↓"}, + {"id":"m_us_real_rate","label":"Taux réel US 10Y","node_type":"input_manual","category":"monetary","unit":"bps","coefficient_to_pips":-0.6,"display_col":1,"description":"Taux réel US ↑ → capitaux retournent US → EEM ↓"}, + {"id":"m_fed_path", "label":"Anticipation Fed 12m","node_type":"input_manual","category":"monetary","unit":"bps","coefficient_to_pips":-0.5,"display_col":1,"description":"Cuts Fed → dollar faible → EM avantageux → EEM ↑"}, + {"id":"m_china_pmi","label":"PMI manufacturier Chine","node_type":"input_manual","category":"macro","unit":"pts","coefficient_to_pips": 0.6,"display_col":1,"description":"Chine ~28% index. Expansion → EEM ↑"}, + {"id":"m_china_stimulus","label":"Stimulus Chine","node_type":"input_manual","category":"macro","unit":"score","coefficient_to_pips": 0.8,"display_col":1,"description":"PBOC/fiscal. Annonces majeures → EEM spike"}, + {"id":"m_commodity_index","label":"Indice commodités","node_type":"input_manual","category":"commodity","unit":"score","coefficient_to_pips": 0.4,"display_col":1,"description":"Exportateurs EM (Brésil, Afrique du Sud) profitent."}, + {"id":"m_em_spread","label":"Spread souverain EM (EMBI)","node_type":"input_manual","category":"credit","unit":"bps","coefficient_to_pips":-0.5,"display_col":1,"description":"Spread ↑ = stress EM → sorties → EEM ↓"}, + {"id":"m_risk_appetite","label":"Appétit risque","node_type":"input_manual","category":"sentiment","unit":"score","coefficient_to_pips": 0.6,"display_col":1,"description":"Risk-on → search for yield EM → EEM ↑"}, + {"id":"m_vix", "label":"Niveau VIX", "node_type":"input_manual","category":"sentiment","unit":"pts","coefficient_to_pips":-0.5,"display_col":1,"description":"VIX spike → sorties EM → EEM ↓"}, + {"id":"layer_dollar","label":"▶ Dollar & Taux US","node_type":"intermediate","category":"intermediate","unit":"pips","display_col":2, + "formula":"in_cb + in_macro + m_dxy_inv + m_us_real_rate + m_fed_path", + "description":"Dollar et taux réels = driver macro principal des EM."}, + {"id":"layer_china","label":"▶ Chine & Croissance EM","node_type":"intermediate","category":"intermediate","unit":"pips","display_col":2, + "formula":"m_china_pmi + m_china_stimulus + in_growth + m_commodity_index + in_commodity", + "description":"Moteur Chine + exportateurs commodités."}, + {"id":"layer_risk","label":"▶ Risque & Flux EM","node_type":"intermediate","category":"intermediate","unit":"pips","display_col":2, + "formula":"in_geo + in_credit + in_trade + m_em_spread + m_risk_appetite + m_vix + in_sentiment + in_technical", + "description":"Prime de risque EM, flux capitaux, sentiment global."}, + {"id":"eem","label":"EEM — Impact Net","node_type":"output","category":"output","unit":"pips","display_col":3, + "formula":"layer_dollar + layer_china + layer_risk","description":"Pression nette cumulée EEM"}, ] }, -# ═══════════════════════════════════════════════════════════════════════════════ +# ══════════════════════════════════════════════════════════════════════════════ "QQQ": { "name": "QQQ (NASDAQ-100 Tech)", "output_node": "qqq", - "description": "ETF NASDAQ-100 — tech et croissance US, sensible aux taux réels et bénéfices tech", + "description": "Tech US — taux réels, bénéfices big tech, IA, réglementation", "nodes": [ - {"id":"us_real_rate_10y","label":"Taux réel US 10Y (duration tech)","type":"structural","category":"monetary","unit":"bps","coefficient":-1.5, - "description":"PRINCIPAL driver : tech = duration longue (flux futurs). Taux réels ↑ → actualisation ↑ → PE tech ↓ → QQQ ↓."}, - {"id":"fed_path_12m","label":"Anticipation Fed 12m","type":"structural","category":"monetary","unit":"bps","coefficient":-0.8, - "description":"Cuts attendus → taux discount ↓ → PE tech expansion → QQQ ↑."}, - {"id":"big_tech_eps_revision","label":"Révisions BPA Big Tech (M7)","type":"structural","category":"earnings","unit":"%","coefficient":15.0, - "description":"Révisions bénéfices Magnificent 7 (AAPL, MSFT, NVDA, GOOGL, AMZN, META, TSLA). +1% ≈ +15 pts QQQ."}, - {"id":"ai_capex_cycle","label":"Cycle capex IA","type":"structural","category":"tech","unit":"score","coefficient":0.8, - "description":"Score momentum investissements IA (Hyperscalers capex, NVDA demande GPU). Positif = QQQ ↑."}, - {"id":"semiconductor_cycle","label":"Cycle semi-conducteurs","type":"structural","category":"tech","unit":"score","coefficient":0.6, - "description":"Phase cycle semis (book-to-bill, inventaires, commandes). Upcycle = QQQ ↑."}, - {"id":"tech_pe_multiple","label":"Multiple PE tech (NTM)","type":"structural","category":"valuation","unit":"x","coefficient":12.0, - "description":"PE forward NASDAQ-100. Expansion = QQQ ↑. Actuellement élevé → sensible aux déceptions."}, - {"id":"regulation_risk_tech","label":"Risque réglementaire tech","type":"structural","category":"political","unit":"score","coefficient":-0.5, - "description":"Risque antitrust, régulation IA, vie privée (EU AI Act, FTC actions). Hausse → QQQ ↓."}, - {"id":"us_consumer_spending","label":"Dépenses consommateur US","type":"structural","category":"macro","unit":"score","coefficient":0.4, - "description":"Dépenses techno consommateur (iPhone, cloud, publicité). Solide = revenus tech → QQQ ↑."}, - {"id":"cloud_growth_enterprise","label":"Croissance cloud enterprise","type":"structural","category":"tech","unit":"%","coefficient":0.6, - "description":"Croissance AWS/Azure/GCP. Moteur marges operating → QQQ."}, - {"id":"vix_level","label":"Niveau VIX","type":"structural","category":"sentiment","unit":"pts","coefficient":-0.8, - "description":"VIX. Spike → vente tech en premier (beta élevé) → QQQ ↓ plus que SP500."}, - {"id":"retail_options_flow","label":"Flux options retail (call buying)","type":"structural","category":"flows","unit":"score","coefficient":0.4, - "description":"Momentum gamma/delta flows retail sur options tech. FOMO = QQQ squeeze haussier."}, - {"id":"china_tech_risk","label":"Risque restrictions tech US-Chine","type":"structural","category":"political","unit":"score","coefficient":-0.4, - "description":"Restrictions export chips, sanctions tech US-Chine. Hausse → revenus tech ↓ → QQQ ↓."}, - {"id":"ev_central_bank","label":"Pression Banques Centrales","type":"event_driven","category":"central_bank","unit":"pips","description":"Fed pivot → QQQ amplificateur."}, - {"id":"ev_macro_surprise","label":"Surprise Macro","type":"event_driven","category":"monetary_shock","unit":"pips","description":"CPI, NFP → réévaluation Fed → QQQ."}, - {"id":"ev_geopolitical","label":"Risque Géopolitique","type":"event_driven","category":"geopolitical","unit":"pips","description":"Tensions → risk-off → sell tech."}, - {"id":"ev_trade_policy","label":"Choc Commercial","type":"event_driven","category":"trade_policy","unit":"pips","description":"Tarifs tech, restrictions exports."}, - {"id":"ev_growth_shock","label":"Choc Croissance","type":"event_driven","category":"growth_shock","unit":"pips","description":"Récession → dépenses IT coupées → QQQ."}, - {"id":"ev_credit_stress","label":"Stress Crédit","type":"event_driven","category":"credit_stress","unit":"pips","description":"Conditions financières → tech financement."}, - {"id":"ev_commodity","label":"Choc Commodités","type":"event_driven","category":"commodity","unit":"pips","description":"Énergie → data centers coûts."}, - {"id":"ev_sentiment","label":"Sentiment & Positionnement","type":"event_driven","category":"sentiment","unit":"pips","description":"Flux CTAs, hedge funds tech."}, - {"id":"ev_technical","label":"Momentum Technique","type":"event_driven","category":"technical","unit":"pips","description":"Niveaux QQQ, MA200, RSI."}, - {"id":"qqq","label":"QQQ Impact Net","type":"output","category":"output","unit":"pips","description":"Pression nette cumulée QQQ"}, + {"id":"in_cb", "label":"Banques Centrales","node_type":"input_event","category":"central_bank","unit":"pips","display_col":0,"event_category":"central_bank","description":"Fed pivot → QQQ amplificateur (duration longue)."}, + {"id":"in_macro", "label":"Surprises Macro", "node_type":"input_event","category":"monetary_shock","unit":"pips","display_col":0,"event_category":"monetary_shock","description":"CPI, NFP → réévaluation Fed → QQQ."}, + {"id":"in_geo", "label":"Géopolitique", "node_type":"input_event","category":"geopolitical","unit":"pips","display_col":0,"event_category":"geopolitical","description":"Risk-off → vente tech en premier."}, + {"id":"in_trade", "label":"Choc Commercial", "node_type":"input_event","category":"trade_policy","unit":"pips","display_col":0,"event_category":"trade_policy","description":"Restrictions export chips US-Chine."}, + {"id":"in_growth", "label":"Choc Croissance", "node_type":"input_event","category":"growth_shock","unit":"pips","display_col":0,"event_category":"growth_shock","description":"Récession → dépenses IT coupées."}, + {"id":"in_credit", "label":"Stress Crédit", "node_type":"input_event","category":"credit_stress","unit":"pips","display_col":0,"event_category":"credit_stress","description":"Conditions financières → financement tech."}, + {"id":"in_commodity","label":"Choc Commodités", "node_type":"input_event","category":"commodity","unit":"pips","display_col":0,"event_category":"commodity","description":"Énergie → data centers coûts."}, + {"id":"in_sentiment","label":"Sentiment/Flux", "node_type":"input_event","category":"sentiment","unit":"pips","display_col":0,"event_category":"sentiment","description":"CTA, hedge fund tech positions."}, + {"id":"in_technical","label":"Technique", "node_type":"input_event","category":"technical","unit":"pips","display_col":0,"event_category":"technical","description":"QQQ niveaux, MA200, cassures."}, + {"id":"m_real_rate","label":"Taux réel US 10Y (duration)","node_type":"input_manual","category":"monetary","unit":"bps","coefficient_to_pips":-1.5,"display_col":1,"description":"PRINCIPAL DRIVER : tech = duration longue. Taux réels ↑ → PE tech ↓ → QQQ ↓"}, + {"id":"m_fed_path", "label":"Anticipation Fed 12m","node_type":"input_manual","category":"monetary","unit":"bps","coefficient_to_pips":-0.8,"display_col":1,"description":"Cuts → taux discount ↓ → PE tech expansion → QQQ ↑"}, + {"id":"m_m7_eps", "label":"Révisions BPA Magnificent 7","node_type":"input_manual","category":"earnings","unit":"%","coefficient_to_pips":15.0,"display_col":1,"description":"+1% révision M7 ≈ +15 pts QQQ. AAPL, MSFT, NVDA, GOOGL, AMZN, META, TSLA"}, + {"id":"m_ai_capex", "label":"Cycle capex IA","node_type":"input_manual","category":"tech","unit":"score","coefficient_to_pips": 0.8,"display_col":1,"description":"Hyperscalers capex, NVDA GPU demand. Momentum = QQQ ↑"}, + {"id":"m_semi_cycle","label":"Cycle semi-conducteurs","node_type":"input_manual","category":"tech","unit":"score","coefficient_to_pips": 0.6,"display_col":1,"description":"Upcycle (book-to-bill, inventaires) = QQQ ↑"}, + {"id":"m_tech_pe", "label":"Multiple PE tech (NTM)","node_type":"input_manual","category":"valuation","unit":"x","coefficient_to_pips":12.0,"display_col":1,"description":"PE forward NASDAQ-100. Expansion = QQQ ↑"}, + {"id":"m_reg_risk", "label":"Risque réglementaire tech","node_type":"input_manual","category":"political","unit":"score","coefficient_to_pips":-0.5,"display_col":1,"description":"Antitrust, EU AI Act, FTC. Hausse → QQQ ↓"}, + {"id":"m_vix", "label":"Niveau VIX", "node_type":"input_manual","category":"sentiment","unit":"pts","coefficient_to_pips":-0.8,"display_col":1,"description":"VIX spike → vente tech (beta élevé) → QQQ ↓ plus que SP500"}, + {"id":"m_retail_options","label":"Flux options retail","node_type":"input_manual","category":"flows","unit":"score","coefficient_to_pips": 0.4,"display_col":1,"description":"FOMO gamma squeeze. Momentum = QQQ ↑ explosif"}, + {"id":"m_cloud_growth","label":"Croissance cloud enterprise","node_type":"input_manual","category":"tech","unit":"%","coefficient_to_pips": 0.6,"display_col":1,"description":"AWS/Azure/GCP. Marges operating tech → QQQ"}, + {"id":"m_china_tech_risk","label":"Restrictions tech US-Chine","node_type":"input_manual","category":"political","unit":"score","coefficient_to_pips":-0.4,"display_col":1,"description":"Export bans, sanctions. Revenus tech ↓ → QQQ ↓"}, + {"id":"layer_rates","label":"▶ Taux & Valorisation","node_type":"intermediate","category":"intermediate","unit":"pips","display_col":2, + "formula":"in_cb + in_macro + m_real_rate + m_fed_path + m_tech_pe", + "description":"Taux réels = principal driver de la valorisation tech via taux d'actualisation."}, + {"id":"layer_tech_fundamental","label":"▶ Fondamentaux Tech","node_type":"intermediate","category":"intermediate","unit":"pips","display_col":2, + "formula":"m_m7_eps + m_ai_capex + m_semi_cycle + m_cloud_growth", + "description":"BPA Magnificent 7, cycle IA, semis, cloud. Foundation long terme."}, + {"id":"layer_risk","label":"▶ Risque & Réglementation","node_type":"intermediate","category":"intermediate","unit":"pips","display_col":2, + "formula":"in_geo + in_credit + in_growth + in_trade + in_commodity + m_reg_risk + m_china_tech_risk + m_vix", + "description":"Risques systémiques, réglementaires, géopolitiques tech."}, + {"id":"layer_flows","label":"▶ Flux & Momentum","node_type":"intermediate","category":"intermediate","unit":"pips","display_col":2, + "formula":"in_sentiment + in_technical + m_retail_options", + "description":"Flux spéculatifs, options retail, momentum technique."}, + {"id":"qqq","label":"QQQ — Impact Net","node_type":"output","category":"output","unit":"pips","display_col":3, + "formula":"layer_rates + layer_tech_fundamental + layer_risk + layer_flows","description":"Pression nette cumulée QQQ"}, ] }, } # end INSTRUMENT_MODELS -# ── Category labels (French) ──────────────────────────────────────────────────── -CAT_LABELS: dict[str, str] = { - "monetary": "Monétaire", - "inflation": "Inflation", - "macro": "Macro / Croissance", - "political": "Politique", - "flows": "Flux & Réserves", - "positioning": "Positionnement", - "sentiment": "Sentiment & Risque", - "credit": "Crédit", - "earnings": "Bénéfices", - "valuation": "Valorisation", - "tech": "Technologie", - "commodity": "Commodités", - "supply": "Offre", - "output": "Résultat", - # event-driven categories (from causal_graph_templates.category) - "central_bank": "Banques Centrales", - "monetary_shock": "Surprise Macro", - "geopolitical": "Géopolitique", - "trade_policy": "Commerce / Tarifs", - "growth_shock": "Choc Croissance", - "commodity": "Commodités", - "credit_stress": "Stress Crédit", - "sentiment": "Sentiment", - "technical": "Technique", - "positioning": "Positionnement", - "unclassified": "Non Classifié", -} - - -# ── DB init ───────────────────────────────────────────────────────────────────── +# ── DB ───────────────────────────────────────────────────────────────────────── def init_instrument_model_tables(conn): conn.executescript(""" CREATE TABLE IF NOT EXISTS instrument_models ( - id INTEGER PRIMARY KEY, - instrument TEXT UNIQUE NOT NULL, - graph_json TEXT NOT NULL, - updated_at TEXT DEFAULT (datetime('now')) + id INTEGER PRIMARY KEY, + instrument TEXT UNIQUE NOT NULL, + graph_json TEXT NOT NULL, + updated_at TEXT DEFAULT (datetime('now')) ); CREATE TABLE IF NOT EXISTS instrument_node_overrides ( - id INTEGER PRIMARY KEY, - instrument TEXT NOT NULL, - node_id TEXT NOT NULL, - value REAL NOT NULL, - note TEXT, - set_at TEXT DEFAULT (datetime('now')), + id INTEGER PRIMARY KEY, + instrument TEXT NOT NULL, + node_id TEXT NOT NULL, + value REAL NOT NULL, + note TEXT, + set_at TEXT DEFAULT (datetime('now')), UNIQUE(instrument, node_id) ); """) @@ -451,46 +426,35 @@ def init_instrument_model_tables(conn): def seed_instrument_models(conn): - """Insert/update instrument models on startup. Never overwrites user overrides.""" + """Seed/update models on startup. Safe to call repeatedly.""" init_instrument_model_tables(conn) for inst, model in INSTRUMENT_MODELS.items(): - existing = conn.execute( - "SELECT id FROM instrument_models WHERE instrument=?", (inst,) - ).fetchone() graph_json = json.dumps({ - "name": model["name"], + "name": model["name"], "description": model["description"], "output_node": model["output_node"], - "nodes": model["nodes"], + "nodes": model["nodes"], }) - if existing: - conn.execute( - "UPDATE instrument_models SET graph_json=?, updated_at=datetime('now') WHERE instrument=?", - (graph_json, inst) - ) - else: - conn.execute( - "INSERT INTO instrument_models (instrument, graph_json) VALUES (?,?)", - (inst, graph_json) - ) + conn.execute(""" + INSERT INTO instrument_models (instrument, graph_json) + VALUES (?,?) + ON CONFLICT(instrument) DO UPDATE SET graph_json=excluded.graph_json, updated_at=datetime('now') + """, (inst, graph_json)) conn.commit() -# ── Value computation ─────────────────────────────────────────────────────────── +# ── Event contribution by category (with lifecycle) ──────────────────────────── -def _compute_event_driven_values(conn, instrument: str, ref_date: date_type) -> dict[str, float]: - """ - Pour chaque catégorie event-driven : somme des pips × decay sur toutes les analyses actives. - Retourne {category: float} en pips. - """ - extended_from = ref_date - timedelta(days=180) +def _compute_event_by_category(conn, instrument: str, ref_date: date_type) -> dict[str, float]: + """Sum active event contributions per template category, applying lifecycle.""" inst_upper = instrument.upper() + inst_lower = inst_upper.lower() + extended_from = ref_date - timedelta(days=365) rows = conn.execute(""" SELECT a.prediction_json, - e.start_date, e.end_date AS event_end_date, e.sub_type, - t.category, - t.calibration_json + e.start_date, e.end_date, e.sub_type, + t.category, t.calibration_json FROM causal_event_analyses a JOIN market_events e ON e.id = a.market_event_id JOIN causal_graph_templates t ON t.id = a.template_id @@ -500,8 +464,6 @@ def _compute_event_driven_values(conn, instrument: str, ref_date: date_type) -> """, (inst_upper, str(extended_from), str(ref_date))).fetchall() by_cat: dict[str, float] = {} - inst_lower = inst_upper.lower() - for row in rows: r = dict(row) try: @@ -518,21 +480,22 @@ def _compute_event_driven_values(conn, instrument: str, ref_date: date_type) -> if inst_lower in k.lower(): try: pips = float(v); break except (TypeError, ValueError): pass - if pips is None or pips == 0: continue absorption = max(1, int(calib.get("absorption_days", 7))) dtype = str(calib.get("decay_type", "exp")) + rise = max(0, int(calib.get("rise_days", 0))) + plateau = max(0, int(calib.get("plateau_days", 0))) - # Guidance events: dynamic absorption until meeting date - ev_end = r.get("event_end_date") + # Guidance events: dynamic absorption until meeting + ev_end = r.get("end_date") if ev_end and str(r.get("sub_type", "")).startswith("rate_guidance"): try: - meeting = date_type.fromisoformat(ev_end[:10]) + meeting = date_type.fromisoformat(ev_end[:10]) ev_start = date_type.fromisoformat(r["start_date"][:10]) absorption = max(1, (meeting - ev_start).days) - dtype = "linear" + dtype = "linear"; rise = 0; plateau = 0 except ValueError: pass @@ -541,21 +504,65 @@ def _compute_event_driven_values(conn, instrument: str, ref_date: date_type) -> except ValueError: continue - days_elapsed = (ref_date - ev_date).days - df = _decay(days_elapsed, absorption, dtype) + days = (ref_date - ev_date).days + df = _lifecycle(days, rise, plateau, absorption, dtype) if df < 0.01: continue cat = r["category"] - by_cat[cat] = by_cat.get(cat, 0.0) + round(pips * df, 2) + by_cat[cat] = round(by_cat.get(cat, 0.0) + pips * df, 2) return by_cat +# ── Graph evaluation ─────────────────────────────────────────────────────────── + +def _build_inputs(graph_def: dict, overrides: dict, ev_by_cat: dict) -> dict: + """ + Build the inputs dict for evaluate_graph(): + - input_event nodes → value from event contributions (category mapping) + - input_manual nodes → user_value × coefficient_to_pips + Any node with an override uses that value directly (already in pips for events; + for manual nodes the override IS the native-unit value → converted to pips). + """ + inputs: dict[str, float] = {} + for node in graph_def["nodes"]: + nid = node["id"] + ntype = node.get("node_type", "") + ov = overrides.get(nid) + + if ntype == "input_event": + if ov: + inputs[nid] = float(ov["value"]) # override IS pips + else: + cat = node.get("event_category", "") + inputs[nid] = ev_by_cat.get(cat, 0.0) + + elif ntype == "input_manual": + if ov: + coeff = float(node.get("coefficient_to_pips", 1.0)) + inputs[nid] = float(ov["value"]) * coeff + else: + inputs[nid] = 0.0 # neutral + + return inputs + + +def _graph_json_for_eval(graph_def: dict) -> dict: + """Convert our model graph_def to the format expected by evaluate_graph().""" + nodes = [] + for n in graph_def["nodes"]: + entry: dict = {"id": n["id"]} + if n.get("formula"): + entry["formula"] = n["formula"] + nodes.append(entry) + return {"nodes": nodes, "coefficients": {}} + + +# ── Public API ───────────────────────────────────────────────────────────────── + def get_model_state(conn, instrument: str, at_date: Optional[str] = None) -> Optional[dict]: - """ - Retourne le graphe instrument avec les valeurs courantes de chaque nœud. - """ + """Full model state: all node values computed via DAG evaluation.""" inst_upper = instrument.upper() row = conn.execute( "SELECT graph_json FROM instrument_models WHERE instrument=?", (inst_upper,) @@ -563,107 +570,202 @@ def get_model_state(conn, instrument: str, at_date: Optional[str] = None) -> Opt if not row: return None - graph = json.loads(row["graph_json"]) + graph_def = json.loads(row["graph_json"]) try: ref_date = date_type.fromisoformat(at_date) if at_date else datetime.utcnow().date() except ValueError: ref_date = datetime.utcnow().date() - # Load user overrides - override_rows = conn.execute( + overrides = {r["node_id"]: dict(r) for r in conn.execute( "SELECT node_id, value, note, set_at FROM instrument_node_overrides WHERE instrument=?", (inst_upper,) - ).fetchall() - overrides: dict[str, dict] = {r["node_id"]: dict(r) for r in override_rows} + ).fetchall()} - # Compute event-driven values by category - ev_by_cat = _compute_event_driven_values(conn, inst_upper, ref_date) + ev_by_cat = _compute_event_by_category(conn, inst_upper, ref_date) + inputs = _build_inputs(graph_def, overrides, ev_by_cat) + + # DAG evaluation (propagates through intermediate nodes via formulas) + from services.causal_graphs import evaluate_graph + gj = _graph_json_for_eval(graph_def) + all_vals = evaluate_graph(gj, inputs) + + output_id = graph_def["output_node"] + net_pips = round(float(all_vals.get(output_id, 0.0)), 1) - # Build node states - net_pips = 0.0 nodes_out = [] + for node in graph_def["nodes"]: + nid = node["id"] + ntype = node.get("node_type", "") + val = round(float(all_vals.get(nid, 0.0)), 1) + ov = overrides.get(nid) + state = dict(node) + state["computed_value"] = val + state["pip_contribution"] = val - for node in graph["nodes"]: - nid = node["id"] - ntype = node["type"] - coeff = node.get("coefficient", 1.0) - cat = node["category"] - state = dict(node) - - if ntype == "structural": - ov = overrides.get(nid) + if ntype == "input_event": + cat = node.get("event_category", "") + state["source"] = "manual" if ov else ("events" if val != 0.0 else "neutral") + state["raw_value"] = ov["value"] if ov else round(ev_by_cat.get(cat, 0.0), 1) if ov: - state["current_value"] = ov["value"] - state["pip_contribution"] = round(ov["value"] * coeff, 1) - state["source"] = "manual" - state["override_note"] = ov.get("note", "") + state["override_note"] = ov.get("note", "") state["override_set_at"] = ov.get("set_at", "") - else: - state["current_value"] = 0.0 - state["pip_contribution"] = 0.0 - state["source"] = "neutral" - net_pips += state["pip_contribution"] - elif ntype == "event_driven": - pips = ev_by_cat.get(cat, 0.0) - # manual override possible too - ov = overrides.get(nid) + elif ntype == "input_manual": + state["source"] = "manual" if ov else "neutral" + state["raw_value"] = ov["value"] if ov else 0.0 # in native unit if ov: - pips = ov["value"] - state["source"] = "manual" - state["override_note"] = ov.get("note", "") - else: - state["source"] = "events" if pips != 0 else "neutral" - state["current_value"] = round(pips, 1) - state["pip_contribution"] = round(pips, 1) - net_pips += pips + state["override_note"] = ov.get("note", "") + state["override_set_at"] = ov.get("set_at", "") - elif ntype == "output": - state["current_value"] = None # filled after loop - state["pip_contribution"] = None + elif ntype in ("intermediate", "output"): state["source"] = "computed" nodes_out.append(state) - net_pips = round(net_pips, 1) - - # Fill output node - for n in nodes_out: - if n["type"] == "output": - n["current_value"] = net_pips - n["pip_contribution"] = net_pips - direction = "bullish" if net_pips > 5 else "bearish" if net_pips < -5 else "neutral" return { "instrument": inst_upper, - "name": graph["name"], - "description": graph.get("description", ""), + "name": graph_def["name"], + "description": graph_def.get("description", ""), "at_date": str(ref_date), "net_pips": net_pips, "direction": direction, "nodes": nodes_out, - "output_node": graph["output_node"], + "output_node": output_id, } -def set_node_override(conn, instrument: str, node_id: str, value: float, note: str = "") -> bool: +def simulate_timeline( + conn, instrument: str, period: str = "1y" +) -> list[dict]: + """ + Simulate all node values day by day over the period. + Returns [{date, nodes: {id: value}, net_pips}]. + Uses lifecycle (rise/plateau/decay) for event contributions. + Manual overrides are static (applied uniformly across the period). + """ 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"]) + output_id = graph_def["output_node"] + + period_days = {"5d":7,"1mo":35,"3mo":95,"6mo":190,"1y":370,"2y":740} + lookback = period_days.get(period, 370) + today = datetime.utcnow().date() + date_from = today - timedelta(days=lookback) + + # Load overrides (static) + overrides = {r["node_id"]: dict(r) for r in conn.execute( + "SELECT node_id, value, note, set_at FROM instrument_node_overrides WHERE instrument=?", + (inst_upper,) + ).fetchall()} + + # Load ALL relevant events (extended lookback for decay) + inst_lower = inst_upper.lower() + extended = date_from - timedelta(days=365) + rows = conn.execute(""" + SELECT a.prediction_json, e.start_date, e.end_date, e.sub_type, + t.category, t.calibration_json + FROM causal_event_analyses a + JOIN market_events e ON e.id = a.market_event_id + JOIN causal_graph_templates t ON t.id = a.template_id + WHERE a.instrument=? AND e.start_date>=? AND e.start_date<=? + """, (inst_upper, str(extended), str(today))).fetchall() + + # Pre-parse events + events = [] + for r in rows: + r = dict(r) + try: + preds = json.loads(r["prediction_json"] or "{}") + calib = json.loads(r["calibration_json"] or "{}") + except Exception: + continue + pips: Optional[float] = None + if inst_lower in preds: + pips = float(preds[inst_lower]) + else: + for k, v in preds.items(): + if inst_lower in k.lower(): + try: pips = float(v); break + except (TypeError, ValueError): pass + if not pips: + continue + absorption = max(1, int(calib.get("absorption_days", 7))) + dtype = str(calib.get("decay_type", "exp")) + rise = max(0, int(calib.get("rise_days", 0))) + plateau = max(0, int(calib.get("plateau_days", 0))) + ev_end = r.get("end_date") + if ev_end and str(r.get("sub_type","")).startswith("rate_guidance"): + try: + meeting = date_type.fromisoformat(ev_end[:10]) + ev_start = date_type.fromisoformat(r["start_date"][:10]) + absorption = max(1, (meeting - ev_start).days) + dtype = "linear"; rise = 0; plateau = 0 + except ValueError: + pass + try: + ev_date = date_type.fromisoformat(r["start_date"][:10]) + except ValueError: + continue + events.append({ + "ev_date": ev_date, "category": r["category"], "pips": pips, + "rise": rise, "plateau": plateau, "absorption": absorption, "dtype": dtype, + }) + + from services.causal_graphs import evaluate_graph + gj = _graph_json_for_eval(graph_def) + + timeline = [] + cur = date_from + while cur <= today: + # Event contributions for this day + ev_by_cat: dict[str, float] = {} + for ev in events: + if ev["ev_date"] > cur: + continue + days = (cur - ev["ev_date"]).days + df = _lifecycle(days, ev["rise"], ev["plateau"], ev["absorption"], ev["dtype"]) + if df < 0.01: + continue + cat = ev["category"] + ev_by_cat[cat] = ev_by_cat.get(cat, 0.0) + round(ev["pips"] * df, 2) + + inputs = _build_inputs(graph_def, overrides, ev_by_cat) + vals = evaluate_graph(gj, inputs) + net = round(float(vals.get(output_id, 0.0)), 1) + + timeline.append({ + "date": str(cur), + "net_pips": net, + "nodes": {k: round(float(v), 1) for k, v in vals.items()}, + }) + cur += timedelta(days=1) + + return timeline + + +def set_node_override(conn, instrument: str, node_id: str, value: float, note: str = "") -> bool: conn.execute(""" INSERT INTO instrument_node_overrides (instrument, node_id, value, note, set_at) VALUES (?,?,?,?,datetime('now')) ON CONFLICT(instrument, node_id) DO UPDATE SET value=excluded.value, note=excluded.note, set_at=datetime('now') - """, (inst_upper, node_id, value, note)) + """, (instrument.upper(), node_id, value, note)) conn.commit() return True def clear_node_override(conn, instrument: str, node_id: str) -> bool: - inst_upper = instrument.upper() conn.execute( "DELETE FROM instrument_node_overrides WHERE instrument=? AND node_id=?", - (inst_upper, node_id) + (instrument.upper(), node_id) ) conn.commit() return True diff --git a/frontend/src/pages/InstrumentModels.tsx b/frontend/src/pages/InstrumentModels.tsx index 4ffad79..52b8be9 100644 --- a/frontend/src/pages/InstrumentModels.tsx +++ b/frontend/src/pages/InstrumentModels.tsx @@ -1,500 +1,634 @@ /** * InstrumentModels — graphes causaux exhaustifs par instrument. - * Vue tableau + graphe, nœuds éditables, filtres catégorie/type. + * Architecture 3 couches : inputs (events + manuels) → intermédiaires → output + * Vues : DAG visuel | Tableau éditable | Timeline évolution */ -import { useEffect, useState, useCallback, useRef } from 'react' -import axios from 'axios' +import { useState, useEffect, useCallback, useMemo, useRef } from 'react' +import { + RefreshCw, Edit3, X, Trash2, ChevronDown, ChevronUp, + TrendingUp, TrendingDown, Minus, LineChart, Table2, Network, +} from 'lucide-react' import clsx from 'clsx' +import axios from 'axios' const api = axios.create({ baseURL: '/api' }) -// ── Types ────────────────────────────────────────────────────────────────────── +// ── Types ───────────────────────────────────────────────────────────────────── + +type NodeType = 'input_event' | 'input_manual' | 'intermediate' | 'output' interface ModelNode { - id: string; label: string; type: 'structural' | 'event_driven' | 'output' - category: string; unit: string; description: string - coefficient?: number - current_value: number | null; pip_contribution: number | null + id: string + label: string + node_type: NodeType + category: string + unit: string + description: string + coefficient_to_pips?: number + event_category?: string + formula?: string + display_col: number + // computed + computed_value: number + pip_contribution: number + raw_value?: number source: 'manual' | 'events' | 'neutral' | 'computed' - override_note?: string; override_set_at?: string + override_note?: string + override_set_at?: string } + interface ModelState { - instrument: string; name: string; description: string - at_date: string; net_pips: number; direction: 'bullish' | 'bearish' | 'neutral' - nodes: ModelNode[]; output_node: string + instrument: string + name: string + description: string + at_date: string + net_pips: number + direction: 'bullish' | 'bearish' | 'neutral' + nodes: ModelNode[] + output_node: string } -interface ModelMeta { - instrument: string; name: string; description: string - n_structural: number; n_event_driven: number + +interface TimelinePoint { + date: string + net_pips: number + nodes: Record } // ── Constants ───────────────────────────────────────────────────────────────── const INSTRUMENTS = ['EURUSD','USDJPY','XAUUSD','SP500','TLT','GBPUSD','EEM','QQQ'] -const CAT_COLOR: Record = { - monetary: 'bg-blue-900/40 text-blue-300 border-blue-700/40', - inflation: 'bg-orange-900/40 text-orange-300 border-orange-700/40', - macro: 'bg-teal-900/40 text-teal-300 border-teal-700/40', - political: 'bg-purple-900/40 text-purple-300 border-purple-700/40', - flows: 'bg-cyan-900/40 text-cyan-300 border-cyan-700/40', - positioning: 'bg-indigo-900/40 text-indigo-300 border-indigo-700/40', - sentiment: 'bg-pink-900/40 text-pink-300 border-pink-700/40', - credit: 'bg-red-900/40 text-red-300 border-red-700/40', - earnings: 'bg-emerald-900/40 text-emerald-300 border-emerald-700/40', - valuation: 'bg-lime-900/40 text-lime-300 border-lime-700/40', - tech: 'bg-violet-900/40 text-violet-300 border-violet-700/40', - commodity: 'bg-amber-900/40 text-amber-300 border-amber-700/40', - supply: 'bg-stone-900/40 text-stone-300 border-stone-700/40', - // event-driven categories - central_bank: 'bg-blue-900/40 text-blue-300 border-blue-700/40', - monetary_shock: 'bg-teal-900/40 text-teal-300 border-teal-700/40', - geopolitical: 'bg-red-900/40 text-red-300 border-red-700/40', - trade_policy: 'bg-orange-900/40 text-orange-300 border-orange-700/40', - growth_shock: 'bg-emerald-900/40 text-emerald-300 border-emerald-700/40', - credit_stress: 'bg-rose-900/40 text-rose-300 border-rose-700/40', - technical: 'bg-slate-700/40 text-slate-300 border-slate-600/40', - output: 'bg-violet-900/40 text-violet-300 border-violet-700/40', +const NODE_TYPE_META: Record = { + input_event: { label: 'Events', color: 'text-sky-400', bg: 'bg-sky-900/30 border-sky-700/40' }, + input_manual: { label: 'Manuel', color: 'text-violet-400', bg: 'bg-violet-900/30 border-violet-700/40' }, + intermediate: { label: 'Intermédiaire', color: 'text-amber-400', bg: 'bg-amber-900/20 border-amber-700/40' }, + output: { label: 'Résultat', color: 'text-emerald-400', bg: 'bg-emerald-900/20 border-emerald-700/40' }, } -const CAT_LABELS: Record = { - monetary:'Monétaire', inflation:'Inflation', macro:'Macro/Croissance', - political:'Politique', flows:'Flux & Réserves', positioning:'Positionnement', - sentiment:'Sentiment', credit:'Crédit', earnings:'Bénéfices', - valuation:'Valorisation', tech:'Technologie', commodity:'Commodités', - supply:'Offre', output:'Résultat', - central_bank:'Banques Centrales', monetary_shock:'Surprise Macro', - geopolitical:'Géopolitique', trade_policy:'Commerce', growth_shock:'Croissance', - credit_stress:'Stress Crédit', technical:'Technique', +const SOURCE_META: Record = { + manual: { dot: 'bg-violet-400', label: 'Override' }, + events: { dot: 'bg-sky-400', label: 'Events' }, + neutral: { dot: 'bg-slate-600', label: 'Neutre' }, + computed: { dot: 'bg-amber-400', label: 'Calculé' }, } -function catBadge(cat: string) { - const cls = CAT_COLOR[cat] ?? 'bg-slate-700/40 text-slate-400 border-slate-600/40' +const PERIODS = ['5d','1mo','3mo','6mo','1y','2y'] as const +type Period = typeof PERIODS[number] + +const LAYER_COLORS: Record = { + 'net_pips': 'bg-emerald-500', + 'layer_monetary': 'bg-blue-400', + 'layer_rates': 'bg-blue-400', + 'layer_growth': 'bg-violet-400', + 'layer_uk_macro': 'bg-violet-400', + 'layer_risk': 'bg-red-400', + 'layer_risk_credit': 'bg-red-400', + 'layer_positioning': 'bg-orange-400', + 'layer_policy': 'bg-orange-400', + 'layer_flows': 'bg-orange-400', + 'layer_demand': 'bg-teal-400', + 'layer_refuge': 'bg-pink-400', + 'layer_tech_fundamental': 'bg-purple-400', + 'layer_supply': 'bg-slate-400', + 'layer_china': 'bg-yellow-400', + 'layer_dollar': 'bg-sky-400', + 'layer_global': 'bg-gray-400', + 'layer_macro': 'bg-lime-400', +} + +const CANVAS_COLORS: Record = { + 'net_pips': '#10b981', + 'layer_monetary': '#60a5fa', + 'layer_rates': '#60a5fa', + 'layer_growth': '#a78bfa', + 'layer_uk_macro': '#a78bfa', + 'layer_risk': '#f87171', + 'layer_risk_credit': '#f87171', + 'layer_positioning': '#fb923c', + 'layer_policy': '#fb923c', + 'layer_flows': '#fb923c', + 'layer_demand': '#2dd4bf', + 'layer_refuge': '#f472b6', + 'layer_tech_fundamental': '#c084fc', + 'layer_supply': '#94a3b8', + 'layer_china': '#fbbf24', + 'layer_dollar': '#38bdf8', + 'layer_global': '#cbd5e1', + 'layer_macro': '#a3e635', +} + +// ── Helpers ─────────────────────────────────────────────────────────────────── + +function pipColor(v: number) { + if (v > 0.5) return 'text-emerald-400' + if (v < -0.5) return 'text-red-400' + return 'text-slate-400' +} + +function pipBg(v: number) { + if (v > 0.5) return 'bg-emerald-500' + if (v < -0.5) return 'bg-red-500' + return 'bg-slate-600' +} + +function fmt(v: number) { + const s = v > 0 ? '+' : '' + return `${s}${v.toFixed(1)}` +} + +function DirectionBadge({ direction, pips }: { direction: string; pips: number }) { + const icon = + direction === 'bullish' ? : + direction === 'bearish' ? : + + const cls = + direction === 'bullish' ? 'text-emerald-400 border-emerald-700/50 bg-emerald-900/30' : + direction === 'bearish' ? 'text-red-400 border-red-700/50 bg-red-900/30' : + 'text-slate-400 border-slate-700/40 bg-slate-800/50' return ( - - {CAT_LABELS[cat] ?? cat} + + {icon} {fmt(pips)} pips · {direction.toUpperCase()} ) } -function pipColor(v: number | null) { - if (v === null) return 'text-slate-500' - if (v > 0) return 'text-emerald-400' - if (v < 0) return 'text-red-400' - return 'text-slate-500' -} +// ── Node Edit Modal ─────────────────────────────────────────────────────────── -function fmt(v: number | null, unit?: string) { - if (v === null) return '—' - const s = (v > 0 ? '+' : '') + v - return unit ? `${s} ${unit}` : s -} - -// ── Node Edit Modal ──────────────────────────────────────────────────────────── - -function NodeEditModal({ - node, instrument, onClose, onSaved -}: { - node: ModelNode; instrument: string - onClose: () => void; onSaved: () => void +function NodeEditModal({ node, instrument, onClose, onSaved }: { + node: ModelNode; instrument: string; onClose: () => void; onSaved: () => void }) { - const [val, setVal] = useState(String(node.current_value ?? 0)) - const [note, setNote] = useState(node.override_note ?? '') + const [val, setVal] = useState(node.raw_value !== undefined ? String(node.raw_value) : '') + const [note, setNote] = useState(node.override_note || '') const [saving, setSaving] = useState(false) - const [error, setError] = useState('') + const [clrg, setClrg] = useState(false) - const hasOverride = node.source === 'manual' + const coeff = node.coefficient_to_pips ?? 1.0 + const preview = node.node_type === 'input_manual' + ? (parseFloat(val) || 0) * coeff + : (parseFloat(val) || 0) async function save() { - const num = parseFloat(val) - if (isNaN(num)) { setError('Valeur invalide'); return } setSaving(true) try { - await api.put(`/instrument-models/${instrument}/nodes/${node.id}/override`, { value: num, note }) - onSaved() - } catch { setError('Erreur lors de la sauvegarde') } - finally { setSaving(false) } + await api.put(`/instrument-models/${instrument}/nodes/${node.id}/override`, { + value: parseFloat(val) || 0, note + }) + onSaved(); onClose() + } finally { setSaving(false) } } async function clear() { - setSaving(true) + setClrg(true) try { await api.delete(`/instrument-models/${instrument}/nodes/${node.id}/override`) - onSaved() - } catch { setError('Erreur') } - finally { setSaving(false) } + onSaved(); onClose() + } finally { setClrg(false) } } - const coeff = node.coefficient ?? 1 - const preview = node.type === 'structural' ? parseFloat(val || '0') * coeff : parseFloat(val || '0') - return (
{ if (e.target === e.currentTarget) onClose() }}> -
-
-
-
{node.label}
-
{catBadge(node.category)} - {node.unit} -
+ onClick={onClose}> +
e.stopPropagation()}> +
+
+
{node.label}
+
{node.description}
- +
-

{node.description}

- - {node.type === 'structural' && ( -
- Coefficient: {coeff} {node.unit}/pips - → Impact: {preview >= 0 ? '+' : ''}{preview.toFixed(1)} pips -
- )} - - {node.type === 'event_driven' && ( -
- {node.source === 'events' - ? `Valeur auto depuis events actifs: ${fmt(node.current_value, 'pips')}` - : 'Aucun event actif (0 pips auto). Override possible.'} -
- )} -
- + setVal(e.target.value)} - className="w-full bg-dark-900 border border-slate-600/60 rounded-lg px-3 py-2 text-sm text-slate-200 focus:border-violet-500/60 focus:outline-none" - autoFocus + type="number" step="any" value={val} onChange={e => setVal(e.target.value)} + placeholder="0" + className="w-full bg-dark-700 border border-slate-700/50 rounded px-3 py-2 text-white text-sm focus:outline-none focus:border-blue-500/60" />
setNote(e.target.value)} - placeholder="Ex: Anticipation Fed -50bps d'ici fin 2026" - className="w-full bg-dark-900 border border-slate-600/60 rounded-lg px-3 py-2 text-sm text-slate-200 focus:border-violet-500/60 focus:outline-none" + type="text" value={note} onChange={e => setNote(e.target.value)} + placeholder="Ex: NFP très fort, marché hawkish…" + className="w-full bg-dark-700 border border-slate-700/50 rounded px-3 py-2 text-white text-sm focus:outline-none focus:border-blue-500/60" />
- {error &&

{error}

} + {val !== '' && ( +
0 ? 'bg-emerald-900/30 text-emerald-400' + : preview < 0 ? 'bg-red-900/30 text-red-400' + : 'bg-slate-800 text-slate-400')}> + Impact estimé : {fmt(preview)} pips +
+ )} -
- - {hasOverride && ( - )} -
- - {hasOverride && node.override_set_at && ( -

- Override défini le {node.override_set_at?.slice(0,10)} - {node.override_note ? ` — "${node.override_note}"` : ''} -

- )}
) } -// ── Graph View (SVG DAG) ─────────────────────────────────────────────────────── +// ── DAG View ────────────────────────────────────────────────────────────────── -function GraphView({ model, onNodeClick }: { model: ModelState; onNodeClick: (n: ModelNode) => void }) { - const structural = model.nodes.filter(n => n.type === 'structural') - const eventDriven = model.nodes.filter(n => n.type === 'event_driven') - const outputNode = model.nodes.find(n => n.type === 'output') - - const NODE_W = 190, NODE_H = 46, GAP_Y = 10 - const COL_X = { structural: 20, event: 240, output: 460 } - const SVG_W = 660 - const leftH = structural.length * (NODE_H + GAP_Y) - const rightH = eventDriven.length * (NODE_H + GAP_Y) - const SVG_H = Math.max(leftH, rightH, 120) + 40 - - function nodeY(arr: ModelNode[], i: number, totalH: number) { - const groupH = arr.length * (NODE_H + GAP_Y) - const startY = (totalH - groupH) / 2 + 20 - return startY + i * (NODE_H + GAP_Y) - } - - const outputY = SVG_H / 2 - NODE_H / 2 - const outX = COL_X.output - const outCX = outX + NODE_W / 2 +const COL_LABELS = ['Events actifs', 'Variables manuelles', 'Couches intermédiaires', 'Résultat net'] +function NodeCard({ node, onEdit }: { node: ModelNode; onEdit: (n: ModelNode) => void }) { + const meta = NODE_TYPE_META[node.node_type] + const v = node.pip_contribution + const canEdit = node.node_type === 'input_event' || node.node_type === 'input_manual' return ( -
- - {/* Structural → output edges */} - {structural.map((n, i) => { - const y = nodeY(structural, i, SVG_H) - const cx = COL_X.structural + NODE_W - const cy = y + NODE_H / 2 - const pips = n.pip_contribution ?? 0 - const color = pips > 0 ? '#34d399' : pips < 0 ? '#f87171' : '#475569' - return ( - - ) - })} - {/* Event → output edges */} - {eventDriven.map((n, i) => { - const y = nodeY(eventDriven, i, SVG_H) - const cx = COL_X.event + NODE_W - const cy = y + NODE_H / 2 - const pips = n.pip_contribution ?? 0 - const color = pips > 0 ? '#34d399' : pips < 0 ? '#f87171' : '#475569' - return ( - - ) - })} +
canEdit && onEdit(node)} + title={node.description} + > + {node.source === 'manual' && } + {node.source === 'events' && } - {/* Structural nodes */} - {structural.map((n, i) => { - const y = nodeY(structural, i, SVG_H) - const pips = n.pip_contribution ?? 0 - const border = pips > 0 ? '#059669' : pips < 0 ? '#dc2626' : '#334155' - const bg = n.source === 'manual' ? '#1e1b4b' : '#0f172a' - return ( - onNodeClick(n)}> - - {n.source === 'manual' && ( - - )} - {n.label} - 0 ? '#34d399' : pips < 0 ? '#f87171' : '#64748b'} - fontWeight="600"> - {n.source === 'neutral' ? `0 ${n.unit}` : `${pips >= 0 ? '+' : ''}${pips} pips`} - - - ) - })} +
{node.label}
- {/* Event-driven nodes */} - {eventDriven.map((n, i) => { - const y = nodeY(eventDriven, i, SVG_H) - const pips = n.pip_contribution ?? 0 - const border = pips > 0 ? '#059669' : pips < 0 ? '#dc2626' : '#334155' - return ( - onNodeClick(n)}> - - {n.label} - 0 ? '#34d399' : pips < 0 ? '#f87171' : '#64748b'} - fontWeight="600"> - {pips === 0 ? '0 pips' : `${pips >= 0 ? '+' : ''}${pips} pips`} - - - ) - })} +
+ {fmt(v)} pips +
- {/* Output node */} - {outputNode && ( - - 0 ? '#064e3b' : model.net_pips < 0 ? '#450a0a' : '#1e293b'} - stroke={model.net_pips > 0 ? '#059669' : model.net_pips < 0 ? '#dc2626' : '#475569'} - strokeWidth={2} /> - - Impact Net - - 0 ? '#34d399' : model.net_pips < 0 ? '#f87171' : '#94a3b8'} - textAnchor="middle"> - {model.net_pips >= 0 ? '+' : ''}{model.net_pips} pips - - - )} + {Math.abs(v) > 0.05 && ( +
+
+
+ )} - {/* Column labels */} - - FACTEURS STRUCTURELS - - - PRESSIONS ÉVÉNEMENTIELLES - - - RÉSULTAT - - + {node.node_type === 'intermediate' && node.formula && ( +
+ {node.formula.split('+').length} sources +
+ )}
) } -// ── Table View ───────────────────────────────────────────────────────────────── +function DagView({ nodes, instrument, onEdit }: { + nodes: ModelNode[]; instrument: string; onEdit: (n: ModelNode) => void +}) { + const cols: ModelNode[][] = [[], [], [], []] + for (const n of nodes) cols[Math.min(n.display_col ?? 0, 3)].push(n) + + return ( +
+
+ {COL_LABELS.map((lbl, ci) => ( +
+
+ {lbl} + ({cols[ci].length}) +
+ {cols[ci].map(n => )} +
+ ))} +
+ + {/* Legend */} +
+ {(Object.entries(SOURCE_META) as [string, { dot: string; label: string }][]).map(([src, m]) => ( +
+ + {m.label} +
+ ))} +
+ + Cliquer pour modifier les inputs events / manuels +
+
+
+ ) +} + +// ── Table View ──────────────────────────────────────────────────────────────── type SortKey = 'label' | 'category' | 'pip_contribution' | 'source' -type FilterType = 'all' | 'structural' | 'event_driven' -function TableView({ model, onNodeClick }: { model: ModelState; onNodeClick: (n: ModelNode) => void }) { - const [sortKey, setSortKey] = useState('pip_contribution') - const [sortDir, setSortDir] = useState<1 | -1>(-1) - const [filterType, setFilterType] = useState('all') +function TableView({ nodes, onEdit }: { nodes: ModelNode[]; onEdit: (n: ModelNode) => void }) { + const [sort, setSort] = useState('pip_contribution') + const [sortDir, setSortDir] = useState<1 | -1>(-1) + const [filterType, setFilterType] = useState('all') const [filterCat, setFilterCat] = useState('all') + const [expanded, setExpanded] = useState(null) - const cats = Array.from(new Set(model.nodes.map(n => n.category))).filter(c => c !== 'output') + const categories = useMemo(() => { + const s = new Set(nodes.map(n => n.category)) + return ['all', ...Array.from(s).sort()] + }, [nodes]) - function toggleSort(key: SortKey) { - if (sortKey === key) setSortDir(d => d === 1 ? -1 : 1) - else { setSortKey(key); setSortDir(-1) } + const maxAbs = useMemo(() => + Math.max(...nodes.map(n => Math.abs(n.pip_contribution)), 1), [nodes]) + + const sorted = useMemo(() => { + const filtered = nodes.filter(n => + (filterType === 'all' || n.node_type === filterType) && + (filterCat === 'all' || n.category === filterCat) + ) + return [...filtered].sort((a, b) => { + if (sort === 'pip_contribution') return (a.pip_contribution - b.pip_contribution) * sortDir + const av = sort === 'label' ? a.label : sort === 'category' ? a.category : a.source + const bv = sort === 'label' ? b.label : sort === 'category' ? b.category : b.source + return String(av).localeCompare(String(bv)) * sortDir + }) + }, [nodes, filterType, filterCat, sort, sortDir]) + + function toggle(key: SortKey) { + if (sort === key) setSortDir(d => d === 1 ? -1 : 1) + else { setSort(key); setSortDir(-1) } } - const rows = model.nodes - .filter(n => n.type !== 'output') - .filter(n => filterType === 'all' || n.type === filterType) - .filter(n => filterCat === 'all' || n.category === filterCat) - .sort((a, b) => { - let va: any = a[sortKey]; let vb: any = b[sortKey] - if (sortKey === 'pip_contribution') { - va = Math.abs(a.pip_contribution ?? 0) - vb = Math.abs(b.pip_contribution ?? 0) - } - if (typeof va === 'string') return sortDir * va.localeCompare(vb) - return sortDir * ((va ?? 0) - (vb ?? 0)) - }) - - function SortTh({ k, label }: { k: SortKey; label: string }) { - const active = sortKey === k + function SortBtn({ k, label }: { k: SortKey; label: string }) { return ( - toggleSort(k)}> - {label} {active ? (sortDir === -1 ? '↓' : '↑') : ''} - + ) } + const TYPE_OPTS = [ + { value:'all', label:'Tous' }, + { value:'input_event', label:'Events' }, + { value:'input_manual', label:'Manuels' }, + { value:'intermediate', label:'Interméd.' }, + { value:'output', label:'Output' }, + ] + return ( -
- {/* Filters */} -
-
- {(['all','structural','event_driven'] as FilterType[]).map(t => ( - - ))} -
- +
+
+ {TYPE_OPTS.map(o => ( + + ))} - - {rows.length} facteurs
- {/* Table */} -
- - - - - - - - - -
TypeValeur +
+ + + + + + + + + + - - {rows.map(node => { - const pips = node.pip_contribution ?? 0 - const barW = model.net_pips !== 0 - ? Math.min(100, Math.abs(pips) / Math.max(...model.nodes.map(n => Math.abs(n.pip_contribution ?? 0))) * 100) - : 0 - return ( + + {sorted.map(node => { + const meta = NODE_TYPE_META[node.node_type] + const v = node.pip_contribution + const isExp = expanded === node.id + const canEdit = node.node_type === 'input_event' || node.node_type === 'input_manual' + return [ node.type !== 'output' && onNodeClick(node)} - className="hover:bg-slate-800/30 cursor-pointer transition-colors group"> - + + + + + + + , + isExp && ( + + - - - - - - - - ) + {node.node_type === 'intermediate' && node.formula && ( +
{node.formula}
+ )} + {node.override_note &&
Note : {node.override_note}
} + {node.override_set_at &&
Modifié : {node.override_set_at.slice(0,10)}
} + + + ), + ].filter(Boolean) })}
TBar
-
- {node.source === 'manual' && ( - + className={clsx('border-b border-slate-700/20 hover:bg-dark-700/20 transition-colors cursor-pointer', isExp && 'bg-dark-700/10')}> +
+ + {node.node_type === 'input_event' ? 'EV' : node.node_type === 'input_manual' ? 'M' : node.node_type === 'intermediate' ? 'INT' : 'OUT'} + + setExpanded(isExp ? null : node.id)}> + + {node.label} + {node.source === 'manual' && } + + {node.category}{fmt(v)} + + + {SOURCE_META[node.source]?.label} + + +
+
+
+
+ {canEdit && ( + + )} +
+ +
{node.description}
+ {node.node_type === 'input_manual' && node.coefficient_to_pips !== undefined && ( +
+ Coeff : {node.coefficient_to_pips} pips/{node.unit} + {node.raw_value !== undefined ? ` · ${node.raw_value} ${node.unit} → ${fmt(node.pip_contribution)} pips` : ''} +
)} -
-
{node.label}
-
{node.description.slice(0, 60)}…
-
- -
{catBadge(node.category)} - - {node.type === 'structural' ? 'Structurel' : 'Événementiel'} - - - {node.type === 'structural' && node.source === 'manual' ? ( - - {fmt(node.current_value, node.unit)} - - ) : ( - - )} - -
-
-
0 ? 'bg-emerald-600/70' : pips < 0 ? 'bg-red-600/70' : 'bg-slate-700')} - style={{ width: `${barW}%` }} /> -
- - {pips === 0 ? '0' : `${pips >= 0 ? '+' : ''}${pips}`} - -
-
- - {node.source === 'manual' ? '✎ Manuel' : - node.source === 'events' ? '⚡ Events' : - node.source === 'computed'? '∑ Calculé' : '— Neutre'} - - {node.override_note && ( -
- {node.override_note} -
- )} -
+ {sorted.length === 0 && ( +
Aucun nœud pour ce filtre
+ )} +
+ + ) +} + +// ── Timeline Chart ───────────────────────────────────────────────────────────── + +function TimelineView({ instrument }: { instrument: string }) { + const [period, setPeriod] = useState('3mo') + const [data, setData] = useState([]) + const [loading, setLoading] = useState(false) + const [layerKeys, setLayerKeys] = useState([]) + const [shown, setShown] = useState>(new Set(['net_pips'])) + const canvasRef = useRef(null) + + useEffect(() => { + setLoading(true) + api.get(`/instrument-models/${instrument}/timeline?period=${period}`) + .then(r => { + const pts: TimelinePoint[] = r.data + setData(pts) + if (pts.length > 0) { + const keys = Object.keys(pts[0].nodes).filter(k => k.startsWith('layer_')) + setLayerKeys(keys) + setShown(new Set(['net_pips', ...keys])) + } + }) + .catch(() => setData([])) + .finally(() => setLoading(false)) + }, [instrument, period]) + + useEffect(() => { + const canvas = canvasRef.current + if (!canvas || data.length === 0) return + const ctx = canvas.getContext('2d') + if (!ctx) return + + const W = canvas.width = canvas.offsetWidth + const H = canvas.height = 260 + ctx.clearRect(0, 0, W, H) + + const keys = Array.from(shown) + if (keys.length === 0) return + + let minV = Infinity, maxV = -Infinity + for (const pt of data) { + for (const k of keys) { + const v = k === 'net_pips' ? pt.net_pips : (pt.nodes[k] ?? 0) + if (v < minV) minV = v + if (v > maxV) maxV = v + } + } + const pad = Math.max(5, 0.15 * (maxV - minV)) + minV -= pad; maxV += pad + + const mL = 52, mR = 16, mT = 12, mB = 28 + const cW = W - mL - mR, cH = H - mT - mB + const toX = (i: number) => mL + (i / Math.max(data.length - 1, 1)) * cW + const toY = (v: number) => mT + (1 - (v - minV) / (maxV - minV)) * cH + + // Grid + ctx.lineWidth = 1 + const nG = 5 + for (let i = 0; i <= nG; i++) { + const v = minV + (i / nG) * (maxV - minV) + const y = toY(v) + ctx.strokeStyle = '#1e293b'; ctx.beginPath(); ctx.moveTo(mL, y); ctx.lineTo(W - mR, y); ctx.stroke() + ctx.fillStyle = '#64748b'; ctx.font = '10px sans-serif'; ctx.textAlign = 'right' + ctx.fillText(fmt(v), mL - 4, y + 3.5) + } + + if (minV < 0 && maxV > 0) { + const y0 = toY(0) + ctx.strokeStyle = '#334155'; ctx.setLineDash([3,3]); ctx.lineWidth = 1 + ctx.beginPath(); ctx.moveTo(mL, y0); ctx.lineTo(W - mR, y0); ctx.stroke() + ctx.setLineDash([]) + } + + // X labels + const step = Math.max(1, Math.floor(data.length / Math.floor(cW / 60))) + ctx.fillStyle = '#64748b'; ctx.font = '10px sans-serif'; ctx.textAlign = 'center' + for (let i = 0; i < data.length; i += step) { + ctx.fillText(data[i].date.slice(5), toX(i), H - 4) + } + + // Lines + for (const k of keys) { + const color = CANVAS_COLORS[k] || '#94a3b8' + const isNet = k === 'net_pips' + ctx.strokeStyle = color; ctx.lineWidth = isNet ? 2.5 : 1.5 + ctx.setLineDash(isNet ? [] : [4, 3]) + ctx.beginPath() + data.forEach((pt, i) => { + const v = k === 'net_pips' ? pt.net_pips : (pt.nodes[k] ?? 0) + i === 0 ? ctx.moveTo(toX(i), toY(v)) : ctx.lineTo(toX(i), toY(v)) + }) + ctx.stroke() + } + ctx.setLineDash([]) + }, [data, shown]) + + function toggleLayer(k: string) { + setShown(prev => { + const n = new Set(prev) + n.has(k) ? n.delete(k) : n.add(k) + return n + }) + } + + if (loading) return
Calcul timeline…
+ if (!loading && data.length === 0) return
Aucune donnée pour {instrument}
+ + return ( +
+
+ {PERIODS.map(p => ( + + ))} + {data.length} jours +
+ +
+ {/* Net pips toggle */} + + {layerKeys.map(k => ( + + ))} +
+ +
+
) @@ -502,134 +636,176 @@ function TableView({ model, onNodeClick }: { model: ModelState; onNodeClick: (n: // ── Main Page ────────────────────────────────────────────────────────────────── +type ViewMode = 'dag' | 'table' | 'timeline' + export default function InstrumentModels() { - const [selected, setSelected] = useState('EURUSD') - const [model, setModel] = useState(null) - const [metas, setMetas] = useState([]) - const [loading, setLoading] = useState(false) - const [view, setView] = useState<'table' | 'graph'>('table') - const [editNode, setEditNode] = useState(null) - const refreshKey = useRef(0) + const [instrument, setInstrument] = useState('EURUSD') + const [view, setView] = useState('dag') + const [state, setState] = useState(null) + const [loading, setLoading] = useState(false) + const [editNode, setEditNode] = useState(null) + const [refreshKey, setRefreshKey] = useState(0) - const load = useCallback((inst = selected) => { + const load = useCallback(() => { setLoading(true) - api.get(`/instrument-models/${inst}`) - .then(r => setModel(r.data)) - .catch(() => setModel(null)) + api.get(`/instrument-models/${instrument}`) + .then(r => setState(r.data)) + .catch(() => setState(null)) .finally(() => setLoading(false)) - }, [selected]) + }, [instrument]) - useEffect(() => { - api.get('/instrument-models').then(r => setMetas(r.data)).catch(() => {}) - }, []) + useEffect(() => { load() }, [load, refreshKey]) - useEffect(() => { load(selected) }, [selected]) + function onSaved() { setRefreshKey(k => k + 1) } - function onNodeClick(node: ModelNode) { - if (node.type === 'output') return - setEditNode(node) - } + const layerSummary = useMemo(() => { + if (!state) return [] + return state.nodes + .filter(n => n.node_type === 'intermediate') + .map(n => ({ + ...n, + pct: state.net_pips !== 0 + ? Math.round((n.pip_contribution / Math.abs(state.net_pips)) * 100) + : 0, + })) + .sort((a, b) => Math.abs(b.pip_contribution) - Math.abs(a.pip_contribution)) + }, [state]) - function onSaved() { - setEditNode(null) - load(selected) - } - - const dir = model?.direction - const dirCls = dir === 'bullish' ? 'text-emerald-400' : dir === 'bearish' ? 'text-red-400' : 'text-slate-400' - const dirLabel = dir === 'bullish' ? '▲ HAUSSIER' : dir === 'bearish' ? '▼ BAISSIER' : '◼ NEUTRE' + const VIEW_BTNS: { key: ViewMode; Icon: typeof Network; label: string }[] = [ + { key: 'dag', Icon: Network, label: 'DAG' }, + { key: 'table', Icon: Table2, label: 'Tableau' }, + { key: 'timeline', Icon: LineChart, label: 'Timeline' }, + ] return ( -
+
{/* Header */} -
+
-

Modèles d'Instruments

-

Graphes causaux exhaustifs — facteurs structurels + pressions événementielles

-
-
- - +

Modèles Instruments

+

+ Graphes causaux exhaustifs — propagation DAG 3 couches par instrument +

+
{/* Instrument tabs */} -
- {INSTRUMENTS.map(inst => { - const meta = metas.find(m => m.instrument === inst) - return ( - - ) - })} +
+ {INSTRUMENTS.map(inst => ( + + ))}
- {/* Net pressure summary */} - {model && ( -
-
-
Pression nette
-
- {model.net_pips >= 0 ? '+' : ''}{model.net_pips} pips + {/* Net pressure banner */} + {state && ( +
+
+
+
+ {state.name} — Pression Nette Cumulée +
+ +
{state.description}
+
+ + {/* Layer contributions summary */} + {layerSummary.length > 0 && ( +
+ {layerSummary.map(l => ( +
+
+ {l.label.replace('▶ ', '').split(' ').slice(0, 3).join(' ')} +
+
{fmt(l.pip_contribution)}
+
{l.pct > 0 ? '+' : ''}{l.pct}%
+
+ ))} +
+ )} + +
+
{state.nodes.filter(n => n.source === 'events').length} events actifs
+
{state.nodes.filter(n => n.source === 'manual').length} overrides manuels
+
{state.at_date}
-
- {dirLabel} -
-
- {model.nodes.filter(n => n.type === 'structural').length} facteurs structurels ·{' '} - {model.nodes.filter(n => n.type === 'event_driven').length} pressions événementielles -
-
- Au {model.at_date} · {model.name} -
- +
+ )} + + {/* View selector */} + {state && ( +
+ {VIEW_BTNS.map(({ key, Icon, label }) => ( + + ))} + + {state.nodes.filter(n=>n.node_type==='input_event').length} EV + · {state.nodes.filter(n=>n.node_type==='input_manual').length} M + · {state.nodes.filter(n=>n.node_type==='intermediate').length} INT + · {state.nodes.length} nœuds total +
)} {/* Main content */} - {loading && ( -
Chargement du modèle…
- )} - - {!loading && model && ( -
- {view === 'table' - ? - : - } + {state && ( +
+ {view === 'dag' && } + {view === 'table' && } + {view === 'timeline' && }
)} - {!loading && !model && ( -
Modèle introuvable pour {selected}
+ {/* Quick stats */} + {state && view !== 'timeline' && ( +
+ {[ + { label: 'Inputs Events', value: state.nodes.filter(n=>n.node_type==='input_event').length, sub: `${state.nodes.filter(n=>n.source==='events').length} actifs` }, + { label: 'Inputs Manuels', value: state.nodes.filter(n=>n.node_type==='input_manual').length, sub: `${state.nodes.filter(n=>n.source==='manual').length} overrides` }, + { label: 'Couches interméd.',value: state.nodes.filter(n=>n.node_type==='intermediate').length, sub: 'propagation DAG' }, + { label: 'Pression nette', value: `${fmt(state.net_pips)} pips`, sub: state.direction }, + ].map(card => ( +
+
{card.label}
+
{card.value}
+
{card.sub}
+
+ ))} +
)} - {/* Edit modal */} - {editNode && model && ( + {!state && !loading && ( +
+ Modèle introuvable pour {instrument}. Vérifier que le backend est démarré. +
+ )} + + {loading && !state && ( +
+ Chargement du modèle… +
+ )} + + {editNode && ( setEditNode(null)} - onSaved={onSaved} + node={editNode} instrument={instrument} + onClose={() => setEditNode(null)} onSaved={onSaved} /> )}