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:
OpenSquared
2026-06-22 14:00:35 +02:00
parent 3edbd6b0b7
commit d178615c74
6 changed files with 346 additions and 29 deletions

View File

@@ -576,6 +576,29 @@ def init_db():
except Exception:
pass
c.execute("""CREATE TABLE IF NOT EXISTS economic_events (
id INTEGER PRIMARY KEY AUTOINCREMENT,
event_name TEXT NOT NULL,
series_id TEXT NOT NULL,
event_date TEXT NOT NULL,
actual_value REAL,
actual_unit TEXT DEFAULT '',
forecast_value REAL,
previous_value REAL,
surprise_pct REAL,
surprise_zscore REAL,
surprise_direction TEXT DEFAULT 'neutral',
assets_impacted TEXT DEFAULT '[]',
source TEXT DEFAULT 'FRED',
fetched_at TEXT NOT NULL DEFAULT (datetime('now')),
UNIQUE(series_id, event_date)
)""")
try:
c.execute("CREATE INDEX IF NOT EXISTS idx_econ_date ON economic_events(event_date DESC)")
c.execute("CREATE INDEX IF NOT EXISTS idx_econ_series ON economic_events(series_id, event_date DESC)")
except Exception:
pass
try:
c.execute("CREATE INDEX IF NOT EXISTS idx_kb_category ON knowledge_base(category, status)")
c.execute("CREATE INDEX IF NOT EXISTS idx_rs_version ON reasoning_state(version DESC)")
@@ -3742,3 +3765,99 @@ def get_latest_trade_assessments(limit: int = 20) -> List[Dict[str, Any]]:
d["issues"] = []
result.append(d)
return result
# ── Economic events (Phase 2) ─────────────────────────────────────────────────
def save_economic_event(
event_name: str,
series_id: str,
event_date: str,
actual_value: Optional[float],
actual_unit: str = "",
forecast_value: Optional[float] = None,
previous_value: Optional[float] = None,
surprise_pct: Optional[float] = None,
surprise_zscore: Optional[float] = None,
surprise_direction: str = "neutral",
assets_impacted: Optional[List] = None,
source: str = "FRED",
) -> Optional[int]:
conn = get_conn()
try:
conn.execute(
"""INSERT INTO economic_events
(event_name, series_id, event_date, actual_value, actual_unit,
forecast_value, previous_value, surprise_pct, surprise_zscore,
surprise_direction, assets_impacted, source)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?)
ON CONFLICT(series_id, event_date) DO UPDATE SET
actual_value=excluded.actual_value,
forecast_value=excluded.forecast_value,
previous_value=excluded.previous_value,
surprise_pct=excluded.surprise_pct,
surprise_zscore=excluded.surprise_zscore,
surprise_direction=excluded.surprise_direction,
fetched_at=datetime('now')""",
(
event_name, series_id, event_date, actual_value, actual_unit,
forecast_value, previous_value, surprise_pct, surprise_zscore,
surprise_direction,
json.dumps(assets_impacted or []),
source,
),
)
conn.commit()
return conn.execute("SELECT last_insert_rowid()").fetchone()[0]
finally:
conn.close()
def get_recent_economic_surprises(days: int = 30, min_zscore: float = 0.0) -> List[Dict[str, Any]]:
conn = get_conn()
try:
cutoff = (datetime.utcnow() - timedelta(days=days)).strftime("%Y-%m-%d")
rows = conn.execute(
"""SELECT * FROM economic_events
WHERE event_date >= ?
AND ABS(COALESCE(surprise_zscore, 0)) >= ?
ORDER BY event_date DESC, ABS(COALESCE(surprise_zscore, 0)) DESC
LIMIT 20""",
(cutoff, min_zscore),
).fetchall()
result = []
for r in rows:
d = dict(r)
try:
d["assets_impacted"] = json.loads(d.get("assets_impacted") or "[]")
except Exception:
d["assets_impacted"] = []
result.append(d)
return result
finally:
conn.close()
def get_economic_events_for_calendar(days_back: int = 30, days_forward: int = 14) -> List[Dict[str, Any]]:
"""Return stored economic events for calendar enrichment."""
conn = get_conn()
try:
cutoff_past = (datetime.utcnow() - timedelta(days=days_back)).strftime("%Y-%m-%d")
cutoff_future = (datetime.utcnow() + timedelta(days=days_forward)).strftime("%Y-%m-%d")
rows = conn.execute(
"""SELECT * FROM economic_events
WHERE event_date BETWEEN ? AND ?
ORDER BY event_date DESC""",
(cutoff_past, cutoff_future),
).fetchall()
result = []
for r in rows:
d = dict(r)
try:
d["assets_impacted"] = json.loads(d.get("assets_impacted") or "[]")
except Exception:
d["assets_impacted"] = []
result.append(d)
return result
finally:
conn.close()