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:
OpenSquared
2026-06-25 16:26:13 +02:00
parent 9ed59c5ca2
commit a68896cf67
8 changed files with 1590 additions and 3 deletions

View File

@@ -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()