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