diff --git a/backend/services/causal_graphs.py b/backend/services/causal_graphs.py index df6db2d..39b8970 100644 --- a/backend/services/causal_graphs.py +++ b/backend/services/causal_graphs.py @@ -31,10 +31,6 @@ def _n(id_, label, type_, x, y, formula=None, unit="", instrument=None, descript 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 @@ -64,184 +60,256 @@ 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 + # CPI, PCE, NFP, ADP, GDP, PMI, ISM, Retail Sales, Jobless Claims, JOLTS... # ═══════════════════════════════════════════════════════════════════════════════ { "name": "Macro Data Surprise", "slug": "MACRO_DATA_SURPRISE", "category": "monetary_shock", "heuristic_ver": 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 (-).", + "instruments": ["EURUSD", "SP500", "XAUUSD", "QQQ", "TLT", "USDJPY", "EEM", "GDX", "SLV"], + "description": "Scheduled economic release vs consensus -> OIS repricing -> full asset repricing", + "ai_rationale": "Hawkish surprise: OIS up -> 2Y up -> USD up -> EURUSD/USDJPY/EEM react; 10Y up -> TLT down, GDX/XAUUSD down. Growth channel -> SP500/QQQ up. Dovish miss: all reversed.", "graph_json": { "nodes": [ - _n("macro_surprise", "Macro Surprise", "input", 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"), + _n("macro_surprise", "Macro Surprise", "input", 400, 50, unit="sigma"), + _n("ois_repricing", "Fed OIS Repricing", "observable", 180, 180, formula="macro_surprise * {{coef_surp_ois}}", unit="bps"), + _n("growth_exp", "Growth Expectation", "latent", 580, 180, formula="macro_surprise * {{coef_surp_growth}}"), + _n("inflation_risk", "Inflation Risk", "latent", 820, 180, formula="macro_surprise * {{coef_surp_infl}}"), + _n("us_2y", "Delta US 2Y", "observable", 80, 330, formula="ois_repricing * {{coef_ois_2y}}", unit="%"), + _n("us_10y", "Delta US 10Y", "observable", 340, 330, formula="ois_repricing * {{coef_ois_10y}} + inflation_risk * {{coef_infl_10y}}", unit="%"), + _n("fin_conditions", "Financial Conditions", "latent", 620, 330, formula="ois_repricing * {{coef_ois_fc}} + us_10y * {{coef_10y_fc}}"), + _n("usd_strength", "USD Strength", "observable", 80, 470, formula="us_2y * {{coef_2y_usd}}", unit="%"), + # ── Primary outputs row 1 ────────────────────────────────────────── + _n("eurusd", "EUR/USD", "market_asset", 60, 620, formula="-usd_strength * {{coef_usd_eurusd}}", unit="pips", instrument="EURUSD"), + _n("sp500", "S&P 500", "market_asset", 260, 620, formula="growth_exp * {{coef_growth_sp}} - fin_conditions * {{coef_fc_sp}}", unit="pts", instrument="SP500"), + _n("xauusd", "Gold XAU/USD", "market_asset", 460, 620, formula="-us_10y * {{coef_10y_gold}} - usd_strength * {{coef_usd_gold}}", unit="$/oz", instrument="XAUUSD"), + # ── Additional outputs row 2 ────────────────────────────────────── + _n("qqq", "QQQ Nasdaq", "market_asset", 60, 790, formula="growth_exp * {{coef_growth_qqq}} - fin_conditions * {{coef_fc_qqq}}", unit="pts", instrument="QQQ"), + _n("tlt", "TLT 20Y Bond", "market_asset", 230, 790, formula="-us_10y * {{coef_10y_tlt}}", unit="pts", instrument="TLT"), + _n("usdjpy", "USD/JPY", "market_asset", 400, 790, formula="usd_strength * {{coef_usd_usdjpy}}", unit="pips", instrument="USDJPY"), + _n("eem", "EEM Emerg Mkt", "market_asset", 570, 790, formula="-usd_strength * {{coef_usd_eem}} + growth_exp * {{coef_growth_eem}}", unit="pts", instrument="EEM"), + _n("gdx", "GDX Gold Mine", "market_asset", 740, 790, formula="-us_10y * {{coef_10y_gdx}} - usd_strength * {{coef_usd_gdx}}", unit="pts", instrument="GDX"), + _n("slv", "SLV Silver", "market_asset", 910, 790, formula="-us_10y * {{coef_10y_slv}} - usd_strength * {{coef_usd_slv}}", unit="pts", instrument="SLV"), ], "edges": [ - _e("macro_surprise", "ois_repricing", "solid", "rate_repricing", strength=3, sign="positive", 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"), + _e("macro_surprise", "ois_repricing", "solid", "rate_repricing", strength=3, sign="positive"), + _e("macro_surprise", "growth_exp", "dashed", "growth_channel", strength=2, sign="positive"), + _e("macro_surprise", "inflation_risk", "dashed", "inflation_ch", strength=2, sign="positive"), + _e("ois_repricing", "us_2y", "solid", "short_end", strength=3, sign="positive"), + _e("ois_repricing", "us_10y", "dashed", "long_end", strength=1, sign="positive"), + _e("inflation_risk", "us_10y", "solid", "term_premium", strength=2, sign="positive"), + _e("ois_repricing", "fin_conditions", "solid", "policy_tighten", strength=2, sign="positive"), + _e("us_10y", "fin_conditions", "solid", "discount_rate", strength=2, sign="positive"), + _e("us_2y", "usd_strength", "solid", "carry_diff", strength=3, sign="positive"), + # row 1 outputs + _e("usd_strength", "eurusd", "solid", "fx_channel", strength=3, sign="negative"), + _e("growth_exp", "sp500", "solid", "earnings_ch", strength=2, sign="positive"), + _e("fin_conditions", "sp500", "solid", "valuation_ch", strength=3, sign="negative"), + _e("us_10y", "xauusd", "solid", "real_rate_gold",strength=2, sign="negative"), + _e("usd_strength", "xauusd", "dashed", "usd_gold", strength=2, sign="negative"), + # row 2 outputs + _e("growth_exp", "qqq", "solid", "tech_growth", strength=3, sign="positive", label="tech/growth beta"), + _e("fin_conditions", "qqq", "solid", "rate_compress", strength=3, sign="negative", label="high duration sensitivity"), + _e("us_10y", "tlt", "solid", "bond_price", strength=3, sign="negative", label="price = inverse yield"), + _e("usd_strength", "usdjpy", "solid", "carry_ch", strength=2, sign="positive", label="USD carry -> JPY weakens"), + _e("usd_strength", "eem", "solid", "usd_eem", strength=2, sign="negative", label="USD -> EM outflows"), + _e("growth_exp", "eem", "dashed", "growth_eem", strength=2, sign="positive"), + _e("us_10y", "gdx", "solid", "gold_miners", strength=2, sign="negative"), + _e("usd_strength", "gdx", "dashed", "usd_miners", strength=2, sign="negative"), + _e("us_10y", "slv", "solid", "silver_rates", strength=2, sign="negative"), + _e("usd_strength", "slv", "dashed", "silver_usd", strength=1, sign="negative"), ], "coefficients": { - "coef_surp_ois": _c(8.0, "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)"), + "coef_surp_ois": _c(8.0, "1sigma -> OIS bps"), + "coef_surp_growth": _c(0.30, "1sigma -> growth exp"), + "coef_surp_infl": _c(0.15, "1sigma -> inflation risk"), + "coef_ois_2y": _c(0.028, "OIS bps -> 2Y yield"), + "coef_ois_10y": _c(0.008, "OIS bps -> 10Y yield"), + "coef_infl_10y": _c(0.12, "Inflation -> 10Y premium"), + "coef_ois_fc": _c(0.40, "OIS -> financial conditions"), + "coef_10y_fc": _c(0.60, "10Y -> financial conditions"), + "coef_2y_usd": _c(0.50, "2Y -> USD strength"), + "coef_usd_eurusd": _c(80, "USD -> EURUSD pips fall"), + "coef_growth_sp": _c(60, "Growth -> SP500 pts"), + "coef_fc_sp": _c(50, "Fin cond -> SP500 pts fall"), + "coef_10y_gold": _c(15, "1% 10Y -> Gold fall $/oz"), + "coef_usd_gold": _c(10, "USD -> Gold fall $/oz"), + "coef_growth_qqq": _c(90, "Growth -> QQQ pts (higher beta)"), + "coef_fc_qqq": _c(75, "Fin cond -> QQQ pts fall (duration)"), + "coef_10y_tlt": _c(16, "1% 10Y -> TLT pts fall (~16y duration)"), + "coef_usd_usdjpy": _c(100, "USD strength -> USDJPY pips up"), + "coef_usd_eem": _c(25, "USD -> EEM pts fall"), + "coef_growth_eem": _c(12, "Growth -> EEM pts up"), + "coef_10y_gdx": _c(30, "1% 10Y -> GDX pts fall (2x gold beta)"), + "coef_usd_gdx": _c(20, "USD -> GDX pts fall"), + "coef_10y_slv": _c(8, "1% 10Y -> SLV pts fall (less monetary than gold)"), + "coef_usd_slv": _c(5, "USD -> SLV pts fall"), }, - "instruments": ["EURUSD", "XAUUSD", "SP500"], + "instruments": ["EURUSD", "SP500", "XAUUSD", "QQQ", "TLT", "USDJPY", "EEM", "GDX", "SLV"], "input_mapping": { - "macro_surprise": {"source": "surprise", "unit": "sigma", "description": "(actual-consensus)/std_dev", "range": [-3, 3]} + "macro_surprise": {"source": "surprise", "unit": "sigma", "range": [-3, 3]} }, }, }, # ═══════════════════════════════════════════════════════════════════════════════ # 2. CENTRAL_BANK_DECISION - # Covers: FOMC, ECB, BoJ, BoE, BoC, RBA, SNB, PBOC — any CB rate decision + # FOMC, ECB, BoJ, BoE, BoC, RBA, SNB, PBOC # ═══════════════════════════════════════════════════════════════════════════════ { "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.", + "instruments": ["EURUSD", "SP500", "XAUUSD", "QQQ", "TLT", "USDJPY", "GBPUSD", "XLF", "EEM"], + "description": "CB rate decision + forward guidance -> full curve repricing -> carry FX, bonds, equities", + "ai_rationale": "Rate hike -> 2Y/10Y up -> carry differential -> EURUSD/USDJPY/GBPUSD down; TLT down; QQQ hardest hit (duration); XLF benefits from steeper curve. Dovish: all reversed. EEM suffers from USD rate rises (capital outflows).", "graph_json": { "nodes": [ - _n("rate_surprise_bps", "Rate Surprise (bps)", "input", 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"), + _n("rate_surprise_bps", "Rate Surprise bps", "input", 140, 50, unit="bps"), + _n("tone_score", "Guidance Tone", "input", 500, 50, unit="score"), + _n("rate_channel", "Rate Path Revision", "observable", 140, 190, formula="rate_surprise_bps / 25 * {{coef_rate_mult}}"), + _n("guidance_channel", "Forward Guidance", "latent", 500, 190, formula="tone_score * {{coef_tone}}"), + _n("us_2y", "Delta 2Y Yield", "observable", 80, 340, formula="rate_channel * {{coef_rate_2y}} + guidance_channel * {{coef_tone_2y}}", unit="%"), + _n("us_10y", "Delta 10Y Yield", "observable", 320, 340, formula="rate_channel * {{coef_rate_10y}} + guidance_channel * {{coef_tone_10y}}", unit="%"), + _n("risk_appetite", "Risk Appetite", "latent", 600, 340, formula="-rate_channel * {{coef_rate_risk}} + guidance_channel * {{coef_tone_risk}}"), + _n("carry_diff", "Carry Differential", "observable", 80, 470, formula="us_2y * {{coef_2y_carry}}", unit="%"), + # ── Row 1 outputs ───────────────────────────────────────────────── + _n("eurusd", "EUR/USD", "market_asset", 60, 620, formula="-carry_diff * {{coef_carry_eurusd}}", unit="pips", instrument="EURUSD"), + _n("sp500", "S&P 500", "market_asset", 270, 620, formula="risk_appetite * {{coef_risk_sp}} - us_10y * {{coef_10y_sp}}", unit="pts", instrument="SP500"), + _n("xauusd", "Gold XAU/USD", "market_asset", 480, 620, formula="-us_10y * {{coef_10y_gold}} - carry_diff * {{coef_carry_gold}}", unit="$/oz", instrument="XAUUSD"), + # ── Row 2 outputs ───────────────────────────────────────────────── + _n("qqq", "QQQ Nasdaq", "market_asset", 60, 790, formula="risk_appetite * {{coef_risk_qqq}} - us_10y * {{coef_10y_qqq}}", unit="pts", instrument="QQQ"), + _n("tlt", "TLT 20Y Bond", "market_asset", 230, 790, formula="-us_10y * {{coef_10y_tlt}}", unit="pts", instrument="TLT"), + _n("usdjpy", "USD/JPY", "market_asset", 400, 790, formula="-carry_diff * {{coef_carry_usdjpy}}", unit="pips", instrument="USDJPY"), + _n("gbpusd", "GBP/USD", "market_asset", 570, 790, formula="-carry_diff * {{coef_carry_gbpusd}}", unit="pips", instrument="GBPUSD"), + _n("xlf", "XLF Financials","market_asset", 740, 790, formula="us_10y * {{coef_10y_xlf}} + risk_appetite * {{coef_risk_xlf}}", unit="pts", instrument="XLF"), + _n("eem", "EEM Emerg Mkt", "market_asset", 910, 790, formula="-carry_diff * {{coef_carry_eem}}", unit="pts", instrument="EEM"), ], "edges": [ - _e("rate_surprise_bps", "rate_channel", "solid", "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"), + _e("rate_surprise_bps", "rate_channel", "solid", "rate_reprice", strength=3, sign="positive"), + _e("tone_score", "guidance_channel","dashed","fwd_guidance", strength=3, sign="positive"), + _e("rate_channel", "us_2y", "solid", "short_anchor", strength=3, sign="positive"), + _e("guidance_channel","us_2y", "dashed", "bleed_2y", strength=1, sign="positive"), + _e("rate_channel", "us_10y", "solid", "level_shift", strength=2, sign="positive"), + _e("guidance_channel","us_10y", "dashed", "long_end_guid", strength=2, sign="positive"), + _e("rate_channel", "risk_appetite","dashed", "cost_capital", strength=2, sign="negative"), + _e("guidance_channel","risk_appetite","dashed","confidence", strength=2, sign="positive"), + _e("us_2y", "carry_diff", "solid", "carry", strength=3, sign="positive"), + # row 1 + _e("carry_diff", "eurusd", "solid", "fx_carry", strength=3, sign="negative"), + _e("risk_appetite", "sp500", "solid", "risk_on_sp", strength=2, sign="positive"), + _e("us_10y", "sp500", "solid", "discount_sp", strength=3, sign="negative"), + _e("us_10y", "xauusd", "solid", "real_rate_gold",strength=3, sign="negative"), + _e("carry_diff", "xauusd", "dashed", "opp_cost_gold", strength=2, sign="negative"), + # row 2 + _e("risk_appetite", "qqq", "solid", "risk_on_qqq", strength=2, sign="positive"), + _e("us_10y", "qqq", "solid", "duration_qqq", strength=3, sign="negative", label="tech=long duration"), + _e("us_10y", "tlt", "solid", "bond_price", strength=3, sign="negative"), + _e("carry_diff", "usdjpy", "solid", "carry_jpy", strength=2, sign="negative", label="USD carry -> JPY weakens"), + _e("carry_diff", "gbpusd", "solid", "carry_gbp", strength=2, sign="negative"), + _e("us_10y", "xlf", "solid", "nim_steepen", strength=2, sign="positive", label="steeper curve -> NIM"), + _e("risk_appetite", "xlf", "dashed", "risk_banks", strength=2, sign="positive"), + _e("carry_diff", "eem", "solid", "em_outflows", strength=2, sign="negative", label="USD hike -> EM outflows"), ], "coefficients": { - "coef_rate_mult": _c(1.5, "Decision surprise -> rate path revision multiplier"), - "coef_tone": _c(0.8, "Guidance tone -> signal"), - "coef_rate_2y": _c(0.70, "Rate path -> 2Y yield"), + "coef_rate_mult": _c(1.5, "Decision -> rate path revision"), + "coef_tone": _c(0.8, "Tone -> guidance signal"), + "coef_rate_2y": _c(0.70, "Rate path -> 2Y"), "coef_tone_2y": _c(0.03, "Guidance -> 2Y bleed"), - "coef_rate_10y": _c(0.30, "Rate path -> 10Y yield"), + "coef_rate_10y": _c(0.30, "Rate path -> 10Y"), "coef_tone_10y": _c(0.07, "Guidance -> 10Y long end"), "coef_rate_risk": _c(0.30, "Rate hike -> risk appetite negative"), "coef_tone_risk": _c(0.40, "Positive guidance -> risk appetite"), "coef_2y_carry": _c(1.0, "2Y -> carry differential"), - "coef_carry_eurusd": _c(120, "1% carry -> 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)"), + "coef_carry_eurusd": _c(120, "1% carry -> EURUSD pips fall"), + "coef_risk_sp": _c(40, "Risk appetite -> SP500 pts"), + "coef_10y_sp": _c(80, "1% 10Y -> SP500 pts fall"), + "coef_10y_gold": _c(20, "1% 10Y -> Gold fall $/oz"), + "coef_carry_gold": _c(15, "Carry -> Gold fall"), + "coef_risk_qqq": _c(50, "Risk appetite -> QQQ pts"), + "coef_10y_qqq": _c(110, "1% 10Y -> QQQ pts fall (high duration)"), + "coef_10y_tlt": _c(16, "1% 10Y -> TLT pts fall"), + "coef_carry_usdjpy": _c(150, "1% carry -> USDJPY pips (USD up -> JPY down)"), + "coef_carry_gbpusd": _c(80, "1% carry -> GBPUSD pips fall"), + "coef_10y_xlf": _c(15, "1% 10Y -> XLF pts (curve steepens -> NIM up)"), + "coef_risk_xlf": _c(20, "Risk appetite -> XLF pts"), + "coef_carry_eem": _c(20, "1% carry -> EEM pts fall"), }, - "instruments": ["EURUSD", "XAUUSD", "SP500"], + "instruments": ["EURUSD", "SP500", "XAUUSD", "QQQ", "TLT", "USDJPY", "GBPUSD", "XLF", "EEM"], "input_mapping": { "rate_surprise_bps": {"source": "surprise_bps", "unit": "bps"}, - "tone_score": {"source": "user_input", "unit": "score", "range": [-3, 3]}, + "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 + # Earnings, 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.", + "instruments": ["SP500", "QQQ", "IWM", "EURUSD", "XAUUSD", "HYG"], + "description": "Corporate/macro growth signal -> earnings revision + risk appetite -> equities, credit, FX, Gold", + "ai_rationale": "Earnings beats revise EPS upward and compress credit spreads (HYG up). QQQ > SP500 > IWM in tech-driven beats; IWM > SP500 in domestic growth signals. EURUSD follows risk appetite. Gold falls as safe haven demand recedes.", "graph_json": { "nodes": [ - _n("growth_trigger", "Growth Signal", "input", 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"), + _n("growth_trigger", "Growth Signal", "input", 350, 50, unit="%"), + _n("earnings_revision", "Earnings Revision", "latent", 180, 200, formula="growth_trigger * {{coef_trig_earn}}"), + _n("econ_activity", "Economic Activity", "observable", 520, 200, formula="growth_trigger * {{coef_trig_activity}}"), + _n("risk_appetite", "Risk Appetite", "latent", 350, 360, formula="earnings_revision * {{coef_earn_risk}} + econ_activity * {{coef_act_risk}}"), + _n("us_10y", "Delta US 10Y", "observable", 100, 360, formula="econ_activity * {{coef_act_10y}}", unit="%"), + # ── Row 1 outputs ───────────────────────────────────────────────── + _n("sp500", "S&P 500", "market_asset", 60, 540, formula="earnings_revision * {{coef_earn_sp}} + risk_appetite * {{coef_risk_sp}} - us_10y * {{coef_10y_sp}}", unit="pts", instrument="SP500"), + _n("qqq", "QQQ Nasdaq", "market_asset", 250, 540, formula="earnings_revision * {{coef_earn_qqq}} + risk_appetite * {{coef_risk_qqq}} - us_10y * {{coef_10y_qqq}}", unit="pts", instrument="QQQ"), + _n("iwm", "IWM Russell", "market_asset", 440, 540, formula="econ_activity * {{coef_act_iwm}} + risk_appetite * {{coef_risk_iwm}}", unit="pts", instrument="IWM"), + # ── Row 2 outputs ───────────────────────────────────────────────── + _n("eurusd", "EUR/USD", "market_asset", 60, 710, formula="risk_appetite * {{coef_risk_eurusd}}", unit="pips", instrument="EURUSD"), + _n("xauusd", "Gold XAU/USD", "market_asset", 280, 710, formula="-risk_appetite * {{coef_risk_gold}}", unit="$/oz", instrument="XAUUSD"), + _n("hyg", "HYG HY Bond", "market_asset", 500, 710, formula="risk_appetite * {{coef_risk_hyg}}", unit="pts", instrument="HYG"), ], "edges": [ - _e("growth_trigger", "earnings_revision","solid", "eps_channel", strength=3, sign="positive", 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"), + _e("growth_trigger", "earnings_revision","solid", "eps_channel", strength=3, sign="positive"), + _e("growth_trigger", "econ_activity", "dashed", "activity_proxy",strength=2, sign="positive"), + _e("growth_trigger", "us_10y", "dashed", "growth_premium",strength=1, sign="positive"), + _e("earnings_revision", "risk_appetite", "solid", "confidence", strength=3, sign="positive"), + _e("econ_activity", "risk_appetite", "solid", "sentiment", strength=2, sign="positive"), + _e("earnings_revision", "sp500", "solid", "eps_sp", strength=3, sign="positive"), + _e("risk_appetite", "sp500", "solid", "risk_sp", strength=2, sign="positive"), + _e("us_10y", "sp500", "dashed", "discount_sp", strength=1, sign="negative"), + _e("earnings_revision", "qqq", "solid", "eps_tech", strength=3, sign="positive", label="tech earnings beta"), + _e("risk_appetite", "qqq", "solid", "risk_qqq", strength=2, sign="positive"), + _e("us_10y", "qqq", "dashed", "duration_qqq",strength=2, sign="negative"), + _e("econ_activity", "iwm", "solid", "domestic_act",strength=3, sign="positive", label="domestic growth"), + _e("risk_appetite", "iwm", "solid", "risk_iwm", strength=2, sign="positive"), + _e("risk_appetite", "eurusd","solid", "risk_on_fx", strength=2, sign="positive"), + _e("risk_appetite", "xauusd","dashed", "safe_exit", strength=2, sign="negative"), + _e("risk_appetite", "hyg", "solid", "spread_compress",strength=3, sign="positive", label="spreads compress"), ], "coefficients": { - "coef_trig_earn": _c(0.50, "Growth signal -> earnings revision"), - "coef_trig_activity": _c(0.40, "Growth signal -> activity proxy"), - "coef_act_10y": _c(0.05, "Activity -> 10Y growth premium"), - "coef_earn_risk": _c(0.60, "Earnings 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"), + "coef_trig_earn": _c(0.50, "Growth signal -> earnings revision"), + "coef_trig_activity":_c(0.40,"Growth signal -> activity proxy"), + "coef_act_10y": _c(0.05, "Activity -> 10Y growth premium"), + "coef_earn_risk": _c(0.60, "Earnings -> risk appetite"), + "coef_act_risk": _c(0.40, "Activity -> risk appetite"), + "coef_earn_sp": _c(80, "Earnings -> SP500 pts"), + "coef_risk_sp": _c(40, "Risk appetite -> SP500 pts"), + "coef_10y_sp": _c(30, "10Y -> SP500 pts fall"), + "coef_earn_qqq": _c(110, "Earnings -> QQQ pts (higher beta)"), + "coef_risk_qqq": _c(55, "Risk appetite -> QQQ pts"), + "coef_10y_qqq": _c(50, "10Y -> QQQ pts fall"), + "coef_act_iwm": _c(60, "Activity -> IWM pts (domestic)"), + "coef_risk_iwm": _c(35, "Risk appetite -> IWM pts"), + "coef_risk_eurusd": _c(30, "Risk appetite -> EURUSD pips"), + "coef_risk_gold": _c(15, "Risk appetite up -> Gold falls"), + "coef_risk_hyg": _c(0.50, "Risk appetite -> HYG pts (spread compress)"), }, - "instruments": ["SP500", "EURUSD", "XAUUSD"], + "instruments": ["SP500", "QQQ", "IWM", "EURUSD", "XAUUSD", "HYG"], "input_mapping": { "growth_trigger": {"source": "user_input", "unit": "%", "range": [-5, 5]} }, @@ -250,61 +318,81 @@ BUILT_IN_TEMPLATES = [ # ═══════════════════════════════════════════════════════════════════════════════ # 4. GEOPOLITICAL_RISK_OFF - # Covers: military conflicts, sanctions, coups, election shocks, political crises + # Military conflicts, sanctions, coups, election shocks # ═══════════════════════════════════════════════════════════════════════════════ { "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.", + "instruments": ["EURUSD", "XAUUSD", "SP500", "BRENT", "USDJPY", "VXX", "TLT", "HYG", "UNG", "SLV"], + "description": "Geopolitical shock -> risk premium + safe haven demand -> multi-asset risk-off repricing", + "ai_rationale": "Gold/TLT/JPY safe havens rise; EUR/SP500/HYG fall. VXX spikes. USDJPY falls (JPY strengthens as safe haven). BRENT and UNG spike if energy-producing region. EU equities most exposed to European conflicts.", "graph_json": { "nodes": [ - _n("geo_shock", "Geopolitical Shock", "input", 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"), + _n("geo_shock", "Geopolitical Shock", "input", 350, 50, unit="score"), + _n("risk_premium", "Risk Premium", "latent", 160, 200, formula="geo_shock * {{coef_shock_rp}}"), + _n("safe_haven_dem", "Safe Haven Demand", "latent", 520, 200, formula="geo_shock * {{coef_shock_shd}}"), + _n("oil_supply_risk", "Oil/Gas Supply Risk", "latent", 820, 200, formula="geo_shock * {{coef_shock_oil}}"), + _n("vix_spike", "Delta VIX", "observable", 160, 360, formula="risk_premium * {{coef_rp_vix}}", unit="pts"), + _n("usd_safe_haven", "USD Safe Haven", "observable", 440, 360, formula="safe_haven_dem * {{coef_shd_usd}}"), + _n("gold_demand", "Gold Safe Haven", "observable", 700, 360, formula="safe_haven_dem * {{coef_shd_gold}}"), + # ── Row 1 outputs ───────────────────────────────────────────────── + _n("eurusd", "EUR/USD", "market_asset", 60, 550, formula="-usd_safe_haven * {{coef_usd_eurusd}} - risk_premium * {{coef_rp_eurusd}}", unit="pips", instrument="EURUSD"), + _n("sp500", "S&P 500", "market_asset", 280, 550, formula="-risk_premium * {{coef_rp_sp}} - vix_spike * {{coef_vix_sp}}", unit="pts", instrument="SP500"), + _n("xauusd", "Gold XAU/USD", "market_asset", 500, 550, formula="gold_demand * {{coef_gold_xau}}", unit="$/oz", instrument="XAUUSD"), + _n("brent", "Brent Crude", "market_asset", 720, 550, formula="oil_supply_risk * {{coef_oilrisk_brent}}", unit="$/bbl", instrument="BRENT"), + # ── Row 2 outputs ───────────────────────────────────────────────── + _n("usdjpy", "USD/JPY", "market_asset", 60, 730, formula="-safe_haven_dem * {{coef_shd_usdjpy}}", unit="pips", instrument="USDJPY"), + _n("vxx", "VXX Vol ETN", "market_asset", 240, 730, formula="vix_spike * {{coef_vix_vxx}}", unit="pts", instrument="VXX"), + _n("tlt", "TLT 20Y Bond", "market_asset", 420, 730, formula="safe_haven_dem * {{coef_shd_tlt}}", unit="pts", instrument="TLT"), + _n("hyg", "HYG HY Bond", "market_asset", 600, 730, formula="-risk_premium * {{coef_rp_hyg}}", unit="pts", instrument="HYG"), + _n("ung", "UNG Nat Gas", "market_asset", 780, 730, formula="oil_supply_risk * {{coef_oilrisk_ung}}", unit="pts", instrument="UNG"), + _n("slv", "SLV Silver", "market_asset", 960, 730, formula="safe_haven_dem * {{coef_shd_slv}}", unit="pts", instrument="SLV"), ], "edges": [ - _e("geo_shock", "risk_premium", "solid", "uncertainty", strength=3, sign="positive", 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"), + _e("geo_shock", "risk_premium", "solid", "uncertainty", strength=3, sign="positive"), + _e("geo_shock", "safe_haven_dem", "solid", "flight_quality",strength=3, sign="positive"), + _e("geo_shock", "oil_supply_risk","dashed", "supply_threat", strength=2, sign="positive"), + _e("risk_premium", "vix_spike", "solid", "fear_gauge", strength=3, sign="positive"), + _e("safe_haven_dem", "usd_safe_haven", "solid", "usd_flight", strength=2, sign="positive"), + _e("safe_haven_dem", "gold_demand", "solid", "gold_flight", strength=3, sign="positive"), + # row 1 + _e("usd_safe_haven", "eurusd", "solid", "usd_apprec", strength=3, sign="negative"), + _e("risk_premium", "eurusd", "dashed", "eu_exposure", strength=2, sign="negative"), + _e("risk_premium", "sp500", "solid", "risk_reprice", strength=3, sign="negative"), + _e("vix_spike", "sp500", "solid", "vol_crush_eq", strength=2, sign="negative"), + _e("gold_demand", "xauusd", "solid", "gold_move", strength=3, sign="positive"), + _e("oil_supply_risk","brent", "solid", "supply_brent", strength=3, sign="positive"), + # row 2 + _e("safe_haven_dem", "usdjpy", "solid", "jpy_safe_haven",strength=3, sign="negative", label="JPY safe haven -> USDJPY falls"), + _e("vix_spike", "vxx", "solid", "vix_vxx", strength=3, sign="positive", label="VIX up -> VXX up"), + _e("safe_haven_dem", "tlt", "solid", "tsy_flight", strength=3, sign="positive", label="flight to treasuries"), + _e("risk_premium", "hyg", "solid", "spread_widen", strength=2, sign="negative", label="spreads widen"), + _e("oil_supply_risk","ung", "solid", "gas_supply", strength=2, sign="positive", label="EU energy supply"), + _e("safe_haven_dem", "slv", "dashed", "silver_partial",strength=1, sign="positive"), ], "coefficients": { "coef_shock_rp": _c(0.30, "Geo shock -> risk premium"), "coef_shock_shd": _c(0.40, "Geo shock -> safe haven demand"), - "coef_shock_oil": _c(0.20, "Geo shock -> oil supply risk (region-weighted)"), + "coef_shock_oil": _c(0.20, "Geo shock -> oil/gas supply risk"), "coef_rp_vix": _c(2.0, "Risk premium -> Delta VIX pts"), - "coef_shd_usd": _c(0.50, "Safe haven 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_shd_usd": _c(0.50, "Safe haven -> USD flow"), + "coef_shd_gold": _c(0.80, "Safe haven -> gold demand"), + "coef_usd_eurusd": _c(60, "USD safe haven -> EURUSD pips fall"), + "coef_rp_eurusd": _c(40, "Risk premium -> EURUSD fall"), + "coef_rp_sp": _c(80, "Risk premium -> SP500 pts fall"), + "coef_vix_sp": _c(30, "VIX spike -> SP500 pts fall"), "coef_gold_xau": _c(25, "Gold demand -> XAU/USD rise"), - "coef_shd_xau": _c(15, "Safe haven -> XAU/USD additional"), - "coef_oilrisk_brent": _c(5.0, "Oil supply risk -> Brent USD"), + "coef_oilrisk_brent": _c(5.0, "Oil risk -> Brent $/bbl"), + "coef_shd_usdjpy": _c(80, "Safe haven -> USDJPY pips fall (JPY up)"), + "coef_vix_vxx": _c(2.5, "1pt VIX -> VXX pts (futures basis)"), + "coef_shd_tlt": _c(3.0, "Safe haven -> TLT pts up"), + "coef_rp_hyg": _c(0.80, "Risk premium -> HYG pts fall"), + "coef_oilrisk_ung": _c(3.0, "Gas supply risk -> UNG pts"), + "coef_shd_slv": _c(0.30, "Safe haven -> SLV pts (partial)"), }, - "instruments": ["EURUSD", "XAUUSD", "SP500", "BRENT"], + "instruments": ["EURUSD", "XAUUSD", "SP500", "BRENT", "USDJPY", "VXX", "TLT", "HYG", "UNG", "SLV"], "input_mapping": { "geo_shock": {"source": "impact_score_scaled", "unit": "score", "range": [1, 10]} }, @@ -313,51 +401,62 @@ BUILT_IN_TEMPLATES = [ # ═══════════════════════════════════════════════════════════════════════════════ # 5. COMMODITY_SUPPLY_SHOCK - # Covers: OPEC decisions, sanctions on producers, pipeline disruptions, WASDE + # OPEC, 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.", + "instruments": ["BRENT", "XAUUSD", "EURUSD", "SP500", "UNG", "XLE", "SLV"], + "description": "Supply disruption/glut -> Brent + energy inflation -> EU trade deficit -> multi-asset", + "ai_rationale": "OPEC cuts hit BRENT and XLE (energy sector stocks) first. Energy inflation pressures EU trade balance (EURUSD down) and inflates Gold/SLV. UNG follows as cross-commodity supply chain linkage. S&P suffers from margin pressure.", "graph_json": { "nodes": [ - _n("supply_shock", "Supply Shock", "input", 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"), + _n("supply_shock", "Supply Shock", "input", 350, 50, unit="mbpd"), + _n("supply_deficit", "Supply Deficit", "observable", 160, 200, formula="supply_shock * {{coef_shock_deficit}}"), + _n("energy_inflation","Energy Inflation", "latent", 600, 200, formula="supply_shock * {{coef_shock_einfl}}"), + _n("eu_import_cost", "EU Import Cost", "latent", 440, 370, formula="energy_inflation * {{coef_einfl_import}}"), + _n("infl_breakeven", "Inflation Expect", "latent", 780, 370, formula="energy_inflation * {{coef_einfl_breakeven}}"), + # ── Row 1 outputs ───────────────────────────────────────────────── + _n("brent", "Brent Crude", "market_asset", 60, 560, formula="supply_deficit * {{coef_deficit_brent}}", unit="$/bbl", instrument="BRENT"), + _n("eurusd", "EUR/USD", "market_asset", 260, 560, formula="-eu_import_cost * {{coef_import_eurusd}}", unit="pips", instrument="EURUSD"), + _n("sp500", "S&P 500", "market_asset", 460, 560, formula="-eu_import_cost * {{coef_import_sp}} - infl_breakeven * {{coef_infl_sp}}", unit="pts", instrument="SP500"), + _n("xauusd", "Gold XAU/USD", "market_asset", 660, 560, formula="infl_breakeven * {{coef_infl_gold}}", unit="$/oz", instrument="XAUUSD"), + # ── Row 2 outputs ───────────────────────────────────────────────── + _n("xle", "XLE Energy", "market_asset", 60, 740, formula="supply_deficit * {{coef_deficit_xle}}", unit="pts", instrument="XLE"), + _n("ung", "UNG Nat Gas", "market_asset", 260, 740, formula="energy_inflation * {{coef_einfl_ung}}", unit="pts", instrument="UNG"), + _n("slv", "SLV Silver", "market_asset", 460, 740, formula="infl_breakeven * {{coef_infl_slv}}", unit="pts", instrument="SLV"), ], "edges": [ - _e("supply_shock", "supply_deficit", "solid", "production_gap", strength=3, sign="positive", 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"), + _e("supply_shock", "supply_deficit", "solid", "production_gap", strength=3, sign="positive"), + _e("supply_shock", "energy_inflation", "dashed", "cost_push", strength=2, sign="positive"), + _e("supply_deficit", "brent", "solid", "spot_premium", strength=3, sign="positive"), + _e("energy_inflation","eu_import_cost", "solid", "trade_balance", strength=2, sign="positive"), + _e("energy_inflation","infl_breakeven", "solid", "breakeven_reprice",strength=2, sign="positive"), + _e("eu_import_cost", "eurusd", "solid", "terms_of_trade", strength=2, sign="negative"), + _e("eu_import_cost", "sp500", "dashed", "margin_squeeze", strength=2, sign="negative"), + _e("infl_breakeven", "sp500", "solid", "multiple_compress",strength=2, sign="negative"), + _e("infl_breakeven", "xauusd", "solid", "infl_hedge", strength=3, sign="positive"), + _e("supply_deficit", "xle", "solid", "energy_stocks", strength=3, sign="positive", label="oil price -> energy stocks"), + _e("energy_inflation","ung", "dashed", "energy_chain", strength=2, sign="positive", label="cross-commodity link"), + _e("infl_breakeven", "slv", "solid", "industrial_infl", strength=2, sign="positive", label="silver: industrial + inflation"), ], "coefficients": { - "coef_shock_deficit": _c(1.0, "Supply shock mb/d -> deficit 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"), + "coef_shock_deficit": _c(1.0, "Supply shock mb/d -> deficit"), + "coef_shock_einfl": _c(0.10, "Supply shock -> energy inflation"), + "coef_deficit_brent": _c(3.0, "1mb/d deficit -> Brent $/bbl"), + "coef_einfl_import": _c(0.60, "Energy inflation -> EU import cost"), + "coef_einfl_breakeven": _c(0.40, "Energy inflation -> breakeven"), + "coef_import_eurusd": _c(50, "EU import cost -> EURUSD pips fall"), + "coef_import_sp": _c(40, "Import cost -> SP500 pts fall"), + "coef_infl_sp": _c(30, "Inflation exp -> SP500 pts fall"), + "coef_infl_gold": _c(20, "Inflation exp -> Gold $/oz"), + "coef_deficit_xle": _c(1.5, "1mb/d deficit -> XLE pts"), + "coef_einfl_ung": _c(1.0, "Energy inflation -> UNG pts"), + "coef_infl_slv": _c(5, "Inflation exp -> SLV pts"), }, - "instruments": ["BRENT", "XAUUSD", "EURUSD", "SP500"], + "instruments": ["BRENT", "XAUUSD", "EURUSD", "SP500", "UNG", "XLE", "SLV"], "input_mapping": { "supply_shock": {"source": "user_input", "unit": "mbpd", "range": [-3, 3]} }, @@ -366,59 +465,77 @@ BUILT_IN_TEMPLATES = [ # ═══════════════════════════════════════════════════════════════════════════════ # 6. TRADE_POLICY_SHOCK - # Covers: tariff announcements, trade restrictions, protectionism, export bans + # Tariffs, 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.", + "instruments": ["EURUSD", "SP500", "XAUUSD", "EEM", "QQQ", "GBPUSD", "IWM"], + "description": "Tariff announcement -> stagflationary shock -> EUR/GBP down SP500/QQQ down EEM down Gold up", + "ai_rationale": "Tariffs create stagflation: import cost rise (inflation) + trade volume drop (growth). EEM is hit hardest via global trade contraction and USD strength. QQQ suffers from tech supply chain disruption (AAPL/NVDA exposed). GBPUSD exposed as UK trade-dependent. IWM domestic plays can benefit initially but input cost inflation hurts margins.", "graph_json": { "nodes": [ - _n("tariff_shock", "Trade Restriction", "input", 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"), + _n("tariff_shock", "Trade Restriction", "input", 350, 50, unit="%"), + _n("import_cost_rise", "Import Cost Rise", "observable", 160, 200, formula="tariff_shock * {{coef_tariff_cost}}"), + _n("trade_volume_drop","Trade Volume Drop", "observable", 600, 200, formula="-tariff_shock * {{coef_tariff_vol}}"), + _n("policy_uncertainty","Policy Uncertainty", "latent", 350, 200, formula="tariff_shock * {{coef_tariff_unc}}"), + _n("stagflation_risk", "Stagflation Risk", "latent", 350, 380, formula="import_cost_rise * {{coef_cost_stag}} - trade_volume_drop * {{coef_vol_stag}}"), + _n("corp_margin", "Corp Margin Pressure", "latent", 650, 380, formula="import_cost_rise * {{coef_cost_margin}}"), + # ── Row 1 outputs ───────────────────────────────────────────────── + _n("eurusd", "EUR/USD", "market_asset", 60, 570, formula="-stagflation_risk * {{coef_stag_eurusd}} - trade_volume_drop * {{coef_vol_eurusd}}", unit="pips", instrument="EURUSD"), + _n("sp500", "S&P 500", "market_asset", 300, 570, formula="-corp_margin * {{coef_margin_sp}} - policy_uncertainty * {{coef_unc_sp}}", unit="pts", instrument="SP500"), + _n("xauusd", "Gold XAU/USD", "market_asset", 540, 570, formula="stagflation_risk * {{coef_stag_gold}} + policy_uncertainty * {{coef_unc_gold}}", unit="$/oz", instrument="XAUUSD"), + # ── Row 2 outputs ───────────────────────────────────────────────── + _n("eem", "EEM Emerg Mkt", "market_asset", 60, 750, formula="-trade_volume_drop * {{coef_vol_eem}} - stagflation_risk * {{coef_stag_eem}}", unit="pts", instrument="EEM"), + _n("qqq", "QQQ Nasdaq", "market_asset", 260, 750, formula="-corp_margin * {{coef_margin_qqq}} - policy_uncertainty * {{coef_unc_qqq}}", unit="pts", instrument="QQQ"), + _n("gbpusd", "GBP/USD", "market_asset", 460, 750, formula="-stagflation_risk * {{coef_stag_gbpusd}} - trade_volume_drop * {{coef_vol_gbpusd}}", unit="pips", instrument="GBPUSD"), + _n("iwm", "IWM Russell", "market_asset", 660, 750, formula="-policy_uncertainty * {{coef_unc_iwm}}", unit="pts", instrument="IWM"), ], "edges": [ - _e("tariff_shock", "import_cost_rise", "solid", "price_passthru", strength=3, sign="positive", 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"), + _e("tariff_shock", "import_cost_rise", "solid", "price_passthru", strength=3, sign="positive"), + _e("tariff_shock", "trade_volume_drop", "solid", "trade_contract", strength=3, sign="negative"), + _e("tariff_shock", "policy_uncertainty", "solid", "capex_freeze", strength=3, sign="positive"), + _e("import_cost_rise", "stagflation_risk", "solid", "cost_stag", strength=2, sign="positive"), + _e("trade_volume_drop", "stagflation_risk", "solid", "growth_stag", strength=2, sign="negative"), + _e("import_cost_rise", "corp_margin", "solid", "supply_chain", strength=3, sign="positive"), + _e("stagflation_risk", "eurusd", "solid", "eu_stag", strength=2, sign="negative"), + _e("trade_volume_drop", "eurusd", "dashed", "export_shock", strength=2, sign="negative"), + _e("corp_margin", "sp500", "solid", "eps_compress", strength=3, sign="negative"), + _e("policy_uncertainty","sp500", "solid", "capex_sentiment",strength=2, sign="negative"), + _e("stagflation_risk", "xauusd", "solid", "stag_hedge", strength=3, sign="positive"), + _e("policy_uncertainty","xauusd", "dashed", "unc_gold", strength=2, sign="positive"), + _e("trade_volume_drop", "eem", "solid", "global_trade_eem",strength=3, sign="negative", label="EM most exposed"), + _e("stagflation_risk", "eem", "dashed", "usd_eem", strength=2, sign="negative"), + _e("corp_margin", "qqq", "solid", "tech_supply_ch", strength=3, sign="negative", label="AAPL/NVDA supply chain"), + _e("policy_uncertainty","qqq", "dashed", "tech_capex", strength=2, sign="negative"), + _e("stagflation_risk", "gbpusd", "solid", "uk_trade", strength=2, sign="negative"), + _e("trade_volume_drop", "gbpusd", "dashed", "uk_exports", strength=2, sign="negative"), + _e("policy_uncertainty","iwm", "dashed", "domestic_capex", strength=2, sign="negative"), ], "coefficients": { - "coef_tariff_cost": _c(0.30, "10% tariff -> ~3% import cost 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"), + "coef_tariff_cost": _c(0.30, "10% tariff -> ~3% import cost"), + "coef_tariff_vol": _c(0.50, "Tariff -> trade volume drop"), + "coef_tariff_unc": _c(0.40, "Tariff -> policy uncertainty"), + "coef_cost_stag": _c(0.50, "Cost -> stagflation index"), + "coef_vol_stag": _c(0.50, "Vol drop -> growth component"), + "coef_cost_margin": _c(0.60, "Cost -> margin pressure"), + "coef_stag_eurusd": _c(60, "Stagflation -> EURUSD pips fall"), + "coef_vol_eurusd": _c(30, "Trade drop -> EURUSD pips fall"), + "coef_margin_sp": _c(60, "Margin -> SP500 pts fall"), + "coef_unc_sp": _c(40, "Uncertainty -> SP500 pts fall"), + "coef_stag_gold": _c(20, "Stagflation -> Gold $/oz"), + "coef_unc_gold": _c(15, "Uncertainty -> Gold $/oz"), + "coef_vol_eem": _c(25, "Trade drop -> EEM pts fall"), + "coef_stag_eem": _c(20, "Stagflation -> EEM pts fall (USD up)"), + "coef_margin_qqq": _c(80, "Margin -> QQQ pts fall"), + "coef_unc_qqq": _c(50, "Uncertainty -> QQQ pts fall"), + "coef_stag_gbpusd": _c(40, "Stagflation -> GBPUSD pips fall"), + "coef_vol_gbpusd": _c(30, "Trade drop -> GBPUSD pips fall"), + "coef_unc_iwm": _c(30, "Uncertainty -> IWM pts fall"), }, - "instruments": ["EURUSD", "SP500", "XAUUSD"], + "instruments": ["EURUSD", "SP500", "XAUUSD", "EEM", "QQQ", "GBPUSD", "IWM"], "input_mapping": { "tariff_shock": {"source": "user_input", "unit": "%", "range": [0, 50]} }, @@ -427,57 +544,85 @@ BUILT_IN_TEMPLATES = [ # ═══════════════════════════════════════════════════════════════════════════════ # 7. CREDIT_SYSTEMIC_EVENT - # Covers: bank failure, sovereign debt crisis, credit event, financial contagion + # Bank failure, sovereign debt crisis, HY blowout, 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.", + "instruments": ["EURUSD", "SP500", "XAUUSD", "HYG", "TLT", "VXX", "XLF", "USDJPY", "BTC"], + "description": "Credit event -> spread widening + deleveraging -> systemic risk-off + USD liquidity flight", + "ai_rationale": "HYG is the primary instrument (spreads widen = HYG down). TLT initially up (flight to quality) then volatile (sold for cash). VXX spikes hard. XLF (financials) crushed. USDJPY falls (JPY safe haven). BTC complex: initially up as alternative store of value, then down on margin calls.", "graph_json": { "nodes": [ - _n("credit_trigger", "Credit Event", "input", 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"), + _n("credit_trigger", "Credit Event", "input", 350, 50, unit="bps"), + _n("spread_widening", "Spread Widening", "observable", 160, 200, formula="credit_trigger * {{coef_trig_spread}}", unit="bps"), + _n("counterparty_risk","Counterparty Risk", "latent", 600, 200, formula="credit_trigger * {{coef_trig_cp}}"), + _n("fin_conditions", "Financial Conditions", "observable", 160, 370, formula="spread_widening * {{coef_spread_fc}}"), + _n("liq_premium", "Liquidity Premium", "latent", 440, 370, formula="spread_widening * {{coef_spread_liq}} + counterparty_risk * {{coef_cp_liq}}"), + _n("deleveraging", "Forced Deleveraging", "latent", 750, 370, formula="counterparty_risk * {{coef_cp_delev}}"), + # ── Row 1 outputs ───────────────────────────────────────────────── + _n("eurusd", "EUR/USD", "market_asset", 60, 570, formula="-liq_premium * {{coef_liq_eurusd}} - fin_conditions * {{coef_fc_eurusd}}", unit="pips", instrument="EURUSD"), + _n("sp500", "S&P 500", "market_asset", 300, 570, formula="-fin_conditions * {{coef_fc_sp}} - deleveraging * {{coef_delev_sp}}", unit="pts", instrument="SP500"), + _n("xauusd", "Gold XAU/USD", "market_asset", 540, 570, formula="liq_premium * {{coef_liq_gold}} - deleveraging * {{coef_delev_gold}}", unit="$/oz", instrument="XAUUSD"), + # ── Row 2 outputs ───────────────────────────────────────────────── + _n("hyg", "HYG HY Bond", "market_asset", 60, 760, formula="-spread_widening * {{coef_spread_hyg}} - deleveraging * {{coef_delev_hyg}}", unit="pts", instrument="HYG"), + _n("tlt", "TLT 20Y Bond", "market_asset", 240, 760, formula="liq_premium * {{coef_liq_tlt}} - deleveraging * {{coef_delev_tlt}}", unit="pts", instrument="TLT"), + _n("vxx", "VXX Vol ETN", "market_asset", 420, 760, formula="spread_widening * {{coef_spread_vxx}}", unit="pts", instrument="VXX"), + _n("xlf", "XLF Financials","market_asset", 600, 760, formula="-spread_widening * {{coef_spread_xlf}} - fin_conditions * {{coef_fc_xlf}}", unit="pts", instrument="XLF"), + _n("usdjpy", "USD/JPY", "market_asset", 780, 760, formula="-liq_premium * {{coef_liq_usdjpy}}", unit="pips", instrument="USDJPY"), + _n("btc", "BTC-USD", "market_asset", 960, 760, formula="liq_premium * {{coef_liq_btc}} - deleveraging * {{coef_delev_btc}}", unit="pts", instrument="BTC"), ], "edges": [ - _e("credit_trigger", "spread_widening", "solid", "contagion", strength=3, sign="positive", 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("credit_trigger", "spread_widening", "solid", "contagion", strength=3, sign="positive"), + _e("credit_trigger", "counterparty_risk","solid", "systemic_fear", strength=3, sign="positive"), + _e("spread_widening", "fin_conditions", "solid", "tightening", strength=3, sign="positive"), _e("spread_widening", "liq_premium", "solid", "cash_demand", strength=2, sign="positive"), - _e("counterparty_risk","liq_premium", "solid", "cash_hoarding", strength=3, sign="positive", 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"), + _e("counterparty_risk","liq_premium", "solid", "cash_hoarding", strength=3, sign="positive"), + _e("counterparty_risk","deleveraging", "solid", "margin_calls", strength=3, sign="positive"), + _e("liq_premium", "eurusd", "solid", "usd_flight", strength=3, sign="negative"), + _e("fin_conditions", "eurusd", "dashed", "fin_cond_fx", strength=2, sign="negative"), + _e("fin_conditions", "sp500", "solid", "valuation", strength=3, sign="negative"), + _e("deleveraging", "sp500", "solid", "forced_sell", strength=3, sign="negative"), + _e("liq_premium", "xauusd", "dashed", "safe_initial", strength=2, sign="positive"), + _e("deleveraging", "xauusd", "dashed", "sold_margin", strength=2, sign="negative"), + _e("spread_widening", "hyg", "solid", "direct_spread", strength=3, sign="negative", label="HY spreads -> HYG falls"), + _e("deleveraging", "hyg", "solid", "forced_selling_hy",strength=2, sign="negative"), + _e("liq_premium", "tlt", "solid", "tsy_safe", strength=3, sign="positive", label="flight to treasuries"), + _e("deleveraging", "tlt", "dashed", "sold_tsy", strength=2, sign="negative", label="sold for cash"), + _e("spread_widening", "vxx", "solid", "vol_spike", strength=3, sign="positive"), + _e("spread_widening", "xlf", "solid", "bank_stress", strength=3, sign="negative", label="banks = credit exposure"), + _e("fin_conditions", "xlf", "solid", "npa_risk", strength=2, sign="negative"), + _e("liq_premium", "usdjpy", "solid", "jpy_safe", strength=2, sign="negative", label="JPY safe haven"), + _e("liq_premium", "btc", "dashed", "alt_store", strength=2, sign="positive", label="alt store of value (initial)"), + _e("deleveraging", "btc", "dashed", "btc_margin", strength=2, sign="negative", label="margin call selling"), ], "coefficients": { - "coef_trig_spread": _c(1.0, "Credit event -> HY spread widening"), + "coef_trig_spread": _c(1.0, "Credit event -> spread widening"), "coef_trig_cp": _c(0.60, "Credit event -> counterparty risk"), - "coef_spread_fc": _c(0.50, "Spread widening -> fin conditions tightening"), + "coef_spread_fc": _c(0.50, "Spread -> financial conditions"), "coef_spread_liq": _c(0.40, "Spread -> liquidity premium"), - "coef_cp_liq": _c(0.60, "Counterparty 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)"), + "coef_cp_liq": _c(0.60, "Counterparty -> liquidity premium"), + "coef_cp_delev": _c(0.50, "Counterparty -> deleveraging"), + "coef_liq_eurusd": _c(80, "Liquidity -> EURUSD pips fall"), + "coef_fc_eurusd": _c(40, "Fin cond -> EURUSD fall"), + "coef_fc_sp": _c(100, "Fin cond -> SP500 pts fall"), + "coef_delev_sp": _c(80, "Deleveraging -> SP500 pts fall"), + "coef_liq_gold": _c(10, "Liquidity -> Gold rise (initial)"), + "coef_delev_gold": _c(15, "Deleveraging -> Gold fall (sold)"), + "coef_spread_hyg": _c(0.05, "1bps spread -> HYG pts fall"), + "coef_delev_hyg": _c(0.30, "Deleveraging -> HYG pts fall"), + "coef_liq_tlt": _c(3.0, "Liquidity -> TLT pts up (initial)"), + "coef_delev_tlt": _c(1.5, "Deleveraging -> TLT pts fall"), + "coef_spread_vxx": _c(0.05, "1bps spread -> VXX pts up"), + "coef_spread_xlf": _c(0.04, "1bps spread -> XLF pts fall"), + "coef_fc_xlf": _c(0.60, "Fin cond -> XLF pts fall"), + "coef_liq_usdjpy": _c(60, "Liquidity -> USDJPY pips fall (JPY up)"), + "coef_liq_btc": _c(200, "Liquidity -> BTC pts up (alt store)"), + "coef_delev_btc": _c(400, "Deleveraging -> BTC pts fall (margin calls)"), }, - "instruments": ["EURUSD", "SP500", "XAUUSD"], + "instruments": ["EURUSD", "SP500", "XAUUSD", "HYG", "TLT", "VXX", "XLF", "USDJPY", "BTC"], "input_mapping": { "credit_trigger": {"source": "user_input", "unit": "bps", "range": [50, 500]} }, @@ -486,44 +631,47 @@ BUILT_IN_TEMPLATES = [ # ═══════════════════════════════════════════════════════════════════════════════ # 8. TECHNICAL_MOMENTUM_BREAKOUT - # Covers: MA crosses, RSI extremes, Bollinger squeezes, 52-week H/L, key level breaks + # MA crosses, RSI extremes, key level breaks, 52w H/L # ═══════════════════════════════════════════════════════════════════════════════ { "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.", + "instruments": ["EURUSD", "SP500", "QQQ"], + "description": "Technical breakout/breakdown -> stop cascade + momentum -> instrument price action", + "ai_rationale": "Technical events operate via microstructure: breakout triggers stops, amplifying the move. No fundamental anchor. The correlated asset move is weaker. Applies to any instrument — the primary and correlated move fields should be overridden per event.", "graph_json": { "nodes": [ - _n("tech_signal", "Technical Signal", "input", 300, 50, unit="sigma", 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"), + _n("tech_signal", "Technical Signal", "input", 300, 50, unit="sigma"), + _n("momentum_score", "Momentum Score", "observable", 160, 220, formula="tech_signal * {{coef_sig_mom}}"), + _n("stop_cascade", "Stop Order Cascade","latent", 500, 220, formula="tech_signal * {{coef_sig_stop}}"), + _n("trend_strength", "Trend Strength", "latent", 300, 400, formula="momentum_score * {{coef_mom_trend}} + stop_cascade * {{coef_stop_trend}}"), + _n("eurusd", "Primary Instrument", "market_asset", 160, 600, formula="trend_strength * {{coef_trend_inst}} + stop_cascade * {{coef_stop_inst}}", unit="pips", instrument="EURUSD"), + _n("sp500", "Correlated Asset A", "market_asset", 400, 600, formula="trend_strength * {{coef_trend_corr}}", unit="pts", instrument="SP500"), + _n("qqq", "Correlated Asset B", "market_asset", 620, 600, formula="trend_strength * {{coef_trend_corr2}}", unit="pts", instrument="QQQ"), ], "edges": [ - _e("tech_signal", "momentum_score", "solid", "signal_strength",strength=3, sign="positive", 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"), + _e("tech_signal", "momentum_score", "solid", "signal_strength",strength=3, sign="positive"), + _e("tech_signal", "stop_cascade", "solid", "stop_trigger", strength=3, sign="positive"), + _e("momentum_score", "trend_strength", "solid", "trend_confirm", strength=3, sign="positive"), + _e("stop_cascade", "trend_strength", "solid", "amplification", strength=2, sign="positive"), + _e("trend_strength", "eurusd", "solid", "price_action", strength=3, sign="positive"), + _e("stop_cascade", "eurusd", "solid", "liquidity_grab", strength=2, sign="positive"), + _e("trend_strength", "sp500", "dashed", "spillover_a", strength=2, sign="positive"), + _e("trend_strength", "qqq", "dashed", "spillover_b", strength=2, sign="positive"), ], "coefficients": { - "coef_sig_mom": _c(1.0, "Signal sigma -> momentum score"), - "coef_sig_stop": _c(0.60, "Signal sigma -> stop cascade magnitude"), + "coef_sig_mom": _c(1.0, "Signal -> momentum score"), + "coef_sig_stop": _c(0.60, "Signal -> stop cascade"), "coef_mom_trend": _c(0.70, "Momentum -> trend strength"), - "coef_stop_trend": _c(0.50, "Stop cascade -> trend amplification"), - "coef_trend_inst": _c(50, "Trend strength -> primary instrument (pips)"), + "coef_stop_trend": _c(0.50, "Stop cascade -> amplification"), + "coef_trend_inst": _c(50, "Trend -> primary instrument (pips)"), "coef_stop_inst": _c(30, "Stop cascade -> extra move (pips)"), - "coef_trend_corr": _c(20, "Trend -> correlated asset (pts)"), + "coef_trend_corr": _c(20, "Trend -> correlated A (pts)"), + "coef_trend_corr2":_c(25, "Trend -> correlated B (pts)"), }, - "instruments": ["EURUSD", "SP500"], + "instruments": ["EURUSD", "SP500", "QQQ"], "input_mapping": { "tech_signal": {"source": "user_input", "unit": "sigma", "range": [-5, 5]} }, @@ -532,48 +680,66 @@ BUILT_IN_TEMPLATES = [ # ═══════════════════════════════════════════════════════════════════════════════ # 9. SENTIMENT_POSITIONING_EXTREME - # Covers: VIX spike extremes, COT crowded positions, skew extremes, yield curve signals + # VIX extremes, COT crowded, put/call skew extremes — contrarian setup # ═══════════════════════════════════════════════════════════════════════════════ { "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.", + "instruments": ["SP500", "QQQ", "EURUSD", "XAUUSD", "VXX", "HYG", "BTC"], + "description": "Extreme fear / greed -> crowded trade unwind -> contrarian mean reversion", + "ai_rationale": "+sigma = extreme fear/oversold (contrarian buy signal). VXX collapses as fear recedes. HYG recovers as credit spreads compress. BTC follows the risk appetite extreme. Mean reversion typically takes 3-10 days to materialize once the flow exhausts.", "graph_json": { "nodes": [ - _n("sent_trigger", "Sentiment Extreme", "input", 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"), + _n("sent_trigger", "Sentiment Extreme", "input", 350, 50, unit="sigma"), + _n("pos_imbalance", "Positioning Imbalance","observable", 160, 210, formula="sent_trigger * {{coef_sent_pos}}"), + _n("vol_regime", "Volatility Regime", "observable", 600, 210, formula="-sent_trigger * {{coef_sent_vol}}"), + _n("crowd_risk", "Crowded Trade Risk", "latent", 160, 380, formula="pos_imbalance * {{coef_pos_crowd}}"), + _n("mr_force", "Mean Reversion Force","latent", 400, 380, formula="crowd_risk * {{coef_crowd_mr}}"), + # ── Row 1 outputs ───────────────────────────────────────────────── + _n("sp500", "S&P 500", "market_asset", 60, 580, formula="mr_force * {{coef_mr_sp}} + vol_regime * {{coef_vol_sp}}", unit="pts", instrument="SP500"), + _n("qqq", "QQQ Nasdaq", "market_asset", 260, 580, formula="mr_force * {{coef_mr_qqq}} + vol_regime * {{coef_vol_qqq}}", unit="pts", instrument="QQQ"), + _n("eurusd", "EUR/USD", "market_asset", 460, 580, formula="mr_force * {{coef_mr_eurusd}}", unit="pips", instrument="EURUSD"), + _n("xauusd", "Gold XAU/USD", "market_asset", 660, 580, formula="-mr_force * {{coef_mr_gold}}", unit="$/oz", instrument="XAUUSD"), + # ── Row 2 outputs ───────────────────────────────────────────────── + _n("vxx", "VXX Vol ETN", "market_asset", 60, 760, formula="-mr_force * {{coef_mr_vxx}} - vol_regime * {{coef_vol_vxx}}", unit="pts", instrument="VXX"), + _n("hyg", "HYG HY Bond", "market_asset", 280, 760, formula="mr_force * {{coef_mr_hyg}}", unit="pts", instrument="HYG"), + _n("btc", "BTC-USD", "market_asset", 500, 760, formula="mr_force * {{coef_mr_btc}}", unit="pts", instrument="BTC"), ], "edges": [ - _e("sent_trigger", "pos_imbalance", "solid", "flow_data", strength=3, sign="positive", 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"), + _e("sent_trigger", "pos_imbalance","solid", "flow_data", strength=3, sign="positive"), + _e("sent_trigger", "vol_regime", "dashed", "vol_signal", strength=2, sign="negative"), + _e("pos_imbalance", "crowd_risk", "solid", "crowd_build", strength=3, sign="positive"), + _e("crowd_risk", "mr_force", "solid", "squeeze", strength=3, sign="positive"), + _e("mr_force", "sp500", "solid", "contrarian_sp", strength=3, sign="positive"), + _e("vol_regime", "sp500", "dashed", "vol_compress", strength=2, sign="positive"), + _e("mr_force", "qqq", "solid", "contrarian_qqq", strength=3, sign="positive"), + _e("vol_regime", "qqq", "dashed", "vol_qqq", strength=2, sign="positive"), + _e("mr_force", "eurusd", "solid", "unwind_fx", strength=2, sign="positive"), + _e("mr_force", "xauusd", "dashed", "safe_exit", strength=2, sign="negative"), + _e("mr_force", "vxx", "solid", "vol_crush", strength=3, sign="negative", label="fear recedes -> VXX falls"), + _e("vol_regime", "vxx", "solid", "vol_collapse", strength=3, sign="negative"), + _e("mr_force", "hyg", "solid", "spread_recovery",strength=2, sign="positive"), + _e("mr_force", "btc", "dashed", "btc_recovery", strength=2, sign="positive"), ], "coefficients": { - "coef_sent_pos": _c(1.0, "Sentiment 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)"), + "coef_sent_pos": _c(1.0, "Sentiment -> positioning imbalance"), + "coef_sent_vol": _c(0.50, "Fear extreme -> vol regime"), + "coef_pos_crowd": _c(0.80, "Positioning -> crowded trade risk"), + "coef_crowd_mr": _c(0.70, "Crowded trade -> mean reversion force"), + "coef_mr_sp": _c(60, "MR force -> SP500 pts"), + "coef_vol_sp": _c(30, "Vol compression -> SP500 pts"), + "coef_mr_qqq": _c(80, "MR force -> QQQ pts (higher beta)"), + "coef_vol_qqq": _c(40, "Vol compression -> QQQ pts"), + "coef_mr_eurusd": _c(40, "MR force -> EURUSD pips"), + "coef_mr_gold": _c(15, "Fear recedes -> Gold falls"), + "coef_mr_vxx": _c(3.0, "MR force -> VXX pts fall"), + "coef_vol_vxx": _c(4.0, "Vol collapse -> VXX pts fall"), + "coef_mr_hyg": _c(0.40, "MR force -> HYG pts up"), + "coef_mr_btc": _c(300, "MR force -> BTC pts"), }, - "instruments": ["SP500", "EURUSD", "XAUUSD"], + "instruments": ["SP500", "QQQ", "EURUSD", "XAUUSD", "VXX", "HYG", "BTC"], "input_mapping": { "sent_trigger": {"source": "user_input", "unit": "sigma", "range": [-4, 4]} }, @@ -582,48 +748,56 @@ BUILT_IN_TEMPLATES = [ # ═══════════════════════════════════════════════════════════════════════════════ # 10. COMMODITY_INVENTORY_REPORT - # Covers: EIA crude oil weekly, EIA products, API crude, WASDE grain stocks + # EIA crude weekly, EIA gas storage, API crude, DOE # ═══════════════════════════════════════════════════════════════════════════════ { "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.", + "instruments": ["BRENT", "EURUSD", "XAUUSD", "UNG", "XLE"], + "description": "Weekly inventory surprise -> supply/demand balance -> Brent + XLE + UNG", + "ai_rationale": "EIA crude draw vs consensus -> Brent spot up + backwardation. XLE (energy sector ETF) follows Brent with some lag. EIA gas storage report separately drives UNG. Secondary inflation channel hits EURUSD and gold.", "graph_json": { "nodes": [ - _n("inv_surprise", "Inventory Surprise", "input", 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"), + _n("inv_surprise", "Inventory Surprise", "input", 350, 50, unit="mb", description="+mb = draw (bullish), -mb = build"), + _n("sd_balance", "Supply/Demand Balance","observable", 200, 220, formula="inv_surprise * {{coef_surp_balance}}"), + _n("price_pressure", "Spot Price Pressure", "latent", 200, 390, formula="sd_balance * {{coef_balance_pressure}}"), + _n("fwd_curve_shift", "Forward Curve Shift", "observable", 560, 390, formula="sd_balance * {{coef_balance_fwd}}"), + _n("energy_cost_fwd", "Energy Cost Outlook", "latent", 740, 390, formula="price_pressure * {{coef_pressure_cost}}"), + # ── Row 1 outputs ───────────────────────────────────────────────── + _n("brent", "Brent Crude", "market_asset", 60, 580, formula="price_pressure * {{coef_pressure_brent}} + fwd_curve_shift * {{coef_fwd_brent}}", unit="$/bbl", instrument="BRENT"), + _n("eurusd", "EUR/USD", "market_asset", 310, 580, formula="-energy_cost_fwd * {{coef_energy_eurusd}}", unit="pips", instrument="EURUSD"), + _n("xauusd", "Gold XAU/USD", "market_asset", 560, 580, formula="energy_cost_fwd * {{coef_energy_gold}}", unit="$/oz", instrument="XAUUSD"), + # ── Row 2 outputs ───────────────────────────────────────────────── + _n("xle", "XLE Energy ETF","market_asset", 60, 760, formula="price_pressure * {{coef_pressure_xle}}", unit="pts", instrument="XLE"), + _n("ung", "UNG Nat Gas", "market_asset", 310, 760, formula="sd_balance * {{coef_balance_ung}}", unit="pts", instrument="UNG"), ], "edges": [ - _e("inv_surprise", "sd_balance", "solid", "tightness_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"), + _e("inv_surprise", "sd_balance", "solid", "tightness", strength=3, sign="positive"), + _e("sd_balance", "price_pressure", "solid", "spot_impact", strength=3, sign="positive"), + _e("sd_balance", "fwd_curve_shift","solid", "curve_structure",strength=2, sign="positive"), + _e("price_pressure", "energy_cost_fwd","dashed", "cost_outlook", strength=2, sign="positive"), + _e("price_pressure", "brent", "solid", "spot_move", strength=3, sign="positive"), + _e("fwd_curve_shift","brent", "solid", "backwardation", strength=2, sign="positive"), + _e("energy_cost_fwd","eurusd", "dashed", "trade_deficit", strength=2, sign="negative"), + _e("energy_cost_fwd","xauusd", "dashed", "infl_hedge", strength=2, sign="positive"), + _e("price_pressure", "xle", "solid", "energy_stocks", strength=3, sign="positive", label="oil -> energy sector"), + _e("sd_balance", "ung", "dashed", "gas_correlation",strength=2, sign="positive", label="EIA gas storage parallel"), ], "coefficients": { - "coef_surp_balance": _c(1.0, "1mb surprise -> 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)"), + "coef_surp_balance": _c(1.0, "1mb surprise -> balance signal"), + "coef_balance_pressure":_c(0.8, "Balance -> spot pressure"), + "coef_balance_fwd": _c(0.4, "Balance -> forward curve"), + "coef_pressure_cost": _c(0.3, "Pressure -> energy cost outlook"), + "coef_pressure_brent": _c(0.5, "1mb -> Brent $/bbl"), + "coef_fwd_brent": _c(0.2, "Curve shift -> Brent $/bbl"), + "coef_energy_eurusd": _c(20, "Energy cost -> EURUSD pips fall"), + "coef_energy_gold": _c(5, "Energy cost -> Gold $/oz"), + "coef_pressure_xle": _c(0.4, "Oil pressure -> XLE pts"), + "coef_balance_ung": _c(0.3, "Balance -> UNG pts (gas proxy)"), }, - "instruments": ["BRENT", "EURUSD", "XAUUSD"], + "instruments": ["BRENT", "EURUSD", "XAUUSD", "UNG", "XLE"], "input_mapping": { "inv_surprise": {"source": "surprise", "unit": "mb", "range": [-10, 10]} }, @@ -632,7 +806,7 @@ BUILT_IN_TEMPLATES = [ # ═══════════════════════════════════════════════════════════════════════════════ # 11. INSTITUTIONAL_FLOW - # Covers: COT repositioning, FX intervention, end-of-month rebalancing + # COT repositioning, FX intervention, end-of-month rebalancing # ═══════════════════════════════════════════════════════════════════════════════ { "name": "Institutional Flow & Repositioning", @@ -640,37 +814,34 @@ BUILT_IN_TEMPLATES = [ "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.", + "description": "Large institutional flow -> order flow imbalance -> momentum + mean reversion risk", + "ai_rationale": "COT repositioning or FX intervention moves prices via order flow mechanics (not fundamentals). Move is self-limiting — creates a mean reversion setup once the flow exhausts. Low confidence, short-lived signal.", "graph_json": { "nodes": [ - _n("flow_trigger", "Institutional Flow", "input", 300, 50, unit="$B", 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"), + _n("flow_trigger", "Institutional Flow", "input", 300, 50, unit="$B"), + _n("order_imbalance", "Order Flow Imbalance", "observable", 150, 220, formula="flow_trigger * {{coef_flow_imbalance}}"), + _n("market_impact", "Market Impact", "latent", 150, 390, formula="order_imbalance * {{coef_imbalance_impact}}"), + _n("momentum_sig", "Flow Momentum", "latent", 450, 390, formula="market_impact * {{coef_impact_mom}} + order_imbalance * {{coef_imbalance_mom}}"), + _n("eurusd", "EUR/USD", "market_asset", 100, 590, formula="momentum_sig * {{coef_mom_eurusd}}", unit="pips", instrument="EURUSD"), + _n("sp500", "S&P 500", "market_asset", 350, 590, formula="momentum_sig * {{coef_mom_sp}}", unit="pts", instrument="SP500"), + _n("xauusd", "Gold XAU/USD", "market_asset", 600, 590, formula="momentum_sig * {{coef_mom_gold}}", unit="$/oz", instrument="XAUUSD"), ], "edges": [ - _e("flow_trigger", "order_imbalance","solid", "execution_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"), + _e("flow_trigger", "order_imbalance","solid", "execution", strength=3, sign="positive"), + _e("order_imbalance", "market_impact", "solid", "price_impact",strength=3, sign="positive"), + _e("market_impact", "momentum_sig", "solid", "momentum", strength=2, sign="positive"), + _e("order_imbalance", "momentum_sig", "solid", "order_flow", strength=2, sign="positive"), + _e("momentum_sig", "eurusd", "solid", "fx_flow", strength=3, sign="positive"), + _e("momentum_sig", "sp500", "solid", "eq_flow", strength=2, sign="positive"), + _e("momentum_sig", "xauusd", "dashed", "gold_flow",strength=2, sign="positive"), ], "coefficients": { - "coef_flow_imbalance": _c(0.50, "$1B flow -> order imbalance score"), + "coef_flow_imbalance": _c(0.50, "$1B -> order imbalance"), "coef_imbalance_impact": _c(0.60, "Imbalance -> market impact"), - "coef_impact_mom": _c(0.70, "Market impact -> momentum"), + "coef_impact_mom": _c(0.70, "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_eurusd": _c(30, "Momentum -> EURUSD pips"), + "coef_mom_sp": _c(15, "Momentum -> SP500 pts"), "coef_mom_gold": _c(8, "Momentum -> Gold $/oz"), }, "instruments": ["EURUSD", "SP500", "XAUUSD"], @@ -681,9 +852,7 @@ BUILT_IN_TEMPLATES = [ }, # ═══════════════════════════════════════════════════════════════════════════════ - # 12. UNCLASSIFIED_IMPACT - # Fallback: any event that doesn't fit the 11 regimes above - # Low confidence — direct instrument impact without causal chain + # 12. UNCLASSIFIED_IMPACT — fallback (confidence < 0.4) # ═══════════════════════════════════════════════════════════════════════════════ { "name": "Unclassified Market Impact", @@ -691,27 +860,27 @@ BUILT_IN_TEMPLATES = [ "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.", + "description": "Fallback — event doesn't fit established regimes. Low confidence, no causal chain.", + "ai_rationale": "Direct trigger to output with no intermediate nodes. Precision scores from this template are marked low-confidence and excluded from calibration stats.", "graph_json": { "nodes": [ - _n("unknown_trigger","Market Trigger", "input", 300, 80, unit="score", 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"), + _n("unknown_trigger","Market Trigger", "input", 300, 80, unit="score"), + _n("market_reaction","Market Reaction", "latent", 300, 300, formula="unknown_trigger * {{coef_trig_react}}"), + _n("eurusd", "EUR/USD", "market_asset", 100, 520, formula="market_reaction * {{coef_react_eurusd}}", unit="pips", instrument="EURUSD"), + _n("sp500", "S&P 500", "market_asset", 300, 520, formula="market_reaction * {{coef_react_sp}}", unit="pts", instrument="SP500"), + _n("xauusd", "Gold XAU/USD", "market_asset", 520, 520, formula="market_reaction * {{coef_react_gold}}", unit="$/oz", instrument="XAUUSD"), ], "edges": [ - _e("unknown_trigger","market_reaction","dashed", "direct_impact",strength=1, sign="positive", label="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"), + _e("unknown_trigger","market_reaction","dashed","direct_impact",strength=1, sign="positive", label="low confidence"), + _e("market_reaction","eurusd", "dashed","output_fx", strength=1, sign="positive"), + _e("market_reaction","sp500", "dashed","output_eq", strength=1, sign="positive"), + _e("market_reaction","xauusd", "dashed","output_gold",strength=1, sign="positive"), ], "coefficients": { - "coef_trig_react": _c(0.50, "Trigger 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)"), + "coef_trig_react": _c(0.50, "Trigger -> market reaction"), + "coef_react_eurusd": _c(20, "Reaction -> EURUSD pips"), + "coef_react_sp": _c(15, "Reaction -> SP500 pts"), + "coef_react_gold": _c(5, "Reaction -> Gold $/oz"), }, "instruments": ["EURUSD", "SP500", "XAUUSD"], "input_mapping": { @@ -763,25 +932,22 @@ def init_tables(conn): 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( + """Insert/update built-in templates. One-shot migration: removes old non-regime templates.""" + existing = conn.execute( "SELECT name FROM causal_graph_templates WHERE created_by='system'" ).fetchall() - 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,)) + new_names = {t["name"] for t in BUILT_IN_TEMPLATES} + for row in existing: + if row["name"] not in new_names: + conn.execute("DELETE FROM causal_graph_templates WHERE name=? AND created_by='system'", (row["name"],)) for t in BUILT_IN_TEMPLATES: ver = t.get("heuristic_ver", 1) - existing = conn.execute( + existing_row = conn.execute( "SELECT id, heuristic_ver FROM causal_graph_templates WHERE name = ?", (t["name"],) ).fetchone() - if existing: - if (existing["heuristic_ver"] or 1) < ver: + if existing_row: + if (existing_row["heuristic_ver"] or 1) < ver: conn.execute(""" UPDATE causal_graph_templates SET graph_json=?, description=?, ai_rationale=?, instruments=?, @@ -856,16 +1022,14 @@ def update_coefficients(conn, template_id: int, coef_updates: dict): def update_calibration(conn, template_id: int, analysis_result: dict): - """Met à jour les stats de calibration après une nouvelle analyse.""" + """Update calibration stats after a new analysis.""" tmpl = get_template(conn, template_id) if not tmpl: return calib = tmpl.get("calibration_json") or {} n = calib.get("n_events", 0) + 1 - # 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 @@ -891,10 +1055,9 @@ def update_calibration(conn, template_id: int, analysis_result: dict): conn.commit() -# ── Évaluateur de graphe ─────────────────────────────────────────────────────── +# ── Graph evaluator ──────────────────────────────────────────────────────────── 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, @@ -908,16 +1071,16 @@ def _safe_eval(expr: str, context: dict) -> float: return float(node.value) if isinstance(node, ast.Name): if node.id not in context: - raise ValueError(f"Variable inconnue : {node.id}") + raise ValueError(f"Unknown variable: {node.id}") return float(context[node.id]) if isinstance(node, ast.BinOp): op = _ops.get(type(node.op)) if op is None: - raise ValueError(f"Opérateur non supporté : {type(node.op)}") + raise ValueError(f"Unsupported op: {type(node.op)}") return op(_eval(node.left), _eval(node.right)) if isinstance(node, ast.UnaryOp) and isinstance(node.op, ast.USub): return -_eval(node.operand) - raise ValueError(f"Nœud AST non supporté : {type(node)}") + raise ValueError(f"Unsupported AST node: {type(node)}") tree = ast.parse(expr.strip(), mode="eval") return _eval(tree.body) @@ -925,8 +1088,8 @@ def _safe_eval(expr: str, context: dict) -> float: 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). + Evaluate a causal graph from inputs. Returns {node_id: value}. + {{coef_xxx}} placeholders are substituted from graph_json["coefficients"]. """ coefs = {k: v["value"] for k, v in graph_json.get("coefficients", {}).items()} if coef_overrides: @@ -939,7 +1102,6 @@ def evaluate_graph(graph_json: dict, inputs: dict, coef_overrides: Optional[dict 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: