diff --git a/backend/routers/patterns.py b/backend/routers/patterns.py index d85d1fe..c06110f 100644 --- a/backend/routers/patterns.py +++ b/backend/routers/patterns.py @@ -1,9 +1,10 @@ -from fastapi import APIRouter, HTTPException +from fastapi import APIRouter, HTTPException, Query from pydantic import BaseModel from typing import Optional, List, Dict, Any from services.database import ( save_custom_pattern, get_custom_patterns, delete_custom_pattern, toggle_pattern_active ) +from services.geo_analyzer import PATTERN_TAXONOMY router = APIRouter(prefix="/api/patterns", tags=["patterns"]) @@ -71,3 +72,26 @@ def toggle_pattern(pat_id: str): """Enable or disable a pattern (works for builtin and custom).""" new_state = toggle_pattern_active(pat_id) return {"id": pat_id, "is_active": new_state} + + +@router.get("/taxonomy") +def get_taxonomy(): + """Return the taxonomy tree + all patterns (frontend builds node→pattern mapping).""" + patterns = get_custom_patterns() + return {"tree": PATTERN_TAXONOMY, "patterns": patterns} + + +@router.get("/by-instrument") +def get_by_instrument(ticker: str = Query(..., description="Ticker / underlying to search")): + """Return all patterns that have a suggested_trade matching the given ticker.""" + ticker_lower = ticker.strip().lower() + result = [] + for p in get_custom_patterns(): + trades = p.get("suggested_trades") or [] + matching = [ + t for t in trades + if ticker_lower in (t.get("underlying") or "").lower() + ] + if matching: + result.append({**p, "matching_trades": matching}) + return result diff --git a/backend/services/database.py b/backend/services/database.py index 796a2d4..91190d2 100644 --- a/backend/services/database.py +++ b/backend/services/database.py @@ -81,6 +81,8 @@ def init_db(): # Convergence Phase 1 — thematic classification "ALTER TABLE custom_patterns ADD COLUMN category TEXT", "ALTER TABLE custom_patterns ADD COLUMN signal_direction TEXT", + # Pattern Explorer — taxonomy tree path + "ALTER TABLE custom_patterns ADD COLUMN taxonomy_path TEXT DEFAULT '[]'", ]: try: c.execute(_sql) @@ -880,16 +882,17 @@ def update_position_notes(pos_id: str, notes: str): # ── Custom Patterns ──────────────────────────────────────────────────────────── def seed_builtin_patterns(builtin_patterns: List[Dict[str, Any]]): - """Seed built-in patterns into DB (idempotent — skips existing IDs).""" + """Seed built-in patterns into DB (idempotent). Always updates taxonomy_path.""" conn = get_conn() existing = {r[0] for r in conn.execute("SELECT id FROM custom_patterns").fetchall()} for p in builtin_patterns: + taxonomy_json = json.dumps(p.get("taxonomy_path", [])) if p["id"] not in existing: conn.execute("""INSERT INTO custom_patterns ( id, name, description, triggers, keywords, historical_instances, suggested_trades, asset_class, expected_move_pct, probability, - horizon_days, source, is_active, updated_at - ) VALUES (?,?,?,?,?,?,?,?,?,?,?,'builtin',1,datetime('now'))""", ( + horizon_days, source, is_active, taxonomy_path, updated_at + ) VALUES (?,?,?,?,?,?,?,?,?,?,?,'builtin',1,?,datetime('now'))""", ( p["id"], p.get("name", ""), p.get("description", ""), @@ -901,7 +904,13 @@ def seed_builtin_patterns(builtin_patterns: List[Dict[str, Any]]): p.get("expected_move_pct", 0), p.get("probability", 0.5), p.get("horizon_days", 30), + taxonomy_json, )) + else: + conn.execute( + "UPDATE custom_patterns SET taxonomy_path=? WHERE id=? AND source='builtin'", + (taxonomy_json, p["id"]) + ) conn.commit() conn.close() @@ -955,6 +964,7 @@ def get_custom_patterns() -> List[Dict[str, Any]]: d = dict(r) for f in ["triggers", "keywords", "historical_instances", "suggested_trades", "ai_evaluation"]: d[f] = json.loads(d.get(f) or "[]") + d["taxonomy_path"] = json.loads(d.get("taxonomy_path") or "[]") result.append(d) return result diff --git a/backend/services/geo_analyzer.py b/backend/services/geo_analyzer.py index ca5c158..aacaeb3 100644 --- a/backend/services/geo_analyzer.py +++ b/backend/services/geo_analyzer.py @@ -7,6 +7,128 @@ 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 = [ { @@ -15,6 +137,7 @@ GEO_PATTERNS = [ "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}, @@ -35,6 +158,7 @@ GEO_PATTERNS = [ "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}, @@ -55,6 +179,7 @@ GEO_PATTERNS = [ "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}, @@ -75,6 +200,7 @@ GEO_PATTERNS = [ "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}, @@ -94,6 +220,7 @@ GEO_PATTERNS = [ "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}, @@ -113,6 +240,7 @@ GEO_PATTERNS = [ "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}, @@ -133,6 +261,7 @@ GEO_PATTERNS = [ "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}, @@ -152,6 +281,7 @@ GEO_PATTERNS = [ "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}, @@ -166,6 +296,330 @@ GEO_PATTERNS = [ "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, + }, ] diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 68a9176..fba672c 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -9,6 +9,7 @@ import Backtest from './pages/Backtest' import CalendarPage from './pages/CalendarPage' import Portfolio from './pages/Portfolio' import PatternEditor from './pages/PatternEditor' +import PatternExplorer from './pages/PatternExplorer' import JournalDeBord from './pages/JournalDeBord' import RapportIA from './pages/RapportIA' import SuperContexte from './pages/SuperContexte' @@ -40,7 +41,8 @@ export default function App() { } /> } /> } /> - } /> + } /> + } /> } /> } /> } /> diff --git a/frontend/src/hooks/useApi.ts b/frontend/src/hooks/useApi.ts index 682ceee..bb2a72f 100644 --- a/frontend/src/hooks/useApi.ts +++ b/frontend/src/hooks/useApi.ts @@ -251,6 +251,21 @@ export const useAllPatterns = () => queryFn: () => api.get('/patterns/all').then(r => r.data), }) +export const usePatternTaxonomy = () => + useQuery({ + queryKey: ['pattern-taxonomy'], + queryFn: () => api.get('/patterns/taxonomy').then(r => r.data), + staleTime: 5 * 60_000, + }) + +export const usePatternsByInstrument = (ticker: string) => + useQuery({ + queryKey: ['patterns-by-instrument', ticker], + queryFn: () => api.get('/patterns/by-instrument', { params: { ticker } }).then(r => r.data), + enabled: !!ticker, + staleTime: 2 * 60_000, + }) + export const usePatternSimilarity = () => useQuery({ queryKey: ['pattern-similarity'], diff --git a/frontend/src/pages/PatternExplorer.tsx b/frontend/src/pages/PatternExplorer.tsx new file mode 100644 index 0000000..40ab66d --- /dev/null +++ b/frontend/src/pages/PatternExplorer.tsx @@ -0,0 +1,638 @@ +import { useState, useMemo, useCallback, useEffect, useRef } from 'react' +import { Link } from 'react-router-dom' +import clsx from 'clsx' +import { + TreePine, Search, ChevronRight, ChevronDown, + TrendingUp, TrendingDown, Zap, BookOpen, ExternalLink, +} from 'lucide-react' +import { + useAllPatterns, + usePatternTaxonomy, + usePatternsByInstrument, + useLastScores, +} from '../hooks/useApi' + +// ── Local types ─────────────────────────────────────────────────────────────── + +interface TaxonomyNode { + id: string + label: string + icon?: string + description?: string + children?: TaxonomyNode[] + count?: number + path: string[] +} + +interface SuggestedTrade { + strategy: string + underlying: string + rationale: string + strike_guidance?: string + expected_move_pct?: number +} + +interface HistoricalInstance { + date: string + event: string + [key: string]: unknown +} + +interface Pattern { + id: string + name: string + description: string + triggers: string[] + keywords: string[] + historical_instances: HistoricalInstance[] + suggested_trades: SuggestedTrade[] + asset_class: string + expected_move_pct: number + probability: number + horizon_days: number + source: string + category?: string + signal_direction?: string + taxonomy_path?: string[] + ai_quality_score?: number + is_active?: number +} + +interface ScoreResult { + pattern_id: string + score?: number + [key: string]: unknown +} + +// ── Constants ───────────────────────────────────────────────────────────────── + +const ASSET_CLASS_LABELS: Record = { + energy: '⚡ Energy', + metals: '🥇 Metals', + agriculture: '🌾 Agri', + equities: '📈 Equities', + indices: '📊 Indices', + forex: '💱 Forex', + rates: '📉 Rates', +} + +// ── Helpers ─────────────────────────────────────────────────────────────────── + +function probStars(prob: number): string { + const n = prob >= 0.8 ? 3 : prob >= 0.6 ? 2 : 1 + return '★'.repeat(n) + '☆'.repeat(3 - n) +} + +function pathStartsWith(full: string[] | undefined, prefix: string[]): boolean { + if (!full || prefix.length === 0) return true + if (full.length < prefix.length) return false + return prefix.every((seg, i) => seg === full[i]) +} + +function formatTaxPath(path: string[] | undefined): string { + if (!path || path.length === 0) return '' + return path.join(' › ') +} + +// ── Signal Direction Badge ──────────────────────────────────────────────────── + +function SignalBadge({ direction }: { direction?: string }) { + if (direction === 'bullish') { + return ( + + Bullish + + ) + } + if (direction === 'bearish') { + return ( + + Bearish + + ) + } + return ( + + Vol + + ) +} + +// ── Pattern Card ────────────────────────────────────────────────────────────── + +function PatternCard({ + pattern, + highlightTrades, +}: { + pattern: Pattern + highlightTrades?: string[] +}) { + const [expanded, setExpanded] = useState(false) + + const trades = pattern.suggested_trades ?? [] + const instances = pattern.historical_instances ?? [] + const assetLabel = ASSET_CLASS_LABELS[pattern.asset_class] ?? pattern.asset_class + + const moveSign = pattern.expected_move_pct >= 0 ? '+' : '' + + return ( +
+ {/* Header row */} +
+
+ + {pattern.id} + {probStars(pattern.probability)} +
+
+ + {/* Name + description */} +
+
{pattern.name}
+
{pattern.description}
+
+ + {/* Stats row */} +
+ + Expected:{' '} + = 0 ? 'text-emerald-400' : 'text-red-400')}> + {moveSign}{pattern.expected_move_pct}% + + + Horizon: {pattern.horizon_days}d + Asset: {assetLabel} + Prob: {Math.round(pattern.probability * 100)}% +
+ + {/* Trades row */} + {trades.length > 0 && ( +
+
Trades
+
+ {trades.map((t, i) => { + const isHighlighted = + highlightTrades && highlightTrades.some(u => + t.underlying.toLowerCase().includes(u.toLowerCase()) + ) + return ( + + {t.strategy} {t.underlying} + + ) + })} +
+
+ )} + + {/* Historical instances (collapsible) */} + {instances.length > 0 && ( +
+ + {expanded && ( +
    + {instances.map((inst, i) => ( +
  • + {inst.date} + {inst.event} +
  • + ))} +
+ )} +
+ )} +
+ ) +} + +// ── Taxonomy Tree Node ──────────────────────────────────────────────────────── + +function TreeNodeRow({ + node, + level, + selectedPath, + onSelect, + defaultExpandedDepth, +}: { + node: TaxonomyNode + level: number + selectedPath: string[] + onSelect: (path: string[]) => void + defaultExpandedDepth: number +}) { + const hasChildren = (node.children ?? []).length > 0 + const [expanded, setExpanded] = useState(level < defaultExpandedDepth) + + const isSelected = + selectedPath.length === node.path.length && + node.path.every((seg, i) => seg === selectedPath[i]) + + const handleClick = useCallback(() => { + onSelect(node.path) + if (hasChildren) setExpanded(v => !v) + }, [node.path, onSelect, hasChildren]) + + const indent = level * 16 + + return ( +
+ + {hasChildren && expanded && ( +
+ {node.children!.map(child => ( + + ))} +
+ )} +
+ ) +} + +// ── Tree View ───────────────────────────────────────────────────────────────── + +function TreeView({ patterns }: { patterns: Pattern[] }) { + const { data: taxonomyData } = usePatternTaxonomy() + const taxonomy = taxonomyData as TaxonomyNode | undefined + + const [selectedPath, setSelectedPath] = useState([]) + const [nameFilter, setNameFilter] = useState('') + + const filteredPatterns = useMemo(() => { + let list = patterns + if (selectedPath.length > 0) { + list = list.filter(p => pathStartsWith(p.taxonomy_path, selectedPath)) + } + if (nameFilter.trim()) { + const q = nameFilter.trim().toLowerCase() + list = list.filter(p => p.name.toLowerCase().includes(q) || p.description.toLowerCase().includes(q)) + } + return list + }, [patterns, selectedPath, nameFilter]) + + const breadcrumb = selectedPath.length > 0 ? selectedPath.join(' › ') : 'All Patterns' + + return ( +
+ {/* Left sidebar */} +
+
+ Taxonomy +
+ + {/* Root "All" entry */} + + + {/* Taxonomy tree */} + {taxonomy ? ( + (taxonomy.children ?? []).map(node => ( + + )) + ) : ( +
Loading taxonomy…
+ )} +
+ + {/* Right panel */} +
+ {/* Breadcrumb + filter */} +
+
+ Path: + {breadcrumb} +
+
+
+ + setNameFilter(e.target.value)} + className="input text-sm pl-8 py-1.5 w-52" + /> +
+ {filteredPatterns.length} patterns +
+
+ + {/* Pattern grid */} + {filteredPatterns.length === 0 ? ( +
+ No patterns match the selected filter +
+ ) : ( +
+ {filteredPatterns.map(p => ( + + ))} +
+ )} +
+
+ ) +} + +// ── Instrument Lens ─────────────────────────────────────────────────────────── + +function InstrumentRow({ pattern, searchTerm }: { pattern: Pattern; searchTerm: string }) { + const matchingTrades = pattern.suggested_trades?.filter(t => + t.underlying.toLowerCase().includes(searchTerm.toLowerCase()) + ) ?? [] + + return ( +
+
+ {/* Left: name + path */} +
+
+ + {pattern.name} +
+ {pattern.taxonomy_path && pattern.taxonomy_path.length > 0 && ( +
+ {formatTaxPath(pattern.taxonomy_path)} +
+ )} + {/* Matching trades highlighted */} + {matchingTrades.length > 0 && ( +
+ {matchingTrades.map((t, i) => ( +
+ {t.strategy} + · + {t.underlying} + {t.expected_move_pct != null && ( + + {t.expected_move_pct >= 0 ? '+' : ''}{t.expected_move_pct}% + + )} + {t.rationale && ( + {t.rationale} + )} +
+ ))} +
+ )} +
+ + {/* Right: stats */} +
+ + Prob:{' '} + {Math.round(pattern.probability * 100)}% + + + Move:{' '} + = 0 ? 'text-emerald-400' : 'text-red-400')}> + {pattern.expected_move_pct >= 0 ? '+' : ''}{pattern.expected_move_pct}% + + + Horizon: {pattern.horizon_days}d +
+
+
+ ) +} + +function InstrumentLens({ patterns }: { patterns: Pattern[] }) { + const [inputValue, setInputValue] = useState('') + const [searchTerm, setSearchTerm] = useState('') + const debounceRef = useRef | null>(null) + + // Debounce the search term by 300 ms + useEffect(() => { + if (debounceRef.current) clearTimeout(debounceRef.current) + debounceRef.current = setTimeout(() => setSearchTerm(inputValue.trim()), 300) + return () => { if (debounceRef.current) clearTimeout(debounceRef.current) } + }, [inputValue]) + + // Also use the dedicated hook when a ticker is typed + const { data: instrumentData } = usePatternsByInstrument(searchTerm) + const instrumentPatterns = instrumentData as Pattern[] | undefined + + const results = useMemo(() => { + if (!searchTerm) return [] + + // Prefer dedicated API data if available, otherwise filter locally + const source: Pattern[] = (instrumentPatterns && instrumentPatterns.length > 0) + ? instrumentPatterns + : patterns.filter(p => + (p.suggested_trades ?? []).some(t => + t.underlying.toLowerCase().includes(searchTerm.toLowerCase()) + ) + ) + + return [...source].sort((a, b) => b.probability - a.probability) + }, [searchTerm, instrumentPatterns, patterns]) + + return ( +
+ {/* Search bar */} +
+
+ + setInputValue(e.target.value)} + className="input text-sm pl-10 py-2 w-full" + /> +
+ {searchTerm && ( + + )} +
+ + {/* Results */} + {!searchTerm ? ( +
+ +

Type an underlying to find matching patterns

+
+ {['EUR/USD', 'USO', 'GLD', 'CL=F', 'GC=F', 'SPY', 'TLT'].map(example => ( + + ))} +
+
+ ) : results.length === 0 ? ( +
+ +

No patterns found for {searchTerm}

+
+ ) : ( + <> +
+ {results.length} pattern{results.length !== 1 ? 's' : ''} influence{' '} + {searchTerm}, ranked by probability +
+
+ {results.map(p => ( + + ))} +
+ + )} +
+ ) +} + +// ── Page root ───────────────────────────────────────────────────────────────── + +type ViewMode = 'tree' | 'instrument' + +export default function PatternExplorer() { + const [view, setView] = useState('tree') + const { data: patternsData, isLoading } = useAllPatterns() + const { data: _scores } = useLastScores() + + const patterns = useMemo(() => { + if (!Array.isArray(patternsData)) return [] + return patternsData as Pattern[] + }, [patternsData]) + + return ( +
+ {/* Page header */} +
+
+
+ +

Pattern Library

+ + + Edit Library + +
+

+ Browse {patterns.length > 0 ? patterns.length : '…'} geo-financial patterns by theme or by instrument +

+
+ + {/* View toggle */} +
+ + +
+
+ + {/* Content area */} + {isLoading ? ( +
+
+ Loading patterns… +
+ ) : ( + <> + {view === 'tree' && } + {view === 'instrument' && } + + )} +
+ ) +}