feat: causal lab

This commit is contained in:
OpenSquared
2026-06-28 17:03:37 +02:00
parent 6a683411ed
commit bab09a1b09

View File

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