Files
OpenFin/backend/services/geo_analyzer.py
2026-07-14 16:23:18 +02:00

882 lines
45 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
Geopolitical pattern engine.
Scores current events against historical templates and generates trade signals.
"""
from datetime import datetime, timedelta
from typing import List, Dict, Any, Optional
import json
# ── Pattern Taxonomy Tree ─────────────────────────────────────────────────────
PATTERN_TAXONOMY = {
"id": "root",
"label": "Pattern Library",
"children": [
{
"id": "geopolitical",
"label": "Geopolitical",
"children": [
{
"id": "armed_conflict",
"label": "Armed Conflict",
"children": [
{"id": "armed_conflict.middle_east", "label": "Middle East"},
{"id": "armed_conflict.europe", "label": "Europe / Ukraine"},
{"id": "armed_conflict.asia_pacific", "label": "Asia-Pacific"},
],
},
{
"id": "sanctions",
"label": "Sanctions & Trade War",
"children": [
{"id": "sanctions.us_china", "label": "USChina"},
{"id": "sanctions.russia", "label": "Russia"},
{"id": "sanctions.iran", "label": "Iran"},
],
},
{
"id": "territorial",
"label": "Territorial Disputes",
"children": [
{"id": "territorial.taiwan", "label": "Taiwan Strait"},
{"id": "territorial.south_china_sea", "label": "South China Sea"},
],
},
{
"id": "elections",
"label": "Elections & Political Risk",
"children": [
{"id": "elections.us", "label": "US Elections"},
{"id": "elections.europe", "label": "Europe"},
{"id": "elections.emerging", "label": "Emerging Markets"},
],
},
],
},
{
"id": "monetary_policy",
"label": "Monetary Policy",
"children": [
{"id": "monetary_policy.fed", "label": "Federal Reserve"},
{"id": "monetary_policy.ecb", "label": "European Central Bank"},
{"id": "monetary_policy.boj", "label": "Bank of Japan"},
{"id": "monetary_policy.pivot", "label": "Policy Pivot / Surprise"},
{"id": "monetary_policy.divergence", "label": "Central Bank Divergence"},
],
},
{
"id": "economic",
"label": "Economic Shocks",
"children": [
{"id": "economic.banking_crisis", "label": "Banking Crisis"},
{"id": "economic.recession", "label": "Recession Fears"},
{"id": "economic.inflation", "label": "Inflation Surge"},
{"id": "economic.china_slowdown", "label": "China Slowdown"},
{"id": "economic.debt_ceiling", "label": "US Debt Ceiling"},
],
},
{
"id": "commodity",
"label": "Commodity Shocks",
"children": [
{
"id": "commodity.energy",
"label": "Energy",
"children": [
{"id": "commodity.energy.crude", "label": "Crude Oil (OPEC / supply)"},
{"id": "commodity.energy.gas", "label": "Natural Gas"},
],
},
{
"id": "commodity.agriculture",
"label": "Agriculture",
"children": [
{"id": "commodity.agriculture.wheat", "label": "Wheat / Grains"},
{"id": "commodity.agriculture.softs", "label": "Softs (Coffee, Sugar)"},
],
},
{
"id": "commodity.metals",
"label": "Metals",
"children": [
{"id": "commodity.metals.gold", "label": "Gold (Safe Haven)"},
{"id": "commodity.metals.copper", "label": "Copper (China / Growth)"},
],
},
],
},
{
"id": "risk_off",
"label": "Risk-Off Events",
"children": [
{"id": "risk_off.pandemic", "label": "Pandemic / Health Crisis"},
{"id": "risk_off.financial_contagion", "label": "Financial Contagion"},
{"id": "risk_off.natural_disaster", "label": "Natural Disaster"},
{"id": "risk_off.nuclear", "label": "Nuclear Risk"},
],
},
{
"id": "market_structure",
"label": "Market Structure",
"children": [
{"id": "market_structure.volatility", "label": "Volatility Regime (VIX)"},
{"id": "market_structure.positioning", "label": "Positioning Extremes"},
{"id": "market_structure.sentiment", "label": "Sentiment Extremes"},
{"id": "market_structure.liquidity", "label": "Liquidity Crisis"},
],
},
],
}
# ── Historical geopolitical pattern library ───────────────────────────────────
GEO_PATTERNS = [
{
"id": "P001",
"name": "Middle East Military Escalation → Oil Spike",
"description": "Armed conflict or threat in Gulf region triggers Brent/WTI crude spike +10-20% within 2-4 weeks",
"triggers": ["military", "energy", "sanctions"],
"keywords": ["Iran", "Israel", "Saudi", "Gulf", "Strait of Hormuz", "OPEC"],
"taxonomy_path": ["geopolitical", "armed_conflict", "armed_conflict.middle_east"],
"historical_instances": [
{"date": "2019-09-14", "event": "Attack on Saudi Aramco facilities", "brent_move": +14.6, "days": 2},
{"date": "2020-01-03", "event": "Soleimani assassination", "brent_move": +4.4, "days": 1},
{"date": "2022-02-24", "event": "Russia invades Ukraine", "brent_move": +28.0, "days": 10},
],
"suggested_trades": [
{"strategy": "Bull Call Spread", "underlying": "USO", "rationale": "Oil ETF call spread, limited risk"},
{"strategy": "Long Call", "underlying": "CL=F", "rationale": "WTI crude direct exposure"},
],
"asset_class": "energy",
"expected_move_pct": 12.0,
"probability": 0.65,
"horizon_days": 30,
},
{
"id": "P002",
"name": "US Tariff Announcement → Agriculture Selloff",
"description": "Trump/US tariff threats on China cause immediate selloff in soy, corn, wheat (retaliatory risk)",
"triggers": ["trade_war", "political_speech"],
"keywords": ["tariff", "China", "trade", "soybean", "agriculture", "import duty"],
"taxonomy_path": ["geopolitical", "sanctions", "sanctions.us_china"],
"historical_instances": [
{"date": "2018-07-06", "event": "US-China trade war tariffs", "zs_move": -10.2, "days": 30},
{"date": "2019-05-10", "event": "Trump tariff escalation tweet", "zs_move": -5.8, "days": 5},
{"date": "2025-02-01", "event": "Trump 25% tariff on Canada/Mexico", "zw_move": -3.4, "days": 3},
],
"suggested_trades": [
{"strategy": "Bear Put Spread", "underlying": "SOYB", "rationale": "Downside hedge on soy ETF"},
{"strategy": "Long Put", "underlying": "ZS=F", "rationale": "Soybean futures put"},
],
"asset_class": "agriculture",
"expected_move_pct": -8.0,
"probability": 0.70,
"horizon_days": 21,
},
{
"id": "P003",
"name": "Geopolitical Risk Flight → Gold Rally",
"description": "Major geopolitical uncertainty drives safe-haven demand for gold +5-15%",
"triggers": ["military", "health_crisis", "financial_crisis", "elections"],
"keywords": ["nuclear", "war", "crisis", "uncertainty", "safe haven", "debt ceiling"],
"taxonomy_path": ["commodity", "commodity.metals", "commodity.metals.gold"],
"historical_instances": [
{"date": "2022-02-24", "event": "Ukraine invasion", "gc_move": +6.8, "days": 14},
{"date": "2023-10-07", "event": "Hamas attack on Israel", "gc_move": +9.2, "days": 30},
{"date": "2020-03-01", "event": "COVID-19 fear peak", "gc_move": +12.1, "days": 45},
],
"suggested_trades": [
{"strategy": "Long Call", "underlying": "GLD", "rationale": "Gold ETF call for safe-haven rally"},
{"strategy": "Bull Call Spread", "underlying": "GC=F", "rationale": "Gold futures spread, capped risk"},
],
"asset_class": "metals",
"expected_move_pct": 7.5,
"probability": 0.72,
"horizon_days": 30,
},
{
"id": "P004",
"name": "Fed Hawkish Pivot → Dollar Surge / EM Currency Crash",
"description": "Fed signals higher-for-longer rates → USD Index rallies, EUR/USD drops",
"triggers": ["political_speech"],
"keywords": ["Fed", "interest rate", "hike", "hawkish", "inflation", "FOMC", "Powell"],
"taxonomy_path": ["monetary_policy", "monetary_policy.fed"],
"historical_instances": [
{"date": "2022-06-15", "event": "Fed 75bps hike", "dxy_move": +3.2, "days": 5},
{"date": "2023-03-22", "event": "Fed signals further hikes", "eurusd_move": -1.8, "days": 7},
],
"suggested_trades": [
{"strategy": "Bear Put Spread", "underlying": "FXE", "rationale": "EUR/USD put spread"},
{"strategy": "Long Call", "underlying": "UUP", "rationale": "Dollar index ETF call"},
],
"asset_class": "forex",
"expected_move_pct": 3.0,
"probability": 0.68,
"horizon_days": 14,
},
{
"id": "P005",
"name": "China Economic Slowdown → Copper/Metals Selloff",
"description": "Weak Chinese PMI or stimulus disappointment drives copper lower (China = 50%+ of global demand)",
"triggers": ["resource_scarcity", "trade_war"],
"keywords": ["China", "PMI", "slowdown", "recession", "property", "Evergrande", "copper demand"],
"taxonomy_path": ["economic", "economic.china_slowdown"],
"historical_instances": [
{"date": "2015-08-24", "event": "China Black Monday", "hg_move": -8.4, "days": 5},
{"date": "2022-11-01", "event": "China PMI contraction", "hg_move": -5.2, "days": 10},
],
"suggested_trades": [
{"strategy": "Long Put", "underlying": "COPX", "rationale": "Copper miners ETF put"},
{"strategy": "Bear Put Spread", "underlying": "HG=F", "rationale": "Copper futures spread"},
],
"asset_class": "metals",
"expected_move_pct": -6.5,
"probability": 0.60,
"horizon_days": 21,
},
{
"id": "P006",
"name": "Ukraine/Russia War Escalation → Wheat Spike + Defense Rally",
"description": "New escalation in Russia-Ukraine conflict → wheat/fertilizer spike, defense stocks rally",
"triggers": ["military", "resource_scarcity"],
"keywords": ["Russia", "Ukraine", "Zelensky", "Kyiv", "grain corridor", "Black Sea", "NATO"],
"taxonomy_path": ["geopolitical", "armed_conflict", "armed_conflict.europe"],
"historical_instances": [
{"date": "2022-02-24", "event": "Full-scale invasion", "zw_move": +50.0, "days": 45},
{"date": "2022-07-22", "event": "Grain deal collapse threat", "zw_move": +6.3, "days": 3},
{"date": "2023-07-17", "event": "Russia exits grain deal", "zw_move": +8.5, "days": 2},
],
"suggested_trades": [
{"strategy": "Long Call", "underlying": "WEAT", "rationale": "Wheat ETF call on supply shock"},
{"strategy": "Bull Call Spread", "underlying": "LMT", "rationale": "Lockheed defense stock spread"},
],
"asset_class": "agriculture",
"expected_move_pct": 15.0,
"probability": 0.58,
"horizon_days": 45,
},
{
"id": "P007",
"name": "Natural Gas Supply Disruption → NG Price Spike",
"description": "Pipeline disruption, LNG strike, or extreme weather drives natural gas +20-40%",
"triggers": ["energy", "natural_disaster", "military"],
"keywords": ["pipeline", "LNG", "natural gas", "Nord Stream", "gas supply", "storage"],
"taxonomy_path": ["commodity", "commodity.energy", "commodity.energy.gas"],
"historical_instances": [
{"date": "2022-09-26", "event": "Nord Stream pipeline explosion", "ng_move": +18.0, "days": 5},
{"date": "2021-02-10", "event": "Texas winter storm Uri", "ng_move": +40.0, "days": 3},
],
"suggested_trades": [
{"strategy": "Long Call", "underlying": "UNG", "rationale": "Natural gas ETF call"},
{"strategy": "Bull Call Spread", "underlying": "NG=F", "rationale": "NG futures spread, capped risk"},
],
"asset_class": "energy",
"expected_move_pct": 25.0,
"probability": 0.55,
"horizon_days": 14,
},
{
"id": "P008",
"name": "Pandemic / Health Crisis → VIX Spike + Market Selloff",
"description": "New pandemic scare or major health crisis → VIX spike, equity selloff, gold bid",
"triggers": ["health_crisis"],
"keywords": ["pandemic", "virus", "outbreak", "WHO", "lockdown", "COVID", "mpox", "H5N1"],
"taxonomy_path": ["risk_off", "risk_off.pandemic"],
"historical_instances": [
{"date": "2020-02-24", "event": "COVID-19 global spread fear", "spx_move": -34.0, "days": 30},
{"date": "2022-11-25", "event": "China COVID lockdowns", "spx_move": -3.5, "days": 3},
],
"suggested_trades": [
{"strategy": "Long Put", "underlying": "SPY", "rationale": "S&P 500 put for equity protection"},
{"strategy": "Long Call", "underlying": "^VIX", "rationale": "VIX call for volatility spike"},
{"strategy": "Long Call", "underlying": "GLD", "rationale": "Gold safe-haven call"},
],
"asset_class": "indices",
"expected_move_pct": -12.0,
"probability": 0.45,
"horizon_days": 30,
},
# ── New patterns P009-P023 ──────────────────────────────────────────────────
{
"id": "P009",
"name": "BoJ YCC Break / Ultra-Loose Exit → JPY Surge",
"description": "Bank of Japan abandons yield curve control or signals rate normalisation → JPY rally, JGB yields spike, global carry unwind",
"triggers": ["political_speech"],
"keywords": ["Bank of Japan", "BoJ", "YCC", "yield curve control", "yen", "JPY", "Ueda", "Kuroda", "JGB"],
"taxonomy_path": ["monetary_policy", "monetary_policy.boj"],
"historical_instances": [
{"date": "2022-12-20", "event": "BoJ widens YCC band to ±0.5%", "usdjpy_move": -3.7, "days": 1},
{"date": "2023-07-28", "event": "BoJ further loosens YCC to 1%", "usdjpy_move": -2.2, "days": 2},
{"date": "2024-03-19", "event": "BoJ raises rates for first time since 2007", "usdjpy_move": -0.8, "days": 1},
],
"suggested_trades": [
{"strategy": "Long Put", "underlying": "FXY", "rationale": "JPY ETF put — gains as USD/JPY falls (yen strengthens)"},
{"strategy": "Bear Put Spread", "underlying": "YCS", "rationale": "Ultra-short yen ETF put spread"},
{"strategy": "Long Call", "underlying": "FXY", "rationale": "Direct long JPY via ETF call"},
],
"asset_class": "forex",
"expected_move_pct": -4.0,
"probability": 0.60,
"horizon_days": 7,
},
{
"id": "P010",
"name": "Regional Banking Crisis → Financial Contagion",
"description": "Bank run or failure of regional lenders triggers systemic fear, credit spreads widen, equities sell off",
"triggers": ["financial_crisis"],
"keywords": ["bank run", "SVB", "Silicon Valley Bank", "FDIC", "deposit", "contagion", "credit suisse", "regional bank"],
"taxonomy_path": ["economic", "economic.banking_crisis"],
"historical_instances": [
{"date": "2023-03-10", "event": "SVB collapse — largest US bank failure since 2008", "kre_move": -27.0, "days": 5},
{"date": "2023-03-19", "event": "Credit Suisse emergency merger with UBS", "cs_move": -60.0, "days": 2},
{"date": "2023-05-01", "event": "First Republic Bank seized by FDIC", "kre_move": -8.5, "days": 3},
],
"suggested_trades": [
{"strategy": "Long Put", "underlying": "KRE", "rationale": "Regional banks ETF put — direct contagion play"},
{"strategy": "Long Call", "underlying": "GLD", "rationale": "Gold flight-to-safety bid"},
{"strategy": "Bear Put Spread", "underlying": "XLF", "rationale": "Broad financial sector downside hedge"},
],
"asset_class": "indices",
"expected_move_pct": -15.0,
"probability": 0.55,
"horizon_days": 14,
},
{
"id": "P011",
"name": "Taiwan Strait Military Tension → Semis Selloff",
"description": "PLA military exercises or blockade threat → global semiconductor supply fear, Taiwan semis/TSMC selloff",
"triggers": ["military"],
"keywords": ["Taiwan", "TSMC", "PLA", "strait", "China invasion", "semiconductor", "chip supply", "Pelosi"],
"taxonomy_path": ["geopolitical", "territorial", "territorial.taiwan"],
"historical_instances": [
{"date": "2022-08-02", "event": "Pelosi visits Taiwan — PLA launches live-fire drills", "tsm_move": -6.5, "days": 3},
{"date": "2023-04-08", "event": "PLA encirclement drills after Tsai-McCarthy meeting", "tsm_move": -3.2, "days": 2},
],
"suggested_trades": [
{"strategy": "Long Put", "underlying": "TSM", "rationale": "TSMC put — direct Taiwan semi exposure"},
{"strategy": "Bear Put Spread", "underlying": "SOXX", "rationale": "Semis ETF put spread on supply disruption risk"},
{"strategy": "Long Call", "underlying": "GLD", "rationale": "Safe-haven gold bid on regional military tension"},
],
"asset_class": "equities",
"expected_move_pct": -8.0,
"probability": 0.50,
"horizon_days": 14,
},
{
"id": "P012",
"name": "OPEC+ Surprise Production Cut → Crude Oil Spike",
"description": "Unexpected OPEC+ output cut announcement drives Brent/WTI +5-15% in days",
"triggers": ["energy", "political_speech"],
"keywords": ["OPEC", "Saudi Arabia", "production cut", "barrel", "oil output", "supply cut"],
"taxonomy_path": ["commodity", "commodity.energy", "commodity.energy.crude"],
"historical_instances": [
{"date": "2023-04-02", "event": "OPEC+ surprise 1.16Mb/d cut", "brent_move": +6.3, "days": 1},
{"date": "2022-10-05", "event": "OPEC+ cuts 2Mb/d despite US pressure", "brent_move": +11.0, "days": 5},
{"date": "2020-04-12", "event": "Historic OPEC+ 9.7Mb/d cut deal", "cl_move": +20.0, "days": 2},
],
"suggested_trades": [
{"strategy": "Bull Call Spread", "underlying": "USO", "rationale": "Oil ETF bull spread, defined risk"},
{"strategy": "Long Call", "underlying": "CL=F", "rationale": "WTI crude futures call for directional upside"},
{"strategy": "Long Call", "underlying": "XLE", "rationale": "Energy sector ETF call for broader sector play"},
],
"asset_class": "energy",
"expected_move_pct": 8.0,
"probability": 0.65,
"horizon_days": 14,
},
{
"id": "P013",
"name": "Fed Dovish Pivot → Risk-On Rally",
"description": "Fed signals rate cuts ahead of schedule → equities rally, USD weakens, risk assets bid",
"triggers": ["political_speech"],
"keywords": ["Fed cut", "dovish", "pivot", "rate cut", "easing", "QE", "FOMC pause", "Jackson Hole"],
"taxonomy_path": ["monetary_policy", "monetary_policy.pivot"],
"historical_instances": [
{"date": "2023-11-01", "event": "Fed holds rates, signals peak", "spy_move": +5.9, "days": 5},
{"date": "2023-12-13", "event": "Fed dots signal 3 cuts in 2024", "spy_move": +3.5, "days": 2},
{"date": "2019-07-31", "event": "First Fed cut since 2008", "spy_move": +2.1, "days": 1},
],
"suggested_trades": [
{"strategy": "Bull Call Spread", "underlying": "SPY", "rationale": "S&P 500 bull spread on risk-on rally"},
{"strategy": "Long Call", "underlying": "QQQ", "rationale": "Nasdaq call — rate-sensitive growth names"},
{"strategy": "Bear Put Spread", "underlying": "UUP", "rationale": "USD index downside on dovish turn"},
],
"asset_class": "indices",
"expected_move_pct": 5.0,
"probability": 0.70,
"horizon_days": 14,
},
{
"id": "P014",
"name": "US Debt Ceiling Standoff → T-Bill Stress",
"description": "Congress deadlock on debt ceiling → short-term T-bill yields spike, credit risk perception rises, risk-off",
"triggers": ["political_speech", "financial_crisis"],
"keywords": ["debt ceiling", "default", "Treasury", "X-date", "fiscal cliff", "Congress", "spending"],
"taxonomy_path": ["economic", "economic.debt_ceiling"],
"historical_instances": [
{"date": "2023-05-01", "event": "US 1-month T-bill yield spikes to 5.9%", "vix_move": +22.0, "days": 14},
{"date": "2011-08-02", "event": "S&P downgrades US credit", "spy_move": -16.0, "days": 10},
{"date": "2013-10-01", "event": "US government shutdown + debt ceiling standoff", "vix_move": +40.0, "days": 16},
],
"suggested_trades": [
{"strategy": "Long Call", "underlying": "GLD", "rationale": "Safe-haven gold on fiscal credibility risk"},
{"strategy": "Bear Put Spread", "underlying": "SPY", "rationale": "Equity hedge during debt ceiling uncertainty"},
{"strategy": "Long Put", "underlying": "TLT", "rationale": "Long-duration Treasury put on fiscal risk premium"},
],
"asset_class": "indices",
"expected_move_pct": -5.0,
"probability": 0.55,
"horizon_days": 21,
},
{
"id": "P015",
"name": "Iran Nuclear Escalation → Oil + Gold Spike",
"description": "Iran nuclear programme breakthrough, US/Israel strike threat, or IAEA crisis → oil and gold both bid",
"triggers": ["military", "sanctions"],
"keywords": ["Iran", "nuclear", "enrichment", "IAEA", "sanctions", "Strait of Hormuz", "Israel strike"],
"taxonomy_path": ["geopolitical", "sanctions", "sanctions.iran"],
"historical_instances": [
{"date": "2020-01-08", "event": "Iran missiles hit US bases in Iraq (after Soleimani)", "brent_move": +3.5, "days": 1},
{"date": "2024-04-01", "event": "Israel strikes Iran consulate in Damascus", "brent_move": +4.0, "days": 3},
{"date": "2024-04-13", "event": "Iran direct drone/missile attack on Israel", "brent_move": +3.0, "days": 1},
],
"suggested_trades": [
{"strategy": "Bull Call Spread", "underlying": "USO", "rationale": "Oil supply risk premium via ETF spread"},
{"strategy": "Long Call", "underlying": "GLD", "rationale": "Dual safe-haven gold bid on military escalation"},
],
"asset_class": "energy",
"expected_move_pct": 6.0,
"probability": 0.58,
"horizon_days": 10,
},
{
"id": "P016",
"name": "North Korea Missile / Nuclear Test → Asian Risk-Off",
"description": "DPRK ICBM launch or nuclear test → KRW selloff, Nikkei/Kospi dip, gold bid",
"triggers": ["military"],
"keywords": ["North Korea", "DPRK", "Kim Jong-un", "missile", "ICBM", "nuclear test", "Korea"],
"taxonomy_path": ["geopolitical", "armed_conflict", "armed_conflict.asia_pacific"],
"historical_instances": [
{"date": "2022-11-18", "event": "DPRK fires ICBM over Japan", "krw_move": -0.8, "days": 1},
{"date": "2017-09-03", "event": "North Korea 6th nuclear test", "krw_move": -1.5, "days": 2},
{"date": "2023-03-16", "event": "DPRK fires ballistic missile", "nky_move": -0.4, "days": 1},
],
"suggested_trades": [
{"strategy": "Long Call", "underlying": "GLD", "rationale": "Safe-haven gold on regional military tension"},
{"strategy": "Bear Put Spread", "underlying": "EWY", "rationale": "South Korea ETF put on escalation risk"},
],
"asset_class": "indices",
"expected_move_pct": -2.0,
"probability": 0.45,
"horizon_days": 7,
},
{
"id": "P017",
"name": "VIX Backwardation (Crisis Regime) → Vol Premium Collapse",
"description": "VIX term structure inverts (spot > 3m future) signalling acute stress; historically mean-reverts fast → vol sellers reload",
"triggers": ["financial_crisis"],
"keywords": ["VIX", "backwardation", "volatility spike", "contango flip", "fear gauge", "VX futures"],
"taxonomy_path": ["market_structure", "market_structure.volatility"],
"historical_instances": [
{"date": "2020-03-16", "event": "COVID crash peak backwardation VIX=82", "vix_move": +82.0, "days": 1},
{"date": "2022-01-24", "event": "Fed tightening fear, VIX=38, backwardation", "vix_move": +38.0, "days": 14},
{"date": "2018-02-05", "event": "Volmageddon — inverse VIX ETPs implode", "vix_move": +115.0, "days": 1},
],
"suggested_trades": [
{"strategy": "Bear Put Spread", "underlying": "SPY", "rationale": "Hedge equity downside during vol spike"},
{"strategy": "Bull Call Spread", "underlying": "SPY", "rationale": "Mean-reversion entry once VIX normalises"},
{"strategy": "Short Put", "underlying": "VXX", "rationale": "VIX ETF put — backwardation normalises quickly"},
],
"asset_class": "indices",
"expected_move_pct": -8.0,
"probability": 0.60,
"horizon_days": 10,
},
{
"id": "P018",
"name": "ECB Surprise Rate Move → EUR Volatility",
"description": "ECB delivers surprise hike, cut, or emergency action outside meeting → EUR/USD sharp move",
"triggers": ["political_speech"],
"keywords": ["ECB", "Lagarde", "euro", "interest rate", "refi rate", "fragmentation", "BTP", "PEPP"],
"taxonomy_path": ["monetary_policy", "monetary_policy.ecb"],
"historical_instances": [
{"date": "2022-07-21", "event": "ECB surprise 50bps hike (vs 25 expected)", "eurusd_move": +1.4, "days": 1},
{"date": "2022-09-08", "event": "ECB hikes 75bps (largest ever)", "eurusd_move": +0.8, "days": 1},
{"date": "2024-06-06", "event": "ECB first rate cut in 5 years", "eurusd_move": -0.3, "days": 1},
],
"suggested_trades": [
{"strategy": "Long Straddle", "underlying": "FXE", "rationale": "EUR/USD straddle on surprise — benefits from direction"},
{"strategy": "Bull Call Spread", "underlying": "FXE", "rationale": "EUR call spread if dovish surprise expected"},
{"strategy": "Bear Put Spread", "underlying": "FXE", "rationale": "EUR put spread if hawkish surprise"},
],
"asset_class": "forex",
"expected_move_pct": 1.5,
"probability": 0.55,
"horizon_days": 5,
},
{
"id": "P019",
"name": "Extreme Fear Sentiment → Contrarian Buy Signal",
"description": "CNN F&G < 20 + AAII bears > 45% → historically strong mean-reversion buy within 4-8 weeks",
"triggers": ["financial_crisis", "political_speech"],
"keywords": ["fear & greed", "AAII", "bearish sentiment", "extreme fear", "capitulation", "put/call ratio"],
"taxonomy_path": ["market_structure", "market_structure.sentiment"],
"historical_instances": [
{"date": "2022-10-13", "event": "CNN F&G=15, AAII Bears=60% — S&P bottoms", "spy_move": +19.0, "days": 60},
{"date": "2020-03-23", "event": "COVID low — CNN F&G=2 extreme fear", "spy_move": +34.0, "days": 30},
{"date": "2023-03-13", "event": "SVB panic — CNN F&G=22, bottom", "spy_move": +9.0, "days": 21},
],
"suggested_trades": [
{"strategy": "Bull Call Spread", "underlying": "SPY", "rationale": "Contrarian equity entry on extreme fear reading"},
{"strategy": "Long Call", "underlying": "QQQ", "rationale": "Growth recovery play post-capitulation"},
],
"asset_class": "indices",
"expected_move_pct": 12.0,
"probability": 0.68,
"horizon_days": 45,
},
{
"id": "P020",
"name": "South China Sea Incident → Shipping / Energy Risk-Off",
"description": "Collision, blockade, or military incident in SCS → shipping disruption, energy supply risk, regional selloff",
"triggers": ["military"],
"keywords": ["South China Sea", "Philippines", "Vietnam", "shipping lane", "Spratly", "Paracel", "Taiwan"],
"taxonomy_path": ["geopolitical", "territorial", "territorial.south_china_sea"],
"historical_instances": [
{"date": "2023-08-05", "event": "China water cannon at Ayungin Shoal", "psei_move": -1.5, "days": 2},
{"date": "2024-02-05", "event": "Heightened China-Philippines naval standoff", "eem_move": -0.8, "days": 3},
],
"suggested_trades": [
{"strategy": "Long Call", "underlying": "GLD", "rationale": "Safe-haven bid on regional naval tension"},
{"strategy": "Bull Call Spread", "underlying": "USO", "rationale": "Oil supply disruption premium via ETF"},
{"strategy": "Bear Put Spread", "underlying": "EEM", "rationale": "EM equity downside on Asian risk-off"},
],
"asset_class": "energy",
"expected_move_pct": 3.0,
"probability": 0.40,
"horizon_days": 10,
},
{
"id": "P021",
"name": "European Energy Crisis → EUR Selloff + NG Spike",
"description": "Russian gas cutoff, LNG supply shortfall, or extreme winter demand drives European natural gas +30%+, EUR weakens",
"triggers": ["energy", "military", "resource_scarcity"],
"keywords": ["Europe energy", "gas storage", "TTF", "Gazprom", "LNG", "energy crisis", "winter", "sanctions"],
"taxonomy_path": ["geopolitical", "armed_conflict", "armed_conflict.europe"],
"historical_instances": [
{"date": "2022-06-15", "event": "Gazprom cuts Nord Stream 1 flows 60%", "ttf_move": +60.0, "days": 10},
{"date": "2022-08-26", "event": "Nord Stream shut entirely, gas crisis peak", "eurusd_move": -3.5, "days": 7},
],
"suggested_trades": [
{"strategy": "Long Call", "underlying": "UNG", "rationale": "US nat gas proxy for European supply squeeze"},
{"strategy": "Bear Put Spread", "underlying": "FXE", "rationale": "EUR/USD put spread on energy terms-of-trade shock"},
],
"asset_class": "energy",
"expected_move_pct": 20.0,
"probability": 0.52,
"horizon_days": 30,
},
{
"id": "P022",
"name": "Inflation Surprise (CPI Hot Print) → Equity Drawdown",
"description": "Above-consensus CPI print reignites rate hike fear → equities sell off, USD spikes, growth/duration hit hardest",
"triggers": ["political_speech"],
"keywords": ["CPI", "inflation", "hot print", "core inflation", "PCE", "surprise", "rate hike expectations"],
"taxonomy_path": ["economic", "economic.inflation"],
"historical_instances": [
{"date": "2022-09-13", "event": "Aug CPI +8.3% vs 8.1% exp — worst drop in 2y", "spy_move": -4.3, "days": 1},
{"date": "2023-02-14", "event": "Jan CPI +6.4% vs 6.2% exp", "spy_move": -1.4, "days": 1},
{"date": "2022-06-10", "event": "May CPI +8.6% — 40-year high", "spy_move": -5.8, "days": 2},
],
"suggested_trades": [
{"strategy": "Bear Put Spread", "underlying": "QQQ", "rationale": "Nasdaq put spread — growth names most rate-sensitive"},
{"strategy": "Long Call", "underlying": "UUP", "rationale": "USD call on hawkish repricing"},
{"strategy": "Bear Put Spread", "underlying": "TLT", "rationale": "Long-duration bond downside on rate expectations reset"},
],
"asset_class": "indices",
"expected_move_pct": -4.0,
"probability": 0.65,
"horizon_days": 5,
},
{
"id": "P023",
"name": "Flash Crash / Liquidity Crisis → Rapid Selloff + Recovery",
"description": "Sudden illiquidity event (algo cascade, forced deleveraging) → intraday/multi-day panic, followed by sharp recovery",
"triggers": ["financial_crisis"],
"keywords": ["flash crash", "liquidity crisis", "deleveraging", "margin call", "carry unwind", "forced selling"],
"taxonomy_path": ["market_structure", "market_structure.liquidity"],
"historical_instances": [
{"date": "2024-08-05", "event": "JPY carry unwind — Nikkei -12% in one day", "nky_move": -12.4, "days": 1},
{"date": "2020-03-12", "event": "COVID crash peak — S&P -10% in one session", "spy_move": -10.0, "days": 1},
{"date": "2010-05-06", "event": "Flash Crash — Dow -1000pts intraday, recovers same day", "spy_move": -9.2, "days": 1},
],
"suggested_trades": [
{"strategy": "Long Put", "underlying": "SPY", "rationale": "Tail-risk put during systemic deleveraging"},
{"strategy": "Bull Call Spread", "underlying": "SPY", "rationale": "Mean-reversion entry post-capitulation"},
{"strategy": "Long Call", "underlying": "GLD", "rationale": "Gold flight-to-safety during liquidity panic"},
],
"asset_class": "indices",
"expected_move_pct": -10.0,
"probability": 0.50,
"horizon_days": 5,
},
]
GEOPOLITICAL_RISK_WEIGHTS = {
# Covers every category classify_news() can produce (data_fetcher.py GEO_KEYWORDS + "general"
# fallback) — previously "sanctions", "elections" and "general" were silently excluded from the
# aggregate score even when individually AI-scored high.
"military": 0.20,
"energy": 0.15,
"political_speech": 0.12,
"sanctions": 0.10,
"trade_war": 0.10,
"elections": 0.08,
"natural_disaster": 0.08,
"health_crisis": 0.08,
"resource_scarcity": 0.04,
"general": 0.05,
}
# A single extreme-severity article (e.g. imminent war, market-moving Fed shock) must not get diluted
# away just because other categories are quiet that day. This floor is driven by the single worst
# category score and stays inert below ~0.7, then ramps up steeply — solved so a lone 0.90 floors the
# final score at 50 (see MAX_CATEGORY_FLOOR_EXPONENT below).
MAX_CATEGORY_FLOOR_EXPONENT = 6.58
def compute_geo_risk_score(events: List[Dict[str, Any]]) -> Dict[str, Any]:
"""Compute a global geopolitical risk score 0-100 from recent events."""
if not events:
return {"score": 35, "level": "medium", "breakdown": {}}
category_scores: Dict[str, float] = {}
for event in events[:30]:
cat = event.get("category", "general")
impact = event.get("impact_score", 0.1)
if cat in category_scores:
category_scores[cat] = max(category_scores[cat], impact)
else:
category_scores[cat] = impact
weighted = sum(
category_scores.get(cat, 0) * weight
for cat, weight in GEOPOLITICAL_RISK_WEIGHTS.items()
)
# Non-linear floor: the single most severe category score alone can force the score up,
# even if it's the only hot category — breaks the "many quiet categories dilute one severe one" effect.
max_category_impact = max(category_scores.values(), default=0.0)
floor = max_category_impact ** MAX_CATEGORY_FLOOR_EXPONENT
score = min(100, round(max(weighted, floor) * 100, 1))
if score < 25:
level = "low"
elif score < 50:
level = "medium"
elif score < 75:
level = "high"
else:
level = "extreme"
return {
"score": score,
"level": level,
"breakdown": {cat: round(v * 100, 1) for cat, v in category_scores.items()},
"top_risks": sorted(category_scores.items(), key=lambda x: x[1], reverse=True)[:3],
}
# Map asset_class → AI directional field produced by ai_score_news_batch()
_ASSET_AI_DIR: Dict[str, str] = {
"energy": "ai_dir_energy",
"metals": "ai_dir_metals",
"indices": "ai_dir_indices",
"equities": "ai_dir_indices",
}
def _compute_ai_alignment(events: List[Dict[str, Any]], pattern: Dict[str, Any]) -> Dict[str, Any]:
"""Return AI directional alignment between news signals and pattern expected direction.
Uses ai_dir_* fields added by ai_score_news_batch().
Returns alignment bonus (-25..+25) and metadata for display.
"""
asset_class = pattern.get("asset_class", "")
expected_positive = (pattern.get("expected_move_pct") or 0) > 0
ai_field = _ASSET_AI_DIR.get(asset_class)
ai_news = [e for e in events if e.get("ai_scored")]
empty = {"ai_alignment": 0, "ai_contra_signal": False, "ai_insights": [], "ai_scored_count": 0}
if not ai_news or not ai_field:
return empty
bullish = sum(1 for e in ai_news if e.get(ai_field) == "bullish")
bearish = sum(1 for e in ai_news if e.get(ai_field) == "bearish")
resolutions = sum(1 for e in ai_news if e.get("ai_resolution"))
total = len(ai_news)
# Positive alignment = news confirms pattern direction
if expected_positive:
raw = (bullish - bearish) / total
contra = bearish > bullish or (resolutions > 0 and asset_class in ("energy", "metals"))
else:
raw = (bearish - bullish) / total
contra = bullish > bearish
bonus = int(round(max(-25.0, min(25.0, raw * 25))))
insights = [
e["ai_insight"] for e in ai_news
if e.get("ai_insight") and e.get(ai_field, "neutral") != "neutral"
][:3]
return {
"ai_alignment": bonus,
"ai_contra_signal": bool(contra),
"ai_insights": insights,
"ai_scored_count": len(ai_news),
}
def match_patterns(events: List[Dict[str, Any]], patterns: Optional[List[Dict[str, Any]]] = None) -> List[Dict[str, Any]]:
"""Find which historical geo-patterns best match current event feed."""
if not events:
return []
if patterns is None:
patterns = GEO_PATTERNS
current_categories = set(e.get("category", "") for e in events)
current_tags = set()
for e in events:
current_tags.update(e.get("tags", []))
current_text = " ".join(e.get("title", "") + " " + e.get("summary", "") for e in events[:20]).lower()
matches = []
for pattern in patterns:
trigger_match = len(set(pattern["triggers"]) & current_categories) / len(pattern["triggers"])
keyword_match = sum(1 for kw in pattern["keywords"] if kw.lower() in current_text) / len(pattern["keywords"])
similarity = round((trigger_match * 0.5 + keyword_match * 0.5) * 100, 1)
if similarity > 10:
ai = _compute_ai_alignment(events, pattern)
adjusted = max(0, min(100, similarity + ai["ai_alignment"]))
matches.append({
"pattern_id": pattern["id"],
"name": pattern["name"],
"description": pattern["description"],
"similarity": round(adjusted, 1),
"base_similarity": similarity,
"suggested_trades": pattern["suggested_trades"],
"asset_class": pattern["asset_class"],
"expected_move_pct": pattern["expected_move_pct"],
"probability": pattern["probability"],
"horizon_days": pattern["horizon_days"],
"historical_instances": pattern["historical_instances"],
**ai,
})
return sorted(matches, key=lambda x: x["similarity"], reverse=True)[:5]
def generate_trade_ideas(pattern_matches: List[Dict[str, Any]], geo_score: Dict[str, Any]) -> List[Dict[str, Any]]:
"""Convert pattern matches into structured trade ideas with sizing for ~1000€."""
ideas = []
for pm in pattern_matches[:5]:
for i, trade in enumerate(pm["suggested_trades"]): # all suggested trades, not just first
move = pm["expected_move_pct"]
confidence = round(pm["probability"] * pm["similarity"] / 100 * 100)
# Use trade-level asset_class if provided, else fall back to pattern-level
asset_class = trade.get("asset_class") or pm["asset_class"]
ideas.append({
"id": f"IDEA-{pm['pattern_id']}-{i}-{trade['strategy'][:3].upper()}",
"title": f"{trade['strategy']} on {trade['underlying']}",
"rationale": f"[{pm['name']}] {trade['rationale']}. Expected move: {'+' if move > 0 else ''}{move}% in {pm['horizon_days']}d",
"pattern": pm["name"],
"asset_class": asset_class,
"underlying": trade["underlying"],
"strategy": trade["strategy"],
"expected_move_pct": move,
"confidence": min(95, confidence),
"horizon_days": pm["horizon_days"],
"capital_required": 1000,
"risk_level": "high" if abs(move) > 15 else "medium",
"pattern_similarity": pm["similarity"],
})
return ideas
def compute_pattern_relevance(
events: List[Dict[str, Any]],
patterns: Optional[List[Dict[str, Any]]] = None,
) -> List[Dict[str, Any]]:
"""Return ALL patterns with news-keyword relevance score + matching news snippets.
Unlike match_patterns(), no similarity threshold — every active pattern is returned.
"""
if patterns is None:
patterns = GEO_PATTERNS
current_categories = set(e.get("category", "") for e in events)
current_text = " ".join(
e.get("title", "") + " " + e.get("summary", "") for e in events[:30]
).lower()
result = []
for pattern in patterns:
triggers_list = pattern.get("triggers", []) or []
keywords_list = pattern.get("keywords", []) or []
trigger_match = (
len(set(triggers_list) & current_categories) / len(triggers_list)
if triggers_list else 0
)
kw_hits = [kw for kw in keywords_list if kw.lower() in current_text]
keyword_match = len(kw_hits) / len(keywords_list) if keywords_list else 0
relevance = round((trigger_match * 0.5 + keyword_match * 0.5) * 100, 1)
# Find matching news with which keywords triggered
matching_news = []
for e in events[:30]:
text = (e.get("title", "") + " " + e.get("summary", "")).lower()
hits = [kw for kw in keywords_list if kw.lower() in text]
if hits:
matching_news.append({
"title": e.get("title", ""),
"source": e.get("source", ""),
"date": str(e.get("date", ""))[:16],
"impact": round(e.get("impact_score", 0), 2),
"matched_keywords": hits,
"url": e.get("url", ""),
})
matching_news.sort(key=lambda x: x["impact"], reverse=True)
ai = _compute_ai_alignment(events, pattern)
adjusted_relevance = max(0, min(100, relevance + ai["ai_alignment"]))
result.append({
"pattern_id": pattern.get("id", ""),
"name": pattern.get("name", ""),
"description": pattern.get("description", ""),
"asset_class": pattern.get("asset_class", ""),
"relevance": round(adjusted_relevance, 1),
"base_relevance": relevance,
"keyword_hits": len(kw_hits),
"keyword_total": len(keywords_list),
"matched_keywords": kw_hits,
"matching_news": matching_news[:5],
"suggested_trades": pattern.get("suggested_trades", []),
"expected_move_pct": pattern.get("expected_move_pct", 0),
"probability": pattern.get("probability", 0),
"horizon_days": pattern.get("horizon_days", 0),
**ai,
})
result.sort(key=lambda x: x["relevance"], reverse=True)
return result
def get_all_patterns() -> List[Dict[str, Any]]:
return GEO_PATTERNS