Files
OpenFin/backend/services/causal_graphs.py
2026-07-01 21:01:00 +02:00

958 lines
70 KiB
Python

"""
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