From aec9ced74faffde1d0e18dbc9e9b9f91293b0ccb Mon Sep 17 00:00:00 2001 From: OpenSquared Date: Wed, 24 Jun 2026 23:24:24 +0200 Subject: [PATCH] feat: macro regime + 30 historical events + regime confidence fix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add macro_regime (goldilocks/stagflation/recession/etc.) to every instrument snapshot via get_macro_gauges() + score_macro_scenarios() - RegimeCard now shows global macro cycle section (emoji + label + top-3 scenarios) above technical signals - Fix _detect_regime() confidence: capped at 85% max; add late-bull (dist_MA200 > 20%) and correction-in-bull (MA50 > MA200 but momentum < -3%) detection so regime no longer locks at 100% - Add macro_events_bootstrap.py with 30 curated historical events (FOMC 2022-2025, CPI surprises, Ukraine/Hamas/Iran geopolitics, BOJ pivots, Bitcoin ETF, Liberation Day tariffs, SVB crisis, etc.) - POST /api/timeline/bootstrap-macro endpoint (idempotent, deduplicates by name) - Fix event date filter in _get_relevant_events(): overlap logic instead of start-only filter — events extending into the chart window are now included - EventTimelineStrip: add "Signaux Techniques" fallback row for events not matched by any driver keyword (MA crossovers are now always visible) Co-Authored-By: Claude Sonnet 4.6 --- backend/routers/timeline.py | 15 ++ backend/services/instrument_service.py | 86 +++++- backend/services/macro_events_bootstrap.py | 300 +++++++++++++++++++++ frontend/src/pages/InstrumentDashboard.tsx | 111 +++++++- 4 files changed, 495 insertions(+), 17 deletions(-) create mode 100644 backend/services/macro_events_bootstrap.py diff --git a/backend/routers/timeline.py b/backend/routers/timeline.py index 5fd3bd8..5ed158b 100644 --- a/backend/routers/timeline.py +++ b/backend/routers/timeline.py @@ -155,6 +155,21 @@ FORMAT JSON STRICT: raise HTTPException(500, str(e)) +@router.post("/bootstrap-macro") +def bootstrap_macro(force: bool = False) -> Dict[str, Any]: + """ + Seed the market_events table with predefined historical macro + geopolitical events. + Pass ?force=true to re-run even if events already exist. + """ + try: + from services.macro_events_bootstrap import bootstrap_macro_events + result = bootstrap_macro_events(force=force) + return result + except Exception as e: + logger.error(f"[Timeline] bootstrap_macro failed: {e}") + raise HTTPException(500, str(e)) + + @router.get("/day/{ref_date}") def get_day_context(ref_date: str) -> Dict[str, Any]: from services.database import get_events_for_date, get_timeline_context diff --git a/backend/services/instrument_service.py b/backend/services/instrument_service.py index f8ec360..267959f 100644 --- a/backend/services/instrument_service.py +++ b/backend/services/instrument_service.py @@ -326,23 +326,46 @@ def _detect_regime(df: pd.DataFrame, config: Dict) -> Dict[str, Any]: (momentum_20d is not None and abs(momentum_20d) < 1.0) ) + # Late-bull flag: strongly extended above MA200 — risk of reversal + is_late_bull = ( + bull_score > 0.6 and + dist_ma200 is not None and dist_ma200 > 20 + ) + + # Correction-in-bull flag: price above MA200 but momentum turning + is_correction_in_bull = ( + ma50_above_ma200 is True and + momentum_20d is not None and momentum_20d < -3.0 + ) + # ── Map to 5 regime slots ────────────────────────────────────────────────── # Slot 0 = bullish, 1 = bearish, 2 = transition, 3 = volatile, 4 = late/consolidation raw_scores = [0.0] * 5 - # Bullish - raw_scores[0] = max(0.0, bull_score) + if is_correction_in_bull: + # Price still above MA200 but momentum rolling over — partial weight to transition + raw_scores[0] = max(0.0, bull_score * 0.5) + raw_scores[2] = 0.6 + elif is_late_bull: + # Extended above MA200: some probability we're in late / consolidation regime + raw_scores[0] = max(0.0, bull_score * 0.55) + raw_scores[4] = bull_score * 0.5 + else: + # Bullish + raw_scores[0] = max(0.0, bull_score) # Bearish raw_scores[1] = max(0.0, -bull_score) - # Transition - raw_scores[2] = 0.6 if is_transition else 0.0 + # Transition (overrides if flagged) + if not is_correction_in_bull: + raw_scores[2] = 0.6 if is_transition else 0.0 # Volatile raw_scores[3] = 0.7 if is_volatile else 0.0 # Late cycle / consolidation — moderate bull_score but high dist_ma200 - if 0.0 < bull_score < 0.3 and dist_ma200 is not None and dist_ma200 > 5: - raw_scores[4] = 0.5 - else: - raw_scores[4] = max(0.0, 0.3 - abs(bull_score)) if not is_transition else 0.0 + if not is_late_bull and not is_correction_in_bull: + if 0.0 < bull_score < 0.3 and dist_ma200 is not None and dist_ma200 > 5: + raw_scores[4] = 0.5 + else: + raw_scores[4] = max(0.0, 0.3 - abs(bull_score)) if not is_transition else 0.0 # If volatile, suppress the others a bit if is_volatile: @@ -364,7 +387,8 @@ def _detect_regime(df: pd.DataFrame, config: Dict) -> Dict[str, Any]: norm_scores = norm_scores[:n_labels] best_idx = int(np.argmax(norm_scores)) - confidence = _round(norm_scores[best_idx], 3) or 0.0 + # Cap confidence: max 85% for technical regime (always some model uncertainty) + confidence = _round(min(norm_scores[best_idx], 0.85), 3) or 0.0 scores_dict = {} for i, label in enumerate(regime_labels): @@ -515,13 +539,26 @@ def _get_relevant_events( filtered = [] for ev in all_events: ev_start = str(ev.get("start_date", "") or "") - ev_end = str(ev.get("end_date", "") or ev_start) + ev_end = str(ev.get("end_date", "") or "") - # Date range filter - if from_date and ev_start and ev_start < from_date: - continue + # Date range overlap filter: + # Include if event overlaps with [from_date, to_date] + # An event overlaps if: ev_start <= to_date AND (ev_end >= from_date OR ev_end is empty) if to_date and ev_start and ev_start > to_date: continue + if from_date and ev_end and ev_end < from_date: + continue + # If ev_end is empty (point event), include if start is within [-6 months, to_date] + if from_date and not ev_end: + # Allow events whose start is up to 6 months before the chart window + import datetime + try: + start_dt = datetime.date.fromisoformat(ev_start) + from_dt = datetime.date.fromisoformat(from_date) + if (from_dt - start_dt).days > 180: + continue + except Exception: + pass # Keyword / asset relevance ev_name = (ev.get("name") or ev.get("event_name") or "").lower() @@ -614,11 +651,34 @@ async def get_snapshot( logger.warning(f"[instrument_service] Event filtering error: {e}") events = [] + # Macro regime (global cycle: goldilocks / stagflation / recession / etc.) + macro_regime: Dict[str, Any] = {} + try: + from services.data_fetcher import get_macro_gauges, score_macro_scenarios + gauges = get_macro_gauges() + macro_raw = score_macro_scenarios(gauges) + dominant = macro_raw.get("dominant", "incertain") + meta = macro_raw.get("meta", {}) + ranked = macro_raw.get("ranked", []) + macro_regime = { + "dominant": dominant, + "label": meta.get(dominant, {}).get("label", dominant) if isinstance(meta, dict) else dominant, + "color": meta.get(dominant, {}).get("color", "#6b7280") if isinstance(meta, dict) else "#6b7280", + "emoji": meta.get(dominant, {}).get("emoji", "🌍") if isinstance(meta, dict) else "🌍", + "scores": macro_raw.get("scores", {}), + "ranked": ranked[:5], + "asset_bias": (macro_raw.get("asset_bias") or {}).get(dominant, {}), + } + except Exception as _me: + logger.warning(f"[instrument_service] Macro regime fetch error: {_me}") + macro_regime = {"dominant": "incertain", "label": "Incertain", "scores": {}} + return { "instrument": config, "price_data": price_data, "indicators": indicators, "regime": regime, + "macro_regime": macro_regime, "trend": trend, "events": events, "current_price": current_price, diff --git a/backend/services/macro_events_bootstrap.py b/backend/services/macro_events_bootstrap.py new file mode 100644 index 0000000..9761f62 --- /dev/null +++ b/backend/services/macro_events_bootstrap.py @@ -0,0 +1,300 @@ +""" +Bootstrap a curated set of historical macro + geopolitical events into market_events. +These events carry keywords in their names/descriptions that match instrument event_keywords, +so they will appear in instrument snapshots and the timeline driver strip. +""" +import logging +from typing import List, Dict, Any + +logger = logging.getLogger(__name__) + +# ── Predefined events list ───────────────────────────────────────────────────── +# Each event: name, start_date, end_date, level, category, description, +# affected_assets (JSON-like string), impact_score + +MACRO_EVENTS: List[Dict[str, Any]] = [ + # ── Fed / FOMC ──────────────────────────────────────────────────────────── + { + "name": "FOMC — Hausse taux +25pb (début cycle de resserrement)", + "start_date": "2022-03-16", "end_date": None, + "level": "long", "category": "macro", + "description": "Fed commence son cycle de hausse de taux pour combattre l'inflation record. Premier FOMC hawkish depuis 2018. Dollar Index (DXY) s'apprécie fortement. Pression sur les taux réels et les valeurs de croissance.", + "affected_assets": '["SPY","QQQ","TLT","GLD","USDJPY=X","BTC-USD"]', "impact_score": 0.85, + }, + { + "name": "FOMC — Hausse +50pb (accélération resserrement)", + "start_date": "2022-05-04", "end_date": None, + "level": "medium", "category": "macro", + "description": "Fed accélère avec +50pb, le plus fort depuis 2000. Taux cibles 0.75-1.00%. CPI à 8.3% en avril. Signal fort de la Fed pour tuer l'inflation quoi qu'il en coûte.", + "affected_assets": '["SPY","QQQ","TLT","GLD","IWM"]', "impact_score": 0.80, + }, + { + "name": "FOMC — Choc hawkish +75pb (premier 3/4 de point depuis 1994)", + "start_date": "2022-06-15", "end_date": None, + "level": "long", "category": "macro", + "description": "FOMC surprise +75pb, le plus fort depuis 1994. CPI a 9.1% en juin (record 40 ans). Taux cibles 1.50-1.75%. Début du vrai bear market sur les indices. Volatilité VIX explose.", + "affected_assets": '["SPY","QQQ","TLT","GLD","VXX","USDJPY=X","IWM"]', "impact_score": 0.95, + }, + { + "name": "FOMC — Hausse +75pb (4e hausse consécutive hawkish)", + "start_date": "2022-09-21", "end_date": None, + "level": "medium", "category": "macro", + "description": "4e hausse consécutive de +75pb. Dot plot révisé fortement à la hausse. Taux terminal attendu à 4.6% en 2023. Dollar au plus haut depuis 20 ans. Pression extrême sur toutes les classes d'actifs.", + "affected_assets": '["SPY","QQQ","TLT","GLD","EURUSD=X","IWM","HYG"]', "impact_score": 0.90, + }, + { + "name": "FOMC — Hausse +50pb (décélération, pivot en vue)", + "start_date": "2022-12-14", "end_date": None, + "level": "medium", "category": "macro", + "description": "Première décélération à +50pb après 4x+75pb. Taux 4.25-4.50%. Dot plot révèle taux terminal de 5.1% pour 2023. Signal que le pic de la vitesse d'augmentation est passé.", + "affected_assets": '["SPY","QQQ","TLT","GLD","USDJPY=X"]', "impact_score": 0.80, + }, + { + "name": "FOMC — Pause taux (pic du cycle à 5.25-5.50%)", + "start_date": "2023-06-14", "end_date": None, + "level": "long", "category": "macro", + "description": "FOMC marque une pause après 10 hausses consécutives. Taux directeur au pic du cycle à 5.25-5.50%. Début de la période de 'higher for longer'. Marché commence à pricer les baisses futures.", + "affected_assets": '["SPY","QQQ","TLT","GLD","HYG"]', "impact_score": 0.80, + }, + { + "name": "FOMC — Pivot Fed : première baisse de taux -50pb", + "start_date": "2024-09-18", "end_date": None, + "level": "long", "category": "macro", + "description": "Pivot majeur de la Fed : première baisse de taux depuis 2020, -50pb surprend les marchés (consensus attendait -25pb). Taux 4.75-5.00%. Début du cycle d'assouplissement. Signal bullish pour actions, gold, crypto.", + "affected_assets": '["SPY","QQQ","GLD","TLT","BTC-USD","IWM","HYG"]', "impact_score": 0.90, + }, + { + "name": "FOMC — Dot plot hawkish : révision à la baisse des baisses 2025", + "start_date": "2024-12-18", "end_date": None, + "level": "medium", "category": "macro", + "description": "FOMC baisse taux -25pb mais dot plot révise de 4 à 2 baisses prévues en 2025. Ton hawkish surprend. Dollar fort, taux longs remontent. Correction de fin d'année sur les indices.", + "affected_assets": '["SPY","QQQ","TLT","GLD","USDJPY=X"]', "impact_score": 0.85, + }, + { + "name": "FOMC — Pause prolongée (incertitude tarifaire Trump)", + "start_date": "2025-03-19", "end_date": "2025-06-30", + "level": "medium", "category": "macro", + "description": "Fed en pause prolongée face à l'incertitude créée par les tarifs douaniers Trump. CPI autour de 2.8%. Fed attend de voir l'impact inflationniste des tarifs avant de baisser à nouveau.", + "affected_assets": '["SPY","QQQ","TLT","GLD","EURUSD=X"]', "impact_score": 0.75, + }, + + # ── CPI / Inflation ─────────────────────────────────────────────────────── + { + "name": "CPI Juin 2022 — Pic inflationniste 9.1% (sommet 40 ans)", + "start_date": "2022-06-10", "end_date": None, + "level": "long", "category": "macro", + "description": "CPI US atteint 9.1% sur un an, le plus haut depuis 40 ans. Énergie +41.6%, alimentation +10.4%. Oblige la Fed à agir de façon agressive. Choc majeur pour TLT et valeurs de croissance. GLD paradoxalement vendus (dollar fort).", + "affected_assets": '["TLT","SPY","QQQ","GLD","EURUSD=X"]', "impact_score": 0.95, + }, + { + "name": "CPI Novembre 2022 — Inflexion à la baisse (début désinflation)", + "start_date": "2022-11-10", "end_date": None, + "level": "long", "category": "macro", + "description": "CPI 7.7%, sous les 8% attendus pour la première fois depuis plusieurs mois. Signal d'inflexion décisif. Méga-rally intraday sur les indices (+5%), or, crypto. Dollar chute fortement. Début du discours de désinflation.", + "affected_assets": '["SPY","QQQ","GLD","BTC-USD","TLT","EURUSD=X"]', "impact_score": 0.90, + }, + { + "name": "CPI Mars 2024 — Surprise haussière 3.5% (recul désinflation)", + "start_date": "2024-04-10", "end_date": None, + "level": "medium", "category": "macro", + "description": "CPI 3.5% sur un an, au-dessus des attentes. Reporte les anticipations de baisses Fed. Taux 10Y remontent vers 4.5%. Correction sur TLT et valeurs de croissance. GLD résistant grâce aux achats de banques centrales.", + "affected_assets": '["TLT","QQQ","SPY","USDJPY=X"]', "impact_score": 0.80, + }, + + # ── Géopolitique ────────────────────────────────────────────────────────── + { + "name": "Invasion russe de l'Ukraine — Choc géopolitique majeur", + "start_date": "2022-02-24", "end_date": "2023-02-24", + "level": "long", "category": "geopolitique", + "description": "Russie envahit l'Ukraine. Choc géopolitique majeur. Pétrole Brent +30% en semaines. Gaz naturel européen x5. Gold +5%. Dollar fort. Sanctions massives contre Russie. Crise énergétique en Europe.", + "affected_assets": '["GLD","USO","UNG","EURUSD=X","EFA","VXX","SLV"]', "impact_score": 0.98, + }, + { + "name": "Attaque Hamas — Israël (Opération Déluge Al-Aqsa)", + "start_date": "2023-10-07", "end_date": "2024-06-30", + "level": "long", "category": "geopolitique", + "description": "Attaque terroriste du Hamas sur Israël — 1200 morts. Début guerre Gaza. Crainte d'escalade régionale impliquant l'Iran. Prime de risque géopolitique sur pétrole, gold, VIX. Moyen-Orient en tension maximale.", + "affected_assets": '["GLD","USO","VXX","SLV"]', "impact_score": 0.90, + }, + { + "name": "Iran vs Israël — Attaque directe (drones + missiles)", + "start_date": "2024-04-13", "end_date": None, + "level": "medium", "category": "geopolitique", + "description": "Iran lance 300+ drones et missiles sur Israël — première attaque directe de l'Iran. Israel intercepte ~99%. Crainte d'escalade régionale. Pétrole +3%, Gold +2%, VIX +15%. Finalement absorbé rapidement.", + "affected_assets": '["GLD","USO","VXX","EURUSD=X"]', "impact_score": 0.85, + }, + { + "name": "Donald Trump — Retour à la Maison Blanche (tarifs douaniers)", + "start_date": "2025-01-20", "end_date": "2025-12-31", + "level": "long", "category": "geopolitique", + "description": "Investiture de Donald Trump. Annonce de tarifs douaniers massifs (10-25% sur Chine, 25% Canada/Mexique). Guerre commerciale mondiale. Dollar fort, dollar canadien/mexicain sous pression. Inflation importée attendue.", + "affected_assets": '["SPY","QQQ","EEM","EURUSD=X","GBPUSD=X","GLD","BTC-USD","USO"]', "impact_score": 0.90, + }, + { + "name": "Choc tarifaire Trump — Liberation Day (tarifs réciproques)", + "start_date": "2025-04-02", "end_date": "2025-04-09", + "level": "medium", "category": "geopolitique", + "description": "Trump annonce tarifs réciproques massifs le 2 avril. SPY -5% sur la semaine. Crainte de récession + inflation. HYG spreads élargissent. TLT flight-to-quality. Gold résistant. Pause annoncée 9 avril (sauf Chine).", + "affected_assets": '["SPY","QQQ","IWM","TLT","VXX","GLD","EURUSD=X"]', "impact_score": 0.92, + }, + + # ── OPEC / Énergie ──────────────────────────────────────────────────────── + { + "name": "OPEP+ — Réduction surprise production -1.16M b/j", + "start_date": "2023-04-02", "end_date": None, + "level": "medium", "category": "macro", + "description": "OPEP+ annonce réduction volontaire surprise de 1.16M barils/jour. Brent +8% en une séance. Crainte de relance inflationniste. Profite à XOM, ExxonMobil et à GLD (inflation). Mauvaise surprise pour les banques centrales.", + "affected_assets": '["USO","XOM","GLD","EURUSD=X","SPY"]', "impact_score": 0.85, + }, + { + "name": "OPEP+ — Extension coupes production jusqu'à fin 2025", + "start_date": "2024-11-05", "end_date": "2025-12-31", + "level": "long", "category": "macro", + "description": "OPEP+ annonce extension des coupes de production jusqu'à fin 2025. Pétrole soutenu autour de 70-80$. Soutien pour les majors pétrolières. Impact modéré car compensé par hausse production US (schiste).", + "affected_assets": '["USO","XOM","GLD","EEM"]', "impact_score": 0.70, + }, + + # ── BOJ / Yen ───────────────────────────────────────────────────────────── + { + "name": "BOJ — Surprise élargissement YCC ±50pb (début fin des taux négatifs)", + "start_date": "2022-12-20", "end_date": None, + "level": "medium", "category": "macro", + "description": "BOJ surprend le marché en élargissant la bande de contrôle des taux (YCC) de ±25pb à ±50pb. Premier signe de la fin de la politique ultra-accommodante. Yen +3% instantanément. USD/JPY chute de 137 à 131.", + "affected_assets": '["USDJPY=X","TLT","GLD","EFA"]', "impact_score": 0.85, + }, + { + "name": "BOJ — Fin des taux négatifs (ZIRP terminé)", + "start_date": "2024-03-19", "end_date": None, + "level": "long", "category": "macro", + "description": "BOJ relève ses taux pour la première fois depuis 2007. Fin de la politique des taux négatifs (ZIRP). Taux 0-0.1%. Signal historique pour le yen. USD/JPY perd 1.5%. Carry trade commence à se dénouer progressivement.", + "affected_assets": '["USDJPY=X","TLT","GLD","EFA","EURUSD=X"]', "impact_score": 0.88, + }, + { + "name": "BOJ — Hausse surprise +15pb (débâcle carry trade Yen)", + "start_date": "2024-07-31", "end_date": "2024-08-05", + "level": "medium", "category": "macro", + "description": "BOJ hausse ses taux à 0.25% surprise. Yen +7% en 48h. USD/JPY 161 → 141. Débâcle du carry trade yen: VIX spike à 65, SPY -8%, Nikkei -12%. Crise de liquidité brève mais intense. La plus forte correction des marchés de 2024.", + "affected_assets": '["USDJPY=X","SPY","QQQ","VXX","GLD","EFA"]', "impact_score": 0.92, + }, + + # ── IA / Tech ───────────────────────────────────────────────────────────── + { + "name": "Lancement ChatGPT — Révolution IA générative (OpenAI)", + "start_date": "2022-11-30", "end_date": "2023-06-30", + "level": "long", "category": "macro", + "description": "OpenAI lance ChatGPT. 1 million d'utilisateurs en 5 jours. Début de la révolution IA générative. Reclassification de NVIDIA comme infrastructure critique. Accélération des investissements capex IA des hyperscalers.", + "affected_assets": '["NVDA","QQQ","AAPL","SPY"]', "impact_score": 0.95, + }, + { + "name": "NVIDIA Earnings — Guidance blow-out AI (H100 en pénurie)", + "start_date": "2023-05-24", "end_date": None, + "level": "medium", "category": "macro", + "description": "NVIDIA guide pour 11Md$ au Q2 2023 vs 7.2Md$ attendu. Catalyseur IA : la demande GPU H100 explose. NVDA +25% en une séance. Début du mega bull run IA. Entraîne QQQ et les valeurs tech.", + "affected_assets": '["NVDA","QQQ","AAPL","SPY"]', "impact_score": 0.88, + }, + { + "name": "NVIDIA — Passage en trillion de dollars (capex IA hyperscalers)", + "start_date": "2024-06-05", "end_date": None, + "level": "medium", "category": "macro", + "description": "NVIDIA earnings record : revenus data center +427% YoY. L'IA capex des hyperscalers (Amazon, Microsoft, Google) explose. NVDA dépasse Apple brièvement en capitalisation. Confirmation du supercycle IA.", + "affected_assets": '["NVDA","QQQ","AAPL","SPY"]', "impact_score": 0.85, + }, + + # ── Crypto ──────────────────────────────────────────────────────────────── + { + "name": "Approbation ETF Bitcoin spot US (BlackRock, Fidelity)", + "start_date": "2024-01-10", "end_date": None, + "level": "long", "category": "macro", + "description": "SEC approuve 11 ETF Bitcoin spot dont BlackRock (IBIT) et Fidelity (FBTC). Événement historique pour l'adoption institutionnelle du Bitcoin. BTC +20% en 2 semaines. Afflux de capitaux via ETF : >10Md$ en 1 mois.", + "affected_assets": '["BTC-USD","SPY"]', "impact_score": 0.92, + }, + { + "name": "Halving Bitcoin 2024 — Réduction récompense de bloc 50%", + "start_date": "2024-04-20", "end_date": None, + "level": "medium", "category": "macro", + "description": "4e halving Bitcoin : récompense mineurs passe de 6.25 à 3.125 BTC. Offre réduite de 50%. Catalyseur historique de bull markets. Combiné aux ETF, pression acheteuse sur BTC s'intensifie.", + "affected_assets": '["BTC-USD"]', "impact_score": 0.80, + }, + + # ── Crise bancaire ──────────────────────────────────────────────────────── + { + "name": "Effondrement Silicon Valley Bank (SVB) — Crise bancaire US", + "start_date": "2023-03-10", "end_date": "2023-03-31", + "level": "medium", "category": "geopolitique", + "description": "SVB fait faillite, la 2e plus grande banque US à s'effondrer. Panique bancaire régionale. Crédit Suisse sous pression (repris par UBS). Fed crée BTFP pour fournir liquidités. IWM et HYG sous pression. GLD refuge.", + "affected_assets": '["GLD","IWM","HYG","VXX","TLT","SPY"]', "impact_score": 0.88, + }, + + # ── Chine / EM ──────────────────────────────────────────────────────────── + { + "name": "Xi Jinping — 3e mandat historique (Congrès PCC)", + "start_date": "2022-10-16", "end_date": None, + "level": "long", "category": "geopolitique", + "description": "Congrès du PCC confirme Xi Jinping pour un 3e mandat sans précédent. Consolidation du pouvoir. Inquiétudes sur l'économie Chine, immobilier (Evergrande), politique Zéro Covid. EEM sous forte pression. Dollar Offshore CNH affaibli.", + "affected_assets": '["EEM","AAPL","QQQ","EURUSD=X"]', "impact_score": 0.80, + }, + { + "name": "Stimulus Chine — PBOC annonce relance économique massive", + "start_date": "2024-09-24", "end_date": "2024-10-31", + "level": "medium", "category": "macro", + "description": "PBOC annonce un package de relance économique surprise : baisse RRR, soutien immobilier, liquidités injectées. EEM +15% en 10 jours. Cuivre, pétrole, matières premières en hausse. Signal de redémarrage Chine.", + "affected_assets": '["EEM","USO","GLD","SLV","EURUSD=X"]', "impact_score": 0.85, + }, +] + + +def bootstrap_macro_events(force: bool = False) -> Dict[str, Any]: + """ + Insert the curated macro events into market_events table. + Skips events whose name already exists in the DB (idempotent). + Pass force=True to wipe existing bootstrapped events and re-insert. + Returns: {inserted, skipped, total} + """ + from services.database import get_all_market_events, save_market_event + + # Build set of existing event names for deduplication + existing_events = get_all_market_events() + existing_names = {ev.get("name", "") for ev in existing_events} + + if force: + # Delete all existing events that match our bootstrap names + from services.database import delete_market_event + deleted = 0 + for ev in existing_events: + if ev.get("name") in {e["name"] for e in MACRO_EVENTS}: + delete_market_event(ev["id"]) + deleted += 1 + existing_names = set() + logger.info(f"[macro_bootstrap] Force=True: deleted {deleted} existing events") + + inserted = 0 + skipped = 0 + errors = 0 + import json + for ev in MACRO_EVENTS: + if ev["name"] in existing_names: + skipped += 1 + continue + try: + assets_raw = ev.get("affected_assets", "[]") + assets_list = json.loads(assets_raw) if isinstance(assets_raw, str) else assets_raw + save_market_event({ + "name": ev["name"], + "start_date": ev["start_date"], + "end_date": ev.get("end_date"), + "level": ev["level"], + "category": ev["category"], + "description": ev["description"], + "market_impact": ev["description"][:80], + "affected_assets": assets_list, + "impact_score": ev.get("impact_score", 0.75), + }) + inserted += 1 + except Exception as e: + logger.warning(f"[macro_bootstrap] Failed to insert {ev['name'][:40]}: {e}") + errors += 1 + + from services.database import count_market_events + total = count_market_events() + logger.info(f"[macro_bootstrap] Inserted {inserted}, skipped {skipped}, {errors} errors. Total: {total}") + return {"inserted": inserted, "skipped": skipped, "errors": errors, "total": total} diff --git a/frontend/src/pages/InstrumentDashboard.tsx b/frontend/src/pages/InstrumentDashboard.tsx index bc534ca..f67425f 100644 --- a/frontend/src/pages/InstrumentDashboard.tsx +++ b/frontend/src/pages/InstrumentDashboard.tsx @@ -44,11 +44,22 @@ interface TrendMetrics { current_price: number; high_52w: number; low_52w: number } +interface MacroRegime { + dominant: string + label: string + color: string + emoji: string + scores: Record + ranked: string[] + asset_bias: Record +} + interface Snapshot { instrument: InstrumentConfig price_data: PriceCandle[] indicators: Record regime: { current: string; confidence: number; scores: Record; signals: RegimeSignals } + macro_regime: MacroRegime trend: TrendMetrics events: SnapshotEvent[] current_price: number; change_pct: number; change_abs: number; period: string @@ -103,12 +114,31 @@ function pctN(a: number | undefined, b: number | undefined): number { return ((a - b) / b) * 100 } +// ── Macro regime colour mapping ─────────────────────────────────────────────── + +const MACRO_COLOR_MAP: Record = { + goldilocks: 'emerald', + desinflation: 'cyan', + soft_landing: 'blue', + reflation: 'amber', + stagflation: 'orange', + inflation_shock: 'red', + recession: 'red', + crise_liquidite: 'red', + incertain: 'slate', +} + +function macroRegimeColor(dominant: string): string { + return MACRO_COLOR_MAP[dominant] ?? 'slate' +} + // ── RegimeCard (métriques de signaux) ───────────────────────────────────────── function RegimeCard({ - regime, signalsAt, dateLabel, + regime, macroRegime, signalsAt, dateLabel, }: { regime: Snapshot['regime'] + macroRegime: MacroRegime | null signalsAt: RegimeSignals | null dateLabel: string }) { @@ -121,6 +151,8 @@ function RegimeCard({ orange: 'border-orange-700/40 bg-orange-950/30', slate: 'border-slate-700/40 bg-slate-900/30', blue: 'border-blue-700/40 bg-blue-950/30', + cyan: 'border-cyan-700/40 bg-cyan-950/30', + amber: 'border-amber-700/40 bg-amber-950/30', } // 6 signal metrics displayed as a compact grid @@ -163,20 +195,55 @@ function RegimeCard({ }, ] + // Macro regime display + const macroCol = macroRegime ? macroRegimeColor(macroRegime.dominant) : 'slate' + const top3Macro = macroRegime?.ranked?.slice(0, 3) ?? [] + return (
{/* Header */}
- Régime Détecté + Régime
{dateLabel}
- {/* Regime label + confidence */} + {/* Macro regime section (global cycle) */} + {macroRegime && macroRegime.dominant !== 'incertain' && ( +
+
+ Cycle macro global + Fed cycle +
+
+ {macroRegime.emoji || '🌍'} + {macroRegime.label || macroRegime.dominant} +
+ {top3Macro.length > 0 && ( +
+ {top3Macro.map((key, i) => { + const score = macroRegime.scores?.[key] + const c = macroRegimeColor(key) + return ( + + {key} {score !== undefined ? Math.round(score) + '%' : ''} + + ) + })} +
+ )} +
+ )} + + {/* Technical regime label + confidence */}
-
{regime.current}
+
Régime technique
+
{regime.current}
b.weight - a.weight).slice(0, 4) + // Events matched by ANY driver (for fallback row) + const allKeywords = topDrivers.flatMap(d => d.keywords ?? []) + const matchedAnyIdxs = new Set( + events.map((ev, i) => eventMatchesDriver(ev, allKeywords) ? i : -1).filter(i => i >= 0) + ) + const unmatchedEvs = events.filter((_ev, i) => !matchedAnyIdxs.has(i)) + return (
Événements par driver
@@ -453,6 +527,34 @@ function EventTimelineStrip({
) })} + + {/* Fallback row: events not matched by any driver keyword */} + {unmatchedEvs.length > 0 && ( +
+ Signaux tech. +
+ {unmatchedEvs.map((ev, i) => { + const left = leftPct(ev.date) + const width = widthPct(ev.date, ev.end_date) + return ( +
+ {width > 3 && ( + + {ev.title} + + )} +
+ ) + })} +
+
+ )}
) @@ -834,6 +936,7 @@ export default function InstrumentDashboard() {