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:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user