feat: Impact Monitor — AI impact evaluation for eco events & geopolitical news
- New DB tables: event_categories (18 bootstrap categories) + instrument_impacts - impact_categories_bootstrap.py: FOMC/NFP/CPI/GDP/PCE/ISM/BOJ/ECB/BOE/OPEC+ + 7 géopolitical categories with per-instrument sensitivity & direction defaults - impact_service.py: GPT-4o-mini evaluation (0-1 score, direction, rationale) + monitor data aggregation - routers/impact.py: GET/POST endpoints for evaluate/bulk/monitor/adjust/categories - ImpactMonitor.tsx: two-tab page (Évalués / À évaluer), top instruments bar, inline score override modal, category browser - Startup auto-bootstrap for impact categories - Route /impact + sidebar nav entry Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -766,6 +766,42 @@ def init_db():
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# ── Impact categories (event_calendar + geopolitical default impacts) ────────
|
||||
c.execute("""CREATE TABLE IF NOT EXISTS event_categories (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT UNIQUE NOT NULL,
|
||||
type TEXT NOT NULL,
|
||||
sub_type TEXT DEFAULT '',
|
||||
description TEXT DEFAULT '',
|
||||
default_impacts TEXT DEFAULT '[]',
|
||||
is_ai_generated INTEGER DEFAULT 0,
|
||||
updated_at TEXT DEFAULT (datetime('now'))
|
||||
)""")
|
||||
|
||||
# ── Per-event/news instrument impact assessments ──────────────────────────
|
||||
c.execute("""CREATE TABLE IF NOT EXISTS instrument_impacts (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
source_type TEXT NOT NULL,
|
||||
source_id INTEGER,
|
||||
source_name TEXT NOT NULL,
|
||||
source_date TEXT NOT NULL,
|
||||
category_name TEXT DEFAULT '',
|
||||
instrument_id TEXT NOT NULL,
|
||||
impact_score REAL DEFAULT 0.5,
|
||||
direction TEXT DEFAULT 'neutral',
|
||||
rationale TEXT DEFAULT '',
|
||||
confidence REAL DEFAULT 0.7,
|
||||
ai_generated INTEGER DEFAULT 0,
|
||||
manually_adjusted INTEGER DEFAULT 0,
|
||||
adjusted_score REAL,
|
||||
adjusted_direction TEXT,
|
||||
override_rationale TEXT DEFAULT '',
|
||||
created_at TEXT DEFAULT (datetime('now'))
|
||||
)""")
|
||||
c.execute("CREATE INDEX IF NOT EXISTS idx_ii_source ON instrument_impacts(source_type, source_id)")
|
||||
c.execute("CREATE INDEX IF NOT EXISTS idx_ii_date ON instrument_impacts(source_date)")
|
||||
c.execute("CREATE INDEX IF NOT EXISTS idx_ii_instrument ON instrument_impacts(instrument_id)")
|
||||
|
||||
c.execute("""CREATE TABLE IF NOT EXISTS timeline_context (
|
||||
ref_date TEXT PRIMARY KEY,
|
||||
long_event_id INTEGER REFERENCES market_events(id),
|
||||
@@ -4610,3 +4646,143 @@ def count_market_events() -> int:
|
||||
return conn.execute("SELECT COUNT(*) FROM market_events").fetchone()[0]
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
# ── Event categories ──────────────────────────────────────────────────────────
|
||||
|
||||
def get_all_event_categories() -> List[Dict[str, Any]]:
|
||||
conn = get_conn()
|
||||
try:
|
||||
rows = conn.execute("SELECT * FROM event_categories ORDER BY type, name").fetchall()
|
||||
return [dict(r) for r in rows]
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
def get_event_category(name: str) -> Optional[Dict[str, Any]]:
|
||||
conn = get_conn()
|
||||
try:
|
||||
row = conn.execute("SELECT * FROM event_categories WHERE name=?", (name,)).fetchone()
|
||||
return dict(row) if row else None
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
def upsert_event_category(cat: Dict[str, Any]) -> int:
|
||||
import json as _json
|
||||
conn = get_conn()
|
||||
try:
|
||||
defaults = cat.get("default_impacts", [])
|
||||
if isinstance(defaults, list):
|
||||
defaults = _json.dumps(defaults)
|
||||
row = conn.execute("SELECT id FROM event_categories WHERE name=?", (cat["name"],)).fetchone()
|
||||
if row:
|
||||
conn.execute("""UPDATE event_categories SET
|
||||
type=?, sub_type=?, description=?, default_impacts=?,
|
||||
is_ai_generated=?, updated_at=datetime('now')
|
||||
WHERE name=?""",
|
||||
(cat["type"], cat.get("sub_type",""), cat.get("description",""),
|
||||
defaults, int(cat.get("is_ai_generated", 0)), cat["name"]))
|
||||
conn.commit()
|
||||
return row[0]
|
||||
else:
|
||||
cur = conn.execute("""INSERT INTO event_categories
|
||||
(name, type, sub_type, description, default_impacts, is_ai_generated)
|
||||
VALUES (?,?,?,?,?,?)""",
|
||||
(cat["name"], cat["type"], cat.get("sub_type",""),
|
||||
cat.get("description",""), defaults, int(cat.get("is_ai_generated",0))))
|
||||
conn.commit()
|
||||
return cur.lastrowid
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
# ── Instrument impacts ────────────────────────────────────────────────────────
|
||||
|
||||
def save_instrument_impacts(impacts: List[Dict[str, Any]]) -> int:
|
||||
import json as _json
|
||||
if not impacts:
|
||||
return 0
|
||||
conn = get_conn()
|
||||
try:
|
||||
inserted = 0
|
||||
for imp in impacts:
|
||||
conn.execute("""INSERT INTO instrument_impacts
|
||||
(source_type, source_id, source_name, source_date, category_name,
|
||||
instrument_id, impact_score, direction, rationale, confidence, ai_generated)
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?,?)""",
|
||||
(imp["source_type"], imp.get("source_id"), imp["source_name"],
|
||||
imp["source_date"], imp.get("category_name",""),
|
||||
imp["instrument_id"], imp.get("impact_score", 0.5),
|
||||
imp.get("direction","neutral"), imp.get("rationale",""),
|
||||
imp.get("confidence", 0.7), int(imp.get("ai_generated", 0))))
|
||||
inserted += 1
|
||||
conn.commit()
|
||||
return inserted
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
def get_impacts_for_source(source_type: str, source_id: int) -> List[Dict[str, Any]]:
|
||||
conn = get_conn()
|
||||
try:
|
||||
rows = conn.execute(
|
||||
"SELECT * FROM instrument_impacts WHERE source_type=? AND source_id=? ORDER BY impact_score DESC",
|
||||
(source_type, source_id)).fetchall()
|
||||
return [dict(r) for r in rows]
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
def delete_impacts_for_source(source_type: str, source_id: int) -> int:
|
||||
conn = get_conn()
|
||||
try:
|
||||
cur = conn.execute(
|
||||
"DELETE FROM instrument_impacts WHERE source_type=? AND source_id=?",
|
||||
(source_type, source_id))
|
||||
conn.commit()
|
||||
return cur.rowcount
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
def update_impact_adjustment(impact_id: int, adjusted_score: float,
|
||||
adjusted_direction: str, override_rationale: str = "") -> bool:
|
||||
conn = get_conn()
|
||||
try:
|
||||
cur = conn.execute("""UPDATE instrument_impacts SET
|
||||
manually_adjusted=1, adjusted_score=?, adjusted_direction=?, override_rationale=?
|
||||
WHERE id=?""",
|
||||
(adjusted_score, adjusted_direction, override_rationale, impact_id))
|
||||
conn.commit()
|
||||
return cur.rowcount > 0
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
def get_weekly_impact_sources(days: int = 7, min_score: float = 0.3) -> List[Dict[str, Any]]:
|
||||
"""Return distinct sources (events/news) with their max impact score from last N days."""
|
||||
conn = get_conn()
|
||||
try:
|
||||
cutoff = (
|
||||
__import__('datetime').datetime.utcnow() -
|
||||
__import__('datetime').timedelta(days=days)
|
||||
).strftime("%Y-%m-%d")
|
||||
rows = conn.execute("""
|
||||
SELECT source_type, source_id, source_name, source_date, category_name,
|
||||
MAX(COALESCE(adjusted_score, impact_score)) as max_score,
|
||||
COUNT(*) as n_instruments,
|
||||
MAX(created_at) as evaluated_at
|
||||
FROM instrument_impacts
|
||||
WHERE source_date >= ? AND COALESCE(adjusted_score, impact_score) >= ?
|
||||
GROUP BY source_type, source_id
|
||||
ORDER BY source_date DESC, max_score DESC
|
||||
""", (cutoff, min_score)).fetchall()
|
||||
result = []
|
||||
for r in rows:
|
||||
d = dict(r)
|
||||
d["impacts"] = get_impacts_for_source(d["source_type"], d.get("source_id"))
|
||||
result.append(d)
|
||||
return result
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
Reference in New Issue
Block a user