""" Instrument Models — Phase 2 : saturation non-linéaire + régimes adaptatifs. Architecture DAG (3 couches) : Layer 0 — Inputs : - input_event : valeur auto depuis causal_event_analyses (par catégorie template) - input_manual : valeur utilisateur → tanh(x/scale)*coeff → pips (saturation) Layer 1 — Intermediate : formules agrégeant les inputs par domaine Layer 2 — Output : formule avec poids de régime (ex: 1.4*layer_monetary en MONETARY_DOMINANCE) Nouveautés Phase 2 : - _saturate_pips() : tanh(native/scale)*coeff, slope = coeff à l'origine, sature aux extrêmes - detect_regime() : identifie le régime dominant depuis les catégories d'events actifs - REGIME_WEIGHTS : multiplicateurs par couche selon 6 régimes de marché - _apply_regime_weights() : réécrit la formule output avec les poids du régime courant - simulate_timeline() inclut le régime du jour """ import json import math from datetime import datetime, timedelta, date as date_type from typing import Optional # ── Saturation scales (tanh) par unité native ───────────────────────────────── # tanh(x/scale) : slope=1 à l'origine, sature asymptotiquement à ±1 # pips = coefficient_to_pips * scale * tanh(x / scale) # → slope à x=0 : coefficient_to_pips (identique au linéaire) # → max pips : coefficient_to_pips * scale (jamais dépassé) _SATURATION_SCALES: dict[str, float] = { "bps": 200.0, # différentiels de taux : sature autour ±300bps "%": 3.0, # CPI / PIB différentiels : sature autour ±5% "pts%": 3.0, "pts": 15.0, # PMI écart depuis 50 : sature autour ±20pts "score": 3.0, # scores subjectifs -5 à +5 "tonnes": 80.0, # tonnes or / CB buying "Mds$": 40.0, "Mds$/sem": 15.0, "k lots": 80.0, # positions CFTC "$/bbl": 25.0, "x": 5.0, # multiples PE "ratio": 1.5, "t": 80.0, } def _saturate_pips(native: float, coeff: float, unit: str, scale_override: Optional[float] = None) -> float: """Convertit une valeur native en pips avec saturation tanh. Comportement identique au linéaire pour de petites valeurs, sature progressivement pour les valeurs extrêmes. """ scale = scale_override or _SATURATION_SCALES.get(unit) if scale and scale > 0: return coeff * scale * math.tanh(native / scale) return coeff * native # ── Régimes de marché et poids adaptatifs ───────────────────────────────────── # Chaque régime amplifie certaines couches (>1) et atténue les autres (<1) # Les clés couvrent tous les noms de couches intermédiaires possibles. REGIME_WEIGHTS: dict[str, dict[str, float]] = { "MONETARY_DOMINANCE": { # Fed/BCE dominent tout — taux, OIS, anticipations "layer_monetary": 1.40, "layer_rates": 1.40, "layer_growth": 0.80, "layer_uk_macro": 0.80, "layer_macro": 0.80, "layer_risk": 0.70, "layer_risk_credit": 0.70, "layer_refuge": 0.70, "layer_positioning": 1.10, "layer_policy": 1.10, "layer_flows": 1.00, "layer_demand": 1.00, "layer_tech_fundamental": 0.85, "layer_supply": 1.00, "layer_china": 0.90, "layer_dollar": 1.20, "layer_global": 0.80, }, "GEOPOLITICAL_RISK": { # Tensions géo → flight to safety, or, JPY, USD "layer_monetary": 0.80, "layer_rates": 0.80, "layer_growth": 0.90, "layer_uk_macro": 0.85, "layer_macro": 0.85, "layer_risk": 1.50, "layer_risk_credit": 1.50, "layer_refuge": 1.60, "layer_positioning": 1.10, "layer_policy": 1.30, "layer_flows": 1.00, "layer_demand": 1.20, "layer_tech_fundamental": 0.80, "layer_supply": 0.90, "layer_china": 0.80, "layer_dollar": 0.90, "layer_global": 1.30, }, "CREDIT_STRESS": { # Banking stress / liquidity crunch → risk-off massif "layer_monetary": 0.90, "layer_rates": 0.90, "layer_growth": 1.10, "layer_uk_macro": 1.10, "layer_macro": 1.10, "layer_risk": 1.60, "layer_risk_credit": 1.70, "layer_refuge": 1.50, "layer_positioning": 0.80, "layer_policy": 0.80, "layer_flows": 0.70, "layer_demand": 0.80, "layer_tech_fundamental": 0.75, "layer_supply": 0.90, "layer_china": 0.85, "layer_dollar": 1.10, "layer_global": 1.20, }, "GROWTH_SCARE": { # Récession / choc croissance → pivots BC, safe haven, EM sell "layer_monetary": 1.10, "layer_rates": 1.10, "layer_growth": 1.50, "layer_uk_macro": 1.50, "layer_macro": 1.50, "layer_risk": 1.20, "layer_risk_credit": 1.20, "layer_refuge": 1.30, "layer_positioning": 0.90, "layer_policy": 0.90, "layer_flows": 0.85, "layer_demand": 0.80, "layer_tech_fundamental": 0.70, "layer_supply": 0.90, "layer_china": 1.30, "layer_dollar": 0.90, "layer_global": 1.10, }, "COMMODITY_SHOCK": { # Choc énergie/matières premières → inflation, EM, or "layer_monetary": 1.10, "layer_rates": 1.10, "layer_growth": 1.20, "layer_uk_macro": 1.10, "layer_macro": 1.20, "layer_risk": 1.10, "layer_risk_credit": 1.00, "layer_refuge": 1.20, "layer_positioning": 1.00, "layer_policy": 1.00, "layer_flows": 1.00, "layer_demand": 1.40, "layer_tech_fundamental": 0.90, "layer_supply": 1.20, "layer_china": 1.20, "layer_dollar": 0.95, "layer_global": 1.10, }, "BALANCED": { # Régime neutre : aucune amplification }, } # Mapping catégories d'events → régimes candidats _CAT_TO_REGIME: dict[str, str] = { "central_bank": "MONETARY_DOMINANCE", "monetary_shock": "MONETARY_DOMINANCE", "geopolitical": "GEOPOLITICAL_RISK", "trade_policy": "GEOPOLITICAL_RISK", "credit_stress": "CREDIT_STRESS", "growth_shock": "GROWTH_SCARE", "commodity": "COMMODITY_SHOCK", "sentiment": "BALANCED", "technical": "BALANCED", "positioning": "BALANCED", "unclassified": "BALANCED", } # ── Lifecycle & decay ────────────────────────────────────────────────────────── 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) 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", "description": "Taux de change Euro/Dollar — modèle causal 3 couches avec 4 domaines d'influence", "output_node": "eurusd", "price_intercept": 1.10, "pip_to_price": 0.0001, "yf_ticker": "EURUSD=X", "nodes": [ # ── 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": "Carry & safe haven — yield diff 10Y + BoJ + risk appetite", "price_intercept": 145.0, "pip_to_price": 0.01, "yf_ticker": "USDJPY=X", "nodes": [ {"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/Dollar — taux réels, dollar, géopolitique, banques centrales", "price_intercept": 2800.0, "pip_to_price": 1.0, "yf_ticker": "GC=F", "nodes": [ {"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 — taux, bénéfices, risque, liquidités", "price_intercept": 5000.0, "pip_to_price": 1.0, "yf_ticker": "^GSPC", "nodes": [ {"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, inflation, récession, supply", "price_intercept": 85.0, "pip_to_price": 0.01, "yf_ticker": "TLT", "nodes": [ {"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": "Livre sterling/Dollar — BoE, données UK, risque politique", "price_intercept": 1.26, "pip_to_price": 0.0001, "yf_ticker": "GBPUSD=X", "nodes": [ {"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 EM — dollar, Chine, commodités, risk appetite", "price_intercept": 42.0, "pip_to_price": 0.01, "yf_ticker": "EEM", "nodes": [ {"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": "Tech US — taux réels, bénéfices big tech, IA, réglementation", "price_intercept": 480.0, "pip_to_price": 0.10, "yf_ticker": "QQQ", "nodes": [ {"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 # ── Regime detection ─────────────────────────────────────────────────────────── def detect_regime(ev_by_cat: dict[str, float]) -> dict: """ Détermine le régime de marché dominant depuis la pression event par catégorie. Retourne : {regime, label, scores, dominant_cat, weights} """ # Score de chaque catégorie = |pips| cat_scores = {cat: abs(v) for cat, v in ev_by_cat.items() if v != 0} if not cat_scores: return { "regime": "BALANCED", "label": "Équilibré", "scores": {}, "dominant_cat": None, "weights": {}, } # Cumul de score par régime candidat regime_scores: dict[str, float] = {} for cat, score in cat_scores.items(): r = _CAT_TO_REGIME.get(cat, "BALANCED") regime_scores[r] = regime_scores.get(r, 0.0) + score dominant_regime = max(regime_scores, key=lambda r: regime_scores[r]) # Si le score maximal ne dépasse pas 3 pips → BALANCED max_score = regime_scores.get(dominant_regime, 0.0) if max_score < 3.0 or dominant_regime == "BALANCED": dominant_regime = "BALANCED" dominant_cat = max( (c for c in cat_scores if _CAT_TO_REGIME.get(c) == dominant_regime), key=lambda c: cat_scores[c], default=None, ) if dominant_regime != "BALANCED" else None REGIME_LABELS = { "MONETARY_DOMINANCE": "Dominance Monétaire", "GEOPOLITICAL_RISK": "Risque Géopolitique", "CREDIT_STRESS": "Stress Crédit", "GROWTH_SCARE": "Choc Croissance", "COMMODITY_SHOCK": "Choc Commodités", "BALANCED": "Équilibré", } weights = REGIME_WEIGHTS.get(dominant_regime, {}) return { "regime": dominant_regime, "label": REGIME_LABELS.get(dominant_regime, dominant_regime), "scores": {r: round(s, 1) for r, s in regime_scores.items()}, "dominant_cat": dominant_cat, "weights": weights, } def _apply_regime_weights(formula: str, weights: dict[str, float]) -> str: """ Injecte les multiplicateurs de régime dans une formule d'output. Ex: "layer_monetary + layer_risk" + {layer_monetary:1.4, layer_risk:1.5} → "1.40 * layer_monetary + 1.50 * layer_risk" Les termes sans poids restent à 1.0 (non modifiés). """ if not weights: return formula terms = [t.strip() for t in formula.split('+')] out = [] for term in terms: # Extraire l'id de couche (premier token alphanumérique_) layer_id = term.strip() w = weights.get(layer_id, 1.0) if abs(w - 1.0) < 0.01: out.append(layer_id) else: out.append(f"{w:.2f} * {layer_id}") return " + ".join(out) # ── 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')) ); 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) ); CREATE TABLE IF NOT EXISTS event_calibration_overrides ( analysis_id INTEGER PRIMARY KEY, calibration_json TEXT NOT NULL, updated_at TEXT DEFAULT (datetime('now')) ); """) conn.commit() def seed_instrument_models(conn): """Seed/update models on startup. Safe to call repeatedly.""" init_instrument_model_tables(conn) for inst, model in INSTRUMENT_MODELS.items(): graph_json = json.dumps({ "name": model["name"], "description": model["description"], "output_node": model["output_node"], "nodes": model["nodes"], }) 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() # ── Event contribution by category (with lifecycle) ──────────────────────────── 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.id as analysis_id, a.prediction_json, e.start_date, e.end_date, e.sub_type, e.name as title, t.category, COALESCE(o.calibration_json, t.calibration_json) as 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 LEFT JOIN event_calibration_overrides o ON o.analysis_id = a.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] = {} 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")) rise = max(0, int(calib.get("rise_days", 0))) plateau = max(0, int(calib.get("plateau_days", 0))) # 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]) 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 days = (ref_date - ev_date).days df = _lifecycle(days, rise, plateau, absorption, dtype) if df < 0.01: continue cat = r["category"] 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, saturation: bool = True, ) -> dict: """ Build inputs dict for evaluate_graph(). Phase 2 : les nœuds input_manual utilisent _saturate_pips() (tanh) au lieu d'une conversion purement linéaire. """ 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": # Baseline (niveau structurel user) + surprise event (lifecycle) → additif base = float(ov["value"]) if ov else 0.0 events = ev_by_cat.get(node.get("event_category", ""), 0.0) inputs[nid] = base + events elif ntype == "input_manual": coeff = float(node.get("coefficient_to_pips", 1.0)) native = float(ov["value"]) if ov else 0.0 if native == 0.0: inputs[nid] = 0.0 elif saturation: inputs[nid] = _saturate_pips(native, coeff, node.get("unit", ""), node.get("saturation_scale")) else: inputs[nid] = coeff * native return inputs def _graph_json_for_eval(graph_def: dict, regime_weights: Optional[dict] = None) -> dict: """ Convertit le graph_def au format attendu par evaluate_graph(). Phase 2 : si regime_weights est fourni, la formule du nœud output est réécrite avec les multiplicateurs de régime. """ output_id = graph_def.get("output_node", "") nodes = [] for n in graph_def["nodes"]: entry: dict = {"id": n["id"]} formula = n.get("formula") if formula: if n["id"] == output_id and regime_weights: formula = _apply_regime_weights(formula, regime_weights) entry["formula"] = formula nodes.append(entry) return {"nodes": nodes, "coefficients": {}} # ── Public API ───────────────────────────────────────────────────────────────── def get_active_event_details(conn, instrument: str, ref_date: date_type) -> dict: """ Détail des events actifs par catégorie — utilisé pour enrichir les nœuds event du DAG. Retourne : {category: [{analysis_id, title, start_date, days_since, pip_prediction, lifecycle_factor, remaining_pips, calibration}]} """ inst_upper = instrument.upper() inst_lower = inst_upper.lower() extended_from = ref_date - timedelta(days=365) rows = conn.execute(""" SELECT a.id as analysis_id, a.prediction_json, e.id as event_id, e.name as title, e.start_date, e.end_date, e.sub_type, t.category, COALESCE(o.calibration_json, t.calibration_json) as 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 LEFT JOIN event_calibration_overrides o ON o.analysis_id = a.id WHERE a.instrument = ? AND e.start_date >= ? AND e.start_date <= ? ORDER BY e.start_date DESC """, (inst_upper, str(extended_from), str(ref_date))).fetchall() by_cat: dict[str, list] = {} for row in rows: r = dict(row) 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 pips is None: 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 days = (ref_date - ev_date).days lf = _lifecycle(days, rise, plateau, absorption, dtype) if lf < 0.01: continue cat = r["category"] by_cat.setdefault(cat, []).append({ "analysis_id": r["analysis_id"], "event_id": r.get("event_id"), "title": r.get("title", ""), "start_date": r["start_date"][:10], "days_since": days, "pip_prediction": round(pips, 1), "lifecycle_factor": round(lf, 3), "remaining_pips": round(pips * lf, 1), "calibration": { "absorption_days": absorption, "rise_days": rise, "plateau_days": plateau, "decay_type": dtype, }, }) return by_cat def get_model_state(conn, instrument: str, at_date: Optional[str] = None) -> Optional[dict]: """ Full model state : DAG evaluation avec saturation (Phase 2) + poids de régime. Retourne aussi {regime: {regime, label, weights, scores}}. """ inst_upper = instrument.upper() row = conn.execute( "SELECT graph_json FROM instrument_models WHERE instrument=?", (inst_upper,) ).fetchone() if not row: return None 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() 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()} # Machine structurelle pure — aucune injection automatique depuis market_events. # Les events du CausalLab ne touchent plus le graphe; seuls les overrides manuels # (Sync Marche + saisie) et les events virtuels (What-if) contribuent. ev_by_cat: dict[str, float] = {} regime_info = detect_regime(ev_by_cat) # toujours BALANCED hors What-if regime_weights = regime_info["weights"] from services.causal_graphs import evaluate_graph inputs = _build_inputs(graph_def, overrides, ev_by_cat, saturation=True) gj = _graph_json_for_eval(graph_def, regime_weights) all_vals = evaluate_graph(gj, inputs) output_id = graph_def["output_node"] net_pips = round(float(all_vals.get(output_id, 0.0)), 1) structural_pips = net_pips # identique — pas d'events injectés event_pips = round(net_pips - structural_pips, 1) meta = INSTRUMENT_MODELS.get(inst_upper, {}) price_intercept = meta.get("price_intercept", 0.0) pip_to_price = meta.get("pip_to_price", 0.0001) yf_ticker = meta.get("yf_ticker", "") fundamental_level = round(price_intercept + structural_pips * pip_to_price, 6) synthetic_price = round(price_intercept + net_pips * pip_to_price, 6) 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) st = dict(node) st["computed_value"] = val st["pip_contribution"] = val if ntype == "input_event": cat = node.get("event_category", "") event_surprise = round(ev_by_cat.get(cat, 0.0), 1) baseline = float(ov["value"]) if ov else 0.0 st["raw_value"] = baseline st["baseline_value"] = baseline st["event_surprise"] = event_surprise st["source"] = "manual" if ov else ("events" if event_surprise != 0.0 else "neutral") if ov: st["override_note"] = ov.get("note", "") st["override_set_at"] = ov.get("set_at", "") elif ntype == "input_manual": coeff = float(node.get("coefficient_to_pips", 1.0)) native = float(ov["value"]) if ov else 0.0 # Retourne la valeur native sans saturation (pour affichage) st["source"] = "manual" if ov else "neutral" st["raw_value"] = native # pip_contribution inclut la saturation (= all_vals[nid]) # On expose aussi la valeur linéaire pour comparaison st["pip_linear"] = round(coeff * native, 1) st["pip_saturated"] = val st["saturation_pct"] = ( round((1.0 - val / (coeff * native)) * 100, 1) if native != 0 and coeff != 0 else 0.0 ) if ov: st["override_note"] = ov.get("note", "") st["override_set_at"] = ov.get("set_at", "") elif ntype in ("intermediate", "output"): st["source"] = "computed" # Pour les intermédiaires, expose le multiplicateur de régime if ntype == "intermediate": st["regime_weight"] = regime_weights.get(nid, 1.0) nodes_out.append(st) direction = "bullish" if net_pips > 5 else "bearish" if net_pips < -5 else "neutral" event_details = get_active_event_details(conn, inst_upper, ref_date) return { "instrument": inst_upper, "name": graph_def["name"], "description": graph_def.get("description", ""), "at_date": str(ref_date), "net_pips": net_pips, "structural_pips": structural_pips, "event_pips": event_pips, "price_intercept": price_intercept, "pip_to_price": pip_to_price, "yf_ticker": yf_ticker, "fundamental_level": fundamental_level, "synthetic_price": synthetic_price, "direction": direction, "nodes": nodes_out, "output_node": output_id, "regime": regime_info, "event_details": event_details, } def simulate_timeline( conn, instrument: str, period: str = "1y", virtual_events: Optional[list] = None, start_date: Optional[str] = None, ) -> 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). start_date overrides the period-based date_from when provided. """ 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() if start_date: try: date_from = date_type.fromisoformat(start_date[:10]) except ValueError: date_from = today - timedelta(days=lookback) else: 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()} # Machine structurelle — seuls les events virtuels (What-if) entrent dans le calcul. # La timeline de base ne dépend plus de causal_event_analyses. events: list[dict] = [] for ve in (virtual_events or []): try: ev_date = date_type.fromisoformat(str(ve["date"])[:10]) events.append({ "ev_date": ev_date, "category": ve.get("category", "unclassified"), "pips": float(ve.get("pips", 0.0)), "rise": int(ve.get("rise_days", 1)), "plateau": int(ve.get("plateau_days", 0)), "absorption": int(ve.get("absorption_days", 14)), "dtype": ve.get("decay_type", "exp"), "virtual": True, "label": ve.get("label", "Event virtuel"), }) except (KeyError, ValueError): continue meta = INSTRUMENT_MODELS.get(inst_upper, {}) price_intercept = meta.get("price_intercept", 0.0) pip_to_price = meta.get("pip_to_price", 0.0001) from services.causal_graphs import evaluate_graph # Structural pips — calculé une seule fois (overrides statiques, pas d'events) gj_struct = _graph_json_for_eval(graph_def, {}) inputs_struct = _build_inputs(graph_def, overrides, {}, saturation=True) vals_struct = evaluate_graph(gj_struct, inputs_struct) structural_pips = round(float(vals_struct.get(output_id, 0.0)), 1) fundamental_level_base = round(price_intercept + structural_pips * pip_to_price, 6) # Auto-anchor : caler le niveau fondamental sur le prix réel au début de la fenêtre. # start_offset = prix_réel_début - niveau_fondamental_machine # → synthetic_price(t) = prix_réel_début + event_pips(t) * pip_to_price start_offset = 0.0 try: ph_row = conn.execute( """SELECT close FROM price_history_cache WHERE instrument=? AND date>=? ORDER BY date ASC LIMIT 1""", (inst_upper, str(date_from)) ).fetchone() if ph_row is None: # Weekends/jours fériés : on remonte jusqu'à 7 jours avant ph_row = conn.execute( """SELECT close FROM price_history_cache WHERE instrument=? AND date>=? ORDER BY date ASC LIMIT 1""", (inst_upper, str(date_from - timedelta(days=7))) ).fetchone() if ph_row: start_offset = round(float(ph_row["close"]) - fundamental_level_base, 6) except Exception: pass timeline = [] cur = date_from while cur <= today: # Accumule les events virtuels (What-if) actifs ce jour ev_by_cat: dict[str, float] = {} active_events_detail: list[dict] = [] for ev in events: if ev["ev_date"] > cur: continue if ev["ev_date"] < date_from: # Events avant la fenêtre ne portent pas de lifecycle dans la simu # (leur impact est absorbé dans l'auto-anchor du prix de départ) 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"] contribution = round(ev["pips"] * df, 2) ev_by_cat[cat] = ev_by_cat.get(cat, 0.0) + contribution active_events_detail.append({ "label": ev["label"], "pips": ev["pips"], "lifecycle_factor": round(df, 3), "contribution": contribution, "date": str(ev["ev_date"]), "category": cat, }) if ev_by_cat: # Des events virtuels sont actifs → recalcul complet avec régime ri = detect_regime(ev_by_cat) gj = _graph_json_for_eval(graph_def, ri["weights"]) inputs = _build_inputs(graph_def, overrides, ev_by_cat, saturation=True) vals = evaluate_graph(gj, inputs) net = round(float(vals.get(output_id, 0.0)), 1) regime_label = ri["regime"] else: # Pas d'events — on réutilise les valeurs structurelles vals = vals_struct net = structural_pips regime_label = "BALANCED" timeline.append({ "date": str(cur), "net_pips": net, "structural_pips": structural_pips, "event_pips": round(net - structural_pips, 1), "fundamental_level": round(fundamental_level_base + start_offset, 6), "synthetic_price": round(price_intercept + start_offset + net * pip_to_price, 6), "regime": regime_label, "nodes": {k: round(float(v), 1) for k, v in vals.items()}, "active_events": active_events_detail, }) 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') """, (instrument.upper(), node_id, value, note)) conn.commit() return True def clear_node_override(conn, instrument: str, node_id: str) -> bool: conn.execute( "DELETE FROM instrument_node_overrides WHERE instrument=? AND node_id=?", (instrument.upper(), node_id) ) conn.commit() return True