- 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>
397 lines
19 KiB
Python
397 lines
19 KiB
Python
"""
|
|
Timeline Navigator Service.
|
|
|
|
Manages the historical market event database (COVID → today) and generates
|
|
3-level (long/medium/short) temporal context with AI commentary for any date.
|
|
"""
|
|
import json
|
|
import logging
|
|
from datetime import date, datetime, timedelta
|
|
from typing import Any, Dict, List, Optional
|
|
|
|
from services.database import (
|
|
count_market_events,
|
|
get_all_market_events,
|
|
get_events_for_date,
|
|
get_timeline_context,
|
|
save_market_event,
|
|
save_timeline_context,
|
|
)
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
# ── Seed event catalogue ──────────────────────────────────────────────────────
|
|
# Each entry: name, start_date, end_date (None=ongoing), level, category,
|
|
# description, market_impact, affected_assets (list), impact_score
|
|
|
|
_SEED_EVENTS: List[Dict[str, Any]] = [
|
|
# ───── LONG-TERM STRUCTURAL EVENTS ─────
|
|
{
|
|
"name": "COVID-19 Crash", "level": "long", "category": "macro",
|
|
"start_date": "2020-02-20", "end_date": "2020-03-23",
|
|
"description": "Pandémie mondiale — liquidation forcée de tous les actifs risqués, gel des marchés du crédit, volatilité extrême (VIX>80).",
|
|
"market_impact": "SPX -34% en 33 jours. VIX 85. Oil négatif avril. Fuite vers USD et T-Bills.",
|
|
"affected_assets": ["SPX", "VIX", "CL", "HYG", "USD", "Gold"],
|
|
"impact_score": 1.0,
|
|
},
|
|
{
|
|
"name": "Fed QE Infinity & ZIRP", "level": "long", "category": "monetary",
|
|
"start_date": "2020-03-23", "end_date": "2022-03-16",
|
|
"description": "Fed à 0% + QE illimité. Injection de $4T+ en 2 ans. Régime de taux zéro et liquidité abondante pour tous les actifs.",
|
|
"market_impact": "Bull market actions/crypto/immobilier. Compression des primes de risque. Carry trades extrêmes.",
|
|
"affected_assets": ["SPX", "BTC", "Gold", "HYG", "TLT", "EUR/USD"],
|
|
"impact_score": 0.95,
|
|
},
|
|
{
|
|
"name": "Inflation Surge", "level": "long", "category": "macro",
|
|
"start_date": "2021-04-01", "end_date": "2023-06-01",
|
|
"description": "CPI US monte de 2% à 9,1% (juin 2022). Chocs d'offre post-COVID + demande stimulée + énergie. Fin de l'ère TINA.",
|
|
"market_impact": "Fin du bull bond. Rotation value/energy. Pression sur multiples tech.",
|
|
"affected_assets": ["TLT", "GC", "CL", "XLE", "EUR/USD", "TIPS"],
|
|
"impact_score": 0.9,
|
|
},
|
|
{
|
|
"name": "Fed Hike Cycle", "level": "long", "category": "monetary",
|
|
"start_date": "2022-03-16", "end_date": "2023-07-26",
|
|
"description": "525bps de hausse en 17 mois — cycle de resserrement le plus rapide depuis Volcker. Pivot du QE vers QT.",
|
|
"market_impact": "Krach obligataire 2022 (-25% TLT). Tech -35%. Strength USD. Recession fears.",
|
|
"affected_assets": ["TLT", "QQQ", "SPX", "USD", "HYG", "GC"],
|
|
"impact_score": 0.95,
|
|
},
|
|
{
|
|
"name": "Fed Pause (Peak Rates)", "level": "long", "category": "monetary",
|
|
"start_date": "2023-07-26", "end_date": "2024-09-18",
|
|
"description": "Fed maintient 5,25-5,50% pendant 14 mois. Marché anticipe baisse, puis repousse sans cesse. Atterrissage en douceur.",
|
|
"market_impact": "Rally équités (SPX +26% 2023). Curve inversion. Gold accumulation.",
|
|
"affected_assets": ["SPX", "GC", "USD", "TLT", "2Y UST"],
|
|
"impact_score": 0.7,
|
|
},
|
|
{
|
|
"name": "Fed Easing Cycle", "level": "long", "category": "monetary",
|
|
"start_date": "2024-09-18", "end_date": None,
|
|
"description": "Fed commence à baisser : -25bps sept 2024, -25bps nov 2024, -25bps déc 2024. Pause 2025. Nouveau cycle expansif.",
|
|
"market_impact": "USD faiblesse. Or new ATH. Courbe se dépente. Risque inflation secondaire.",
|
|
"affected_assets": ["USD", "GC", "TLT", "EUR/USD", "EM FX"],
|
|
"impact_score": 0.75,
|
|
},
|
|
{
|
|
"name": "Trump 2.0 & Tariff Era", "level": "long", "category": "geopolitical",
|
|
"start_date": "2025-01-20", "end_date": None,
|
|
"description": "Retour de Trump. Tarifs universels 10% + tarifs China jusqu'à 145%. Restructuration de l'ordre commercial mondial.",
|
|
"market_impact": "VIX spike. USD strength initial. Disruption chaînes supply. Inflation tarif.",
|
|
"affected_assets": ["SPX", "USD", "GC", "CL", "EM FX", "ZC", "ZS"],
|
|
"impact_score": 0.85,
|
|
},
|
|
{
|
|
"name": "US-Iran Military Conflict", "level": "long", "category": "geopolitical",
|
|
"start_date": "2025-06-13", "end_date": None,
|
|
"description": "Frappes US sur sites nucléaires iraniens. Escalade militaire directe USA-Iran en Moyen-Orient.",
|
|
"market_impact": "CL +15% initial. Gold safe haven. Risque Hormuz premium. Risk-off global.",
|
|
"affected_assets": ["CL", "NG", "GC", "USD", "VIX", "Tanker stocks"],
|
|
"impact_score": 0.9,
|
|
},
|
|
|
|
# ───── MEDIUM-TERM REGIME EVENTS ─────
|
|
{
|
|
"name": "COVID V-Bottom Recovery", "level": "medium", "category": "macro",
|
|
"start_date": "2020-03-23", "end_date": "2021-01-01",
|
|
"description": "Rebond historique post-crash. Fed backstop + stimulus fiscal. SPX récupère tous les pertes en 5 mois.",
|
|
"market_impact": "Tech FAANG leaders. Growth stocks premium. Small cap lagging.",
|
|
"affected_assets": ["SPX", "QQQ", "IWM", "Gold"],
|
|
"impact_score": 0.85,
|
|
},
|
|
{
|
|
"name": "Pfizer Vaccine Rotation", "level": "medium", "category": "macro",
|
|
"start_date": "2020-11-09", "end_date": "2021-06-01",
|
|
"description": "Annonce vaccin Pfizer (+95% efficacité). Mega-rotation value vs growth. Cyclicals/energy reprennent.",
|
|
"market_impact": "Banks +20% en 2 sem. Energy recovery. Growth stocks stagnent.",
|
|
"affected_assets": ["XLF", "XLE", "IWM", "QQQ", "Oil"],
|
|
"impact_score": 0.7,
|
|
},
|
|
{
|
|
"name": "GameStop & Meme Frenzy", "level": "medium", "category": "market",
|
|
"start_date": "2021-01-13", "end_date": "2021-02-05",
|
|
"description": "Short squeeze GameStop orchestré par retail Reddit. Systemic risk sur HF leverage. Brokers restriction d'achats.",
|
|
"market_impact": "GME +2700%. HF losses $10B+. Systemic risk perceptions élevées.",
|
|
"affected_assets": ["GME", "AMC", "HYG", "VIX"],
|
|
"impact_score": 0.5,
|
|
},
|
|
{
|
|
"name": "Archegos Collapse", "level": "medium", "category": "market",
|
|
"start_date": "2021-03-26", "end_date": "2021-04-10",
|
|
"description": "Family office Archegos default sur $20B de swaps TRS. Forced selling sur VIACOM/Discovery/GS/MS/CS.",
|
|
"market_impact": "CS -15%, Nomura -14%. Leverage disclosure concerns. Media stocks -30%.",
|
|
"affected_assets": ["CS", "Nomura", "Media sector"],
|
|
"impact_score": 0.55,
|
|
},
|
|
{
|
|
"name": "China Tech Crackdown", "level": "medium", "category": "geopolitical",
|
|
"start_date": "2021-07-01", "end_date": "2022-03-01",
|
|
"description": "Pékin régule les géants tech chinois (Alibaba, Didi, Tencent). Suspension IPO Didi. Education privée bannie.",
|
|
"market_impact": "KWEB -60%. Alibaba -70%. Risque réglementaire chinois systémique.",
|
|
"affected_assets": ["BABA", "BIDU", "JD", "KWEB", "FXI"],
|
|
"impact_score": 0.7,
|
|
},
|
|
{
|
|
"name": "Evergrande Crisis", "level": "medium", "category": "macro",
|
|
"start_date": "2021-09-01", "end_date": "2022-01-01",
|
|
"description": "Evergrande (2ème promoteur immobilier chinois) en défaut sur $300B de dettes. Contagion secteur immo chinois.",
|
|
"market_impact": "HY spread China +500bps. CNY pressure. Contagion EM partielle.",
|
|
"affected_assets": ["CNY", "FXI", "HY China bonds", "Iron Ore"],
|
|
"impact_score": 0.65,
|
|
},
|
|
{
|
|
"name": "Ukraine Invasion", "level": "medium", "category": "geopolitical",
|
|
"start_date": "2022-02-24", "end_date": None,
|
|
"description": "Invasion russe de l'Ukraine. Sanctions massives Russia. Disruption grains, gaz, engrais. Réarmement Europe.",
|
|
"market_impact": "NG Europe x10. Wheat +60%. Gold safe haven. EUR weakness. Defense rally.",
|
|
"affected_assets": ["NG", "ZW", "GC", "EUR/USD", "Defense stocks"],
|
|
"impact_score": 0.9,
|
|
},
|
|
{
|
|
"name": "CPI Peak 9.1% & Jackson Hole 2022", "level": "medium", "category": "monetary",
|
|
"start_date": "2022-06-10", "end_date": "2022-10-01",
|
|
"description": "CPI US 9,1% — peak inflation 40 ans. Jackson Hole Powell hawkish extrême. Marchés capitulent sur pivot.",
|
|
"market_impact": "SPX -25% 2022. TLT -30%. EUR/USD parité. DXY 114.",
|
|
"affected_assets": ["SPX", "TLT", "EUR/USD", "USD", "GC"],
|
|
"impact_score": 0.85,
|
|
},
|
|
{
|
|
"name": "SVB Collapse & Banking Crisis", "level": "medium", "category": "market",
|
|
"start_date": "2023-03-08", "end_date": "2023-05-01",
|
|
"description": "Silicon Valley Bank run sur les dépôts. FDIC intervention. Credit Suisse absorbe par UBS. Contagion régionale US.",
|
|
"market_impact": "KBE -25%. TLT +10% (flight to safety). VIX spike. Credit tightening.",
|
|
"affected_assets": ["KBE", "TLT", "VIX", "EUR/USD", "Gold"],
|
|
"impact_score": 0.7,
|
|
},
|
|
{
|
|
"name": "AI Boom / Nvidia Mania", "level": "medium", "category": "market",
|
|
"start_date": "2023-05-25", "end_date": None,
|
|
"description": "Nvidia guidance x3 — début du mega-cycle IA. ChatGPT + Capex hyperscalers. Concentration marchés extrême.",
|
|
"market_impact": "NVDA +800%. Mag7 domination. SPX concentration record. Small cap underperform.",
|
|
"affected_assets": ["NVDA", "MSFT", "GOOGL", "QQQ", "SMH"],
|
|
"impact_score": 0.85,
|
|
},
|
|
{
|
|
"name": "Israel-Hamas War", "level": "medium", "category": "geopolitical",
|
|
"start_date": "2023-10-07", "end_date": None,
|
|
"description": "Attaque Hamas + contre-offensive Gaza. Escalade Hezbollah, Yemen Houthis, Iran proxy. Risque Hormuz.",
|
|
"market_impact": "CL spike initial +5%. Gold safe haven $2100. Défense stocks +15%.",
|
|
"affected_assets": ["CL", "GC", "Defense", "ILS"],
|
|
"impact_score": 0.7,
|
|
},
|
|
{
|
|
"name": "BOJ Fin du YCC", "level": "medium", "category": "monetary",
|
|
"start_date": "2024-03-19", "end_date": None,
|
|
"description": "Bank of Japan abandonne le Yield Curve Control et hausse taux à 0.1% (premier hike en 17 ans). JPY carry unwind.",
|
|
"market_impact": "JPY +5% en 1 mois. Carry trade unwind global. Nikkei -12% août 2024.",
|
|
"affected_assets": ["JPY", "Nikkei", "USD/JPY", "JGB", "EM carry"],
|
|
"impact_score": 0.75,
|
|
},
|
|
{
|
|
"name": "Jackson Hole Pivot 2024", "level": "medium", "category": "monetary",
|
|
"start_date": "2024-08-23", "end_date": "2024-12-31",
|
|
"description": "Powell annonce officiellement la fin du cycle de resserrement et signale des baisses imminentes.",
|
|
"market_impact": "USD -2%. Gold ATH. Equities rally. Curve bull steepening.",
|
|
"affected_assets": ["USD", "GC", "SPX", "TLT", "EUR/USD"],
|
|
"impact_score": 0.75,
|
|
},
|
|
{
|
|
"name": "Trump Tariff Shock (Liberation Day)", "level": "medium", "category": "geopolitical",
|
|
"start_date": "2025-04-02", "end_date": "2025-04-09",
|
|
"description": "Annonce tarifs universels 10% + tarifs pays spécifiques jusqu'à 50%. Pire choc tarifaire depuis 1930.",
|
|
"market_impact": "SPX -15% en 4 séances. VIX 60. Credit spreads +200bps.",
|
|
"affected_assets": ["SPX", "VIX", "USD", "GC", "HYG"],
|
|
"impact_score": 0.9,
|
|
},
|
|
{
|
|
"name": "90-Day Tariff Pause", "level": "medium", "category": "geopolitical",
|
|
"start_date": "2025-04-09", "end_date": "2025-07-09",
|
|
"description": "Trump annonce pause 90 jours sur la majorité des tarifs réciproqués pour permettre des négociations.",
|
|
"market_impact": "SPX +9.5% en 1 séance (3ème meilleure journée historique). VIX collapse.",
|
|
"affected_assets": ["SPX", "VIX", "USD", "EUR/USD", "EM"],
|
|
"impact_score": 0.8,
|
|
},
|
|
{
|
|
"name": "Iran Ceasefire Talks", "level": "medium", "category": "geopolitical",
|
|
"start_date": "2025-06-20", "end_date": None,
|
|
"description": "Négociations de cessez-le-feu US-Iran sous médiation Oman + Suisse. Dé-escalade progressive.",
|
|
"market_impact": "CL -8%. Hormuz risk premium réduit. Gold consolidation.",
|
|
"affected_assets": ["CL", "GC", "USD", "VIX"],
|
|
"impact_score": 0.7,
|
|
},
|
|
|
|
# ───── SHORT-TERM CATALYST EVENTS ─────
|
|
{
|
|
"name": "Fed Meeting — Pause Confirmée", "level": "short", "category": "monetary",
|
|
"start_date": "2025-05-07", "end_date": "2025-05-21",
|
|
"description": "FOMC maintient les taux — attend clarification tarifs avant de bouger. Powell data-dependent.",
|
|
"market_impact": "USD stable. Marchés attendent. Curve stable.",
|
|
"affected_assets": ["USD", "TLT", "SPX"],
|
|
"impact_score": 0.5,
|
|
},
|
|
{
|
|
"name": "US-China Trade Truce", "level": "short", "category": "geopolitical",
|
|
"start_date": "2025-05-12", "end_date": "2025-08-12",
|
|
"description": "Réduction tarifs US-China : 145%→30% (US) et 125%→10% (China) pendant 90 jours suite aux négociations de Genève.",
|
|
"market_impact": "SPX +3%. USD -1%. Relief rally. Chaînes supply se reconstituent.",
|
|
"affected_assets": ["SPX", "USD", "CNY", "ZS", "ZC"],
|
|
"impact_score": 0.7,
|
|
},
|
|
{
|
|
"name": "US Frappe Iran — Escalade", "level": "short", "category": "geopolitical",
|
|
"start_date": "2025-06-13", "end_date": "2025-06-20",
|
|
"description": "Frappes US-Israël sur Natanz et Fordow. Iran riposte sur bases US région. Risk-off maximal.",
|
|
"market_impact": "CL +15%. Gold +5%. VIX 35. USD strength.",
|
|
"affected_assets": ["CL", "GC", "VIX", "USD", "Defense"],
|
|
"impact_score": 0.9,
|
|
},
|
|
{
|
|
"name": "Iran Ceasefire Signal", "level": "short", "category": "geopolitical",
|
|
"start_date": "2025-06-20", "end_date": "2025-07-10",
|
|
"description": "Signal de cessez-le-feu Iran-US via Oman. Début dé-escalade. Marchés absorbent la prime de risque.",
|
|
"market_impact": "CL -8% en 2 séances. VIX retombe. Risk-on partiel.",
|
|
"affected_assets": ["CL", "VIX", "GC", "SPX"],
|
|
"impact_score": 0.65,
|
|
},
|
|
{
|
|
"name": "SNB Meeting — Easing Signalé", "level": "short", "category": "monetary",
|
|
"start_date": "2026-06-19", "end_date": "2026-07-03",
|
|
"description": "Swiss National Bank réunion — signal de poursuite du cycle d'assouplissement. CHF sous pression.",
|
|
"market_impact": "CHF -0.5%. EUR/CHF hausse. Francs positions réduites.",
|
|
"affected_assets": ["CHF", "EUR/CHF", "USD/CHF"],
|
|
"impact_score": 0.45,
|
|
},
|
|
]
|
|
|
|
|
|
def bootstrap_events() -> int:
|
|
"""Seed the market_events table. Idempotent — only runs if table is empty."""
|
|
if count_market_events() > 0:
|
|
logger.info("[Timeline] market_events already seeded, skip bootstrap")
|
|
return 0
|
|
saved = 0
|
|
for ev in _SEED_EVENTS:
|
|
try:
|
|
save_market_event(ev)
|
|
saved += 1
|
|
except Exception as e:
|
|
logger.warning(f"[Timeline] Failed to save event '{ev['name']}': {e}")
|
|
logger.info(f"[Timeline] Bootstrapped {saved} market events")
|
|
return saved
|
|
|
|
|
|
def _days_since(ref_date: str, start_date: str) -> int:
|
|
try:
|
|
d0 = datetime.strptime(start_date[:10], "%Y-%m-%d").date()
|
|
d1 = datetime.strptime(ref_date[:10], "%Y-%m-%d").date()
|
|
return (d1 - d0).days
|
|
except Exception:
|
|
return 0
|
|
|
|
|
|
def generate_daily_commentary(
|
|
ref_date: str,
|
|
long_ev: Optional[Dict],
|
|
medium_ev: Optional[Dict],
|
|
short_ev: Optional[Dict],
|
|
) -> Dict[str, str]:
|
|
"""Call GPT-4o-mini to generate 3 French trading commentaries for ref_date."""
|
|
import os
|
|
from openai import OpenAI
|
|
|
|
api_key = os.environ.get("OPENAI_API_KEY", "")
|
|
if not api_key:
|
|
return {
|
|
"long_commentary": "IA non configurée — ajoutez votre clé OpenAI dans la configuration.",
|
|
"medium_commentary": "",
|
|
"short_commentary": "",
|
|
}
|
|
|
|
client = OpenAI(api_key=api_key)
|
|
|
|
def _ev_summary(ev: Optional[Dict], level: str) -> str:
|
|
if not ev:
|
|
return f"[Pas d'événement {level} actif]"
|
|
days = _days_since(ref_date, ev["start_date"])
|
|
return (
|
|
f"Nom: {ev['name']}\n"
|
|
f"Depuis: {ev['start_date']} ({days} jours)\n"
|
|
f"Description: {ev['description']}\n"
|
|
f"Impact marché: {ev.get('market_impact', '')}"
|
|
)
|
|
|
|
prompt = f"""Tu es un analyste macro senior en trading d'options géopolitiques.
|
|
Date d'analyse: {ref_date}
|
|
|
|
Contexte temporel à cette date:
|
|
|
|
== LONG TERME (régime structurel) ==
|
|
{_ev_summary(long_ev, 'long terme')}
|
|
|
|
== MOYEN TERME (régime courant) ==
|
|
{_ev_summary(medium_ev, 'moyen terme')}
|
|
|
|
== COURT TERME (catalyseur récent) ==
|
|
{_ev_summary(short_ev, 'court terme')}
|
|
|
|
Pour chaque niveau temporel, rédige UN SEUL paragraphe de 3-4 phrases en français.
|
|
Explique ce que signifie ce contexte pour un trader d'options sur commodités/indices/forex à cette date précise.
|
|
Sois concret : implications pour le vol, le positionnement, les risques.
|
|
|
|
FORMAT DE RÉPONSE (JSON strict):
|
|
{{
|
|
"long_commentary": "...",
|
|
"medium_commentary": "...",
|
|
"short_commentary": "..."
|
|
}}"""
|
|
|
|
try:
|
|
response = client.chat.completions.create(
|
|
model="gpt-4o-mini",
|
|
messages=[{"role": "user", "content": prompt}],
|
|
response_format={"type": "json_object"},
|
|
temperature=0.4,
|
|
max_tokens=800,
|
|
)
|
|
result = json.loads(response.choices[0].message.content)
|
|
return {
|
|
"long_commentary": result.get("long_commentary", ""),
|
|
"medium_commentary": result.get("medium_commentary", ""),
|
|
"short_commentary": result.get("short_commentary", ""),
|
|
}
|
|
except Exception as e:
|
|
logger.warning(f"[Timeline] GPT commentary failed for {ref_date}: {e}")
|
|
return {
|
|
"long_commentary": f"Erreur génération IA: {e}",
|
|
"medium_commentary": "",
|
|
"short_commentary": "",
|
|
}
|
|
|
|
|
|
def get_or_generate_context(ref_date: str) -> Dict[str, Any]:
|
|
"""Return timeline context for a date — from cache or freshly generated."""
|
|
cached = get_timeline_context(ref_date)
|
|
if cached and cached.get("long_commentary"):
|
|
events = get_events_for_date(ref_date)
|
|
return {**cached, "events": events}
|
|
|
|
events = get_events_for_date(ref_date)
|
|
commentaries = generate_daily_commentary(
|
|
ref_date,
|
|
events.get("long"),
|
|
events.get("medium"),
|
|
events.get("short"),
|
|
)
|
|
|
|
ctx = {
|
|
"ref_date": ref_date,
|
|
"long_event_id": events["long"]["id"] if events.get("long") else None,
|
|
"medium_event_id": events["medium"]["id"] if events.get("medium") else None,
|
|
"short_event_id": events["short"]["id"] if events.get("short") else None,
|
|
**commentaries,
|
|
"evidence_headlines": [],
|
|
}
|
|
save_timeline_context(ctx)
|
|
return {**ctx, "events": events}
|