feat: Phase 2 — economic event surprise tracker (FRED actuals + z-score)
- economic_events table in DB (series_id, actual, forecast_baseline, surprise_pct, surprise_zscore, direction) - DB helpers: save_economic_event(), get_recent_economic_surprises(), get_economic_events_for_calendar() - fred_fetcher.py: _compute_zscore_surprise() computes 12-period MA as implied consensus + z-score deviation; save_fred_releases_to_db() persists releases per cycle; build_economic_surprise_block() formats significant surprises for AI prompt - auto_cycle.py: saves FRED releases to economic_events each cycle, appends surprise block to fred_block for injection into both suggestion and scoring prompts - data_fetcher.py: get_economic_calendar() now merges static upcoming events with past FRED actuals from DB (Prev/Fcst/Actual/z-score fields populated) - CalendarPage.tsx: past events show colored z-score badge (⚡ for |z|≥1.5, bullish/bearish colors) - EconomicEvent type: added surprise_zscore, surprise_direction, source fields Activates automatically once fred_api_key is set in Configuration. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -255,49 +255,99 @@ def extract_tags(text: str) -> List[str]:
|
||||
|
||||
|
||||
# ── Economic calendar (using free Trading Economics RSS or static) ────────────
|
||||
_FRED_SERIES_TO_CALENDAR = {
|
||||
"PAYEMS": "US Non-Farm Payrolls",
|
||||
"CPIAUCSL": "US CPI (Consumer Price Index)",
|
||||
"UNRATE": "US Unemployment Rate",
|
||||
"GDP": "US GDP (Preliminary)",
|
||||
"ICSA": "US Initial Jobless Claims",
|
||||
"FEDFUNDS": "Fed Funds Rate (FOMC)",
|
||||
"T10Y2Y": "10Y-2Y Yield Spread",
|
||||
}
|
||||
|
||||
|
||||
def get_economic_calendar() -> List[Dict[str, Any]]:
|
||||
"""Return next 30 days of major economic events (static + scraped)."""
|
||||
"""Return recent + upcoming major economic events, enriched with FRED actuals from DB."""
|
||||
from datetime import date, timedelta
|
||||
today = date.today()
|
||||
events = [
|
||||
{"title": "US Non-Farm Payrolls", "country": "US", "importance": "high",
|
||||
|
||||
upcoming_static = [
|
||||
{"title": "US Non-Farm Payrolls", "country": "US", "importance": "high",
|
||||
"date": (today + timedelta(days=(4 - today.weekday()) % 7 + 7)).isoformat(),
|
||||
"asset_impact": ["indices", "forex", "rates"]},
|
||||
{"title": "US CPI (Consumer Price Index)", "country": "US", "importance": "high",
|
||||
{"title": "US CPI (Consumer Price Index)", "country": "US", "importance": "high",
|
||||
"date": (today + timedelta(days=12)).isoformat(),
|
||||
"asset_impact": ["indices", "forex", "metals"]},
|
||||
{"title": "FOMC Meeting / Fed Rate Decision", "country": "US", "importance": "high",
|
||||
{"title": "FOMC Meeting / Fed Rate Decision","country": "US", "importance": "high",
|
||||
"date": (today + timedelta(days=18)).isoformat(),
|
||||
"asset_impact": ["indices", "forex", "metals", "energy"]},
|
||||
{"title": "ECB Rate Decision", "country": "EU", "importance": "high",
|
||||
{"title": "ECB Rate Decision", "country": "EU", "importance": "high",
|
||||
"date": (today + timedelta(days=20)).isoformat(),
|
||||
"asset_impact": ["forex", "indices"]},
|
||||
{"title": "US GDP (Preliminary)", "country": "US", "importance": "high",
|
||||
{"title": "US GDP (Preliminary)", "country": "US", "importance": "high",
|
||||
"date": (today + timedelta(days=25)).isoformat(),
|
||||
"asset_impact": ["indices", "forex"]},
|
||||
{"title": "OPEC+ Meeting", "country": "Global", "importance": "high",
|
||||
{"title": "OPEC+ Meeting", "country": "Global", "importance": "high",
|
||||
"date": (today + timedelta(days=14)).isoformat(),
|
||||
"asset_impact": ["energy"]},
|
||||
{"title": "US Crude Oil Inventories (EIA)", "country": "US", "importance": "medium",
|
||||
{"title": "US Crude Oil Inventories (EIA)", "country": "US", "importance": "medium",
|
||||
"date": (today + timedelta(days=3)).isoformat(),
|
||||
"asset_impact": ["energy"]},
|
||||
{"title": "EU Inflation (CPI)", "country": "EU", "importance": "medium",
|
||||
{"title": "EU Inflation (CPI)", "country": "EU", "importance": "medium",
|
||||
"date": (today + timedelta(days=8)).isoformat(),
|
||||
"asset_impact": ["forex", "indices"]},
|
||||
{"title": "China Trade Balance", "country": "CN", "importance": "medium",
|
||||
{"title": "China Trade Balance", "country": "CN", "importance": "medium",
|
||||
"date": (today + timedelta(days=10)).isoformat(),
|
||||
"asset_impact": ["metals", "agriculture", "forex"]},
|
||||
{"title": "US Unemployment Claims", "country": "US", "importance": "medium",
|
||||
{"title": "US Unemployment Claims", "country": "US", "importance": "medium",
|
||||
"date": (today + timedelta(days=2)).isoformat(),
|
||||
"asset_impact": ["forex", "indices"]},
|
||||
{"title": "USDA Crop Report", "country": "US", "importance": "medium",
|
||||
{"title": "USDA Crop Report", "country": "US", "importance": "medium",
|
||||
"date": (today + timedelta(days=6)).isoformat(),
|
||||
"asset_impact": ["agriculture"]},
|
||||
{"title": "G7 Summit", "country": "Global", "importance": "high",
|
||||
{"title": "G7 Summit", "country": "Global", "importance": "high",
|
||||
"date": (today + timedelta(days=30)).isoformat(),
|
||||
"asset_impact": ["forex", "indices", "metals"]},
|
||||
]
|
||||
return sorted(events, key=lambda x: x["date"])
|
||||
|
||||
# Merge past FRED actuals from DB
|
||||
try:
|
||||
from services.database import get_economic_events_for_calendar
|
||||
db_events = get_economic_events_for_calendar(days_back=45, days_forward=0)
|
||||
past_events = []
|
||||
for ev in db_events:
|
||||
actual = ev.get("actual_value")
|
||||
forecast = ev.get("forecast_value")
|
||||
previous = ev.get("previous_value")
|
||||
unit = ev.get("actual_unit", "")
|
||||
zscore = ev.get("surprise_zscore")
|
||||
past_events.append({
|
||||
"title": ev.get("event_name", ev.get("series_id", "")),
|
||||
"country": "US",
|
||||
"importance": "high" if abs(zscore or 0) >= 1.5 else "medium",
|
||||
"date": ev.get("event_date", ""),
|
||||
"asset_impact": ev.get("assets_impacted", []),
|
||||
"actual": f"{actual:.2f}{unit}" if actual is not None else None,
|
||||
"forecast": f"{forecast:.2f}{unit}" if forecast is not None else None,
|
||||
"previous": f"{previous:.2f}{unit}" if previous is not None else None,
|
||||
"surprise_zscore": zscore,
|
||||
"surprise_direction": ev.get("surprise_direction"),
|
||||
"source": "FRED",
|
||||
})
|
||||
except Exception:
|
||||
past_events = []
|
||||
|
||||
all_events = past_events + upcoming_static
|
||||
# Deduplicate by date+title (past events take precedence)
|
||||
seen = set()
|
||||
result = []
|
||||
for ev in sorted(all_events, key=lambda x: x["date"], reverse=True):
|
||||
key = (ev["date"], ev["title"][:20])
|
||||
if key not in seen:
|
||||
seen.add(key)
|
||||
result.append(ev)
|
||||
|
||||
return sorted(result, key=lambda x: x["date"])
|
||||
|
||||
|
||||
# ── Macro Gauges & Scenario Scoring ──────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user