Files
OpenFin/backend/services/instrument_models.py
2026-07-03 14:48:00 +02:00

1200 lines
85 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
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)
# Guidance EMA : la baseline de la synthétique est l'EMA lissée du prix réel.
# synthetic_price(t) = EMA(t) + event_pips(t) × pip_to_price
# → sans event perturbateur : synthétique colle au lissé historique
# → avec events : déviation proportionnelle à leur contribution
ema_prices: dict[str, float] = {}
last_ema: float = fundamental_level_base # fallback si pas de données prix
try:
# 30j de warmup avant date_from pour que l'EMA soit stabilisée dès le début
warmup_from = str(date_from - timedelta(days=30))
ph_rows = conn.execute(
"""SELECT date, close FROM price_history_cache
WHERE instrument=? AND date>=? ORDER BY date ASC""",
(inst_upper, warmup_from)
).fetchall()
alpha = 0.15 # lissage EMA (~6j de demi-vie)
ema_val: Optional[float] = None
for r in ph_rows:
c = float(r["close"])
ema_val = c if ema_val is None else alpha * c + (1.0 - alpha) * ema_val
if r["date"] >= str(date_from):
ema_prices[r["date"]] = round(ema_val, 6)
if ema_prices:
last_ema = list(ema_prices.values())[-1]
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"
# Guide price : EMA du prix réel si disponible, sinon dernier EMA connu (futur)
date_str = str(cur)
if date_str in ema_prices:
guide_price = ema_prices[date_str]
last_ema = guide_price
else:
guide_price = last_ema # dates futures : tient le dernier EMA connu
event_pips = round(net - structural_pips, 1)
timeline.append({
"date": date_str,
"net_pips": net,
"structural_pips": structural_pips,
"event_pips": event_pips,
"fundamental_level": guide_price,
"synthetic_price": round(guide_price + event_pips * 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