882 lines
45 KiB
Python
882 lines
45 KiB
Python
"""
|
||
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": "US–China"},
|
||
{"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
|