From aecbdc99299e36bc45ffe55bcdd4643d2265e223 Mon Sep 17 00:00:00 2001 From: OpenSquared Date: Thu, 2 Jul 2026 20:31:30 +0200 Subject: [PATCH] feat: instrument model --- backend/main.py | 11 + backend/routers/instrument_models.py | 102 ++++ backend/services/instrument_models.py | 669 +++++++++++++++++++++ frontend/src/App.tsx | 2 + frontend/src/components/layout/Sidebar.tsx | 1 + frontend/src/config/keepAliveMeta.ts | 1 + frontend/src/pages/InstrumentModels.tsx | 637 ++++++++++++++++++++ 7 files changed, 1423 insertions(+) create mode 100644 backend/routers/instrument_models.py create mode 100644 backend/services/instrument_models.py create mode 100644 frontend/src/pages/InstrumentModels.tsx diff --git a/backend/main.py b/backend/main.py index 2176b03..6eefe6a 100644 --- a/backend/main.py +++ b/backend/main.py @@ -16,6 +16,7 @@ from routers import institutional as institutional_router from routers import eco as eco_router from routers import simulator as simulator_router from routers import causal_lab as causal_lab_router +from routers import instrument_models as instrument_models_router from services.database import init_db, get_config, cleanup_stale_running_cycles import os import logging @@ -101,6 +102,15 @@ def startup(): _log.info(f"[Startup] Guidance sync done: {_gr}") except Exception as _e: _log.warning(f"[Startup] Guidance sync failed: {_e}") + # Seed instrument models (graphes causaux exhaustifs par instrument) + try: + from services.database import get_conn as _get_conn + from services.instrument_models import seed_instrument_models as _sim + _im_conn = _get_conn() + _sim(_im_conn); _im_conn.close() + _log.info("[Startup] Instrument models seeded") + except Exception as _e: + _log.warning(f"[Startup] Instrument models seed failed: {_e}") # Auto-bootstrap désactivé — utiliser les boutons dans Cycle Actions / Timeline # Start auto-cycle scheduler if enabled from services.auto_cycle import start_scheduler @@ -222,6 +232,7 @@ app.include_router(ai_desks_router.router) app.include_router(eco_router.router) app.include_router(simulator_router.router) app.include_router(causal_lab_router.router) +app.include_router(instrument_models_router.router) @app.get("/") diff --git a/backend/routers/instrument_models.py b/backend/routers/instrument_models.py new file mode 100644 index 0000000..4ebd2e7 --- /dev/null +++ b/backend/routers/instrument_models.py @@ -0,0 +1,102 @@ +""" +Instrument Models Router — graphes causaux exhaustifs par instrument. +""" +from typing import Any, Dict, List, Optional +from fastapi import APIRouter, HTTPException, Query +from pydantic import BaseModel + +router = APIRouter(prefix="/api/instrument-models", tags=["instrument-models"]) + + +class OverrideBody(BaseModel): + value: float + note: Optional[str] = "" + + +@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() + try: + rows = conn.execute( + "SELECT instrument, updated_at FROM instrument_models ORDER BY instrument" + ).fetchall() + result = [] + for r in rows: + inst = r["instrument"] + meta = INSTRUMENT_MODELS.get(inst, {}) + node_counts = {"structural": 0, "event_driven": 0, "output": 0} + for n in meta.get("nodes", []): + node_counts[n.get("type", "structural")] = node_counts.get(n.get("type", "structural"), 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"], + "updated_at": r["updated_at"], + }) + return result + 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.""" + from services.database import get_conn + from services.instrument_models import get_model_state + conn = get_conn() + try: + state = get_model_state(conn, instrument.upper(), at_date) + if not state: + raise HTTPException(status_code=404, detail=f"Modèle introuvable pour {instrument.upper()}") + return state + finally: + conn.close() + + +@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.""" + from services.database import get_conn + from services.instrument_models import set_node_override + conn = get_conn() + try: + set_node_override(conn, instrument.upper(), node_id, body.value, body.note or "") + return {"ok": True, "instrument": instrument.upper(), "node_id": node_id, "value": body.value} + finally: + conn.close() + + +@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).""" + from services.database import get_conn + from services.instrument_models import clear_node_override + conn = get_conn() + try: + clear_node_override(conn, instrument.upper(), node_id) + return {"ok": True, "instrument": instrument.upper(), "node_id": node_id} + finally: + conn.close() + + +@router.get("/{instrument}/nodes/{node_id}/overrides") +def get_node_overrides(instrument: str) -> List[Dict[str, Any]]: + """Toutes les overrides manuelles pour un instrument.""" + from services.database import get_conn + conn = get_conn() + try: + rows = conn.execute( + "SELECT node_id, value, note, set_at FROM instrument_node_overrides WHERE instrument=? ORDER BY set_at DESC", + (instrument.upper(),) + ).fetchall() + return [dict(r) for r in rows] + finally: + conn.close() diff --git a/backend/services/instrument_models.py b/backend/services/instrument_models.py new file mode 100644 index 0000000..9a95308 --- /dev/null +++ b/backend/services/instrument_models.py @@ -0,0 +1,669 @@ +""" +Instrument Models — graphe causal exhaustif par instrument. + +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) + +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) +""" +import json +import math +from datetime import datetime, timedelta, date as date_type +from typing import Optional + +# ── Helpers ──────────────────────────────────────────────────────────────────── + +def _decay(days: int, absorption: int, dtype: str) -> float: + if days < 0: return 0.0 + if dtype == "step": return 1.0 if days <= absorption else 0.0 + if dtype == "linear": return max(0.0, 1.0 - days / max(absorption, 1)) + lam = 3.0 / max(absorption, 1) + return math.exp(-lam * days) + + +# ── Comprehensive node definitions ───────────────────────────────────────────── +# coefficient (structural only) : native_unit × coefficient = pips impact + +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", + "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)"}, + ] +}, + +# ═══════════════════════════════════════════════════════════════════════════════ +"USDJPY": { + "name": "USD/JPY", "output_node": "usdjpy", + "description": "Taux de change Dollar / Yen — pair carry & safe haven par excellence", + "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"}, + ] +}, + +# ═══════════════════════════════════════════════════════════════════════════════ +"XAUUSD": { + "name": "XAU/USD (Or)", "output_node": "xauusd", + "description": "Or vs Dollar — actif refuge, couverture inflation et géopolitique", + "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"}, + ] +}, + +# ═══════════════════════════════════════════════════════════════════════════════ +"SP500": { + "name": "S&P 500", "output_node": "sp500", + "description": "Indice actions US large cap — baromètre risque global", + "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"}, + ] +}, + +# ═══════════════════════════════════════════════════════════════════════════════ +"TLT": { + "name": "TLT (US Long Bonds)", "output_node": "tlt", + "description": "ETF obligations US 20Y+ — duration longue, sensible aux taux et récession", + "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"}, + ] +}, + +# ═══════════════════════════════════════════════════════════════════════════════ +"GBPUSD": { + "name": "GBP/USD", "output_node": "gbpusd", + "description": "Taux de change Livre sterling / Dollar — sensible aux données UK et BoE", + "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"}, + ] +}, + +# ═══════════════════════════════════════════════════════════════════════════════ +"EEM": { + "name": "EEM (Marchés Émergents)", "output_node": "eem", + "description": "ETF actions marchés émergents — sensible au dollar, Chine et commodités", + "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"}, + ] +}, + +# ═══════════════════════════════════════════════════════════════════════════════ +"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", + "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"}, + ] +}, + +} # 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 ───────────────────────────────────────────────────────────────────── + +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')) + ); + 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')), + UNIQUE(instrument, node_id) + ); + """) + conn.commit() + + +def seed_instrument_models(conn): + """Insert/update instrument models on startup. Never overwrites user overrides.""" + 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"], + "description": model["description"], + "output_node": model["output_node"], + "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.commit() + + +# ── Value computation ─────────────────────────────────────────────────────────── + +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) + inst_upper = instrument.upper() + + 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 + 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_from), str(ref_date))).fetchall() + + by_cat: dict[str, float] = {} + inst_lower = inst_upper.lower() + + for row in rows: + r = dict(row) + try: + predictions = json.loads(r["prediction_json"] or "{}") + calib = json.loads(r["calibration_json"] or "{}") + except Exception: + continue + + pips: Optional[float] = None + if inst_lower in predictions: + pips = float(predictions[inst_lower]) + else: + for k, v in predictions.items(): + 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")) + + # Guidance events: dynamic absorption until meeting date + ev_end = r.get("event_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" + except ValueError: + pass + + try: + ev_date = date_type.fromisoformat(r["start_date"][:10]) + except ValueError: + continue + + days_elapsed = (ref_date - ev_date).days + df = _decay(days_elapsed, absorption, dtype) + if df < 0.01: + continue + + cat = r["category"] + by_cat[cat] = by_cat.get(cat, 0.0) + round(pips * df, 2) + + return by_cat + + +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. + """ + inst_upper = instrument.upper() + row = conn.execute( + "SELECT graph_json FROM instrument_models WHERE instrument=?", (inst_upper,) + ).fetchone() + if not row: + return None + + graph = 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( + "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} + + # Compute event-driven values by category + ev_by_cat = _compute_event_driven_values(conn, inst_upper, ref_date) + + # Build node states + net_pips = 0.0 + nodes_out = [] + + 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 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_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) + 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 + + elif ntype == "output": + state["current_value"] = None # filled after loop + state["pip_contribution"] = None + 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", ""), + "at_date": str(ref_date), + "net_pips": net_pips, + "direction": direction, + "nodes": nodes_out, + "output_node": graph["output_node"], + } + + +def set_node_override(conn, instrument: str, node_id: str, value: float, note: str = "") -> bool: + inst_upper = instrument.upper() + 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)) + 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) + ) + conn.commit() + return True diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 4cd6caa..ed21708 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -35,6 +35,7 @@ import AIDesks from './pages/AIDesks' import MacroSeriesPage from './pages/MacroSeriesPage' import EuroSimulator from './pages/EuroSimulator' import CausalLab from './pages/CausalLab' +import InstrumentModels from './pages/InstrumentModels' import { useCycleWatcher } from './hooks/useApi' // ── Keep-alive pages ────────────────────────────────────────────────────────── @@ -62,6 +63,7 @@ const KEEP_ALIVE_DEFS: { path: string; component: ComponentType }[] = [ { path: '/calendar', component: CalendarPage }, { path: '/simulator', component: EuroSimulator }, { path: '/causal-lab', component: CausalLab }, + { path: '/instrument-models', component: InstrumentModels }, { path: '/macro-series', component: MacroSeriesPage }, { path: '/institutional', component: InstitutionalReports }, { path: '/specialist-desks', component: SpecialistDesks }, diff --git a/frontend/src/components/layout/Sidebar.tsx b/frontend/src/components/layout/Sidebar.tsx index bbeb024..9cfc8c3 100644 --- a/frontend/src/components/layout/Sidebar.tsx +++ b/frontend/src/components/layout/Sidebar.tsx @@ -27,6 +27,7 @@ const nav = [ { to: '/calendar', icon: Calendar, label: 'Calendar' }, { to: '/simulator', icon: Sliders, label: 'EUR/USD Simulator' }, { to: '/causal-lab', icon: FlaskConical, label: 'Lab Causal' }, + { to: '/instrument-models', icon: Microscope, label: 'Modèles Instruments' }, { to: '/macro-series', icon: TrendingUp, label: 'Macro Series' }, { to: '/institutional', icon: Building2, label: 'Inst. Reports' }, { to: '/specialist-desks', icon: Users, label: 'Specialist Desks' }, diff --git a/frontend/src/config/keepAliveMeta.ts b/frontend/src/config/keepAliveMeta.ts index f4eeccb..3ced8e1 100644 --- a/frontend/src/config/keepAliveMeta.ts +++ b/frontend/src/config/keepAliveMeta.ts @@ -33,6 +33,7 @@ export const KEEP_ALIVE_META: KeepAliveMeta[] = [ { path: '/calendar', label: 'Calendar', icon: Calendar }, { path: '/simulator', label: 'Simulator', icon: Sliders }, { path: '/causal-lab', label: 'Lab Causal', icon: FlaskConical }, + { path: '/instrument-models', label: 'Modèles Instr.', icon: Microscope }, { path: '/macro-series', label: 'Macro Series', icon: MacroSeriesIcon }, { path: '/institutional', label: 'Inst. Reports', icon: Building2 }, { path: '/specialist-desks', label: 'Specialist Desks', icon: Users }, diff --git a/frontend/src/pages/InstrumentModels.tsx b/frontend/src/pages/InstrumentModels.tsx new file mode 100644 index 0000000..4ffad79 --- /dev/null +++ b/frontend/src/pages/InstrumentModels.tsx @@ -0,0 +1,637 @@ +/** + * InstrumentModels — graphes causaux exhaustifs par instrument. + * Vue tableau + graphe, nœuds éditables, filtres catégorie/type. + */ +import { useEffect, useState, useCallback, useRef } from 'react' +import axios from 'axios' +import clsx from 'clsx' + +const api = axios.create({ baseURL: '/api' }) + +// ── Types ────────────────────────────────────────────────────────────────────── + +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 + source: 'manual' | 'events' | 'neutral' | 'computed' + 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 +} +interface ModelMeta { + instrument: string; name: string; description: string + n_structural: number; n_event_driven: number +} + +// ── 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 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', +} + +function catBadge(cat: string) { + const cls = CAT_COLOR[cat] ?? 'bg-slate-700/40 text-slate-400 border-slate-600/40' + return ( + + {CAT_LABELS[cat] ?? cat} + + ) +} + +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' +} + +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 +}) { + const [val, setVal] = useState(String(node.current_value ?? 0)) + const [note, setNote] = useState(node.override_note ?? '') + const [saving, setSaving] = useState(false) + const [error, setError] = useState('') + + const hasOverride = node.source === 'manual' + + 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) } + } + + async function clear() { + setSaving(true) + try { + await api.delete(`/instrument-models/${instrument}/nodes/${node.id}/override`) + onSaved() + } catch { setError('Erreur') } + finally { setSaving(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} +
+
+ +
+ +

{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 + /> +
+
+ + 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" + /> +
+
+ + {error &&

{error}

} + +
+ + {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) ─────────────────────────────────────────────────────── + +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 + + 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 ( + + ) + })} + + {/* 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`} + + + ) + })} + + {/* 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`} + + + ) + })} + + {/* 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 + + + )} + + {/* Column labels */} + + FACTEURS STRUCTURELS + + + PRESSIONS ÉVÉNEMENTIELLES + + + RÉSULTAT + + +
+ ) +} + +// ── 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') + const [filterCat, setFilterCat] = useState('all') + + const cats = Array.from(new Set(model.nodes.map(n => n.category))).filter(c => c !== 'output') + + function toggleSort(key: SortKey) { + if (sortKey === key) setSortDir(d => d === 1 ? -1 : 1) + else { setSortKey(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 + return ( + toggleSort(k)}> + {label} {active ? (sortDir === -1 ? '↓' : '↑') : ''} + + ) + } + + return ( +
+ {/* Filters */} +
+
+ {(['all','structural','event_driven'] as FilterType[]).map(t => ( + + ))} +
+ + + + {rows.length} facteurs +
+ + {/* Table */} +
+ + + + + + + + + + + + + {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 ( + node.type !== 'output' && onNodeClick(node)} + className="hover:bg-slate-800/30 cursor-pointer transition-colors group"> + + + + + + + + + ) + })} + +
TypeValeur +
+
+ {node.source === 'manual' && ( + + )} +
+
{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} +
+ )} +
+
+
+ ) +} + +// ── Main Page ────────────────────────────────────────────────────────────────── + +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 load = useCallback((inst = selected) => { + setLoading(true) + api.get(`/instrument-models/${inst}`) + .then(r => setModel(r.data)) + .catch(() => setModel(null)) + .finally(() => setLoading(false)) + }, [selected]) + + useEffect(() => { + api.get('/instrument-models').then(r => setMetas(r.data)).catch(() => {}) + }, []) + + useEffect(() => { load(selected) }, [selected]) + + function onNodeClick(node: ModelNode) { + if (node.type === 'output') return + setEditNode(node) + } + + 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' + + return ( +
+ {/* Header */} +
+
+

Modèles d'Instruments

+

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

+
+
+ + +
+
+ + {/* Instrument tabs */} +
+ {INSTRUMENTS.map(inst => { + const meta = metas.find(m => m.instrument === inst) + return ( + + ) + })} +
+ + {/* Net pressure summary */} + {model && ( +
+
+
Pression nette
+
+ {model.net_pips >= 0 ? '+' : ''}{model.net_pips} pips +
+
+
+ {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} +
+ +
+ )} + + {/* Main content */} + {loading && ( +
Chargement du modèle…
+ )} + + {!loading && model && ( +
+ {view === 'table' + ? + : + } +
+ )} + + {!loading && !model && ( +
Modèle introuvable pour {selected}
+ )} + + {/* Edit modal */} + {editNode && model && ( + setEditNode(null)} + onSaved={onSaved} + /> + )} +
+ ) +}