feat: macro regime + 30 historical events + regime confidence fix

- 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 <noreply@anthropic.com>
This commit is contained in:
OpenSquared
2026-06-24 23:24:24 +02:00
parent aa81598278
commit aec9ced74f
4 changed files with 495 additions and 17 deletions

View File

@@ -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

View File

@@ -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,

View File

@@ -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}

View File

@@ -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<string, number>
ranked: string[]
asset_bias: Record<string, any>
}
interface Snapshot {
instrument: InstrumentConfig
price_data: PriceCandle[]
indicators: Record<string, LinePoint[]>
regime: { current: string; confidence: number; scores: Record<string, number>; 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<string, string> = {
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 (
<div className={clsx('rounded-xl border p-4 space-y-3', borderMap[col])}>
{/* Header */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<BarChart2 className={clsx('w-4 h-4', `text-${col}-400`)} />
<span className="text-xs font-semibold text-slate-400 uppercase tracking-wide">Régime Détecté</span>
<span className="text-xs font-semibold text-slate-400 uppercase tracking-wide">Régime</span>
</div>
<span className="text-xs text-slate-600">{dateLabel}</span>
</div>
{/* Regime label + confidence */}
{/* Macro regime section (global cycle) */}
{macroRegime && macroRegime.dominant !== 'incertain' && (
<div className="rounded-lg border border-slate-700/30 bg-dark-700/40 px-3 py-2 space-y-1.5">
<div className="flex items-center justify-between">
<span className="text-xs text-slate-500 uppercase tracking-wide">Cycle macro global</span>
<span className="text-xs text-slate-600">Fed cycle</span>
</div>
<div className="flex items-center gap-2">
<span className="text-base">{macroRegime.emoji || '🌍'}</span>
<span className={clsx('text-sm font-bold', `text-${macroCol}-300`)}>{macroRegime.label || macroRegime.dominant}</span>
</div>
{top3Macro.length > 0 && (
<div className="flex flex-wrap gap-1 pt-0.5">
{top3Macro.map((key, i) => {
const score = macroRegime.scores?.[key]
const c = macroRegimeColor(key)
return (
<span key={key} className={clsx(
'text-xs px-1.5 py-0.5 rounded border',
i === 0 ? `text-${c}-300 bg-${c}-900/30 border-${c}-700/40` : 'text-slate-500 bg-dark-800/40 border-slate-700/30'
)}>
{key} {score !== undefined ? Math.round(score) + '%' : ''}
</span>
)
})}
</div>
)}
</div>
)}
{/* Technical regime label + confidence */}
<div className={clsx('rounded-lg px-3 py-2', `bg-${col}-900/20`)}>
<div className={clsx('text-base font-bold leading-tight', `text-${col}-300`)}>{regime.current}</div>
<div className="text-xs text-slate-500 mb-0.5">Régime technique</div>
<div className={clsx('text-sm font-bold leading-tight', `text-${col}-300`)}>{regime.current}</div>
<div className="flex items-center gap-2 mt-1">
<div className="flex-1 h-1.5 bg-slate-800 rounded-full overflow-hidden">
<div className={clsx('h-full rounded-full', `bg-${col}-500`)}
@@ -409,6 +476,13 @@ function EventTimelineStrip({
// Top 4 drivers by weight
const topDrivers = [...drivers].sort((a, b) => 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 (
<div className="bg-dark-900/40 rounded-xl border border-slate-700/40 px-4 py-3">
<div className="text-xs text-slate-500 uppercase tracking-wide mb-2.5">Événements par driver</div>
@@ -453,6 +527,34 @@ function EventTimelineStrip({
</div>
)
})}
{/* Fallback row: events not matched by any driver keyword */}
{unmatchedEvs.length > 0 && (
<div className="flex items-center gap-2 h-6">
<span className="text-xs font-semibold shrink-0 truncate w-28 text-slate-500"
style={{ fontSize: 10 }}>Signaux tech.</span>
<div className="relative flex-1 h-full" style={{ paddingRight: 62 }}>
{unmatchedEvs.map((ev, i) => {
const left = leftPct(ev.date)
const width = widthPct(ev.date, ev.end_date)
return (
<div key={i}
title={`${ev.title}\n${ev.date}${ev.end_date ? ' → ' + ev.end_date : ''}\n${ev.description ?? ''}`}
className="absolute top-0 h-full rounded border overflow-hidden bg-slate-700/40 border-slate-600/40"
style={{ left: `${left}%`, width: `${width}%` }}
>
{width > 3 && (
<span className="absolute inset-0 flex items-center px-1 truncate pointer-events-none text-slate-400"
style={{ fontSize: 9 }}>
{ev.title}
</span>
)}
</div>
)
})}
</div>
</div>
)}
</div>
</div>
)
@@ -834,6 +936,7 @@ export default function InstrumentDashboard() {
<div className="grid grid-cols-3 gap-4">
<RegimeCard
regime={snapshot.regime}
macroRegime={snapshot.macro_regime ?? null}
signalsAt={dateSignals}
dateLabel={dateLabel}
/>