From 5126360ce9ab25ae9d1a7c13fe72f5e6dc0ebdda Mon Sep 17 00:00:00 2001 From: OpenSquared Date: Thu, 2 Jul 2026 14:42:50 +0200 Subject: [PATCH] feat: causal lab --- backend/services/causal_graphs.py | 41 +++++++++++++++++++++++++++++-- 1 file changed, 39 insertions(+), 2 deletions(-) diff --git a/backend/services/causal_graphs.py b/backend/services/causal_graphs.py index 290137c..2b39ff4 100644 --- a/backend/services/causal_graphs.py +++ b/backend/services/causal_graphs.py @@ -67,6 +67,7 @@ BUILT_IN_TEMPLATES = [ "slug": "MACRO_DATA_SURPRISE", "category": "monetary_shock", "heuristic_ver": 2, + "calibration_json": {"lag_days": 0, "half_life_days": 4, "absorption_days": 12, "decay_type": "exp"}, "instruments": ["EURUSD", "SP500", "XAUUSD", "QQQ", "TLT", "USDJPY", "EEM", "GDX", "SLV"], "description": "Scheduled economic release vs consensus -> OIS repricing -> full asset repricing", "ai_rationale": "Hawkish surprise: OIS up -> 2Y up -> USD up -> EURUSD/USDJPY/EEM react; 10Y up -> TLT down, GDX/XAUUSD down. Growth channel -> SP500/QQQ up. Dovish miss: all reversed.", @@ -162,6 +163,7 @@ BUILT_IN_TEMPLATES = [ "slug": "CENTRAL_BANK_DECISION", "category": "central_bank", "heuristic_ver": 2, + "calibration_json": {"lag_days": 0, "half_life_days": 5, "absorption_days": 14, "decay_type": "exp"}, "instruments": ["EURUSD", "SP500", "XAUUSD", "QQQ", "TLT", "USDJPY", "GBPUSD", "XLF", "EEM"], "description": "CB rate decision + forward guidance -> full curve repricing -> carry FX, bonds, equities", "ai_rationale": "Rate hike -> 2Y/10Y up -> carry differential -> EURUSD/USDJPY/GBPUSD down; TLT down; QQQ hardest hit (duration); XLF benefits from steeper curve. Dovish: all reversed. EEM suffers from USD rate rises (capital outflows).", @@ -254,6 +256,7 @@ BUILT_IN_TEMPLATES = [ "slug": "GROWTH_CORPORATE_SIGNAL", "category": "growth_shock", "heuristic_ver": 2, + "calibration_json": {"lag_days": 0, "half_life_days": 4, "absorption_days": 10, "decay_type": "exp"}, "instruments": ["SP500", "QQQ", "IWM", "EURUSD", "XAUUSD", "HYG"], "description": "Corporate/macro growth signal -> earnings revision + risk appetite -> equities, credit, FX, Gold", "ai_rationale": "Earnings beats revise EPS upward and compress credit spreads (HYG up). QQQ > SP500 > IWM in tech-driven beats; IWM > SP500 in domestic growth signals. EURUSD follows risk appetite. Gold falls as safe haven demand recedes.", @@ -325,6 +328,7 @@ BUILT_IN_TEMPLATES = [ "slug": "GEOPOLITICAL_RISK_OFF", "category": "geopolitical", "heuristic_ver": 2, + "calibration_json": {"lag_days": 0, "half_life_days": 7, "absorption_days": 21, "decay_type": "linear"}, "instruments": ["EURUSD", "XAUUSD", "SP500", "BRENT", "USDJPY", "VXX", "TLT", "HYG", "UNG", "SLV"], "description": "Geopolitical shock -> risk premium + safe haven demand -> multi-asset risk-off repricing", "ai_rationale": "Gold/TLT/JPY safe havens rise; EUR/SP500/HYG fall. VXX spikes. USDJPY falls (JPY strengthens as safe haven). BRENT and UNG spike if energy-producing region. EU equities most exposed to European conflicts.", @@ -408,6 +412,7 @@ BUILT_IN_TEMPLATES = [ "slug": "COMMODITY_SUPPLY_SHOCK", "category": "commodity", "heuristic_ver": 2, + "calibration_json": {"lag_days": 1, "half_life_days": 5, "absorption_days": 14, "decay_type": "exp"}, "instruments": ["BRENT", "XAUUSD", "EURUSD", "SP500", "UNG", "XLE", "SLV"], "description": "Supply disruption/glut -> Brent + energy inflation -> EU trade deficit -> multi-asset", "ai_rationale": "OPEC cuts hit BRENT and XLE (energy sector stocks) first. Energy inflation pressures EU trade balance (EURUSD down) and inflates Gold/SLV. UNG follows as cross-commodity supply chain linkage. S&P suffers from margin pressure.", @@ -472,6 +477,7 @@ BUILT_IN_TEMPLATES = [ "slug": "TRADE_POLICY_SHOCK", "category": "trade_policy", "heuristic_ver": 2, + "calibration_json": {"lag_days": 1, "half_life_days": 10, "absorption_days": 21, "decay_type": "linear"}, "instruments": ["EURUSD", "SP500", "XAUUSD", "EEM", "QQQ", "GBPUSD", "IWM"], "description": "Tariff announcement -> stagflationary shock -> EUR/GBP down SP500/QQQ down EEM down Gold up", "ai_rationale": "Tariffs create stagflation: import cost rise (inflation) + trade volume drop (growth). EEM is hit hardest via global trade contraction and USD strength. QQQ suffers from tech supply chain disruption (AAPL/NVDA exposed). GBPUSD exposed as UK trade-dependent. IWM domestic plays can benefit initially but input cost inflation hurts margins.", @@ -551,6 +557,7 @@ BUILT_IN_TEMPLATES = [ "slug": "CREDIT_SYSTEMIC_EVENT", "category": "credit_stress", "heuristic_ver": 2, + "calibration_json": {"lag_days": 0, "half_life_days": 7, "absorption_days": 21, "decay_type": "exp"}, "instruments": ["EURUSD", "SP500", "XAUUSD", "HYG", "TLT", "VXX", "XLF", "USDJPY", "BTC"], "description": "Credit event -> spread widening + deleveraging -> systemic risk-off + USD liquidity flight", "ai_rationale": "HYG is the primary instrument (spreads widen = HYG down). TLT initially up (flight to quality) then volatile (sold for cash). VXX spikes hard. XLF (financials) crushed. USDJPY falls (JPY safe haven). BTC complex: initially up as alternative store of value, then down on margin calls.", @@ -638,6 +645,7 @@ BUILT_IN_TEMPLATES = [ "slug": "TECHNICAL_MOMENTUM_BREAKOUT", "category": "technical", "heuristic_ver": 2, + "calibration_json": {"lag_days": 0, "half_life_days": 3, "absorption_days": 7, "decay_type": "exp"}, "instruments": ["EURUSD", "SP500", "QQQ"], "description": "Technical breakout/breakdown -> stop cascade + momentum -> instrument price action", "ai_rationale": "Technical events operate via microstructure: breakout triggers stops, amplifying the move. No fundamental anchor. The correlated asset move is weaker. Applies to any instrument — the primary and correlated move fields should be overridden per event.", @@ -687,6 +695,7 @@ BUILT_IN_TEMPLATES = [ "slug": "SENTIMENT_POSITIONING_EXTREME", "category": "sentiment", "heuristic_ver": 2, + "calibration_json": {"lag_days": 1, "half_life_days": 5, "absorption_days": 10, "decay_type": "linear"}, "instruments": ["SP500", "QQQ", "EURUSD", "XAUUSD", "VXX", "HYG", "BTC"], "description": "Extreme fear / greed -> crowded trade unwind -> contrarian mean reversion", "ai_rationale": "+sigma = extreme fear/oversold (contrarian buy signal). VXX collapses as fear recedes. HYG recovers as credit spreads compress. BTC follows the risk appetite extreme. Mean reversion typically takes 3-10 days to materialize once the flow exhausts.", @@ -755,6 +764,7 @@ BUILT_IN_TEMPLATES = [ "slug": "COMMODITY_INVENTORY_REPORT", "category": "commodity", "heuristic_ver": 2, + "calibration_json": {"lag_days": 0, "half_life_days": 2, "absorption_days": 5, "decay_type": "exp"}, "instruments": ["BRENT", "EURUSD", "XAUUSD", "UNG", "XLE"], "description": "Weekly inventory surprise -> supply/demand balance -> Brent + XLE + UNG", "ai_rationale": "EIA crude draw vs consensus -> Brent spot up + backwardation. XLE (energy sector ETF) follows Brent with some lag. EIA gas storage report separately drives UNG. Secondary inflation channel hits EURUSD and gold.", @@ -813,6 +823,7 @@ BUILT_IN_TEMPLATES = [ "slug": "INSTITUTIONAL_FLOW", "category": "positioning", "heuristic_ver": 2, + "calibration_json": {"lag_days": 1, "half_life_days": 7, "absorption_days": 14, "decay_type": "linear"}, "instruments": ["EURUSD", "SP500", "XAUUSD"], "description": "Large institutional flow -> order flow imbalance -> momentum + mean reversion risk", "ai_rationale": "COT repositioning or FX intervention moves prices via order flow mechanics (not fundamentals). Move is self-limiting — creates a mean reversion setup once the flow exhausts. Low confidence, short-lived signal.", @@ -859,6 +870,7 @@ BUILT_IN_TEMPLATES = [ "slug": "UNCLASSIFIED_IMPACT", "category": "unclassified", "heuristic_ver": 2, + "calibration_json": {"lag_days": 0, "half_life_days": 5, "absorption_days": 12, "decay_type": "exp"}, "instruments": ["EURUSD", "SP500", "XAUUSD"], "description": "Fallback — event doesn't fit established regimes. Low confidence, no causal chain.", "ai_rationale": "Direct trigger to output with no intermediate nodes. Precision scores from this template are marked low-confidence and excluded from calibration stats.", @@ -964,16 +976,41 @@ def seed_templates(conn): conn.execute(""" INSERT INTO causal_graph_templates (name, category, sub_type, instruments, description, graph_json, - ai_rationale, heuristic_ver, created_by) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'system') + calibration_json, ai_rationale, heuristic_ver, created_by) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 'system') """, ( t["name"], t["category"], t.get("sub_type", ""), json.dumps(t.get("instruments", [])), t.get("description", ""), json.dumps(t["graph_json"]), + json.dumps(t.get("calibration_json", {})), t.get("ai_rationale", ""), ver, )) + + # Migration: fill missing temporal params in calibration_json for all built-in templates. + # Only sets keys that are absent — never overwrites existing calibration stats. + for t in BUILT_IN_TEMPLATES: + defaults = t.get("calibration_json", {}) + if not defaults: + continue + row = conn.execute( + "SELECT id, calibration_json FROM causal_graph_templates WHERE name=?", (t["name"],) + ).fetchone() + if not row: + continue + calib = json.loads(row["calibration_json"] or "{}") + dirty = False + for key in ("lag_days", "half_life_days", "absorption_days", "decay_type"): + if calib.get(key) is None and key in defaults: + calib[key] = defaults[key] + dirty = True + if dirty: + conn.execute( + "UPDATE causal_graph_templates SET calibration_json=?, updated_at=datetime('now') WHERE id=?", + (json.dumps(calib), row["id"]) + ) + conn.commit()