feat: Timeline Navigator — contexte historique 3 temporalités COVID → aujourd'hui

- 2 nouvelles tables SQLite : market_events + timeline_context
- 32 événements historiques seedés (long/medium/short de feb 2020 à juin 2026)
- timeline_service.py : bootstrap, get_events_for_date, génération commentaires GPT-4o-mini
- /api/timeline router : GET /day/{date}, GET /events, POST /generate/{date}, POST /bootstrap
- Timeline.tsx : navigateur date avec strip visuel, 3 panneaux contextuels, catalogue d'événements
- Sidebar : entrée Timeline avec icône Layers

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
OpenSquared
2026-06-24 17:45:35 +02:00
parent 281b7e30ba
commit c6178c14d5
7 changed files with 1110 additions and 1 deletions

View File

@@ -734,6 +734,34 @@ def init_db():
UNIQUE(asset, fetch_date)
)""")
# ── Timeline Navigator ────────────────────────────────────────────────────
c.execute("""CREATE TABLE IF NOT EXISTS market_events (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
start_date TEXT NOT NULL,
end_date TEXT,
level TEXT NOT NULL,
category TEXT NOT NULL DEFAULT 'macro',
description TEXT NOT NULL DEFAULT '',
market_impact TEXT DEFAULT '',
affected_assets TEXT DEFAULT '[]',
impact_score REAL DEFAULT 0.5,
parent_event_id INTEGER REFERENCES market_events(id),
created_at TEXT DEFAULT (datetime('now'))
)""")
c.execute("""CREATE TABLE IF NOT EXISTS timeline_context (
ref_date TEXT PRIMARY KEY,
long_event_id INTEGER REFERENCES market_events(id),
medium_event_id INTEGER REFERENCES market_events(id),
short_event_id INTEGER REFERENCES market_events(id),
long_commentary TEXT DEFAULT '',
medium_commentary TEXT DEFAULT '',
short_commentary TEXT DEFAULT '',
evidence_headlines TEXT DEFAULT '[]',
generated_at TEXT DEFAULT (datetime('now'))
)""")
# Seed desk defaults (idempotent)
_desk_count = c.execute("SELECT COUNT(*) FROM asset_class_configs").fetchone()[0]
if _desk_count == 0:
@@ -4431,3 +4459,121 @@ def update_report_result(
return True
finally:
conn.close()
# ── Timeline Navigator ────────────────────────────────────────────────────────
def save_market_event(ev: Dict[str, Any]) -> int:
import json
conn = get_conn()
try:
cur = conn.execute("""INSERT INTO market_events
(name, start_date, end_date, level, category, description, market_impact,
affected_assets, impact_score, parent_event_id)
VALUES (?,?,?,?,?,?,?,?,?,?)""",
(ev["name"], ev["start_date"], ev.get("end_date"),
ev["level"], ev.get("category", "macro"), ev.get("description", ""),
ev.get("market_impact", ""), json.dumps(ev.get("affected_assets", [])),
ev.get("impact_score", 0.5), ev.get("parent_event_id")))
conn.commit()
return cur.lastrowid
finally:
conn.close()
def update_market_event(event_id: int, ev: Dict[str, Any]) -> bool:
import json
conn = get_conn()
try:
conn.execute("""UPDATE market_events SET
name=?, start_date=?, end_date=?, level=?, category=?, description=?,
market_impact=?, affected_assets=?, impact_score=?
WHERE id=?""",
(ev["name"], ev["start_date"], ev.get("end_date"),
ev["level"], ev.get("category", "macro"), ev.get("description", ""),
ev.get("market_impact", ""), json.dumps(ev.get("affected_assets", [])),
ev.get("impact_score", 0.5), event_id))
conn.commit()
return True
finally:
conn.close()
def get_all_market_events() -> List[Dict[str, Any]]:
conn = get_conn()
try:
rows = conn.execute(
"SELECT * FROM market_events ORDER BY start_date DESC"
).fetchall()
return [dict(r) for r in rows]
finally:
conn.close()
def get_events_for_date(ref_date: str) -> Dict[str, Any]:
"""Return the active long/medium/short events for a given date."""
conn = get_conn()
try:
def query_level(level: str) -> Optional[Dict]:
row = conn.execute("""
SELECT * FROM market_events
WHERE level=? AND start_date<=?
AND (end_date IS NULL OR end_date>=?)
ORDER BY start_date DESC LIMIT 1""",
(level, ref_date, ref_date)).fetchone()
return dict(row) if row else None
return {
"long": query_level("long"),
"medium": query_level("medium"),
"short": query_level("short"),
}
finally:
conn.close()
def get_timeline_context(ref_date: str) -> Optional[Dict[str, Any]]:
conn = get_conn()
try:
row = conn.execute(
"SELECT * FROM timeline_context WHERE ref_date=?", (ref_date,)
).fetchone()
return dict(row) if row else None
finally:
conn.close()
def save_timeline_context(ctx: Dict[str, Any]) -> None:
import json
conn = get_conn()
try:
conn.execute("""INSERT INTO timeline_context
(ref_date, long_event_id, medium_event_id, short_event_id,
long_commentary, medium_commentary, short_commentary,
evidence_headlines, generated_at)
VALUES (?,?,?,?,?,?,?,?,datetime('now'))
ON CONFLICT(ref_date) DO UPDATE SET
long_event_id=excluded.long_event_id,
medium_event_id=excluded.medium_event_id,
short_event_id=excluded.short_event_id,
long_commentary=excluded.long_commentary,
medium_commentary=excluded.medium_commentary,
short_commentary=excluded.short_commentary,
evidence_headlines=excluded.evidence_headlines,
generated_at=datetime('now')""",
(ctx["ref_date"],
ctx.get("long_event_id"), ctx.get("medium_event_id"), ctx.get("short_event_id"),
ctx.get("long_commentary", ""), ctx.get("medium_commentary", ""),
ctx.get("short_commentary", ""),
json.dumps(ctx.get("evidence_headlines", []))))
conn.commit()
finally:
conn.close()
def count_market_events() -> int:
conn = get_conn()
try:
return conn.execute("SELECT COUNT(*) FROM market_events").fetchone()[0]
finally:
conn.close()