""" 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=""): """ strength : 1=fin, 2=normal, 3=épais sign : "positive" (teal), "negative" (orange), "neutral" (gris) """ 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 # Covers: CPI, PCE, PPI, NFP, ADP, GDP, PMI, ISM, Retail Sales, Housing, # Jobless Claims, Consumer Confidence, JOLTS — any scheduled data release # ═══════════════════════════════════════════════════════════════════════════════ { "name": "Macro Data Surprise", "slug": "MACRO_DATA_SURPRISE", "category": "monetary_shock", "heuristic_ver": 1, "instruments": ["EURUSD", "XAUUSD", "SP500"], "description": "Scheduled economic release vs consensus → monetary policy repricing (OIS→2Y→10Y) + growth channel → EUR/USD, Gold, S&P", "ai_rationale": "Any scheduled data release that reprices the rate path. Hawkish surprises (hot CPI, strong NFP) strengthen USD via OIS repricing and weaken Gold; dovish misses do the reverse. S&P catches both the growth earnings channel (+) and the valuation discount rate channel (-).", "graph_json": { "nodes": [ _n("macro_surprise", "Macro Surprise", "input", 300, 50, unit="sigma", description="+sigma = beat (hawkish), -sigma = miss (dovish)"), _n("ois_repricing", "Fed OIS Repricing", "observable", 150, 190, formula="macro_surprise * {{coef_surp_ois}}", unit="bps"), _n("growth_exp", "Growth Expectation", "latent", 450, 190, formula="macro_surprise * {{coef_surp_growth}}"), _n("inflation_risk", "Inflation Risk", "latent", 650, 190, formula="macro_surprise * {{coef_surp_infl}}"), _n("us_2y", "Delta US 2Y Yield", "observable", 80, 340, formula="ois_repricing * {{coef_ois_2y}}", unit="%"), _n("us_10y", "Delta US 10Y Yield", "observable", 300, 340, formula="ois_repricing * {{coef_ois_10y}} + inflation_risk * {{coef_infl_10y}}", unit="%"), _n("fin_conditions", "Financial Conditions", "latent", 550, 340, formula="ois_repricing * {{coef_ois_fc}} + us_10y * {{coef_10y_fc}}"), _n("usd_strength", "USD Strength", "observable", 80, 480, formula="us_2y * {{coef_2y_usd}}", unit="%"), _n("eurusd", "EUR/USD", "market_asset", 80, 620, formula="-usd_strength * {{coef_usd_eurusd}}", unit="pips", instrument="EURUSD"), _n("xauusd", "Gold XAU/USD", "market_asset", 550, 620, formula="-us_10y * {{coef_10y_gold}} - usd_strength * {{coef_usd_gold}}", unit="$/oz", instrument="XAUUSD"), _n("sp500", "S&P 500", "market_asset", 300, 620, formula="growth_exp * {{coef_growth_sp}} - fin_conditions * {{coef_fc_sp}}", unit="pts", instrument="SP500"), ], "edges": [ _e("macro_surprise", "ois_repricing", "solid", "rate_repricing", strength=3, sign="positive", label="Fed repricing"), _e("macro_surprise", "growth_exp", "dashed", "growth_channel", strength=2, sign="positive", label="activity signal"), _e("macro_surprise", "inflation_risk", "dashed", "inflation_ch", strength=2, sign="positive", label="price pressure"), _e("ois_repricing", "us_2y", "solid", "short_end", strength=3, sign="positive", label="short-end anchor"), _e("ois_repricing", "us_10y", "dashed", "long_end", strength=1, sign="positive", label="long-end partial"), _e("inflation_risk", "us_10y", "solid", "term_premium", strength=2, sign="positive", label="inflation premium"), _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", label="USD carry"), _e("usd_strength", "eurusd", "solid", "fx_channel", strength=3, sign="negative", label="USD appreciates"), _e("growth_exp", "sp500", "solid", "earnings_ch", strength=2, sign="positive", label="earnings channel"), _e("fin_conditions", "sp500", "solid", "valuation_ch", strength=3, sign="negative", label="valuation compression"), _e("us_10y", "xauusd", "solid", "real_rate_gold", strength=2, sign="negative", label="real rates vs gold"), _e("usd_strength", "xauusd", "dashed", "usd_gold", strength=2, sign="negative", label="USD vs gold"), ], "coefficients": { "coef_surp_ois": _c(8.0, "1 sigma surprise -> OIS repricing (bps)"), "coef_surp_growth": _c(0.30, "1 sigma surprise -> growth expectation"), "coef_surp_infl": _c(0.15, "1 sigma surprise -> inflation risk"), "coef_ois_2y": _c(0.028, "OIS bps -> Delta US 2Y yield (%)"), "coef_ois_10y": _c(0.008, "OIS bps -> Delta US 10Y yield (partial)"), "coef_infl_10y": _c(0.12, "Inflation risk -> 10Y term premium"), "coef_ois_fc": _c(0.40, "OIS -> financial conditions index"), "coef_10y_fc": _c(0.60, "10Y -> financial conditions index"), "coef_2y_usd": _c(0.50, "2Y yield -> USD strength"), "coef_usd_eurusd": _c(80, "1% USD -> EUR/USD pips fall"), "coef_growth_sp": _c(60, "Growth expectation -> S&P pts"), "coef_fc_sp": _c(50, "Financial conditions -> S&P pts fall"), "coef_10y_gold": _c(15, "1% 10Y rise -> Gold fall ($/oz)"), "coef_usd_gold": _c(10, "USD strength -> Gold fall ($/oz)"), }, "instruments": ["EURUSD", "XAUUSD", "SP500"], "input_mapping": { "macro_surprise": {"source": "surprise", "unit": "sigma", "description": "(actual-consensus)/std_dev", "range": [-3, 3]} }, }, }, # ═══════════════════════════════════════════════════════════════════════════════ # 2. CENTRAL_BANK_DECISION # Covers: FOMC, ECB, BoJ, BoE, BoC, RBA, SNB, PBOC — any CB rate decision # ═══════════════════════════════════════════════════════════════════════════════ { "name": "Central Bank Decision", "slug": "CENTRAL_BANK_DECISION", "category": "central_bank", "heuristic_ver": 1, "instruments": ["EURUSD", "XAUUSD", "SP500"], "description": "CB rate decision + forward guidance -> immediate full curve repricing -> FX carry, bonds, risk assets", "ai_rationale": "Unlike data surprises, a CB decision directly moves the policy rate and shifts the forward curve. Two channels: the rate channel (anchors 2Y immediately) and the guidance channel (shifts long-end via expectations). Gold reacts primarily to real rate change; equities to discount rate + risk appetite.", "graph_json": { "nodes": [ _n("rate_surprise_bps", "Rate Surprise (bps)", "input", 110, 45, unit="bps", description="+bps = hawkish, -bps = dovish"), _n("tone_score", "Guidance Tone", "input", 390, 45, unit="score", description="-3 very dovish to +3 very hawkish"), _n("rate_channel", "Rate Path Revision", "observable", 110, 185, formula="rate_surprise_bps / 25 * {{coef_rate_mult}}", unit="bps"), _n("guidance_channel", "Forward Guidance", "latent", 390, 185, formula="tone_score * {{coef_tone}}"), _n("us_2y", "Delta 2Y Yield", "observable", 80, 330, formula="rate_channel * {{coef_rate_2y}} + guidance_channel * {{coef_tone_2y}}", unit="%"), _n("us_10y", "Delta 10Y Yield", "observable", 310, 330, formula="rate_channel * {{coef_rate_10y}} + guidance_channel * {{coef_tone_10y}}", unit="%"), _n("risk_appetite", "Risk Appetite", "latent", 560, 330, 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="%"), _n("eurusd", "EUR/USD", "market_asset", 80, 620, formula="-carry_diff * {{coef_carry_eurusd}}", unit="pips", instrument="EURUSD"), _n("xauusd", "Gold XAU/USD", "market_asset", 560, 620, formula="-us_10y * {{coef_10y_gold}} - carry_diff * {{coef_carry_gold}}", unit="$/oz", instrument="XAUUSD"), _n("sp500", "S&P 500", "market_asset", 310, 620, formula="risk_appetite * {{coef_risk_sp}} - us_10y * {{coef_10y_sp}}", unit="pts", instrument="SP500"), ], "edges": [ _e("rate_surprise_bps", "rate_channel", "solid", "immediate_reprice", strength=3, sign="positive", label="direct rate move"), _e("tone_score", "guidance_channel","dashed", "fwd_guidance", strength=3, sign="positive", label="communication"), _e("rate_channel", "us_2y", "solid", "short_anchor", strength=3, sign="positive", label="short-end"), _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", label="long-end guidance"), _e("rate_channel", "risk_appetite", "dashed", "cost_capital", strength=2, sign="negative", label="cost of capital"), _e("guidance_channel", "risk_appetite", "dashed", "confidence", strength=2, sign="positive", label="confidence channel"), _e("us_2y", "carry_diff", "solid", "carry", strength=3, sign="positive"), _e("carry_diff", "eurusd", "solid", "fx_carry", strength=3, sign="negative", label="USD carry"), _e("risk_appetite", "sp500", "solid", "risk_on_sp", strength=2, sign="positive"), _e("us_10y", "sp500", "solid", "discount_sp", strength=3, sign="negative", label="discount rate"), _e("us_10y", "xauusd", "solid", "real_rate_gold", strength=3, sign="negative", label="real rate vs gold"), _e("carry_diff", "xauusd", "dashed", "opp_cost_gold", strength=2, sign="negative", label="opportunity cost"), ], "coefficients": { "coef_rate_mult": _c(1.5, "Decision surprise -> rate path revision multiplier"), "coef_tone": _c(0.8, "Guidance tone -> signal"), "coef_rate_2y": _c(0.70, "Rate path -> 2Y yield"), "coef_tone_2y": _c(0.03, "Guidance -> 2Y bleed"), "coef_rate_10y": _c(0.30, "Rate path -> 10Y yield"), "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 -> EUR/USD pips fall"), "coef_risk_sp": _c(40, "Risk appetite -> S&P pts"), "coef_10y_sp": _c(80, "1% 10Y -> S&P pts fall (valuation)"), "coef_10y_gold": _c(20, "1% 10Y rise -> Gold fall ($/oz)"), "coef_carry_gold": _c(15, "Carry -> Gold fall (opportunity cost)"), }, "instruments": ["EURUSD", "XAUUSD", "SP500"], "input_mapping": { "rate_surprise_bps": {"source": "surprise_bps", "unit": "bps"}, "tone_score": {"source": "user_input", "unit": "score", "range": [-3, 3]}, }, }, }, # ═══════════════════════════════════════════════════════════════════════════════ # 3. GROWTH_CORPORATE_SIGNAL # Covers: earnings (S&P large cap), M&A, layoffs, guidance, analyst revisions # ═══════════════════════════════════════════════════════════════════════════════ { "name": "Growth & Corporate Signal", "slug": "GROWTH_CORPORATE_SIGNAL", "category": "growth_shock", "heuristic_ver": 1, "instruments": ["SP500", "EURUSD", "XAUUSD"], "description": "Corporate or macro growth signal -> earnings revision + risk appetite -> equities, FX, Gold", "ai_rationale": "Earnings beats, M&A, or positive guidance revise earnings expectations upward and increase risk appetite -> SP500 up. Layoffs or guidance cuts do the reverse. The 10Y yield rises moderately on growth (discount rate headwind), creating the typical dual-channel for equities.", "graph_json": { "nodes": [ _n("growth_trigger", "Growth Signal", "input", 300, 50, unit="%", description="+% = bullish beat, -% = bearish miss"), _n("earnings_revision", "Earnings Revision", "latent", 150, 200, formula="growth_trigger * {{coef_trig_earn}}"), _n("econ_activity", "Economic Activity", "observable", 450, 200, formula="growth_trigger * {{coef_trig_activity}}"), _n("risk_appetite", "Risk Appetite", "latent", 300, 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="%"), _n("sp500", "S&P 500", "market_asset", 150, 540, formula="earnings_revision * {{coef_earn_sp}} + risk_appetite * {{coef_risk_sp}} - us_10y * {{coef_10y_sp}}", unit="pts", instrument="SP500"), _n("eurusd", "EUR/USD", "market_asset", 420, 540, formula="risk_appetite * {{coef_risk_eurusd}}", unit="pips", instrument="EURUSD"), _n("xauusd", "Gold XAU/USD", "market_asset", 650, 540, formula="-risk_appetite * {{coef_risk_gold}}", unit="$/oz", instrument="XAUUSD"), ], "edges": [ _e("growth_trigger", "earnings_revision","solid", "eps_channel", strength=3, sign="positive", label="EPS revision"), _e("growth_trigger", "econ_activity", "dashed", "activity_proxy",strength=2, sign="positive", label="activity signal"), _e("growth_trigger", "us_10y", "dashed", "growth_premium",strength=1, sign="positive", label="growth premium"), _e("earnings_revision", "risk_appetite", "solid", "confidence_ch", strength=3, sign="positive"), _e("econ_activity", "risk_appetite", "solid", "sentiment_ch", strength=2, sign="positive"), _e("earnings_revision", "sp500", "solid", "eps_multiple", strength=3, sign="positive", label="EPS x multiple"), _e("risk_appetite", "sp500", "solid", "sentiment_sp", strength=2, sign="positive"), _e("us_10y", "sp500", "dashed", "discount_drag", strength=1, sign="negative", label="discount rate"), _e("risk_appetite", "eurusd", "solid", "risk_on_fx", strength=2, sign="positive", label="risk-on FX"), _e("risk_appetite", "xauusd", "dashed", "safe_exit", strength=2, sign="negative", label="safe haven exits"), ], "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 revision -> risk appetite"), "coef_act_risk": _c(0.40, "Activity -> risk appetite"), "coef_earn_sp": _c(80, "Earnings revision -> S&P pts"), "coef_risk_sp": _c(40, "Risk appetite -> S&P pts"), "coef_10y_sp": _c(30, "10Y rise -> S&P pts fall"), "coef_risk_eurusd": _c(30, "Risk appetite -> EUR/USD pips"), "coef_risk_gold": _c(15, "Risk appetite up -> Gold falls"), }, "instruments": ["SP500", "EURUSD", "XAUUSD"], "input_mapping": { "growth_trigger": {"source": "user_input", "unit": "%", "range": [-5, 5]} }, }, }, # ═══════════════════════════════════════════════════════════════════════════════ # 4. GEOPOLITICAL_RISK_OFF # Covers: military conflicts, sanctions, coups, election shocks, political crises # ═══════════════════════════════════════════════════════════════════════════════ { "name": "Geopolitical Risk-Off", "slug": "GEOPOLITICAL_RISK_OFF", "category": "geopolitical", "heuristic_ver": 1, "instruments": ["EURUSD", "XAUUSD", "SP500", "BRENT"], "description": "Geopolitical shock -> risk premium + safe-haven demand -> Gold up VIX up EUR down SP500 down (+ oil supply if Middle East)", "ai_rationale": "Geopolitical events generate two simultaneous channels: a risk premium channel (VIX spike -> equities fall, USD safe haven -> EUR fall) and potentially an energy supply channel (if Middle East / energy producers). Gold is the primary safe haven. EUR is uniquely exposed given EU proximity to conflict zones.", "graph_json": { "nodes": [ _n("geo_shock", "Geopolitical Shock", "input", 300, 50, unit="score", description="Severity 1-10 (1=minor, 10=major conflict)"), _n("risk_premium", "Risk Premium", "latent", 150, 200, formula="geo_shock * {{coef_shock_rp}}"), _n("safe_haven_dem", "Safe Haven Demand", "latent", 450, 200, formula="geo_shock * {{coef_shock_shd}}"), _n("oil_supply_risk", "Oil Supply Risk", "latent", 700, 200, formula="geo_shock * {{coef_shock_oil}}", description="Only if energy-producing region"), _n("vix_spike", "Delta VIX", "observable", 150, 360, formula="risk_premium * {{coef_rp_vix}}", unit="pts"), _n("usd_safe_haven", "USD Safe Haven Flow", "observable", 420, 360, formula="safe_haven_dem * {{coef_shd_usd}}"), _n("gold_demand", "Gold Safe Haven", "observable", 680, 360, formula="safe_haven_dem * {{coef_shd_gold}}"), _n("eurusd", "EUR/USD", "market_asset", 100, 560, formula="-usd_safe_haven * {{coef_usd_eurusd}} - risk_premium * {{coef_rp_eurusd}}", unit="pips", instrument="EURUSD"), _n("sp500", "S&P 500", "market_asset", 380, 560, formula="-risk_premium * {{coef_rp_sp}} - vix_spike * {{coef_vix_sp}}", unit="pts", instrument="SP500"), _n("xauusd", "Gold XAU/USD", "market_asset", 660, 560, formula="gold_demand * {{coef_gold_xau}} + safe_haven_dem * {{coef_shd_xau}}", unit="$/oz", instrument="XAUUSD"), _n("brent", "Brent Crude", "market_asset", 880, 560, formula="oil_supply_risk * {{coef_oilrisk_brent}}", unit="$/bbl", instrument="BRENT"), ], "edges": [ _e("geo_shock", "risk_premium", "solid", "uncertainty", strength=3, sign="positive", label="uncertainty premium"), _e("geo_shock", "safe_haven_dem", "solid", "flight_quality", strength=3, sign="positive", label="flight to quality"), _e("geo_shock", "oil_supply_risk","dashed", "supply_threat", strength=2, sign="positive", label="supply threat"), _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"), _e("usd_safe_haven", "eurusd", "solid", "usd_apprec", strength=3, sign="negative", label="USD appreciates"), _e("risk_premium", "eurusd", "dashed", "eu_exposure", strength=2, sign="negative", label="EU geo exposure"), _e("risk_premium", "sp500", "solid", "risk_reprice", strength=3, sign="negative", label="risk repricing"), _e("vix_spike", "sp500", "solid", "vol_crush_eq", strength=2, sign="negative", label="vol crushes equities"), _e("gold_demand", "xauusd", "solid", "gold_move", strength=3, sign="positive"), _e("safe_haven_dem", "xauusd", "dashed", "shd_gold", strength=2, sign="positive"), _e("oil_supply_risk","brent", "solid", "supply_brent", strength=3, 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 supply risk (region-weighted)"), "coef_rp_vix": _c(2.0, "Risk premium -> Delta VIX pts"), "coef_shd_usd": _c(0.50, "Safe haven demand -> USD flow"), "coef_shd_gold": _c(0.80, "Safe haven demand -> gold demand"), "coef_usd_eurusd": _c(60, "USD safe haven -> EUR/USD pips fall"), "coef_rp_eurusd": _c(40, "Risk premium -> EUR/USD fall (EU exposure)"), "coef_rp_sp": _c(80, "Risk premium -> S&P pts fall"), "coef_vix_sp": _c(30, "VIX spike -> S&P pts fall"), "coef_gold_xau": _c(25, "Gold demand -> XAU/USD rise"), "coef_shd_xau": _c(15, "Safe haven -> XAU/USD additional"), "coef_oilrisk_brent": _c(5.0, "Oil supply risk -> Brent USD"), }, "instruments": ["EURUSD", "XAUUSD", "SP500", "BRENT"], "input_mapping": { "geo_shock": {"source": "impact_score_scaled", "unit": "score", "range": [1, 10]} }, }, }, # ═══════════════════════════════════════════════════════════════════════════════ # 5. COMMODITY_SUPPLY_SHOCK # Covers: OPEC decisions, sanctions on producers, pipeline disruptions, WASDE # ═══════════════════════════════════════════════════════════════════════════════ { "name": "Commodity Supply Shock", "slug": "COMMODITY_SUPPLY_SHOCK", "category": "commodity", "heuristic_ver": 1, "instruments": ["BRENT", "XAUUSD", "EURUSD", "SP500"], "description": "Supply disruption/glut in energy or commodities -> Brent move -> energy inflation -> EU trade deficit -> EUR/USD, Gold, S&P", "ai_rationale": "OPEC cuts or supply disruptions hit Brent first, then transmit through energy inflation (cost-push), EU trade balance deterioration (energy importer), and real income squeeze. Gold benefits from inflation expectations. S&P suffers from margin pressure.", "graph_json": { "nodes": [ _n("supply_shock", "Supply Shock", "input", 300, 50, unit="mbpd", description="+mbpd = cut/disruption (bullish oil), -mbpd = increase"), _n("supply_deficit", "Supply Deficit", "observable", 150, 200, formula="supply_shock * {{coef_shock_deficit}}"), _n("energy_inflation","Energy Inflation", "latent", 550, 200, formula="supply_shock * {{coef_shock_einfl}}"), _n("brent", "Brent Crude", "market_asset", 150, 380, formula="supply_deficit * {{coef_deficit_brent}}", unit="$/bbl", instrument="BRENT"), _n("eu_import_cost", "EU Import Cost", "latent", 420, 380, formula="energy_inflation * {{coef_einfl_import}}"), _n("infl_breakeven", "Inflation Expectations", "latent", 700, 380, formula="energy_inflation * {{coef_einfl_breakeven}}"), _n("eurusd", "EUR/USD", "market_asset", 200, 560, formula="-eu_import_cost * {{coef_import_eurusd}}", unit="pips", instrument="EURUSD"), _n("sp500", "S&P 500", "market_asset", 480, 560, formula="-eu_import_cost * {{coef_import_sp}} - infl_breakeven * {{coef_infl_sp}}", unit="pts", instrument="SP500"), _n("xauusd", "Gold XAU/USD", "market_asset", 750, 560, formula="infl_breakeven * {{coef_infl_gold}}", unit="$/oz", instrument="XAUUSD"), ], "edges": [ _e("supply_shock", "supply_deficit", "solid", "production_gap", strength=3, sign="positive", label="production gap"), _e("supply_shock", "energy_inflation","dashed", "cost_push", strength=2, sign="positive", label="cost-push inflation"), _e("supply_deficit", "brent", "solid", "spot_premium", strength=3, sign="positive", label="spot premium"), _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", label="EU trade deficit"), _e("eu_import_cost", "sp500", "dashed", "margin_squeeze", strength=2, sign="negative", label="margin squeeze"), _e("infl_breakeven", "sp500", "solid", "multiple_compress", strength=2, sign="negative", label="multiple compression"), _e("infl_breakeven", "xauusd", "solid", "infl_hedge", strength=3, sign="positive", label="inflation hedge"), ], "coefficients": { "coef_shock_deficit": _c(1.0, "Supply shock mb/d -> deficit mb/d"), "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 pressure"), "coef_einfl_breakeven": _c(0.40, "Energy inflation -> breakeven repricing"), "coef_import_eurusd": _c(50, "EU import cost -> EUR/USD pips fall"), "coef_import_sp": _c(40, "Import cost -> S&P pts fall (margin)"), "coef_infl_sp": _c(30, "Inflation exp -> S&P pts fall (multiples)"), "coef_infl_gold": _c(20, "Inflation exp -> Gold $/oz rise"), }, "instruments": ["BRENT", "XAUUSD", "EURUSD", "SP500"], "input_mapping": { "supply_shock": {"source": "user_input", "unit": "mbpd", "range": [-3, 3]} }, }, }, # ═══════════════════════════════════════════════════════════════════════════════ # 6. TRADE_POLICY_SHOCK # Covers: tariff announcements, trade restrictions, protectionism, export bans # ═══════════════════════════════════════════════════════════════════════════════ { "name": "Trade Policy Shock", "slug": "TRADE_POLICY_SHOCK", "category": "trade_policy", "heuristic_ver": 1, "instruments": ["EURUSD", "SP500", "XAUUSD"], "description": "Tariff / trade restriction announcement -> stagflationary shock (growth down + inflation up) -> EUR down SP500 down Gold up", "ai_rationale": "Trade restrictions create a stagflationary combination: import cost inflation and trade volume contraction (reduced EU exports -> ECB forced dovish). Policy uncertainty freezes capex. Gold benefits from the inflation + uncertainty combination.", "graph_json": { "nodes": [ _n("tariff_shock", "Trade Restriction", "input", 300, 50, unit="%", description="Tariff rate in % (positive = new restriction)"), _n("import_cost_rise", "Import Cost Rise", "observable", 150, 200, formula="tariff_shock * {{coef_tariff_cost}}", unit="%"), _n("trade_volume_drop","Trade Volume Drop", "observable", 550, 200, formula="-tariff_shock * {{coef_tariff_vol}}", unit="%"), _n("policy_uncertainty","Policy Uncertainty", "latent", 300, 200, formula="tariff_shock * {{coef_tariff_unc}}"), _n("stagflation_risk", "Stagflation Risk", "latent", 300, 370, formula="import_cost_rise * {{coef_cost_stag}} - trade_volume_drop * {{coef_vol_stag}}"), _n("corp_margin", "Corporate Margin Press.", "latent", 600, 370, formula="import_cost_rise * {{coef_cost_margin}}"), _n("eurusd", "EUR/USD", "market_asset", 100, 560, formula="-stagflation_risk * {{coef_stag_eurusd}} - trade_volume_drop * {{coef_vol_eurusd}}", unit="pips", instrument="EURUSD"), _n("sp500", "S&P 500", "market_asset", 400, 560, formula="-corp_margin * {{coef_margin_sp}} - policy_uncertainty * {{coef_unc_sp}} - stagflation_risk * {{coef_stag_sp}}", unit="pts", instrument="SP500"), _n("xauusd", "Gold XAU/USD", "market_asset", 700, 560, formula="stagflation_risk * {{coef_stag_gold}} + policy_uncertainty * {{coef_unc_gold}}", unit="$/oz", instrument="XAUUSD"), ], "edges": [ _e("tariff_shock", "import_cost_rise", "solid", "price_passthru", strength=3, sign="positive", label="price pass-through"), _e("tariff_shock", "trade_volume_drop", "solid", "trade_contract", strength=3, sign="negative", label="trade contraction"), _e("tariff_shock", "policy_uncertainty","solid", "capex_freeze", strength=3, sign="positive", label="capex freeze"), _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_stagflation", strength=2, sign="negative", label="EU stagflation exposure"), _e("trade_volume_drop","eurusd", "dashed", "export_shock", strength=2, sign="negative", label="export shock"), _e("corp_margin", "sp500", "solid", "eps_compress", strength=3, sign="negative", label="EPS compression"), _e("policy_uncertainty","sp500", "solid", "capex_sentiment", strength=2, sign="negative"), _e("stagflation_risk", "sp500", "dashed", "stagflation_sp", strength=2, sign="negative"), _e("stagflation_risk", "xauusd", "solid", "stagflation_hedge",strength=3, sign="positive", label="stagflation hedge"), _e("policy_uncertainty","xauusd", "dashed", "uncertainty_gold", strength=2, sign="positive"), ], "coefficients": { "coef_tariff_cost": _c(0.30, "10% tariff -> ~3% import cost rise"), "coef_tariff_vol": _c(0.50, "Tariff -> trade volume reduction"), "coef_tariff_unc": _c(0.40, "Tariff -> policy uncertainty"), "coef_cost_stag": _c(0.50, "Cost rise -> stagflation index"), "coef_vol_stag": _c(0.50, "Volume drop -> growth component"), "coef_cost_margin": _c(0.60, "Cost rise -> margin pressure"), "coef_stag_eurusd": _c(60, "Stagflation -> EUR/USD pips fall"), "coef_vol_eurusd": _c(30, "Trade drop -> EUR/USD pips fall"), "coef_margin_sp": _c(60, "Margin pressure -> S&P pts fall"), "coef_unc_sp": _c(40, "Uncertainty -> S&P pts fall"), "coef_stag_sp": _c(30, "Stagflation -> S&P pts fall"), "coef_stag_gold": _c(20, "Stagflation -> Gold $/oz rise"), "coef_unc_gold": _c(15, "Uncertainty -> Gold $/oz rise"), }, "instruments": ["EURUSD", "SP500", "XAUUSD"], "input_mapping": { "tariff_shock": {"source": "user_input", "unit": "%", "range": [0, 50]} }, }, }, # ═══════════════════════════════════════════════════════════════════════════════ # 7. CREDIT_SYSTEMIC_EVENT # Covers: bank failure, sovereign debt crisis, credit event, financial contagion # ═══════════════════════════════════════════════════════════════════════════════ { "name": "Credit & Systemic Stress", "slug": "CREDIT_SYSTEMIC_EVENT", "category": "credit_stress", "heuristic_ver": 1, "instruments": ["EURUSD", "SP500", "XAUUSD"], "description": "Credit event / bank stress / sovereign crisis -> spread widening -> financial conditions tighten -> systemic risk-off", "ai_rationale": "Credit events trigger spread widening (tightens financial conditions -> equities fall) and counterparty risk (forced deleveraging -> margin calls -> asset sales). USD strengthens via liquidity premium. Gold reaction: initially up (safe haven), then temporarily down if sold to cover margin calls.", "graph_json": { "nodes": [ _n("credit_trigger", "Credit Event", "input", 300, 50, unit="bps", description="HY spread widening equivalent (100bps=moderate, 300bps=severe)"), _n("spread_widening", "Credit Spread Widening", "observable", 150, 200, formula="credit_trigger * {{coef_trig_spread}}", unit="bps"), _n("counterparty_risk","Counterparty Risk", "latent", 550, 200, formula="credit_trigger * {{coef_trig_cp}}"), _n("fin_conditions", "Financial Conditions", "observable", 150, 370, formula="spread_widening * {{coef_spread_fc}}"), _n("liq_premium", "Liquidity Premium", "latent", 400, 370, formula="spread_widening * {{coef_spread_liq}} + counterparty_risk * {{coef_cp_liq}}"), _n("deleveraging", "Forced Deleveraging", "latent", 700, 370, formula="counterparty_risk * {{coef_cp_delev}}"), _n("eurusd", "EUR/USD", "market_asset", 100, 570, formula="-liq_premium * {{coef_liq_eurusd}} - fin_conditions * {{coef_fc_eurusd}}", unit="pips", instrument="EURUSD"), _n("sp500", "S&P 500", "market_asset", 400, 570, formula="-fin_conditions * {{coef_fc_sp}} - deleveraging * {{coef_delev_sp}}", unit="pts", instrument="SP500"), _n("xauusd", "Gold XAU/USD", "market_asset", 700, 570, formula="liq_premium * {{coef_liq_gold}} - deleveraging * {{coef_delev_gold}}", unit="$/oz", instrument="XAUUSD"), ], "edges": [ _e("credit_trigger", "spread_widening", "solid", "contagion", strength=3, sign="positive", label="spread contagion"), _e("credit_trigger", "counterparty_risk","solid", "systemic_fear", strength=3, sign="positive", label="systemic fear"), _e("spread_widening", "fin_conditions", "solid", "tightening", strength=3, sign="positive", label="tightening"), _e("spread_widening", "liq_premium", "solid", "cash_demand", strength=2, sign="positive"), _e("counterparty_risk","liq_premium", "solid", "cash_hoarding", strength=3, sign="positive", label="cash hoarding"), _e("counterparty_risk","deleveraging", "solid", "margin_calls", strength=3, sign="positive", label="margin calls"), _e("liq_premium", "eurusd", "solid", "usd_flight", strength=3, sign="negative", label="USD flight"), _e("fin_conditions", "eurusd", "dashed", "fin_cond_fx", strength=2, sign="negative"), _e("fin_conditions", "sp500", "solid", "valuation_earn",strength=3, sign="negative", label="valuation + earnings"), _e("deleveraging", "sp500", "solid", "forced_sell", strength=3, sign="negative", label="forced selling"), _e("liq_premium", "xauusd", "dashed", "safe_initial", strength=2, sign="positive", label="safe haven (initial)"), _e("deleveraging", "xauusd", "dashed", "sold_margin", strength=2, sign="negative", label="sold to cover losses"), ], "coefficients": { "coef_trig_spread": _c(1.0, "Credit event -> HY spread widening"), "coef_trig_cp": _c(0.60, "Credit event -> counterparty risk"), "coef_spread_fc": _c(0.50, "Spread widening -> fin conditions tightening"), "coef_spread_liq": _c(0.40, "Spread -> liquidity premium"), "coef_cp_liq": _c(0.60, "Counterparty risk -> liquidity premium"), "coef_cp_delev": _c(0.50, "Counterparty risk -> deleveraging"), "coef_liq_eurusd": _c(80, "Liquidity premium -> EUR/USD pips fall"), "coef_fc_eurusd": _c(40, "Fin conditions -> EUR/USD fall"), "coef_fc_sp": _c(100, "Fin conditions tightening -> S&P pts fall"), "coef_delev_sp": _c(80, "Deleveraging -> S&P pts fall"), "coef_liq_gold": _c(10, "Liquidity premium -> Gold rise (initial)"), "coef_delev_gold": _c(15, "Deleveraging -> Gold fall (sold for cash)"), }, "instruments": ["EURUSD", "SP500", "XAUUSD"], "input_mapping": { "credit_trigger": {"source": "user_input", "unit": "bps", "range": [50, 500]} }, }, }, # ═══════════════════════════════════════════════════════════════════════════════ # 8. TECHNICAL_MOMENTUM_BREAKOUT # Covers: MA crosses, RSI extremes, Bollinger squeezes, 52-week H/L, key level breaks # ═══════════════════════════════════════════════════════════════════════════════ { "name": "Technical Momentum Breakout", "slug": "TECHNICAL_MOMENTUM_BREAKOUT", "category": "technical", "heuristic_ver": 1, "instruments": ["EURUSD", "SP500"], "description": "Technical breakout/breakdown through key level -> stop cascade + momentum -> instrument price action", "ai_rationale": "Technical events operate via market microstructure: breakout triggers stop orders, creating a cascade that amplifies the move. Volume confirmation distinguishes true breakouts from false breaks. The correlated asset move is weaker and follows with a lag.", "graph_json": { "nodes": [ _n("tech_signal", "Technical Signal", "input", 300, 50, unit="sigma", description="+sigma = bullish breakout, -sigma = bearish breakdown"), _n("momentum_score", "Momentum Score", "observable", 150, 210, formula="tech_signal * {{coef_sig_mom}}"), _n("stop_cascade", "Stop Order Cascade", "latent", 500, 210, formula="tech_signal * {{coef_sig_stop}}"), _n("trend_strength", "Trend Strength", "latent", 300, 380, formula="momentum_score * {{coef_mom_trend}} + stop_cascade * {{coef_stop_trend}}"), _n("primary_inst", "Primary Instrument", "market_asset", 200, 560, formula="trend_strength * {{coef_trend_inst}} + stop_cascade * {{coef_stop_inst}}", unit="pips", instrument="EURUSD"), _n("correl_move", "Correlated Asset", "market_asset", 500, 560, formula="trend_strength * {{coef_trend_corr}}", unit="pts", instrument="SP500"), ], "edges": [ _e("tech_signal", "momentum_score", "solid", "signal_strength",strength=3, sign="positive", label="signal strength"), _e("tech_signal", "stop_cascade", "solid", "stop_trigger", strength=3, sign="positive", label="triggered stops"), _e("momentum_score", "trend_strength", "solid", "trend_confirm", strength=3, sign="positive"), _e("stop_cascade", "trend_strength", "solid", "amplification", strength=2, sign="positive", label="amplification"), _e("trend_strength", "primary_inst", "solid", "price_action", strength=3, sign="positive", label="price action"), _e("stop_cascade", "primary_inst", "solid", "liquidity_grab", strength=2, sign="positive", label="liquidity grab"), _e("trend_strength", "correl_move", "dashed", "spillover", strength=2, sign="positive", label="spillover"), ], "coefficients": { "coef_sig_mom": _c(1.0, "Signal sigma -> momentum score"), "coef_sig_stop": _c(0.60, "Signal sigma -> stop cascade magnitude"), "coef_mom_trend": _c(0.70, "Momentum -> trend strength"), "coef_stop_trend": _c(0.50, "Stop cascade -> trend amplification"), "coef_trend_inst": _c(50, "Trend strength -> primary instrument (pips)"), "coef_stop_inst": _c(30, "Stop cascade -> extra move (pips)"), "coef_trend_corr": _c(20, "Trend -> correlated asset (pts)"), }, "instruments": ["EURUSD", "SP500"], "input_mapping": { "tech_signal": {"source": "user_input", "unit": "sigma", "range": [-5, 5]} }, }, }, # ═══════════════════════════════════════════════════════════════════════════════ # 9. SENTIMENT_POSITIONING_EXTREME # Covers: VIX spike extremes, COT crowded positions, skew extremes, yield curve signals # ═══════════════════════════════════════════════════════════════════════════════ { "name": "Sentiment & Positioning Extreme", "slug": "SENTIMENT_POSITIONING_EXTREME", "category": "sentiment", "heuristic_ver": 1, "instruments": ["SP500", "EURUSD", "XAUUSD"], "description": "Extreme sentiment / crowded positioning -> mean reversion force -> contrarian price action", "ai_rationale": "When positioning reaches statistical extremes (COT multi-year high, VIX spike above 40, extreme put/call skew), the asymmetry of forced unwinds generates a contrarian signal. The mean reversion is not immediate — it typically takes 3-10 days to materialize. Gold falls when fear recedes.", "graph_json": { "nodes": [ _n("sent_trigger", "Sentiment Extreme", "input", 300, 50, unit="sigma", description="+sigma = extreme fear/oversold (contrarian buy), -sigma = extreme greed"), _n("pos_imbalance", "Positioning Imbalance", "observable", 150, 210, formula="sent_trigger * {{coef_sent_pos}}"), _n("vol_regime", "Volatility Regime", "observable", 550, 210, formula="-sent_trigger * {{coef_sent_vol}}"), _n("crowd_risk", "Crowded Trade Risk", "latent", 150, 380, formula="pos_imbalance * {{coef_pos_crowd}}"), _n("mr_force", "Mean Reversion Force", "latent", 380, 380, formula="crowd_risk * {{coef_crowd_mr}}"), _n("sp500", "S&P 500", "market_asset", 200, 580, formula="mr_force * {{coef_mr_sp}} + vol_regime * {{coef_vol_sp}}", unit="pts", instrument="SP500"), _n("eurusd", "EUR/USD", "market_asset", 480, 580, formula="mr_force * {{coef_mr_eurusd}}", unit="pips", instrument="EURUSD"), _n("xauusd", "Gold XAU/USD", "market_asset", 720, 580, formula="-mr_force * {{coef_mr_gold}}", unit="$/oz", instrument="XAUUSD"), ], "edges": [ _e("sent_trigger", "pos_imbalance", "solid", "flow_data", strength=3, sign="positive", label="flow / positioning data"), _e("sent_trigger", "vol_regime", "dashed", "vol_signal", strength=2, sign="negative", label="vol regime shift"), _e("pos_imbalance", "crowd_risk", "solid", "crowd_build", strength=3, sign="positive"), _e("crowd_risk", "mr_force", "solid", "squeeze_setup",strength=3, sign="positive", label="squeeze setup"), _e("mr_force", "sp500", "solid", "contrarian_sp",strength=3, sign="positive", label="contrarian rally/drop"), _e("vol_regime", "sp500", "dashed", "vol_collapse", strength=2, sign="positive", label="vol compression"), _e("mr_force", "eurusd", "solid", "unwind_fx", strength=2, sign="positive", label="position unwind"), _e("mr_force", "xauusd", "dashed", "safe_exit", strength=2, sign="negative", label="safe haven exit on recovery"), ], "coefficients": { "coef_sent_pos": _c(1.0, "Sentiment extreme -> positioning imbalance"), "coef_sent_vol": _c(0.50, "Extreme fear -> vol expansion (inverted for compression)"), "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, "Mean reversion -> S&P pts"), "coef_vol_sp": _c(30, "Vol compression -> S&P pts"), "coef_mr_eurusd":_c(40, "Mean reversion -> EUR/USD pips"), "coef_mr_gold": _c(15, "Fear recedes -> Gold falls ($/oz)"), }, "instruments": ["SP500", "EURUSD", "XAUUSD"], "input_mapping": { "sent_trigger": {"source": "user_input", "unit": "sigma", "range": [-4, 4]} }, }, }, # ═══════════════════════════════════════════════════════════════════════════════ # 10. COMMODITY_INVENTORY_REPORT # Covers: EIA crude oil weekly, EIA products, API crude, WASDE grain stocks # ═══════════════════════════════════════════════════════════════════════════════ { "name": "Commodity Inventory Report", "slug": "COMMODITY_INVENTORY_REPORT", "category": "commodity", "heuristic_ver": 1, "instruments": ["BRENT", "EURUSD", "XAUUSD"], "description": "Weekly inventory surprise -> supply/demand balance -> Brent spot + forward curve -> EUR/USD and Gold (inflation channel)", "ai_rationale": "EIA / API reports provide weekly updates on oil supply/demand balance. A draw vs consensus signals demand strength or supply tightness -> Brent spot rally + backwardation. Secondary channel: energy inflation expectations impact EU trade balance and gold as inflation hedge.", "graph_json": { "nodes": [ _n("inv_surprise", "Inventory Surprise", "input", 300, 50, unit="mb", description="+mb = draw vs consensus (bullish), -mb = build"), _n("sd_balance", "Supply/Demand Balance", "observable", 200, 210, formula="inv_surprise * {{coef_surp_balance}}"), _n("price_pressure", "Spot Price Pressure", "latent", 200, 380, formula="sd_balance * {{coef_balance_pressure}}"), _n("fwd_curve_shift", "Forward Curve Shift", "observable", 550, 380, formula="sd_balance * {{coef_balance_fwd}}"), _n("energy_cost_fwd", "Energy Cost Outlook", "latent", 700, 380, formula="price_pressure * {{coef_pressure_cost}}"), _n("brent", "Brent Crude", "market_asset", 200, 580, formula="price_pressure * {{coef_pressure_brent}} + fwd_curve_shift * {{coef_fwd_brent}}", unit="$/bbl", instrument="BRENT"), _n("eurusd", "EUR/USD", "market_asset", 520, 580, formula="-energy_cost_fwd * {{coef_energy_eurusd}}", unit="pips", instrument="EURUSD"), _n("xauusd", "Gold XAU/USD", "market_asset", 760, 580, formula="energy_cost_fwd * {{coef_energy_gold}}", unit="$/oz", instrument="XAUUSD"), ], "edges": [ _e("inv_surprise", "sd_balance", "solid", "tightness_sig", strength=3, sign="positive", label="tightness signal"), _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", label="curve structure"), _e("price_pressure", "energy_cost_fwd","dashed", "cost_outlook", strength=2, sign="positive"), _e("price_pressure", "brent", "solid", "spot_move", strength=3, sign="positive", label="spot move"), _e("fwd_curve_shift", "brent", "solid", "backwardation", strength=2, sign="positive", label="curve normalization"), _e("energy_cost_fwd", "eurusd", "dashed", "trade_deficit", strength=2, sign="negative", label="EU trade deficit"), _e("energy_cost_fwd", "xauusd", "dashed", "infl_hedge", strength=2, sign="positive", label="inflation hedge"), ], "coefficients": { "coef_surp_balance": _c(1.0, "1mb surprise -> supply/demand balance signal"), "coef_balance_pressure":_c(0.8, "Balance -> spot price pressure"), "coef_balance_fwd": _c(0.4, "Balance -> forward curve shift"), "coef_pressure_cost": _c(0.3, "Price pressure -> energy cost outlook"), "coef_pressure_brent": _c(0.5, "1mb surprise -> Brent $/bbl move"), "coef_fwd_brent": _c(0.2, "Forward curve shift -> Brent $/bbl"), "coef_energy_eurusd": _c(20, "Energy cost rise -> EUR/USD pips fall"), "coef_energy_gold": _c(5, "Energy cost rise -> Gold $/oz (inflation hedge)"), }, "instruments": ["BRENT", "EURUSD", "XAUUSD"], "input_mapping": { "inv_surprise": {"source": "surprise", "unit": "mb", "range": [-10, 10]} }, }, }, # ═══════════════════════════════════════════════════════════════════════════════ # 11. INSTITUTIONAL_FLOW # Covers: COT repositioning, FX intervention, end-of-month rebalancing # ═══════════════════════════════════════════════════════════════════════════════ { "name": "Institutional Flow & Repositioning", "slug": "INSTITUTIONAL_FLOW", "category": "positioning", "heuristic_ver": 1, "instruments": ["EURUSD", "SP500", "XAUUSD"], "description": "Large institutional flow without direct fundamental trigger -> order flow imbalance -> momentum + mean reversion risk", "ai_rationale": "Institutional repositioning (COT data, FX intervention, end-of-month rebalancing) moves prices via order flow mechanics rather than fundamental repricing. The move is self-limiting: large flows reduce liquidity and create a mean reversion setup once the flow exhausts.", "graph_json": { "nodes": [ _n("flow_trigger", "Institutional Flow", "input", 300, 50, unit="$B", description="+$B = accumulation (buy), -$B = distribution (sell)"), _n("order_imbalance", "Order Flow Imbalance", "observable", 150, 210, formula="flow_trigger * {{coef_flow_imbalance}}"), _n("market_impact", "Market Impact", "latent", 150, 380, formula="order_imbalance * {{coef_imbalance_impact}}"), _n("momentum_sig", "Flow Momentum", "latent", 450, 380, formula="market_impact * {{coef_impact_mom}} + order_imbalance * {{coef_imbalance_mom}}"), _n("mr_risk", "Mean Reversion Risk", "latent", 700, 380, formula="-momentum_sig * {{coef_mom_mr}}"), _n("eurusd", "EUR/USD", "market_asset", 150, 580, formula="momentum_sig * {{coef_mom_eurusd}}", unit="pips", instrument="EURUSD"), _n("sp500", "S&P 500", "market_asset", 430, 580, formula="momentum_sig * {{coef_mom_sp}}", unit="pts", instrument="SP500"), _n("xauusd", "Gold XAU/USD", "market_asset", 700, 580, formula="momentum_sig * {{coef_mom_gold}}", unit="$/oz", instrument="XAUUSD"), ], "edges": [ _e("flow_trigger", "order_imbalance","solid", "execution_impact",strength=3, sign="positive", label="execution impact"), _e("order_imbalance", "market_impact", "solid", "price_impact", strength=3, sign="positive"), _e("market_impact", "momentum_sig", "solid", "momentum_build", strength=2, sign="positive"), _e("order_imbalance", "momentum_sig", "solid", "order_flow", strength=2, sign="positive"), _e("momentum_sig", "mr_risk", "dashed", "exhaustion", strength=2, sign="negative", label="flow exhaustion"), _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 flow -> order imbalance score"), "coef_imbalance_impact": _c(0.60, "Imbalance -> market impact"), "coef_impact_mom": _c(0.70, "Market impact -> momentum"), "coef_imbalance_mom": _c(0.40, "Imbalance -> momentum direct"), "coef_mom_mr": _c(0.40, "Momentum -> mean reversion risk (inverted)"), "coef_mom_eurusd": _c(30, "Momentum -> EUR/USD pips"), "coef_mom_sp": _c(15, "Momentum -> S&P 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: any event that doesn't fit the 11 regimes above # Low confidence — direct instrument impact without causal chain # ═══════════════════════════════════════════════════════════════════════════════ { "name": "Unclassified Market Impact", "slug": "UNCLASSIFIED_IMPACT", "category": "unclassified", "heuristic_ver": 1, "instruments": ["EURUSD", "SP500", "XAUUSD"], "description": "Fallback template — event doesn't fit established regimes. Direct instrument impact without causal chain. Low confidence.", "ai_rationale": "Used when no regime template matches with sufficient confidence. No intermediate causal nodes — direct trigger to output. Precision scores from this template are marked low-confidence and should not be used for calibration.", "graph_json": { "nodes": [ _n("unknown_trigger","Market Trigger", "input", 300, 80, unit="score", description="Impact score: + positive (bullish), - negative (bearish)"), _n("market_reaction","Market Reaction", "latent", 300, 280, formula="unknown_trigger * {{coef_trig_react}}"), _n("eurusd", "EUR/USD", "market_asset", 100, 500, formula="market_reaction * {{coef_react_eurusd}}", unit="pips", instrument="EURUSD"), _n("sp500", "S&P 500", "market_asset", 300, 500, formula="market_reaction * {{coef_react_sp}}", unit="pts", instrument="SP500"), _n("xauusd", "Gold XAU/USD", "market_asset", 520, 500, formula="market_reaction * {{coef_react_gold}}", unit="$/oz", instrument="XAUUSD"), ], "edges": [ _e("unknown_trigger","market_reaction","dashed", "direct_impact",strength=1, sign="positive", label="direct impact (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 score -> market reaction (low confidence)"), "coef_react_eurusd": _c(20, "Reaction -> EUR/USD pips (low confidence)"), "coef_react_sp": _c(15, "Reaction -> S&P pts (low confidence)"), "coef_react_gold": _c(5, "Reaction -> Gold $/oz (low confidence)"), }, "instruments": ["EURUSD", "SP500", "XAUUSD"], "input_mapping": { "unknown_trigger": {"source": "user_input", "unit": "score", "range": [-5, 5]} }, }, }, ] # ── 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 built-in templates; update if heuristic_ver increased. One-shot migration: truncates old non-regime templates.""" # One-shot migration: remove templates that don't have a slug in REGIME_SLUGS existing_slugs = conn.execute( "SELECT name FROM causal_graph_templates WHERE created_by='system'" ).fetchall() for row in existing_slugs: old_name = row["name"] # Check if any new template has this name has_match = any(t["name"] == old_name for t in BUILT_IN_TEMPLATES) if not has_match: conn.execute("DELETE FROM causal_graph_templates WHERE name=? AND created_by='system'", (old_name,)) for t in BUILT_IN_TEMPLATES: ver = t.get("heuristic_ver", 1) existing = conn.execute( "SELECT id, heuristic_ver FROM causal_graph_templates WHERE name = ?", (t["name"],) ).fetchone() if existing: if (existing["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, 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"]), t.get("ai_rationale", ""), ver, )) 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): """Met à jour les stats de calibration après une nouvelle analyse.""" tmpl = get_template(conn, template_id) if not tmpl: return calib = tmpl.get("calibration_json") or {} n = calib.get("n_events", 0) + 1 # Running average of activation score prev_act = calib.get("avg_activation", 0) or 0 new_act = analysis_result.get("activation_score") or 0 # Running average of pred / actual pips 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() # ── Évaluateur de graphe ─────────────────────────────────────────────────────── def _safe_eval(expr: str, context: dict) -> float: """Évalue une expression arithmétique simple avec des variables.""" _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"Variable inconnue : {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"Opérateur non supporté : {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"Nœud AST non supporté : {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: """ Évalue un graphe causal depuis les inputs, retourne dict {node_id: valeur}. Les {{coef_xxx}} sont substitués depuis graph_json["coefficients"] (ou overrides). """ 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", []) # Évaluation en ordre topologique (max N passes) 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