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:
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user