From 10ffc345d69e0c7ff1217789666ea83356a5804d Mon Sep 17 00:00:00 2001 From: OpenSquared Date: Mon, 22 Jun 2026 16:42:45 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20Pattern=20Explorer=20=E2=80=94=20taxono?= =?UTF-8?q?my=20tree=20+=2023=20historical=20patterns=20+=20instrument=20l?= =?UTF-8?q?ens?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - PatternExplorer.tsx: new page with two views: • Tree View — 6-root taxonomy (geopolitical, monetary_policy, economic, commodity, risk_off, market_structure) with collapsible sub-nodes; pattern cards appear on node selection • Instrument Lens — search any ticker (e.g. GLD, FXE) to see every pattern + scenario that references it, with matching trades highlighted - geo_analyzer.py: PATTERN_TAXONOMY tree constant + taxonomy_path on all patterns; 15 new documented patterns P009-P023 (BoJ YCC, SVB crisis, Taiwan semis, OPEC cuts, Fed pivot, Debt ceiling, Iran nuclear, DPRK, VIX backwardation, ECB surprise, Extreme Fear contrarian, S. China Sea, European energy, CPI hot print, Flash crash) - patterns.py: GET /api/patterns/taxonomy, GET /api/patterns/by-instrument - database.py: taxonomy_path migration + seed_builtin_patterns updates path - App.tsx: /patterns → PatternExplorer, /patterns/edit → PatternEditor Co-Authored-By: Claude Sonnet 4.6 --- backend/routers/patterns.py | 26 +- backend/services/database.py | 16 +- backend/services/geo_analyzer.py | 454 ++++++++++++++++++ frontend/src/App.tsx | 4 +- frontend/src/hooks/useApi.ts | 15 + frontend/src/pages/PatternExplorer.tsx | 638 +++++++++++++++++++++++++ 6 files changed, 1148 insertions(+), 5 deletions(-) create mode 100644 frontend/src/pages/PatternExplorer.tsx 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' && } + + )} +
+ ) +}