diff --git a/backend/services/market_event_detector.py b/backend/services/market_event_detector.py index 36419d3..513fe10 100644 --- a/backend/services/market_event_detector.py +++ b/backend/services/market_event_detector.py @@ -3,7 +3,7 @@ Isolated cycle action: Check New Market Events. Scans 4 sources and creates market_events for significant findings: - news : geopolitical/macro news (RSS feeds, rule-scored) - - eco : FRED economic releases with high surprise z-score + - eco : ff_calendar releases with high surprise % (toutes devises, USD inclus) - technical: configurable signal catalog driven by Technical Desk - reports : institutional reports (COT, EIA) with high importance @@ -347,7 +347,7 @@ def _check_ff_calendar_surprises( ) -> List[Dict]: """ Detect surprising releases in ff_calendar for the given currencies. - Used when currencies other than USD are configured (FRED only covers USD). + Détecte les releases surprenantes dans ff_calendar pour toutes les devises données (USD inclus). """ from services.database import get_conn @@ -483,129 +483,20 @@ def _check_eco(desk_cfg: Dict[str, Any]) -> List[Dict[str, Any]]: existing = _existing_event_keys() created: List[Dict] = [] - # ── Local economic_events table (replaces FRED API call) ───────────────── - if "USD" in currencies: - try: - conn = get_conn() - rows = conn.execute( - """SELECT * FROM economic_events - WHERE event_date >= ? AND event_date <= ? - AND ABS(COALESCE(surprise_zscore, 0)) >= ? - ORDER BY event_date DESC, ABS(COALESCE(surprise_zscore, 0)) DESC - LIMIT 50""", - (date_from, date_to, z_threshold), - ).fetchall() - conn.close() - releases = [] - for r in rows: - d = dict(r) - try: - d["assets_impacted"] = json.loads(d.get("assets_impacted") or "[]") - except Exception: - d["assets_impacted"] = [] - releases.append(d) - except Exception as e: - logger.warning(f"[check_events/eco] local query failed: {e}") - releases = [] - - for rel in releases: - s_id = rel.get("series_id", "") - impact = _SERIES_IMPACT.get(s_id, "low") - if _IMPACT_RANKS.get(impact, 1) < min_rank: - continue - - z = abs(rel.get("surprise_zscore") or 0) - s_pct = rel.get("surprise_pct") or 0 - ev_date = (rel.get("event_date") or "")[:10] - ev_name_base = rel.get("event_name", s_id) - direction = rel.get("surprise_direction", "neutral") - - sign = "+" if s_pct >= 0 else "" - ev_name = f"{ev_name_base} — Surprise {sign}{s_pct:.1f}% ({ev_date[:7]})" - - if _is_dup(ev_name, existing): - continue - - # Lookback context from FRED history - context_str = "" - if lookback_releases > 0 and s_id: - try: - conn = get_conn() - hist = conn.execute( - """SELECT event_date, actual_value, forecast_value - FROM economic_events - WHERE series_id = ? AND event_date < ? - ORDER BY event_date DESC LIMIT ?""", - (s_id, ev_date, lookback_releases), - ).fetchall() - conn.close() - if hist: - context_str = " Historique récent: " + ", ".join( - f"{r[0][:7]}: réel={r[1]} consensus={r[2]}" for r in hist - ) - except Exception: - pass - - sub_type = SUBTYPE_FROM_SERIES.get(s_id, s_id[:10]) if s_id else ev_name_base[:10] - level = "long" if z >= 3 else ("medium" if z >= 2 else "short") - assets = rel.get("assets_impacted") or [] - if isinstance(assets, str): - try: - assets = json.loads(assets) - except Exception: - assets = [] - - source_ref = { - "title": f"FRED release: {ev_name_base} ({ev_date})", - "source": "FRED", - "url": f"https://fred.stlouisfed.org/series/{s_id}" if s_id else "", - "date": ev_date, - "original_score": round(min(0.95, 0.35 + z * 0.15), 3), - } - - ev = { - "name": ev_name, - "start_date": ev_date, - "level": level, - "category": "event_calendar", - "sub_type": sub_type, - "description": ( - f"Surprise {direction} {sign}{s_pct:.1f}% vs baseline " - f"(z-score: {z:.1f}σ). " - f"Réel: {rel.get('actual_value', '?')} {rel.get('actual_unit', '')} " - f"/ Prévision: {rel.get('forecast_value', '?')}." - + context_str - ), - "market_impact": "", - "affected_assets": assets, - "impact_score": min(0.95, 0.35 + z * 0.15), - "actual_value": str(rel.get("actual_value", "")), - "expected_value": str(rel.get("forecast_value", "")), - "surprise_pct": float(s_pct), - "source_refs": [source_ref], - "origin": "detector_eco", - } - if create_evt: - result = _save_and_evaluate(ev, existing) - if result: - result["source"] = "eco" - created.append(result) - else: - logger.info(f"[check_events/eco] create_market_event=False — skipping: {ev_name}") - - # ── ff_calendar path (non-USD currencies) ──────────────────────────────── - non_usd = [c for c in currencies if c != "USD"] - if non_usd: - created += _check_ff_calendar_surprises( - currencies=non_usd, - min_impact=min_impact, - date_from=date_from, - date_to=date_to, - min_surprise_pct=ff_surprise_min, - lookback_releases=lookback_releases, - create_evt=create_evt, - existing=existing, - ) + # ── ff_calendar — source unique pour toutes les devises (USD inclus) ────── + # Anciennement : USD → economic_events (FRED), autres → ff_calendar. + # Désormais ff_calendar couvre toutes les devises avec forecast + actual, + # donc on unifie sur une seule source cohérente. + created += _check_ff_calendar_surprises( + currencies=currencies, + min_impact=min_impact, + date_from=date_from, + date_to=date_to, + min_surprise_pct=ff_surprise_min, + lookback_releases=lookback_releases, + create_evt=create_evt, + existing=existing, + ) return created