"""
Bibliothèque de graphes causaux — templates pré-peuplés et helpers.

Chaque template définit une chaîne causale entre un type d'événement de marché
et un ou plusieurs instruments (EURUSD, XAUUSD, SP500, BRENT...).

Structure graph_json :
  nodes[]       — nœuds avec position (x,y) pour le rendu SVG
  edges[]       — arêtes directionnelles entre nœuds
  coefficients  — valeurs heuristiques + calibrées
  instruments[] — instruments cibles du template
  input_mapping — comment extraire les inputs depuis un market_event
"""
import ast
import json
import logging
import operator
from typing import Optional

logger = logging.getLogger(__name__)


# ── Helpers de construction ────────────────────────────────────────────────────

def _n(id_, label, type_, x, y, formula=None, unit="", instrument=None, description=""):
    n = {"id": id_, "label": label, "type": type_, "x": x, "y": y}
    if formula:     n["formula"]     = formula
    if unit:        n["unit"]        = unit
    if instrument:  n["instrument"]  = instrument
    if description: n["description"] = description
    return n

def _e(from_, to_, style="solid", type_="causal", strength=2, sign="neutral", label=""):
    e = {"from": from_, "to": to_, "style": style, "type": type_,
         "strength": strength, "sign": sign}
    if label: e["label"] = label
    return e

def _c(value, description=""):
    return {"value": value, "calibrated": None, "description": description}


# ── Slugs régimes (12 templates exhaustifs — liste fermée pour auto-assign) ───
REGIME_SLUGS = [
    "MACRO_DATA_SURPRISE",
    "CENTRAL_BANK_DECISION",
    "GROWTH_CORPORATE_SIGNAL",
    "GEOPOLITICAL_RISK_OFF",
    "COMMODITY_SUPPLY_SHOCK",
    "TRADE_POLICY_SHOCK",
    "CREDIT_SYSTEMIC_EVENT",
    "TECHNICAL_MOMENTUM_BREAKOUT",
    "SENTIMENT_POSITIONING_EXTREME",
    "COMMODITY_INVENTORY_REPORT",
    "INSTITUTIONAL_FLOW",
    "UNCLASSIFIED_IMPACT",
]

BUILT_IN_TEMPLATES = [

  # ═══════════════════════════════════════════════════════════════════════════════
  # 1. MACRO_DATA_SURPRISE
  # CPI, PCE, NFP, ADP, GDP, PMI, ISM, Retail Sales, Jobless Claims, JOLTS...
  # ═══════════════════════════════════════════════════════════════════════════════
  {
    "name": "Macro Data Surprise",
    "slug": "MACRO_DATA_SURPRISE",
    "category": "monetary_shock",
    "heuristic_ver": 2,
    "calibration_json": {"lag_days": 0, "half_life_days": 4, "absorption_days": 12, "decay_type": "exp"},
    "instruments": ["EURUSD", "SP500", "XAUUSD", "QQQ", "TLT", "USDJPY", "EEM", "GDX", "SLV"],
    "description": "Scheduled economic release vs consensus -> OIS repricing -> full asset repricing",
    "ai_rationale": "Hawkish surprise: OIS up -> 2Y up -> USD up -> EURUSD/USDJPY/EEM react; 10Y up -> TLT down, GDX/XAUUSD down. Growth channel -> SP500/QQQ up. Dovish miss: all reversed.",
    "graph_json": {
        "nodes": [
            _n("macro_surprise",  "Macro Surprise",        "input",        400, 50,  unit="sigma"),
            _n("ois_repricing",   "Fed OIS Repricing",     "observable",   180, 180, formula="macro_surprise * {{coef_surp_ois}}", unit="bps"),
            _n("growth_exp",      "Growth Expectation",    "latent",       580, 180, formula="macro_surprise * {{coef_surp_growth}}"),
            _n("inflation_risk",  "Inflation Risk",        "latent",       820, 180, formula="macro_surprise * {{coef_surp_infl}}"),
            _n("us_2y",           "Delta US 2Y",           "observable",   80,  330, formula="ois_repricing * {{coef_ois_2y}}", unit="%"),
            _n("us_10y",          "Delta US 10Y",          "observable",   340, 330, formula="ois_repricing * {{coef_ois_10y}} + inflation_risk * {{coef_infl_10y}}", unit="%"),
            _n("fin_conditions",  "Financial Conditions",  "latent",       620, 330, formula="ois_repricing * {{coef_ois_fc}} + us_10y * {{coef_10y_fc}}"),
            _n("usd_strength",    "USD Strength",          "observable",   80,  470, formula="us_2y * {{coef_2y_usd}}", unit="%"),
            # ── Primary outputs row 1 ──────────────────────────────────────────
            _n("eurusd",  "EUR/USD",       "market_asset", 60,  620, formula="-usd_strength * {{coef_usd_eurusd}}", unit="pips", instrument="EURUSD"),
            _n("sp500",   "S&P 500",       "market_asset", 260, 620, formula="growth_exp * {{coef_growth_sp}} - fin_conditions * {{coef_fc_sp}}", unit="pts", instrument="SP500"),
            _n("xauusd",  "Gold XAU/USD",  "market_asset", 460, 620, formula="-us_10y * {{coef_10y_gold}} - usd_strength * {{coef_usd_gold}}", unit="$/oz", instrument="XAUUSD"),
            # ── Additional outputs row 2 ──────────────────────────────────────
            _n("qqq",    "QQQ Nasdaq",    "market_asset", 60,  790, formula="growth_exp * {{coef_growth_qqq}} - fin_conditions * {{coef_fc_qqq}}", unit="pts", instrument="QQQ"),
            _n("tlt",    "TLT 20Y Bond",  "market_asset", 230, 790, formula="-us_10y * {{coef_10y_tlt}}", unit="pts", instrument="TLT"),
            _n("usdjpy", "USD/JPY",        "market_asset", 400, 790, formula="usd_strength * {{coef_usd_usdjpy}}", unit="pips", instrument="USDJPY"),
            _n("eem",    "EEM Emerg Mkt", "market_asset", 570, 790, formula="-usd_strength * {{coef_usd_eem}} + growth_exp * {{coef_growth_eem}}", unit="pts", instrument="EEM"),
            _n("gdx",    "GDX Gold Mine", "market_asset", 740, 790, formula="-us_10y * {{coef_10y_gdx}} - usd_strength * {{coef_usd_gdx}}", unit="pts", instrument="GDX"),
            _n("slv",    "SLV Silver",    "market_asset", 910, 790, formula="-us_10y * {{coef_10y_slv}} - usd_strength * {{coef_usd_slv}}", unit="pts", instrument="SLV"),
        ],
        "edges": [
            _e("macro_surprise", "ois_repricing",  "solid",  "rate_repricing",  strength=3, sign="positive"),
            _e("macro_surprise", "growth_exp",     "dashed", "growth_channel",  strength=2, sign="positive"),
            _e("macro_surprise", "inflation_risk", "dashed", "inflation_ch",    strength=2, sign="positive"),
            _e("ois_repricing",  "us_2y",          "solid",  "short_end",       strength=3, sign="positive"),
            _e("ois_repricing",  "us_10y",         "dashed", "long_end",        strength=1, sign="positive"),
            _e("inflation_risk", "us_10y",         "solid",  "term_premium",    strength=2, sign="positive"),
            _e("ois_repricing",  "fin_conditions", "solid",  "policy_tighten",  strength=2, sign="positive"),
            _e("us_10y",         "fin_conditions", "solid",  "discount_rate",   strength=2, sign="positive"),
            _e("us_2y",          "usd_strength",   "solid",  "carry_diff",      strength=3, sign="positive"),
            # row 1 outputs
            _e("usd_strength",   "eurusd",  "solid",  "fx_channel",    strength=3, sign="negative"),
            _e("growth_exp",     "sp500",   "solid",  "earnings_ch",   strength=2, sign="positive"),
            _e("fin_conditions", "sp500",   "solid",  "valuation_ch",  strength=3, sign="negative"),
            _e("us_10y",         "xauusd",  "solid",  "real_rate_gold",strength=2, sign="negative"),
            _e("usd_strength",   "xauusd",  "dashed", "usd_gold",      strength=2, sign="negative"),
            # row 2 outputs
            _e("growth_exp",     "qqq",    "solid",  "tech_growth",   strength=3, sign="positive",  label="tech/growth beta"),
            _e("fin_conditions", "qqq",    "solid",  "rate_compress", strength=3, sign="negative",  label="high duration sensitivity"),
            _e("us_10y",         "tlt",    "solid",  "bond_price",    strength=3, sign="negative",  label="price = inverse yield"),
            _e("usd_strength",   "usdjpy", "solid",  "carry_ch",      strength=2, sign="positive",  label="USD carry -> JPY weakens"),
            _e("usd_strength",   "eem",    "solid",  "usd_eem",       strength=2, sign="negative",  label="USD -> EM outflows"),
            _e("growth_exp",     "eem",    "dashed", "growth_eem",    strength=2, sign="positive"),
            _e("us_10y",         "gdx",    "solid",  "gold_miners",   strength=2, sign="negative"),
            _e("usd_strength",   "gdx",    "dashed", "usd_miners",    strength=2, sign="negative"),
            _e("us_10y",         "slv",    "solid",  "silver_rates",  strength=2, sign="negative"),
            _e("usd_strength",   "slv",    "dashed", "silver_usd",    strength=1, sign="negative"),
        ],
        "coefficients": {
            "coef_surp_ois":    _c(8.0,   "1sigma -> OIS bps"),
            "coef_surp_growth": _c(0.30,  "1sigma -> growth exp"),
            "coef_surp_infl":   _c(0.15,  "1sigma -> inflation risk"),
            "coef_ois_2y":      _c(0.028, "OIS bps -> 2Y yield"),
            "coef_ois_10y":     _c(0.008, "OIS bps -> 10Y yield"),
            "coef_infl_10y":    _c(0.12,  "Inflation -> 10Y premium"),
            "coef_ois_fc":      _c(0.40,  "OIS -> financial conditions"),
            "coef_10y_fc":      _c(0.60,  "10Y -> financial conditions"),
            "coef_2y_usd":      _c(0.50,  "2Y -> USD strength"),
            "coef_usd_eurusd":  _c(80,    "USD -> EURUSD pips fall"),
            "coef_growth_sp":   _c(60,    "Growth -> SP500 pts"),
            "coef_fc_sp":       _c(50,    "Fin cond -> SP500 pts fall"),
            "coef_10y_gold":    _c(15,    "1% 10Y -> Gold fall $/oz"),
            "coef_usd_gold":    _c(10,    "USD -> Gold fall $/oz"),
            "coef_growth_qqq":  _c(90,    "Growth -> QQQ pts (higher beta)"),
            "coef_fc_qqq":      _c(75,    "Fin cond -> QQQ pts fall (duration)"),
            "coef_10y_tlt":     _c(16,    "1% 10Y -> TLT pts fall (~16y duration)"),
            "coef_usd_usdjpy":  _c(100,   "USD strength -> USDJPY pips up"),
            "coef_usd_eem":     _c(25,    "USD -> EEM pts fall"),
            "coef_growth_eem":  _c(12,    "Growth -> EEM pts up"),
            "coef_10y_gdx":     _c(30,    "1% 10Y -> GDX pts fall (2x gold beta)"),
            "coef_usd_gdx":     _c(20,    "USD -> GDX pts fall"),
            "coef_10y_slv":     _c(8,     "1% 10Y -> SLV pts fall (less monetary than gold)"),
            "coef_usd_slv":     _c(5,     "USD -> SLV pts fall"),
        },
        "instruments": ["EURUSD", "SP500", "XAUUSD", "QQQ", "TLT", "USDJPY", "EEM", "GDX", "SLV"],
        "input_mapping": {
            "macro_surprise": {"source": "surprise", "unit": "sigma", "range": [-3, 3]}
        },
    },
  },

  # ═══════════════════════════════════════════════════════════════════════════════
  # 2. CENTRAL_BANK_DECISION
  # FOMC, ECB, BoJ, BoE, BoC, RBA, SNB, PBOC
  # ═══════════════════════════════════════════════════════════════════════════════
  {
    "name": "Central Bank Decision",
    "slug": "CENTRAL_BANK_DECISION",
    "category": "central_bank",
    "heuristic_ver": 2,
    "calibration_json": {"lag_days": 0, "half_life_days": 5, "absorption_days": 14, "decay_type": "exp"},
    "instruments": ["EURUSD", "SP500", "XAUUSD", "QQQ", "TLT", "USDJPY", "GBPUSD", "XLF", "EEM"],
    "description": "CB rate decision + forward guidance -> full curve repricing -> carry FX, bonds, equities",
    "ai_rationale": "Rate hike -> 2Y/10Y up -> carry differential -> EURUSD/USDJPY/GBPUSD down; TLT down; QQQ hardest hit (duration); XLF benefits from steeper curve. Dovish: all reversed. EEM suffers from USD rate rises (capital outflows).",
    "graph_json": {
        "nodes": [
            _n("rate_surprise_bps", "Rate Surprise bps",   "input",        140, 50,  unit="bps"),
            _n("tone_score",        "Guidance Tone",       "input",        500, 50,  unit="score"),
            _n("rate_channel",      "Rate Path Revision",  "observable",   140, 190, formula="rate_surprise_bps / 25 * {{coef_rate_mult}}"),
            _n("guidance_channel",  "Forward Guidance",    "latent",       500, 190, formula="tone_score * {{coef_tone}}"),
            _n("us_2y",  "Delta 2Y Yield",   "observable", 80,  340, formula="rate_channel * {{coef_rate_2y}} + guidance_channel * {{coef_tone_2y}}", unit="%"),
            _n("us_10y", "Delta 10Y Yield",  "observable", 320, 340, formula="rate_channel * {{coef_rate_10y}} + guidance_channel * {{coef_tone_10y}}", unit="%"),
            _n("risk_appetite", "Risk Appetite", "latent", 600, 340, formula="-rate_channel * {{coef_rate_risk}} + guidance_channel * {{coef_tone_risk}}"),
            _n("carry_diff", "Carry Differential", "observable", 80, 470, formula="us_2y * {{coef_2y_carry}}", unit="%"),
            # ── Row 1 outputs ─────────────────────────────────────────────────
            _n("eurusd",  "EUR/USD",      "market_asset", 60,  620, formula="-carry_diff * {{coef_carry_eurusd}}", unit="pips", instrument="EURUSD"),
            _n("sp500",   "S&P 500",      "market_asset", 270, 620, formula="risk_appetite * {{coef_risk_sp}} - us_10y * {{coef_10y_sp}}", unit="pts", instrument="SP500"),
            _n("xauusd",  "Gold XAU/USD", "market_asset", 480, 620, formula="-us_10y * {{coef_10y_gold}} - carry_diff * {{coef_carry_gold}}", unit="$/oz", instrument="XAUUSD"),
            # ── Row 2 outputs ─────────────────────────────────────────────────
            _n("qqq",    "QQQ Nasdaq",   "market_asset", 60,  790, formula="risk_appetite * {{coef_risk_qqq}} - us_10y * {{coef_10y_qqq}}", unit="pts", instrument="QQQ"),
            _n("tlt",    "TLT 20Y Bond", "market_asset", 230, 790, formula="-us_10y * {{coef_10y_tlt}}", unit="pts", instrument="TLT"),
            _n("usdjpy", "USD/JPY",      "market_asset", 400, 790, formula="-carry_diff * {{coef_carry_usdjpy}}", unit="pips", instrument="USDJPY"),
            _n("gbpusd", "GBP/USD",      "market_asset", 570, 790, formula="-carry_diff * {{coef_carry_gbpusd}}", unit="pips", instrument="GBPUSD"),
            _n("xlf",    "XLF Financials","market_asset", 740, 790, formula="us_10y * {{coef_10y_xlf}} + risk_appetite * {{coef_risk_xlf}}", unit="pts", instrument="XLF"),
            _n("eem",    "EEM Emerg Mkt", "market_asset", 910, 790, formula="-carry_diff * {{coef_carry_eem}}", unit="pts", instrument="EEM"),
        ],
        "edges": [
            _e("rate_surprise_bps", "rate_channel",   "solid",  "rate_reprice",  strength=3, sign="positive"),
            _e("tone_score",        "guidance_channel","dashed","fwd_guidance",  strength=3, sign="positive"),
            _e("rate_channel",  "us_2y",        "solid",  "short_anchor",  strength=3, sign="positive"),
            _e("guidance_channel","us_2y",       "dashed", "bleed_2y",      strength=1, sign="positive"),
            _e("rate_channel",  "us_10y",       "solid",  "level_shift",   strength=2, sign="positive"),
            _e("guidance_channel","us_10y",      "dashed", "long_end_guid", strength=2, sign="positive"),
            _e("rate_channel",  "risk_appetite","dashed", "cost_capital",  strength=2, sign="negative"),
            _e("guidance_channel","risk_appetite","dashed","confidence",   strength=2, sign="positive"),
            _e("us_2y",         "carry_diff",   "solid",  "carry",         strength=3, sign="positive"),
            # row 1
            _e("carry_diff",    "eurusd",  "solid",  "fx_carry",      strength=3, sign="negative"),
            _e("risk_appetite", "sp500",   "solid",  "risk_on_sp",    strength=2, sign="positive"),
            _e("us_10y",        "sp500",   "solid",  "discount_sp",   strength=3, sign="negative"),
            _e("us_10y",        "xauusd",  "solid",  "real_rate_gold",strength=3, sign="negative"),
            _e("carry_diff",    "xauusd",  "dashed", "opp_cost_gold", strength=2, sign="negative"),
            # row 2
            _e("risk_appetite", "qqq",    "solid",  "risk_on_qqq",   strength=2, sign="positive"),
            _e("us_10y",        "qqq",    "solid",  "duration_qqq",  strength=3, sign="negative", label="tech=long duration"),
            _e("us_10y",        "tlt",    "solid",  "bond_price",    strength=3, sign="negative"),
            _e("carry_diff",    "usdjpy", "solid",  "carry_jpy",     strength=2, sign="negative", label="USD carry -> JPY weakens"),
            _e("carry_diff",    "gbpusd", "solid",  "carry_gbp",     strength=2, sign="negative"),
            _e("us_10y",        "xlf",    "solid",  "nim_steepen",   strength=2, sign="positive",  label="steeper curve -> NIM"),
            _e("risk_appetite", "xlf",    "dashed", "risk_banks",    strength=2, sign="positive"),
            _e("carry_diff",    "eem",    "solid",  "em_outflows",   strength=2, sign="negative",  label="USD hike -> EM outflows"),
        ],
        "coefficients": {
            "coef_rate_mult":    _c(1.5,   "Decision -> rate path revision"),
            "coef_tone":         _c(0.8,   "Tone -> guidance signal"),
            "coef_rate_2y":      _c(0.70,  "Rate path -> 2Y"),
            "coef_tone_2y":      _c(0.03,  "Guidance -> 2Y bleed"),
            "coef_rate_10y":     _c(0.30,  "Rate path -> 10Y"),
            "coef_tone_10y":     _c(0.07,  "Guidance -> 10Y long end"),
            "coef_rate_risk":    _c(0.30,  "Rate hike -> risk appetite negative"),
            "coef_tone_risk":    _c(0.40,  "Positive guidance -> risk appetite"),
            "coef_2y_carry":     _c(1.0,   "2Y -> carry differential"),
            "coef_carry_eurusd": _c(120,   "1% carry -> EURUSD pips fall"),
            "coef_risk_sp":      _c(40,    "Risk appetite -> SP500 pts"),
            "coef_10y_sp":       _c(80,    "1% 10Y -> SP500 pts fall"),
            "coef_10y_gold":     _c(20,    "1% 10Y -> Gold fall $/oz"),
            "coef_carry_gold":   _c(15,    "Carry -> Gold fall"),
            "coef_risk_qqq":     _c(50,    "Risk appetite -> QQQ pts"),
            "coef_10y_qqq":      _c(110,   "1% 10Y -> QQQ pts fall (high duration)"),
            "coef_10y_tlt":      _c(16,    "1% 10Y -> TLT pts fall"),
            "coef_carry_usdjpy": _c(150,   "1% carry -> USDJPY pips (USD up -> JPY down)"),
            "coef_carry_gbpusd": _c(80,    "1% carry -> GBPUSD pips fall"),
            "coef_10y_xlf":      _c(15,    "1% 10Y -> XLF pts (curve steepens -> NIM up)"),
            "coef_risk_xlf":     _c(20,    "Risk appetite -> XLF pts"),
            "coef_carry_eem":    _c(20,    "1% carry -> EEM pts fall"),
        },
        "instruments": ["EURUSD", "SP500", "XAUUSD", "QQQ", "TLT", "USDJPY", "GBPUSD", "XLF", "EEM"],
        "input_mapping": {
            "rate_surprise_bps": {"source": "surprise_bps", "unit": "bps"},
            "tone_score":        {"source": "user_input", "unit": "score", "range": [-3, 3]},
        },
    },
  },

  # ═══════════════════════════════════════════════════════════════════════════════
  # 3. GROWTH_CORPORATE_SIGNAL
  # Earnings, M&A, layoffs, guidance, analyst revisions
  # ═══════════════════════════════════════════════════════════════════════════════
  {
    "name": "Growth & Corporate Signal",
    "slug": "GROWTH_CORPORATE_SIGNAL",
    "category": "growth_shock",
    "heuristic_ver": 2,
    "calibration_json": {"lag_days": 0, "half_life_days": 4, "absorption_days": 10, "decay_type": "exp"},
    "instruments": ["SP500", "QQQ", "IWM", "EURUSD", "XAUUSD", "HYG"],
    "description": "Corporate/macro growth signal -> earnings revision + risk appetite -> equities, credit, FX, Gold",
    "ai_rationale": "Earnings beats revise EPS upward and compress credit spreads (HYG up). QQQ > SP500 > IWM in tech-driven beats; IWM > SP500 in domestic growth signals. EURUSD follows risk appetite. Gold falls as safe haven demand recedes.",
    "graph_json": {
        "nodes": [
            _n("growth_trigger",    "Growth Signal",        "input",        350, 50,  unit="%"),
            _n("earnings_revision", "Earnings Revision",    "latent",       180, 200, formula="growth_trigger * {{coef_trig_earn}}"),
            _n("econ_activity",     "Economic Activity",    "observable",   520, 200, formula="growth_trigger * {{coef_trig_activity}}"),
            _n("risk_appetite",     "Risk Appetite",        "latent",       350, 360, formula="earnings_revision * {{coef_earn_risk}} + econ_activity * {{coef_act_risk}}"),
            _n("us_10y",            "Delta US 10Y",         "observable",   100, 360, formula="econ_activity * {{coef_act_10y}}", unit="%"),
            # ── Row 1 outputs ─────────────────────────────────────────────────
            _n("sp500",   "S&P 500",      "market_asset", 60,  540, formula="earnings_revision * {{coef_earn_sp}} + risk_appetite * {{coef_risk_sp}} - us_10y * {{coef_10y_sp}}", unit="pts", instrument="SP500"),
            _n("qqq",     "QQQ Nasdaq",   "market_asset", 250, 540, formula="earnings_revision * {{coef_earn_qqq}} + risk_appetite * {{coef_risk_qqq}} - us_10y * {{coef_10y_qqq}}", unit="pts", instrument="QQQ"),
            _n("iwm",     "IWM Russell",  "market_asset", 440, 540, formula="econ_activity * {{coef_act_iwm}} + risk_appetite * {{coef_risk_iwm}}", unit="pts", instrument="IWM"),
            # ── Row 2 outputs ─────────────────────────────────────────────────
            _n("eurusd",  "EUR/USD",      "market_asset", 60,  710, formula="risk_appetite * {{coef_risk_eurusd}}", unit="pips", instrument="EURUSD"),
            _n("xauusd",  "Gold XAU/USD", "market_asset", 280, 710, formula="-risk_appetite * {{coef_risk_gold}}", unit="$/oz", instrument="XAUUSD"),
            _n("hyg",     "HYG HY Bond",  "market_asset", 500, 710, formula="risk_appetite * {{coef_risk_hyg}}", unit="pts", instrument="HYG"),
        ],
        "edges": [
            _e("growth_trigger",    "earnings_revision","solid",  "eps_channel",   strength=3, sign="positive"),
            _e("growth_trigger",    "econ_activity",    "dashed", "activity_proxy",strength=2, sign="positive"),
            _e("growth_trigger",    "us_10y",           "dashed", "growth_premium",strength=1, sign="positive"),
            _e("earnings_revision", "risk_appetite",    "solid",  "confidence",    strength=3, sign="positive"),
            _e("econ_activity",     "risk_appetite",    "solid",  "sentiment",     strength=2, sign="positive"),
            _e("earnings_revision", "sp500",  "solid",  "eps_sp",      strength=3, sign="positive"),
            _e("risk_appetite",     "sp500",  "solid",  "risk_sp",     strength=2, sign="positive"),
            _e("us_10y",            "sp500",  "dashed", "discount_sp", strength=1, sign="negative"),
            _e("earnings_revision", "qqq",   "solid",  "eps_tech",    strength=3, sign="positive", label="tech earnings beta"),
            _e("risk_appetite",     "qqq",   "solid",  "risk_qqq",    strength=2, sign="positive"),
            _e("us_10y",            "qqq",   "dashed", "duration_qqq",strength=2, sign="negative"),
            _e("econ_activity",     "iwm",   "solid",  "domestic_act",strength=3, sign="positive", label="domestic growth"),
            _e("risk_appetite",     "iwm",   "solid",  "risk_iwm",    strength=2, sign="positive"),
            _e("risk_appetite",     "eurusd","solid",  "risk_on_fx",  strength=2, sign="positive"),
            _e("risk_appetite",     "xauusd","dashed", "safe_exit",   strength=2, sign="negative"),
            _e("risk_appetite",     "hyg",   "solid",  "spread_compress",strength=3, sign="positive", label="spreads compress"),
        ],
        "coefficients": {
            "coef_trig_earn":   _c(0.50, "Growth signal -> earnings revision"),
            "coef_trig_activity":_c(0.40,"Growth signal -> activity proxy"),
            "coef_act_10y":     _c(0.05, "Activity -> 10Y growth premium"),
            "coef_earn_risk":   _c(0.60, "Earnings -> risk appetite"),
            "coef_act_risk":    _c(0.40, "Activity -> risk appetite"),
            "coef_earn_sp":     _c(80,   "Earnings -> SP500 pts"),
            "coef_risk_sp":     _c(40,   "Risk appetite -> SP500 pts"),
            "coef_10y_sp":      _c(30,   "10Y -> SP500 pts fall"),
            "coef_earn_qqq":    _c(110,  "Earnings -> QQQ pts (higher beta)"),
            "coef_risk_qqq":    _c(55,   "Risk appetite -> QQQ pts"),
            "coef_10y_qqq":     _c(50,   "10Y -> QQQ pts fall"),
            "coef_act_iwm":     _c(60,   "Activity -> IWM pts (domestic)"),
            "coef_risk_iwm":    _c(35,   "Risk appetite -> IWM pts"),
            "coef_risk_eurusd": _c(30,   "Risk appetite -> EURUSD pips"),
            "coef_risk_gold":   _c(15,   "Risk appetite up -> Gold falls"),
            "coef_risk_hyg":    _c(0.50, "Risk appetite -> HYG pts (spread compress)"),
        },
        "instruments": ["SP500", "QQQ", "IWM", "EURUSD", "XAUUSD", "HYG"],
        "input_mapping": {
            "growth_trigger": {"source": "user_input", "unit": "%", "range": [-5, 5]}
        },
    },
  },

  # ═══════════════════════════════════════════════════════════════════════════════
  # 4. GEOPOLITICAL_RISK_OFF
  # Military conflicts, sanctions, coups, election shocks
  # ═══════════════════════════════════════════════════════════════════════════════
  {
    "name": "Geopolitical Risk-Off",
    "slug": "GEOPOLITICAL_RISK_OFF",
    "category": "geopolitical",
    "heuristic_ver": 2,
    "calibration_json": {"lag_days": 0, "half_life_days": 7, "absorption_days": 21, "decay_type": "linear"},
    "instruments": ["EURUSD", "XAUUSD", "SP500", "BRENT", "USDJPY", "VXX", "TLT", "HYG", "UNG", "SLV"],
    "description": "Geopolitical shock -> risk premium + safe haven demand -> multi-asset risk-off repricing",
    "ai_rationale": "Gold/TLT/JPY safe havens rise; EUR/SP500/HYG fall. VXX spikes. USDJPY falls (JPY strengthens as safe haven). BRENT and UNG spike if energy-producing region. EU equities most exposed to European conflicts.",
    "graph_json": {
        "nodes": [
            _n("geo_shock",       "Geopolitical Shock",   "input",        350, 50,  unit="score"),
            _n("risk_premium",    "Risk Premium",         "latent",       160, 200, formula="geo_shock * {{coef_shock_rp}}"),
            _n("safe_haven_dem",  "Safe Haven Demand",    "latent",       520, 200, formula="geo_shock * {{coef_shock_shd}}"),
            _n("oil_supply_risk", "Oil/Gas Supply Risk",  "latent",       820, 200, formula="geo_shock * {{coef_shock_oil}}"),
            _n("vix_spike",       "Delta VIX",            "observable",   160, 360, formula="risk_premium * {{coef_rp_vix}}", unit="pts"),
            _n("usd_safe_haven",  "USD Safe Haven",       "observable",   440, 360, formula="safe_haven_dem * {{coef_shd_usd}}"),
            _n("gold_demand",     "Gold Safe Haven",      "observable",   700, 360, formula="safe_haven_dem * {{coef_shd_gold}}"),
            # ── Row 1 outputs ─────────────────────────────────────────────────
            _n("eurusd",  "EUR/USD",      "market_asset", 60,  550, formula="-usd_safe_haven * {{coef_usd_eurusd}} - risk_premium * {{coef_rp_eurusd}}", unit="pips", instrument="EURUSD"),
            _n("sp500",   "S&P 500",      "market_asset", 280, 550, formula="-risk_premium * {{coef_rp_sp}} - vix_spike * {{coef_vix_sp}}", unit="pts", instrument="SP500"),
            _n("xauusd",  "Gold XAU/USD", "market_asset", 500, 550, formula="gold_demand * {{coef_gold_xau}}", unit="$/oz", instrument="XAUUSD"),
            _n("brent",   "Brent Crude",  "market_asset", 720, 550, formula="oil_supply_risk * {{coef_oilrisk_brent}}", unit="$/bbl", instrument="BRENT"),
            # ── Row 2 outputs ─────────────────────────────────────────────────
            _n("usdjpy",  "USD/JPY",      "market_asset", 60,  730, formula="-safe_haven_dem * {{coef_shd_usdjpy}}", unit="pips", instrument="USDJPY"),
            _n("vxx",     "VXX Vol ETN",  "market_asset", 240, 730, formula="vix_spike * {{coef_vix_vxx}}", unit="pts", instrument="VXX"),
            _n("tlt",     "TLT 20Y Bond", "market_asset", 420, 730, formula="safe_haven_dem * {{coef_shd_tlt}}", unit="pts", instrument="TLT"),
            _n("hyg",     "HYG HY Bond",  "market_asset", 600, 730, formula="-risk_premium * {{coef_rp_hyg}}", unit="pts", instrument="HYG"),
            _n("ung",     "UNG Nat Gas",  "market_asset", 780, 730, formula="oil_supply_risk * {{coef_oilrisk_ung}}", unit="pts", instrument="UNG"),
            _n("slv",     "SLV Silver",   "market_asset", 960, 730, formula="safe_haven_dem * {{coef_shd_slv}}", unit="pts", instrument="SLV"),
        ],
        "edges": [
            _e("geo_shock",      "risk_premium",   "solid",  "uncertainty",   strength=3, sign="positive"),
            _e("geo_shock",      "safe_haven_dem", "solid",  "flight_quality",strength=3, sign="positive"),
            _e("geo_shock",      "oil_supply_risk","dashed", "supply_threat", strength=2, sign="positive"),
            _e("risk_premium",   "vix_spike",      "solid",  "fear_gauge",    strength=3, sign="positive"),
            _e("safe_haven_dem", "usd_safe_haven", "solid",  "usd_flight",    strength=2, sign="positive"),
            _e("safe_haven_dem", "gold_demand",    "solid",  "gold_flight",   strength=3, sign="positive"),
            # row 1
            _e("usd_safe_haven", "eurusd",  "solid",  "usd_apprec",    strength=3, sign="negative"),
            _e("risk_premium",   "eurusd",  "dashed", "eu_exposure",   strength=2, sign="negative"),
            _e("risk_premium",   "sp500",   "solid",  "risk_reprice",  strength=3, sign="negative"),
            _e("vix_spike",      "sp500",   "solid",  "vol_crush_eq",  strength=2, sign="negative"),
            _e("gold_demand",    "xauusd",  "solid",  "gold_move",     strength=3, sign="positive"),
            _e("oil_supply_risk","brent",   "solid",  "supply_brent",  strength=3, sign="positive"),
            # row 2
            _e("safe_haven_dem", "usdjpy",  "solid",  "jpy_safe_haven",strength=3, sign="negative", label="JPY safe haven -> USDJPY falls"),
            _e("vix_spike",      "vxx",     "solid",  "vix_vxx",       strength=3, sign="positive", label="VIX up -> VXX up"),
            _e("safe_haven_dem", "tlt",     "solid",  "tsy_flight",    strength=3, sign="positive", label="flight to treasuries"),
            _e("risk_premium",   "hyg",     "solid",  "spread_widen",  strength=2, sign="negative", label="spreads widen"),
            _e("oil_supply_risk","ung",     "solid",  "gas_supply",    strength=2, sign="positive",  label="EU energy supply"),
            _e("safe_haven_dem", "slv",     "dashed", "silver_partial",strength=1, sign="positive"),
        ],
        "coefficients": {
            "coef_shock_rp":      _c(0.30, "Geo shock -> risk premium"),
            "coef_shock_shd":     _c(0.40, "Geo shock -> safe haven demand"),
            "coef_shock_oil":     _c(0.20, "Geo shock -> oil/gas supply risk"),
            "coef_rp_vix":        _c(2.0,  "Risk premium -> Delta VIX pts"),
            "coef_shd_usd":       _c(0.50, "Safe haven -> USD flow"),
            "coef_shd_gold":      _c(0.80, "Safe haven -> gold demand"),
            "coef_usd_eurusd":    _c(60,   "USD safe haven -> EURUSD pips fall"),
            "coef_rp_eurusd":     _c(40,   "Risk premium -> EURUSD fall"),
            "coef_rp_sp":         _c(80,   "Risk premium -> SP500 pts fall"),
            "coef_vix_sp":        _c(30,   "VIX spike -> SP500 pts fall"),
            "coef_gold_xau":      _c(25,   "Gold demand -> XAU/USD rise"),
            "coef_oilrisk_brent": _c(5.0,  "Oil risk -> Brent $/bbl"),
            "coef_shd_usdjpy":    _c(80,   "Safe haven -> USDJPY pips fall (JPY up)"),
            "coef_vix_vxx":       _c(2.5,  "1pt VIX -> VXX pts (futures basis)"),
            "coef_shd_tlt":       _c(3.0,  "Safe haven -> TLT pts up"),
            "coef_rp_hyg":        _c(0.80, "Risk premium -> HYG pts fall"),
            "coef_oilrisk_ung":   _c(3.0,  "Gas supply risk -> UNG pts"),
            "coef_shd_slv":       _c(0.30, "Safe haven -> SLV pts (partial)"),
        },
        "instruments": ["EURUSD", "XAUUSD", "SP500", "BRENT", "USDJPY", "VXX", "TLT", "HYG", "UNG", "SLV"],
        "input_mapping": {
            "geo_shock": {"source": "impact_score_scaled", "unit": "score", "range": [1, 10]}
        },
    },
  },

  # ═══════════════════════════════════════════════════════════════════════════════
  # 5. COMMODITY_SUPPLY_SHOCK
  # OPEC, sanctions on producers, pipeline disruptions, WASDE
  # ═══════════════════════════════════════════════════════════════════════════════
  {
    "name": "Commodity Supply Shock",
    "slug": "COMMODITY_SUPPLY_SHOCK",
    "category": "commodity",
    "heuristic_ver": 2,
    "calibration_json": {"lag_days": 1, "half_life_days": 5, "absorption_days": 14, "decay_type": "exp"},
    "instruments": ["BRENT", "XAUUSD", "EURUSD", "SP500", "UNG", "XLE", "SLV"],
    "description": "Supply disruption/glut -> Brent + energy inflation -> EU trade deficit -> multi-asset",
    "ai_rationale": "OPEC cuts hit BRENT and XLE (energy sector stocks) first. Energy inflation pressures EU trade balance (EURUSD down) and inflates Gold/SLV. UNG follows as cross-commodity supply chain linkage. S&P suffers from margin pressure.",
    "graph_json": {
        "nodes": [
            _n("supply_shock",    "Supply Shock",          "input",        350, 50,  unit="mbpd"),
            _n("supply_deficit",  "Supply Deficit",        "observable",   160, 200, formula="supply_shock * {{coef_shock_deficit}}"),
            _n("energy_inflation","Energy Inflation",      "latent",       600, 200, formula="supply_shock * {{coef_shock_einfl}}"),
            _n("eu_import_cost",  "EU Import Cost",        "latent",       440, 370, formula="energy_inflation * {{coef_einfl_import}}"),
            _n("infl_breakeven",  "Inflation Expect",      "latent",       780, 370, formula="energy_inflation * {{coef_einfl_breakeven}}"),
            # ── Row 1 outputs ─────────────────────────────────────────────────
            _n("brent",  "Brent Crude",  "market_asset", 60,  560, formula="supply_deficit * {{coef_deficit_brent}}", unit="$/bbl", instrument="BRENT"),
            _n("eurusd", "EUR/USD",      "market_asset", 260, 560, formula="-eu_import_cost * {{coef_import_eurusd}}", unit="pips", instrument="EURUSD"),
            _n("sp500",  "S&P 500",      "market_asset", 460, 560, formula="-eu_import_cost * {{coef_import_sp}} - infl_breakeven * {{coef_infl_sp}}", unit="pts", instrument="SP500"),
            _n("xauusd", "Gold XAU/USD", "market_asset", 660, 560, formula="infl_breakeven * {{coef_infl_gold}}", unit="$/oz", instrument="XAUUSD"),
            # ── Row 2 outputs ─────────────────────────────────────────────────
            _n("xle",   "XLE Energy",   "market_asset", 60,  740, formula="supply_deficit * {{coef_deficit_xle}}", unit="pts", instrument="XLE"),
            _n("ung",   "UNG Nat Gas",  "market_asset", 260, 740, formula="energy_inflation * {{coef_einfl_ung}}", unit="pts", instrument="UNG"),
            _n("slv",   "SLV Silver",   "market_asset", 460, 740, formula="infl_breakeven * {{coef_infl_slv}}", unit="pts", instrument="SLV"),
        ],
        "edges": [
            _e("supply_shock",    "supply_deficit",   "solid",  "production_gap",  strength=3, sign="positive"),
            _e("supply_shock",    "energy_inflation", "dashed", "cost_push",       strength=2, sign="positive"),
            _e("supply_deficit",  "brent",            "solid",  "spot_premium",    strength=3, sign="positive"),
            _e("energy_inflation","eu_import_cost",   "solid",  "trade_balance",   strength=2, sign="positive"),
            _e("energy_inflation","infl_breakeven",   "solid",  "breakeven_reprice",strength=2, sign="positive"),
            _e("eu_import_cost",  "eurusd",           "solid",  "terms_of_trade",  strength=2, sign="negative"),
            _e("eu_import_cost",  "sp500",            "dashed", "margin_squeeze",  strength=2, sign="negative"),
            _e("infl_breakeven",  "sp500",            "solid",  "multiple_compress",strength=2, sign="negative"),
            _e("infl_breakeven",  "xauusd",           "solid",  "infl_hedge",      strength=3, sign="positive"),
            _e("supply_deficit",  "xle",              "solid",  "energy_stocks",   strength=3, sign="positive", label="oil price -> energy stocks"),
            _e("energy_inflation","ung",              "dashed", "energy_chain",    strength=2, sign="positive", label="cross-commodity link"),
            _e("infl_breakeven",  "slv",              "solid",  "industrial_infl", strength=2, sign="positive", label="silver: industrial + inflation"),
        ],
        "coefficients": {
            "coef_shock_deficit":    _c(1.0,  "Supply shock mb/d -> deficit"),
            "coef_shock_einfl":      _c(0.10, "Supply shock -> energy inflation"),
            "coef_deficit_brent":    _c(3.0,  "1mb/d deficit -> Brent $/bbl"),
            "coef_einfl_import":     _c(0.60, "Energy inflation -> EU import cost"),
            "coef_einfl_breakeven":  _c(0.40, "Energy inflation -> breakeven"),
            "coef_import_eurusd":    _c(50,   "EU import cost -> EURUSD pips fall"),
            "coef_import_sp":        _c(40,   "Import cost -> SP500 pts fall"),
            "coef_infl_sp":          _c(30,   "Inflation exp -> SP500 pts fall"),
            "coef_infl_gold":        _c(20,   "Inflation exp -> Gold $/oz"),
            "coef_deficit_xle":      _c(1.5,  "1mb/d deficit -> XLE pts"),
            "coef_einfl_ung":        _c(1.0,  "Energy inflation -> UNG pts"),
            "coef_infl_slv":         _c(5,    "Inflation exp -> SLV pts"),
        },
        "instruments": ["BRENT", "XAUUSD", "EURUSD", "SP500", "UNG", "XLE", "SLV"],
        "input_mapping": {
            "supply_shock": {"source": "user_input", "unit": "mbpd", "range": [-3, 3]}
        },
    },
  },

  # ═══════════════════════════════════════════════════════════════════════════════
  # 6. TRADE_POLICY_SHOCK
  # Tariffs, protectionism, export bans
  # ═══════════════════════════════════════════════════════════════════════════════
  {
    "name": "Trade Policy Shock",
    "slug": "TRADE_POLICY_SHOCK",
    "category": "trade_policy",
    "heuristic_ver": 2,
    "calibration_json": {"lag_days": 1, "half_life_days": 10, "absorption_days": 21, "decay_type": "linear"},
    "instruments": ["EURUSD", "SP500", "XAUUSD", "EEM", "QQQ", "GBPUSD", "IWM"],
    "description": "Tariff announcement -> stagflationary shock -> EUR/GBP down SP500/QQQ down EEM down Gold up",
    "ai_rationale": "Tariffs create stagflation: import cost rise (inflation) + trade volume drop (growth). EEM is hit hardest via global trade contraction and USD strength. QQQ suffers from tech supply chain disruption (AAPL/NVDA exposed). GBPUSD exposed as UK trade-dependent. IWM domestic plays can benefit initially but input cost inflation hurts margins.",
    "graph_json": {
        "nodes": [
            _n("tariff_shock",     "Trade Restriction",    "input",        350, 50,  unit="%"),
            _n("import_cost_rise", "Import Cost Rise",     "observable",   160, 200, formula="tariff_shock * {{coef_tariff_cost}}"),
            _n("trade_volume_drop","Trade Volume Drop",    "observable",   600, 200, formula="-tariff_shock * {{coef_tariff_vol}}"),
            _n("policy_uncertainty","Policy Uncertainty",  "latent",       350, 200, formula="tariff_shock * {{coef_tariff_unc}}"),
            _n("stagflation_risk", "Stagflation Risk",     "latent",       350, 380, formula="import_cost_rise * {{coef_cost_stag}} - trade_volume_drop * {{coef_vol_stag}}"),
            _n("corp_margin",      "Corp Margin Pressure", "latent",       650, 380, formula="import_cost_rise * {{coef_cost_margin}}"),
            # ── Row 1 outputs ─────────────────────────────────────────────────
            _n("eurusd",  "EUR/USD",      "market_asset", 60,  570, formula="-stagflation_risk * {{coef_stag_eurusd}} - trade_volume_drop * {{coef_vol_eurusd}}", unit="pips", instrument="EURUSD"),
            _n("sp500",   "S&P 500",      "market_asset", 300, 570, formula="-corp_margin * {{coef_margin_sp}} - policy_uncertainty * {{coef_unc_sp}}", unit="pts", instrument="SP500"),
            _n("xauusd",  "Gold XAU/USD", "market_asset", 540, 570, formula="stagflation_risk * {{coef_stag_gold}} + policy_uncertainty * {{coef_unc_gold}}", unit="$/oz", instrument="XAUUSD"),
            # ── Row 2 outputs ─────────────────────────────────────────────────
            _n("eem",    "EEM Emerg Mkt", "market_asset", 60,  750, formula="-trade_volume_drop * {{coef_vol_eem}} - stagflation_risk * {{coef_stag_eem}}", unit="pts", instrument="EEM"),
            _n("qqq",    "QQQ Nasdaq",    "market_asset", 260, 750, formula="-corp_margin * {{coef_margin_qqq}} - policy_uncertainty * {{coef_unc_qqq}}", unit="pts", instrument="QQQ"),
            _n("gbpusd", "GBP/USD",       "market_asset", 460, 750, formula="-stagflation_risk * {{coef_stag_gbpusd}} - trade_volume_drop * {{coef_vol_gbpusd}}", unit="pips", instrument="GBPUSD"),
            _n("iwm",    "IWM Russell",   "market_asset", 660, 750, formula="-policy_uncertainty * {{coef_unc_iwm}}", unit="pts", instrument="IWM"),
        ],
        "edges": [
            _e("tariff_shock",      "import_cost_rise",   "solid",  "price_passthru",  strength=3, sign="positive"),
            _e("tariff_shock",      "trade_volume_drop",  "solid",  "trade_contract",  strength=3, sign="negative"),
            _e("tariff_shock",      "policy_uncertainty", "solid",  "capex_freeze",    strength=3, sign="positive"),
            _e("import_cost_rise",  "stagflation_risk",   "solid",  "cost_stag",       strength=2, sign="positive"),
            _e("trade_volume_drop", "stagflation_risk",   "solid",  "growth_stag",     strength=2, sign="negative"),
            _e("import_cost_rise",  "corp_margin",        "solid",  "supply_chain",    strength=3, sign="positive"),
            _e("stagflation_risk",  "eurusd",   "solid",  "eu_stag",        strength=2, sign="negative"),
            _e("trade_volume_drop", "eurusd",   "dashed", "export_shock",   strength=2, sign="negative"),
            _e("corp_margin",       "sp500",    "solid",  "eps_compress",   strength=3, sign="negative"),
            _e("policy_uncertainty","sp500",    "solid",  "capex_sentiment",strength=2, sign="negative"),
            _e("stagflation_risk",  "xauusd",   "solid",  "stag_hedge",     strength=3, sign="positive"),
            _e("policy_uncertainty","xauusd",   "dashed", "unc_gold",       strength=2, sign="positive"),
            _e("trade_volume_drop", "eem",      "solid",  "global_trade_eem",strength=3, sign="negative", label="EM most exposed"),
            _e("stagflation_risk",  "eem",      "dashed", "usd_eem",        strength=2, sign="negative"),
            _e("corp_margin",       "qqq",      "solid",  "tech_supply_ch", strength=3, sign="negative", label="AAPL/NVDA supply chain"),
            _e("policy_uncertainty","qqq",      "dashed", "tech_capex",     strength=2, sign="negative"),
            _e("stagflation_risk",  "gbpusd",   "solid",  "uk_trade",       strength=2, sign="negative"),
            _e("trade_volume_drop", "gbpusd",   "dashed", "uk_exports",     strength=2, sign="negative"),
            _e("policy_uncertainty","iwm",      "dashed", "domestic_capex", strength=2, sign="negative"),
        ],
        "coefficients": {
            "coef_tariff_cost":   _c(0.30, "10% tariff -> ~3% import cost"),
            "coef_tariff_vol":    _c(0.50, "Tariff -> trade volume drop"),
            "coef_tariff_unc":    _c(0.40, "Tariff -> policy uncertainty"),
            "coef_cost_stag":     _c(0.50, "Cost -> stagflation index"),
            "coef_vol_stag":      _c(0.50, "Vol drop -> growth component"),
            "coef_cost_margin":   _c(0.60, "Cost -> margin pressure"),
            "coef_stag_eurusd":   _c(60,   "Stagflation -> EURUSD pips fall"),
            "coef_vol_eurusd":    _c(30,   "Trade drop -> EURUSD pips fall"),
            "coef_margin_sp":     _c(60,   "Margin -> SP500 pts fall"),
            "coef_unc_sp":        _c(40,   "Uncertainty -> SP500 pts fall"),
            "coef_stag_gold":     _c(20,   "Stagflation -> Gold $/oz"),
            "coef_unc_gold":      _c(15,   "Uncertainty -> Gold $/oz"),
            "coef_vol_eem":       _c(25,   "Trade drop -> EEM pts fall"),
            "coef_stag_eem":      _c(20,   "Stagflation -> EEM pts fall (USD up)"),
            "coef_margin_qqq":    _c(80,   "Margin -> QQQ pts fall"),
            "coef_unc_qqq":       _c(50,   "Uncertainty -> QQQ pts fall"),
            "coef_stag_gbpusd":   _c(40,   "Stagflation -> GBPUSD pips fall"),
            "coef_vol_gbpusd":    _c(30,   "Trade drop -> GBPUSD pips fall"),
            "coef_unc_iwm":       _c(30,   "Uncertainty -> IWM pts fall"),
        },
        "instruments": ["EURUSD", "SP500", "XAUUSD", "EEM", "QQQ", "GBPUSD", "IWM"],
        "input_mapping": {
            "tariff_shock": {"source": "user_input", "unit": "%", "range": [0, 50]}
        },
    },
  },

  # ═══════════════════════════════════════════════════════════════════════════════
  # 7. CREDIT_SYSTEMIC_EVENT
  # Bank failure, sovereign debt crisis, HY blowout, financial contagion
  # ═══════════════════════════════════════════════════════════════════════════════
  {
    "name": "Credit & Systemic Stress",
    "slug": "CREDIT_SYSTEMIC_EVENT",
    "category": "credit_stress",
    "heuristic_ver": 2,
    "calibration_json": {"lag_days": 0, "half_life_days": 7, "absorption_days": 21, "decay_type": "exp"},
    "instruments": ["EURUSD", "SP500", "XAUUSD", "HYG", "TLT", "VXX", "XLF", "USDJPY", "BTC"],
    "description": "Credit event -> spread widening + deleveraging -> systemic risk-off + USD liquidity flight",
    "ai_rationale": "HYG is the primary instrument (spreads widen = HYG down). TLT initially up (flight to quality) then volatile (sold for cash). VXX spikes hard. XLF (financials) crushed. USDJPY falls (JPY safe haven). BTC complex: initially up as alternative store of value, then down on margin calls.",
    "graph_json": {
        "nodes": [
            _n("credit_trigger",   "Credit Event",         "input",        350, 50,  unit="bps"),
            _n("spread_widening",  "Spread Widening",      "observable",   160, 200, formula="credit_trigger * {{coef_trig_spread}}", unit="bps"),
            _n("counterparty_risk","Counterparty Risk",    "latent",       600, 200, formula="credit_trigger * {{coef_trig_cp}}"),
            _n("fin_conditions",   "Financial Conditions", "observable",   160, 370, formula="spread_widening * {{coef_spread_fc}}"),
            _n("liq_premium",      "Liquidity Premium",    "latent",       440, 370, formula="spread_widening * {{coef_spread_liq}} + counterparty_risk * {{coef_cp_liq}}"),
            _n("deleveraging",     "Forced Deleveraging",  "latent",       750, 370, formula="counterparty_risk * {{coef_cp_delev}}"),
            # ── Row 1 outputs ─────────────────────────────────────────────────
            _n("eurusd",  "EUR/USD",      "market_asset", 60,  570, formula="-liq_premium * {{coef_liq_eurusd}} - fin_conditions * {{coef_fc_eurusd}}", unit="pips", instrument="EURUSD"),
            _n("sp500",   "S&P 500",      "market_asset", 300, 570, formula="-fin_conditions * {{coef_fc_sp}} - deleveraging * {{coef_delev_sp}}", unit="pts", instrument="SP500"),
            _n("xauusd",  "Gold XAU/USD", "market_asset", 540, 570, formula="liq_premium * {{coef_liq_gold}} - deleveraging * {{coef_delev_gold}}", unit="$/oz", instrument="XAUUSD"),
            # ── Row 2 outputs ─────────────────────────────────────────────────
            _n("hyg",    "HYG HY Bond",   "market_asset", 60,  760, formula="-spread_widening * {{coef_spread_hyg}} - deleveraging * {{coef_delev_hyg}}", unit="pts", instrument="HYG"),
            _n("tlt",    "TLT 20Y Bond",  "market_asset", 240, 760, formula="liq_premium * {{coef_liq_tlt}} - deleveraging * {{coef_delev_tlt}}", unit="pts", instrument="TLT"),
            _n("vxx",    "VXX Vol ETN",   "market_asset", 420, 760, formula="spread_widening * {{coef_spread_vxx}}", unit="pts", instrument="VXX"),
            _n("xlf",    "XLF Financials","market_asset", 600, 760, formula="-spread_widening * {{coef_spread_xlf}} - fin_conditions * {{coef_fc_xlf}}", unit="pts", instrument="XLF"),
            _n("usdjpy", "USD/JPY",        "market_asset", 780, 760, formula="-liq_premium * {{coef_liq_usdjpy}}", unit="pips", instrument="USDJPY"),
            _n("btc",    "BTC-USD",        "market_asset", 960, 760, formula="liq_premium * {{coef_liq_btc}} - deleveraging * {{coef_delev_btc}}", unit="pts", instrument="BTC"),
        ],
        "edges": [
            _e("credit_trigger",   "spread_widening",  "solid",  "contagion",     strength=3, sign="positive"),
            _e("credit_trigger",   "counterparty_risk","solid",  "systemic_fear", strength=3, sign="positive"),
            _e("spread_widening",  "fin_conditions",   "solid",  "tightening",    strength=3, sign="positive"),
            _e("spread_widening",  "liq_premium",      "solid",  "cash_demand",   strength=2, sign="positive"),
            _e("counterparty_risk","liq_premium",      "solid",  "cash_hoarding", strength=3, sign="positive"),
            _e("counterparty_risk","deleveraging",     "solid",  "margin_calls",  strength=3, sign="positive"),
            _e("liq_premium",      "eurusd",  "solid",  "usd_flight",    strength=3, sign="negative"),
            _e("fin_conditions",   "eurusd",  "dashed", "fin_cond_fx",   strength=2, sign="negative"),
            _e("fin_conditions",   "sp500",   "solid",  "valuation",     strength=3, sign="negative"),
            _e("deleveraging",     "sp500",   "solid",  "forced_sell",   strength=3, sign="negative"),
            _e("liq_premium",      "xauusd",  "dashed", "safe_initial",  strength=2, sign="positive"),
            _e("deleveraging",     "xauusd",  "dashed", "sold_margin",   strength=2, sign="negative"),
            _e("spread_widening",  "hyg",     "solid",  "direct_spread", strength=3, sign="negative", label="HY spreads -> HYG falls"),
            _e("deleveraging",     "hyg",     "solid",  "forced_selling_hy",strength=2, sign="negative"),
            _e("liq_premium",      "tlt",     "solid",  "tsy_safe",      strength=3, sign="positive", label="flight to treasuries"),
            _e("deleveraging",     "tlt",     "dashed", "sold_tsy",      strength=2, sign="negative", label="sold for cash"),
            _e("spread_widening",  "vxx",     "solid",  "vol_spike",     strength=3, sign="positive"),
            _e("spread_widening",  "xlf",     "solid",  "bank_stress",   strength=3, sign="negative", label="banks = credit exposure"),
            _e("fin_conditions",   "xlf",     "solid",  "npa_risk",      strength=2, sign="negative"),
            _e("liq_premium",      "usdjpy",  "solid",  "jpy_safe",      strength=2, sign="negative", label="JPY safe haven"),
            _e("liq_premium",      "btc",     "dashed", "alt_store",     strength=2, sign="positive",  label="alt store of value (initial)"),
            _e("deleveraging",     "btc",     "dashed", "btc_margin",    strength=2, sign="negative",  label="margin call selling"),
        ],
        "coefficients": {
            "coef_trig_spread":  _c(1.0,  "Credit event -> spread widening"),
            "coef_trig_cp":      _c(0.60, "Credit event -> counterparty risk"),
            "coef_spread_fc":    _c(0.50, "Spread -> financial conditions"),
            "coef_spread_liq":   _c(0.40, "Spread -> liquidity premium"),
            "coef_cp_liq":       _c(0.60, "Counterparty -> liquidity premium"),
            "coef_cp_delev":     _c(0.50, "Counterparty -> deleveraging"),
            "coef_liq_eurusd":   _c(80,   "Liquidity -> EURUSD pips fall"),
            "coef_fc_eurusd":    _c(40,   "Fin cond -> EURUSD fall"),
            "coef_fc_sp":        _c(100,  "Fin cond -> SP500 pts fall"),
            "coef_delev_sp":     _c(80,   "Deleveraging -> SP500 pts fall"),
            "coef_liq_gold":     _c(10,   "Liquidity -> Gold rise (initial)"),
            "coef_delev_gold":   _c(15,   "Deleveraging -> Gold fall (sold)"),
            "coef_spread_hyg":   _c(0.05, "1bps spread -> HYG pts fall"),
            "coef_delev_hyg":    _c(0.30, "Deleveraging -> HYG pts fall"),
            "coef_liq_tlt":      _c(3.0,  "Liquidity -> TLT pts up (initial)"),
            "coef_delev_tlt":    _c(1.5,  "Deleveraging -> TLT pts fall"),
            "coef_spread_vxx":   _c(0.05, "1bps spread -> VXX pts up"),
            "coef_spread_xlf":   _c(0.04, "1bps spread -> XLF pts fall"),
            "coef_fc_xlf":       _c(0.60, "Fin cond -> XLF pts fall"),
            "coef_liq_usdjpy":   _c(60,   "Liquidity -> USDJPY pips fall (JPY up)"),
            "coef_liq_btc":      _c(200,  "Liquidity -> BTC pts up (alt store)"),
            "coef_delev_btc":    _c(400,  "Deleveraging -> BTC pts fall (margin calls)"),
        },
        "instruments": ["EURUSD", "SP500", "XAUUSD", "HYG", "TLT", "VXX", "XLF", "USDJPY", "BTC"],
        "input_mapping": {
            "credit_trigger": {"source": "user_input", "unit": "bps", "range": [50, 500]}
        },
    },
  },

  # ═══════════════════════════════════════════════════════════════════════════════
  # 8. TECHNICAL_MOMENTUM_BREAKOUT
  # MA crosses, RSI extremes, key level breaks, 52w H/L
  # ═══════════════════════════════════════════════════════════════════════════════
  {
    "name": "Technical Momentum Breakout",
    "slug": "TECHNICAL_MOMENTUM_BREAKOUT",
    "category": "technical",
    "heuristic_ver": 2,
    "calibration_json": {"lag_days": 0, "half_life_days": 3, "absorption_days": 7, "decay_type": "exp"},
    "instruments": ["EURUSD", "SP500", "QQQ"],
    "description": "Technical breakout/breakdown -> stop cascade + momentum -> instrument price action",
    "ai_rationale": "Technical events operate via microstructure: breakout triggers stops, amplifying the move. No fundamental anchor. The correlated asset move is weaker. Applies to any instrument — the primary and correlated move fields should be overridden per event.",
    "graph_json": {
        "nodes": [
            _n("tech_signal",    "Technical Signal",  "input",        300, 50,  unit="sigma"),
            _n("momentum_score", "Momentum Score",    "observable",   160, 220, formula="tech_signal * {{coef_sig_mom}}"),
            _n("stop_cascade",   "Stop Order Cascade","latent",       500, 220, formula="tech_signal * {{coef_sig_stop}}"),
            _n("trend_strength", "Trend Strength",    "latent",       300, 400, formula="momentum_score * {{coef_mom_trend}} + stop_cascade * {{coef_stop_trend}}"),
            _n("eurusd",  "Primary Instrument",  "market_asset", 160, 600, formula="trend_strength * {{coef_trend_inst}} + stop_cascade * {{coef_stop_inst}}", unit="pips", instrument="EURUSD"),
            _n("sp500",   "Correlated Asset A",  "market_asset", 400, 600, formula="trend_strength * {{coef_trend_corr}}", unit="pts", instrument="SP500"),
            _n("qqq",     "Correlated Asset B",  "market_asset", 620, 600, formula="trend_strength * {{coef_trend_corr2}}", unit="pts", instrument="QQQ"),
        ],
        "edges": [
            _e("tech_signal",    "momentum_score", "solid",  "signal_strength",strength=3, sign="positive"),
            _e("tech_signal",    "stop_cascade",   "solid",  "stop_trigger",   strength=3, sign="positive"),
            _e("momentum_score", "trend_strength", "solid",  "trend_confirm",  strength=3, sign="positive"),
            _e("stop_cascade",   "trend_strength", "solid",  "amplification",  strength=2, sign="positive"),
            _e("trend_strength", "eurusd",         "solid",  "price_action",   strength=3, sign="positive"),
            _e("stop_cascade",   "eurusd",         "solid",  "liquidity_grab", strength=2, sign="positive"),
            _e("trend_strength", "sp500",          "dashed", "spillover_a",    strength=2, sign="positive"),
            _e("trend_strength", "qqq",            "dashed", "spillover_b",    strength=2, sign="positive"),
        ],
        "coefficients": {
            "coef_sig_mom":    _c(1.0,  "Signal -> momentum score"),
            "coef_sig_stop":   _c(0.60, "Signal -> stop cascade"),
            "coef_mom_trend":  _c(0.70, "Momentum -> trend strength"),
            "coef_stop_trend": _c(0.50, "Stop cascade -> amplification"),
            "coef_trend_inst": _c(50,   "Trend -> primary instrument (pips)"),
            "coef_stop_inst":  _c(30,   "Stop cascade -> extra move (pips)"),
            "coef_trend_corr": _c(20,   "Trend -> correlated A (pts)"),
            "coef_trend_corr2":_c(25,   "Trend -> correlated B (pts)"),
        },
        "instruments": ["EURUSD", "SP500", "QQQ"],
        "input_mapping": {
            "tech_signal": {"source": "user_input", "unit": "sigma", "range": [-5, 5]}
        },
    },
  },

  # ═══════════════════════════════════════════════════════════════════════════════
  # 9. SENTIMENT_POSITIONING_EXTREME
  # VIX extremes, COT crowded, put/call skew extremes — contrarian setup
  # ═══════════════════════════════════════════════════════════════════════════════
  {
    "name": "Sentiment & Positioning Extreme",
    "slug": "SENTIMENT_POSITIONING_EXTREME",
    "category": "sentiment",
    "heuristic_ver": 2,
    "calibration_json": {"lag_days": 1, "half_life_days": 5, "absorption_days": 10, "decay_type": "linear"},
    "instruments": ["SP500", "QQQ", "EURUSD", "XAUUSD", "VXX", "HYG", "BTC"],
    "description": "Extreme fear / greed -> crowded trade unwind -> contrarian mean reversion",
    "ai_rationale": "+sigma = extreme fear/oversold (contrarian buy signal). VXX collapses as fear recedes. HYG recovers as credit spreads compress. BTC follows the risk appetite extreme. Mean reversion typically takes 3-10 days to materialize once the flow exhausts.",
    "graph_json": {
        "nodes": [
            _n("sent_trigger",  "Sentiment Extreme",   "input",        350, 50,  unit="sigma"),
            _n("pos_imbalance", "Positioning Imbalance","observable",  160, 210, formula="sent_trigger * {{coef_sent_pos}}"),
            _n("vol_regime",    "Volatility Regime",   "observable",   600, 210, formula="-sent_trigger * {{coef_sent_vol}}"),
            _n("crowd_risk",    "Crowded Trade Risk",  "latent",       160, 380, formula="pos_imbalance * {{coef_pos_crowd}}"),
            _n("mr_force",      "Mean Reversion Force","latent",       400, 380, formula="crowd_risk * {{coef_crowd_mr}}"),
            # ── Row 1 outputs ─────────────────────────────────────────────────
            _n("sp500",   "S&P 500",      "market_asset", 60,  580, formula="mr_force * {{coef_mr_sp}} + vol_regime * {{coef_vol_sp}}", unit="pts", instrument="SP500"),
            _n("qqq",     "QQQ Nasdaq",   "market_asset", 260, 580, formula="mr_force * {{coef_mr_qqq}} + vol_regime * {{coef_vol_qqq}}", unit="pts", instrument="QQQ"),
            _n("eurusd",  "EUR/USD",      "market_asset", 460, 580, formula="mr_force * {{coef_mr_eurusd}}", unit="pips", instrument="EURUSD"),
            _n("xauusd",  "Gold XAU/USD", "market_asset", 660, 580, formula="-mr_force * {{coef_mr_gold}}", unit="$/oz", instrument="XAUUSD"),
            # ── Row 2 outputs ─────────────────────────────────────────────────
            _n("vxx",  "VXX Vol ETN",  "market_asset", 60,  760, formula="-mr_force * {{coef_mr_vxx}} - vol_regime * {{coef_vol_vxx}}", unit="pts", instrument="VXX"),
            _n("hyg",  "HYG HY Bond",  "market_asset", 280, 760, formula="mr_force * {{coef_mr_hyg}}", unit="pts", instrument="HYG"),
            _n("btc",  "BTC-USD",      "market_asset", 500, 760, formula="mr_force * {{coef_mr_btc}}", unit="pts", instrument="BTC"),
        ],
        "edges": [
            _e("sent_trigger",  "pos_imbalance","solid",  "flow_data",    strength=3, sign="positive"),
            _e("sent_trigger",  "vol_regime",   "dashed", "vol_signal",   strength=2, sign="negative"),
            _e("pos_imbalance", "crowd_risk",   "solid",  "crowd_build",  strength=3, sign="positive"),
            _e("crowd_risk",    "mr_force",     "solid",  "squeeze",      strength=3, sign="positive"),
            _e("mr_force",      "sp500",    "solid",  "contrarian_sp",  strength=3, sign="positive"),
            _e("vol_regime",    "sp500",    "dashed", "vol_compress",   strength=2, sign="positive"),
            _e("mr_force",      "qqq",     "solid",  "contrarian_qqq", strength=3, sign="positive"),
            _e("vol_regime",    "qqq",     "dashed", "vol_qqq",        strength=2, sign="positive"),
            _e("mr_force",      "eurusd",  "solid",  "unwind_fx",      strength=2, sign="positive"),
            _e("mr_force",      "xauusd",  "dashed", "safe_exit",      strength=2, sign="negative"),
            _e("mr_force",      "vxx",     "solid",  "vol_crush",      strength=3, sign="negative", label="fear recedes -> VXX falls"),
            _e("vol_regime",    "vxx",     "solid",  "vol_collapse",   strength=3, sign="negative"),
            _e("mr_force",      "hyg",     "solid",  "spread_recovery",strength=2, sign="positive"),
            _e("mr_force",      "btc",     "dashed", "btc_recovery",   strength=2, sign="positive"),
        ],
        "coefficients": {
            "coef_sent_pos":  _c(1.0,  "Sentiment -> positioning imbalance"),
            "coef_sent_vol":  _c(0.50, "Fear extreme -> vol regime"),
            "coef_pos_crowd": _c(0.80, "Positioning -> crowded trade risk"),
            "coef_crowd_mr":  _c(0.70, "Crowded trade -> mean reversion force"),
            "coef_mr_sp":     _c(60,   "MR force -> SP500 pts"),
            "coef_vol_sp":    _c(30,   "Vol compression -> SP500 pts"),
            "coef_mr_qqq":    _c(80,   "MR force -> QQQ pts (higher beta)"),
            "coef_vol_qqq":   _c(40,   "Vol compression -> QQQ pts"),
            "coef_mr_eurusd": _c(40,   "MR force -> EURUSD pips"),
            "coef_mr_gold":   _c(15,   "Fear recedes -> Gold falls"),
            "coef_mr_vxx":    _c(3.0,  "MR force -> VXX pts fall"),
            "coef_vol_vxx":   _c(4.0,  "Vol collapse -> VXX pts fall"),
            "coef_mr_hyg":    _c(0.40, "MR force -> HYG pts up"),
            "coef_mr_btc":    _c(300,  "MR force -> BTC pts"),
        },
        "instruments": ["SP500", "QQQ", "EURUSD", "XAUUSD", "VXX", "HYG", "BTC"],
        "input_mapping": {
            "sent_trigger": {"source": "user_input", "unit": "sigma", "range": [-4, 4]}
        },
    },
  },

  # ═══════════════════════════════════════════════════════════════════════════════
  # 10. COMMODITY_INVENTORY_REPORT
  # EIA crude weekly, EIA gas storage, API crude, DOE
  # ═══════════════════════════════════════════════════════════════════════════════
  {
    "name": "Commodity Inventory Report",
    "slug": "COMMODITY_INVENTORY_REPORT",
    "category": "commodity",
    "heuristic_ver": 2,
    "calibration_json": {"lag_days": 0, "half_life_days": 2, "absorption_days": 5, "decay_type": "exp"},
    "instruments": ["BRENT", "EURUSD", "XAUUSD", "UNG", "XLE"],
    "description": "Weekly inventory surprise -> supply/demand balance -> Brent + XLE + UNG",
    "ai_rationale": "EIA crude draw vs consensus -> Brent spot up + backwardation. XLE (energy sector ETF) follows Brent with some lag. EIA gas storage report separately drives UNG. Secondary inflation channel hits EURUSD and gold.",
    "graph_json": {
        "nodes": [
            _n("inv_surprise",    "Inventory Surprise",  "input",        350, 50,  unit="mb", description="+mb = draw (bullish), -mb = build"),
            _n("sd_balance",      "Supply/Demand Balance","observable",  200, 220, formula="inv_surprise * {{coef_surp_balance}}"),
            _n("price_pressure",  "Spot Price Pressure", "latent",       200, 390, formula="sd_balance * {{coef_balance_pressure}}"),
            _n("fwd_curve_shift", "Forward Curve Shift", "observable",   560, 390, formula="sd_balance * {{coef_balance_fwd}}"),
            _n("energy_cost_fwd", "Energy Cost Outlook", "latent",       740, 390, formula="price_pressure * {{coef_pressure_cost}}"),
            # ── Row 1 outputs ─────────────────────────────────────────────────
            _n("brent",  "Brent Crude",  "market_asset", 60,  580, formula="price_pressure * {{coef_pressure_brent}} + fwd_curve_shift * {{coef_fwd_brent}}", unit="$/bbl", instrument="BRENT"),
            _n("eurusd", "EUR/USD",      "market_asset", 310, 580, formula="-energy_cost_fwd * {{coef_energy_eurusd}}", unit="pips", instrument="EURUSD"),
            _n("xauusd", "Gold XAU/USD", "market_asset", 560, 580, formula="energy_cost_fwd * {{coef_energy_gold}}", unit="$/oz", instrument="XAUUSD"),
            # ── Row 2 outputs ─────────────────────────────────────────────────
            _n("xle",  "XLE Energy ETF","market_asset", 60,  760, formula="price_pressure * {{coef_pressure_xle}}", unit="pts", instrument="XLE"),
            _n("ung",  "UNG Nat Gas",   "market_asset", 310, 760, formula="sd_balance * {{coef_balance_ung}}", unit="pts", instrument="UNG"),
        ],
        "edges": [
            _e("inv_surprise",   "sd_balance",     "solid",  "tightness",      strength=3, sign="positive"),
            _e("sd_balance",     "price_pressure", "solid",  "spot_impact",    strength=3, sign="positive"),
            _e("sd_balance",     "fwd_curve_shift","solid",  "curve_structure",strength=2, sign="positive"),
            _e("price_pressure", "energy_cost_fwd","dashed", "cost_outlook",   strength=2, sign="positive"),
            _e("price_pressure", "brent",          "solid",  "spot_move",      strength=3, sign="positive"),
            _e("fwd_curve_shift","brent",          "solid",  "backwardation",  strength=2, sign="positive"),
            _e("energy_cost_fwd","eurusd",         "dashed", "trade_deficit",  strength=2, sign="negative"),
            _e("energy_cost_fwd","xauusd",         "dashed", "infl_hedge",     strength=2, sign="positive"),
            _e("price_pressure", "xle",            "solid",  "energy_stocks",  strength=3, sign="positive", label="oil -> energy sector"),
            _e("sd_balance",     "ung",            "dashed", "gas_correlation",strength=2, sign="positive", label="EIA gas storage parallel"),
        ],
        "coefficients": {
            "coef_surp_balance":    _c(1.0,  "1mb surprise -> balance signal"),
            "coef_balance_pressure":_c(0.8,  "Balance -> spot pressure"),
            "coef_balance_fwd":     _c(0.4,  "Balance -> forward curve"),
            "coef_pressure_cost":   _c(0.3,  "Pressure -> energy cost outlook"),
            "coef_pressure_brent":  _c(0.5,  "1mb -> Brent $/bbl"),
            "coef_fwd_brent":       _c(0.2,  "Curve shift -> Brent $/bbl"),
            "coef_energy_eurusd":   _c(20,   "Energy cost -> EURUSD pips fall"),
            "coef_energy_gold":     _c(5,    "Energy cost -> Gold $/oz"),
            "coef_pressure_xle":    _c(0.4,  "Oil pressure -> XLE pts"),
            "coef_balance_ung":     _c(0.3,  "Balance -> UNG pts (gas proxy)"),
        },
        "instruments": ["BRENT", "EURUSD", "XAUUSD", "UNG", "XLE"],
        "input_mapping": {
            "inv_surprise": {"source": "surprise", "unit": "mb", "range": [-10, 10]}
        },
    },
  },

  # ═══════════════════════════════════════════════════════════════════════════════
  # 11. INSTITUTIONAL_FLOW
  # COT repositioning, FX intervention, end-of-month rebalancing
  # ═══════════════════════════════════════════════════════════════════════════════
  {
    "name": "Institutional Flow & Repositioning",
    "slug": "INSTITUTIONAL_FLOW",
    "category": "positioning",
    "heuristic_ver": 2,
    "calibration_json": {"lag_days": 1, "half_life_days": 7, "absorption_days": 14, "decay_type": "linear"},
    "instruments": ["EURUSD", "SP500", "XAUUSD"],
    "description": "Large institutional flow -> order flow imbalance -> momentum + mean reversion risk",
    "ai_rationale": "COT repositioning or FX intervention moves prices via order flow mechanics (not fundamentals). Move is self-limiting — creates a mean reversion setup once the flow exhausts. Low confidence, short-lived signal.",
    "graph_json": {
        "nodes": [
            _n("flow_trigger",    "Institutional Flow",   "input",        300, 50,  unit="$B"),
            _n("order_imbalance", "Order Flow Imbalance", "observable",   150, 220, formula="flow_trigger * {{coef_flow_imbalance}}"),
            _n("market_impact",   "Market Impact",        "latent",       150, 390, formula="order_imbalance * {{coef_imbalance_impact}}"),
            _n("momentum_sig",    "Flow Momentum",        "latent",       450, 390, formula="market_impact * {{coef_impact_mom}} + order_imbalance * {{coef_imbalance_mom}}"),
            _n("eurusd",  "EUR/USD",      "market_asset", 100, 590, formula="momentum_sig * {{coef_mom_eurusd}}", unit="pips", instrument="EURUSD"),
            _n("sp500",   "S&P 500",      "market_asset", 350, 590, formula="momentum_sig * {{coef_mom_sp}}", unit="pts", instrument="SP500"),
            _n("xauusd",  "Gold XAU/USD", "market_asset", 600, 590, formula="momentum_sig * {{coef_mom_gold}}", unit="$/oz", instrument="XAUUSD"),
        ],
        "edges": [
            _e("flow_trigger",    "order_imbalance","solid",  "execution",   strength=3, sign="positive"),
            _e("order_imbalance", "market_impact",  "solid",  "price_impact",strength=3, sign="positive"),
            _e("market_impact",   "momentum_sig",   "solid",  "momentum",    strength=2, sign="positive"),
            _e("order_imbalance", "momentum_sig",   "solid",  "order_flow",  strength=2, sign="positive"),
            _e("momentum_sig",    "eurusd",  "solid",  "fx_flow",  strength=3, sign="positive"),
            _e("momentum_sig",    "sp500",   "solid",  "eq_flow",  strength=2, sign="positive"),
            _e("momentum_sig",    "xauusd",  "dashed", "gold_flow",strength=2, sign="positive"),
        ],
        "coefficients": {
            "coef_flow_imbalance":   _c(0.50, "$1B -> order imbalance"),
            "coef_imbalance_impact": _c(0.60, "Imbalance -> market impact"),
            "coef_impact_mom":       _c(0.70, "Impact -> momentum"),
            "coef_imbalance_mom":    _c(0.40, "Imbalance -> momentum direct"),
            "coef_mom_eurusd":       _c(30,   "Momentum -> EURUSD pips"),
            "coef_mom_sp":           _c(15,   "Momentum -> SP500 pts"),
            "coef_mom_gold":         _c(8,    "Momentum -> Gold $/oz"),
        },
        "instruments": ["EURUSD", "SP500", "XAUUSD"],
        "input_mapping": {
            "flow_trigger": {"source": "cot_report", "field": "net_non_commercial", "unit": "$B", "range": [-20, 20]}
        },
    },
  },

  # ═══════════════════════════════════════════════════════════════════════════════
  # 12. UNCLASSIFIED_IMPACT — fallback (confidence < 0.4)
  # ═══════════════════════════════════════════════════════════════════════════════
  {
    "name": "Unclassified Market Impact",
    "slug": "UNCLASSIFIED_IMPACT",
    "category": "unclassified",
    "heuristic_ver": 2,
    "calibration_json": {"lag_days": 0, "half_life_days": 5, "absorption_days": 12, "decay_type": "exp"},
    "instruments": ["EURUSD", "SP500", "XAUUSD"],
    "description": "Fallback — event doesn't fit established regimes. Low confidence, no causal chain.",
    "ai_rationale": "Direct trigger to output with no intermediate nodes. Precision scores from this template are marked low-confidence and excluded from calibration stats.",
    "graph_json": {
        "nodes": [
            _n("unknown_trigger","Market Trigger",   "input",        300, 80,  unit="score"),
            _n("market_reaction","Market Reaction",  "latent",       300, 300, formula="unknown_trigger * {{coef_trig_react}}"),
            _n("eurusd",  "EUR/USD",      "market_asset", 100, 520, formula="market_reaction * {{coef_react_eurusd}}", unit="pips", instrument="EURUSD"),
            _n("sp500",   "S&P 500",      "market_asset", 300, 520, formula="market_reaction * {{coef_react_sp}}", unit="pts", instrument="SP500"),
            _n("xauusd",  "Gold XAU/USD", "market_asset", 520, 520, formula="market_reaction * {{coef_react_gold}}", unit="$/oz", instrument="XAUUSD"),
        ],
        "edges": [
            _e("unknown_trigger","market_reaction","dashed","direct_impact",strength=1, sign="positive", label="low confidence"),
            _e("market_reaction","eurusd",  "dashed","output_fx",  strength=1, sign="positive"),
            _e("market_reaction","sp500",   "dashed","output_eq",  strength=1, sign="positive"),
            _e("market_reaction","xauusd",  "dashed","output_gold",strength=1, sign="positive"),
        ],
        "coefficients": {
            "coef_trig_react":   _c(0.50, "Trigger -> market reaction"),
            "coef_react_eurusd": _c(20,   "Reaction -> EURUSD pips"),
            "coef_react_sp":     _c(15,   "Reaction -> SP500 pts"),
            "coef_react_gold":   _c(5,    "Reaction -> Gold $/oz"),
        },
        "instruments": ["EURUSD", "SP500", "XAUUSD"],
        "input_mapping": {
            "unknown_trigger": {"source": "user_input", "unit": "score", "range": [-5, 5]}
        },
    },
  },

  # ═══════════════════════════════════════════════════════════════════════════════
  # 13. MACRO_RATE_GUIDANCE  (auto-créé par guidance_sync — pas dans REGIME_SLUGS)
  # Anticipation d'une décision de taux avant l'event : repricing progressif OIS
  # ═══════════════════════════════════════════════════════════════════════════════
  {
    "name": "Macro Rate Guidance",
    "slug": "MACRO_RATE_GUIDANCE",
    "category": "central_bank",
    "sub_type": "guidance",
    "heuristic_ver": 2,
    "calibration_json": {"lag_days": 0, "half_life_days": 30, "absorption_days": 60, "decay_type": "linear"},
    "instruments": ["EURUSD", "TLT", "USDJPY", "SP500", "XAUUSD", "GBPUSD", "EEM", "QQQ"],
    "description": "Anticipation marché d'une décision de taux — pricing-in progressif avant l'event (OIS forwards, carry FX, duration bonds)",
    "ai_rationale": "Quand le consensus se forme autour d'un cut/hike, les OIS forwards repricing graduellement. Le carry FX shift avant la décision. TLT monte si cut attendu. Signal = forecast - previous en bps. Signal=0 = marché neutre (status quo).",
    "graph_json": {
        "nodes": [
            _n("guidance_signal",  "Expected Δ Rate",    "input",        400,  50, unit="bps",
               description="Positif=hike attendu, Négatif=cut attendu (bps)"),
            _n("ois_forward",      "OIS Fwd Shift",      "observable",   180, 200,
               formula="guidance_signal * {{coef_signal_ois}}", unit="bps"),
            _n("usd_carry",        "USD Carry Shift",    "observable",    60, 350,
               formula="ois_forward * {{coef_ois_carry}}", unit="%"),
            _n("rate_expectation", "Rate Path Exp.",     "latent",       400, 350,
               formula="ois_forward * {{coef_ois_re}}"),
            _n("risk_premium",     "Risk Premium",       "latent",       700, 350,
               formula="guidance_signal * {{coef_signal_rp}}",
               description="Négatif si cut (risk-on), positif si hike (risk-off)"),
            # Outputs row 1
            _n("eurusd",  "EUR/USD",    "market_asset",  60,  530, formula="-usd_carry * {{coef_carry_eurusd}}",                           unit="pips", instrument="EURUSD"),
            _n("tlt",     "TLT Bond",   "market_asset", 240,  530, formula="-rate_expectation * {{coef_re_tlt}}",                          unit="pts",  instrument="TLT"),
            _n("usdjpy",  "USD/JPY",    "market_asset", 420,  530, formula="usd_carry * {{coef_carry_usdjpy}}",                            unit="pips", instrument="USDJPY"),
            _n("sp500",   "S&P 500",    "market_asset", 600,  530, formula="-risk_premium * {{coef_rp_sp}} + rate_expectation * {{coef_re_sp}}", unit="pts", instrument="SP500"),
            # Outputs row 2
            _n("xauusd",  "Gold",       "market_asset",  60,  680, formula="-usd_carry * {{coef_carry_gold}} - rate_expectation * {{coef_re_gold}}", unit="$/oz", instrument="XAUUSD"),
            _n("gbpusd",  "GBP/USD",    "market_asset", 240,  680, formula="-usd_carry * {{coef_carry_gbpusd}}",                          unit="pips", instrument="GBPUSD"),
            _n("eem",     "EEM Emerg",  "market_asset", 420,  680, formula="-usd_carry * {{coef_carry_eem}} - risk_premium * {{coef_rp_eem}}", unit="pts", instrument="EEM"),
            _n("qqq",     "QQQ Nasdaq", "market_asset", 600,  680, formula="-rate_expectation * {{coef_re_qqq}} - risk_premium * {{coef_rp_qqq}}", unit="pts", instrument="QQQ"),
        ],
        "edges": [
            _e("guidance_signal", "ois_forward",      "solid",  "policy_fwd_pricing", strength=3, sign="positive",  label="hike→OIS up / cut→OIS dn"),
            _e("guidance_signal", "risk_premium",     "dashed", "risk_channel",        strength=2, sign="positive",  label="hike=risk-off / cut=risk-on"),
            _e("ois_forward",     "usd_carry",        "solid",  "carry_diff",          strength=3, sign="positive"),
            _e("ois_forward",     "rate_expectation", "solid",  "fwd_path",            strength=3, sign="positive"),
            # FX
            _e("usd_carry",       "eurusd",  "solid",  "fx_eurusd",   strength=3, sign="negative", label="USD up → EUR/USD dn"),
            _e("usd_carry",       "usdjpy",  "solid",  "fx_usdjpy",   strength=2, sign="positive",  label="USD up → USD/JPY up"),
            _e("usd_carry",       "gbpusd",  "solid",  "fx_gbpusd",   strength=2, sign="negative"),
            _e("usd_carry",       "eem",     "solid",  "usd_eem",     strength=2, sign="negative"),
            # Bonds
            _e("rate_expectation","tlt",     "solid",  "bond_dur",    strength=3, sign="negative", label="hike exp → TLT dn"),
            # Equities
            _e("risk_premium",    "sp500",   "solid",  "equity_rp",   strength=2, sign="negative"),
            _e("rate_expectation","sp500",   "dashed", "disc_rate",   strength=2, sign="negative", label="hike → discount rate up → SP dn"),
            _e("risk_premium",    "qqq",     "dashed", "tech_rp",     strength=2, sign="negative"),
            _e("rate_expectation","qqq",     "solid",  "tech_dur",    strength=3, sign="negative", label="QQQ = long duration"),
            _e("risk_premium",    "eem",     "dashed", "em_risk",     strength=2, sign="negative"),
            # Gold
            _e("usd_carry",       "xauusd",  "solid",  "usd_gold",   strength=2, sign="negative"),
            _e("rate_expectation","xauusd",  "dashed", "real_rate",  strength=2, sign="negative", label="hike exp → real rates up → gold dn"),
        ],
        "coefficients": {
            "coef_signal_ois":    _c(0.8,  "bps signal → OIS shift (same sign: hike=up, cut=down)"),
            "coef_ois_carry":     _c(0.4,  "OIS shift → USD carry %"),
            "coef_ois_re":        _c(0.9,  "OIS shift → rate expectation index"),
            "coef_signal_rp":     _c(0.3,  "signal bps → risk premium (hike=risk-off positive)"),
            "coef_carry_eurusd":  _c(60,   "1% USD carry → EUR/USD pips fall"),
            "coef_carry_usdjpy":  _c(80,   "1% USD carry → USD/JPY pips rise"),
            "coef_carry_gbpusd":  _c(50,   "1% USD carry → GBP/USD pips fall"),
            "coef_carry_eem":     _c(20,   "1% USD carry → EEM pts fall"),
            "coef_carry_gold":    _c(50,   "1% USD carry → Gold $/oz fall"),
            "coef_re_tlt":        _c(12,   "rate exp index → TLT pts fall (~12y duration)"),
            "coef_rp_sp":         _c(30,   "risk premium → SP500 pts fall"),
            "coef_re_sp":         _c(20,   "rate expectation → SP500 pts fall (discount rate)"),
            "coef_re_qqq":        _c(45,   "rate exp → QQQ pts fall (high duration)"),
            "coef_rp_qqq":        _c(25,   "risk premium → QQQ pts fall"),
            "coef_rp_eem":        _c(15,   "risk premium → EEM pts fall"),
            "coef_re_gold":       _c(15,   "rate exp → Gold $/oz fall (real rate)"),
        },
        "instruments": ["EURUSD", "TLT", "USDJPY", "SP500", "XAUUSD", "GBPUSD", "EEM", "QQQ"],
        "input_mapping": {
            "guidance_signal": {"source": "guidance_bps", "unit": "bps", "range": [-200, 200]}
        },
    },
  },
]


# ── DB Helpers ─────────────────────────────────────────────────────────────────

def init_tables(conn):
    conn.execute("""
        CREATE TABLE IF NOT EXISTS causal_graph_templates (
            id              INTEGER PRIMARY KEY AUTOINCREMENT,
            name            TEXT NOT NULL UNIQUE,
            category        TEXT NOT NULL,
            sub_type        TEXT,
            instruments     TEXT NOT NULL DEFAULT '[]',
            description     TEXT DEFAULT '',
            graph_json      TEXT NOT NULL,
            ai_rationale    TEXT DEFAULT '',
            calibration_json TEXT DEFAULT '{}',
            created_by      TEXT DEFAULT 'system',
            heuristic_ver   INTEGER DEFAULT 1,
            created_at      TEXT DEFAULT (datetime('now')),
            updated_at      TEXT DEFAULT (datetime('now'))
        )
    """)
    conn.execute("""
        CREATE TABLE IF NOT EXISTS causal_event_analyses (
            id                 INTEGER PRIMARY KEY AUTOINCREMENT,
            market_event_id    INTEGER,
            template_id        INTEGER,
            instrument         TEXT NOT NULL DEFAULT 'EURUSD',
            inputs_json        TEXT DEFAULT '{}',
            override_params    TEXT DEFAULT '{}',
            prediction_json    TEXT,
            actual_json        TEXT,
            activation_score   REAL,
            drift_json         TEXT,
            ai_recommendation  TEXT,
            analyzed_at        TEXT DEFAULT (datetime('now')),
            FOREIGN KEY (template_id) REFERENCES causal_graph_templates(id)
        )
    """)
    conn.commit()


def seed_templates(conn):
    """Insert/update built-in templates. One-shot migration: removes old non-regime templates."""
    existing = conn.execute(
        "SELECT name FROM causal_graph_templates WHERE created_by='system'"
    ).fetchall()
    new_names = {t["name"] for t in BUILT_IN_TEMPLATES}
    for row in existing:
        if row["name"] not in new_names:
            conn.execute("DELETE FROM causal_graph_templates WHERE name=? AND created_by='system'", (row["name"],))

    for t in BUILT_IN_TEMPLATES:
        ver = t.get("heuristic_ver", 1)
        existing_row = conn.execute(
            "SELECT id, heuristic_ver FROM causal_graph_templates WHERE name = ?", (t["name"],)
        ).fetchone()
        if existing_row:
            if (existing_row["heuristic_ver"] or 1) < ver:
                conn.execute("""
                    UPDATE causal_graph_templates
                    SET graph_json=?, description=?, ai_rationale=?, instruments=?,
                        heuristic_ver=?, updated_at=datetime('now')
                    WHERE name=?
                """, (
                    json.dumps(t["graph_json"]),
                    t.get("description", ""),
                    t.get("ai_rationale", ""),
                    json.dumps(t.get("instruments", [])),
                    ver, t["name"],
                ))
            continue
        conn.execute("""
            INSERT INTO causal_graph_templates
                (name, category, sub_type, instruments, description, graph_json,
                 calibration_json, ai_rationale, heuristic_ver, created_by)
            VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 'system')
        """, (
            t["name"], t["category"], t.get("sub_type", ""),
            json.dumps(t.get("instruments", [])),
            t.get("description", ""),
            json.dumps(t["graph_json"]),
            json.dumps(t.get("calibration_json", {})),
            t.get("ai_rationale", ""),
            ver,
        ))

    # Migration: fill missing temporal params in calibration_json for all built-in templates.
    # Only sets keys that are absent — never overwrites existing calibration stats.
    for t in BUILT_IN_TEMPLATES:
        defaults = t.get("calibration_json", {})
        if not defaults:
            continue
        row = conn.execute(
            "SELECT id, calibration_json FROM causal_graph_templates WHERE name=?", (t["name"],)
        ).fetchone()
        if not row:
            continue
        calib = json.loads(row["calibration_json"] or "{}")
        dirty = False
        for key in ("lag_days", "half_life_days", "absorption_days", "decay_type"):
            if calib.get(key) is None and key in defaults:
                calib[key] = defaults[key]
                dirty = True
        if dirty:
            conn.execute(
                "UPDATE causal_graph_templates SET calibration_json=?, updated_at=datetime('now') WHERE id=?",
                (json.dumps(calib), row["id"])
            )

    conn.commit()


def get_templates(conn, category: str = "") -> list:
    where = f"WHERE category = '{category}'" if category else ""
    rows = conn.execute(
        f"SELECT * FROM causal_graph_templates {where} ORDER BY category, name"
    ).fetchall()
    result = []
    for r in rows:
        d = dict(r)
        d["graph_json"]       = json.loads(d["graph_json"] or "{}")
        d["instruments"]      = json.loads(d["instruments"] or "[]")
        d["calibration_json"] = json.loads(d["calibration_json"] or "{}")
        result.append(d)
    return result


def get_template(conn, template_id: int) -> Optional[dict]:
    row = conn.execute(
        "SELECT * FROM causal_graph_templates WHERE id = ?", (template_id,)
    ).fetchone()
    if not row:
        return None
    d = dict(row)
    d["graph_json"]       = json.loads(d["graph_json"] or "{}")
    d["instruments"]      = json.loads(d["instruments"] or "[]")
    d["calibration_json"] = json.loads(d["calibration_json"] or "{}")
    return d


def update_coefficients(conn, template_id: int, coef_updates: dict):
    tmpl = get_template(conn, template_id)
    if not tmpl:
        return False
    graph = tmpl["graph_json"]
    for k, v in coef_updates.items():
        if k in graph.get("coefficients", {}):
            graph["coefficients"][k]["value"] = float(v)
    conn.execute(
        "UPDATE causal_graph_templates SET graph_json = ?, updated_at = datetime('now') WHERE id = ?",
        (json.dumps(graph), template_id)
    )
    conn.commit()
    return True


def update_calibration(conn, template_id: int, analysis_result: dict):
    """Update calibration stats after a new analysis."""
    tmpl = get_template(conn, template_id)
    if not tmpl:
        return
    calib = tmpl.get("calibration_json") or {}
    n = calib.get("n_events", 0) + 1
    prev_act = calib.get("avg_activation", 0) or 0
    new_act  = analysis_result.get("activation_score") or 0
    instrument = analysis_result.get("instrument", "EURUSD")
    pred_pips  = analysis_result.get("pred_pips", 0) or 0
    act_pips   = analysis_result.get("actual_pips", 0) or 0

    calib["n_events"]       = n
    calib["avg_activation"] = round((prev_act * (n - 1) + new_act) / n, 3)
    calib["last_analyzed"]  = analysis_result.get("analyzed_at", "")

    if instrument not in calib:
        calib[instrument] = {"n": 0, "avg_pred": 0, "avg_actual": 0}
    ci = calib[instrument]
    ni = ci["n"] + 1
    ci["n"]          = ni
    ci["avg_pred"]   = round((ci["avg_pred"]   * (ni - 1) + pred_pips) / ni, 1)
    ci["avg_actual"] = round((ci["avg_actual"] * (ni - 1) + act_pips)  / ni, 1)
    if ci["avg_pred"] != 0:
        ci["coef_ratio"] = round(ci["avg_actual"] / ci["avg_pred"], 2)

    conn.execute(
        "UPDATE causal_graph_templates SET calibration_json = ?, updated_at = datetime('now') WHERE id = ?",
        (json.dumps(calib), template_id)
    )
    conn.commit()


# ── Graph evaluator ────────────────────────────────────────────────────────────

def _safe_eval(expr: str, context: dict) -> float:
    _ops = {
        ast.Add:  operator.add,
        ast.Sub:  operator.sub,
        ast.Mult: operator.mul,
        ast.Div:  operator.truediv,
        ast.USub: operator.neg,
    }

    def _eval(node):
        if isinstance(node, ast.Constant):
            return float(node.value)
        if isinstance(node, ast.Name):
            if node.id not in context:
                raise ValueError(f"Unknown variable: {node.id}")
            return float(context[node.id])
        if isinstance(node, ast.BinOp):
            op = _ops.get(type(node.op))
            if op is None:
                raise ValueError(f"Unsupported op: {type(node.op)}")
            return op(_eval(node.left), _eval(node.right))
        if isinstance(node, ast.UnaryOp) and isinstance(node.op, ast.USub):
            return -_eval(node.operand)
        raise ValueError(f"Unsupported AST node: {type(node)}")

    tree = ast.parse(expr.strip(), mode="eval")
    return _eval(tree.body)


def evaluate_graph(graph_json: dict, inputs: dict, coef_overrides: Optional[dict] = None) -> dict:
    """
    Evaluate a causal graph from inputs. Returns {node_id: value}.
    {{coef_xxx}} placeholders are substituted from graph_json["coefficients"].
    """
    coefs = {k: v["value"] for k, v in graph_json.get("coefficients", {}).items()}
    if coef_overrides:
        coefs.update({k: float(v) for k, v in coef_overrides.items()})

    def sub(formula: str) -> str:
        for k, v in coefs.items():
            formula = formula.replace(f"{{{{{k}}}}}", str(v))
        return formula

    values = dict(inputs)
    nodes  = graph_json.get("nodes", [])
    for _ in range(len(nodes) + 2):
        changed = False
        for node in nodes:
            if node["id"] in values or not node.get("formula"):
                continue
            try:
                result = _safe_eval(sub(node["formula"]), values)
                values[node["id"]] = round(result, 4)
                changed = True
            except Exception:
                pass
        if not changed:
            break

    return values
