feat: causal lab
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user