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:
@@ -5,6 +5,7 @@ from routers import pattern_lab as pattern_lab_router
|
||||
from routers import specialist_desks as specialist_desks_router
|
||||
from routers import timeline as timeline_router
|
||||
from routers import instruments as instruments_router
|
||||
from routers import impact as impact_router
|
||||
from routers import logs as logs_router
|
||||
from routers import var as var_router
|
||||
from routers import reports as reports_router
|
||||
@@ -82,10 +83,12 @@ def startup():
|
||||
try:
|
||||
from services.macro_events_bootstrap import bootstrap_macro_events
|
||||
from services.eco_calendar_bootstrap import bootstrap_eco_events
|
||||
from services.impact_categories_bootstrap import bootstrap_impact_categories
|
||||
r1 = bootstrap_macro_events()
|
||||
r2 = bootstrap_eco_events()
|
||||
if r1.get("inserted", 0) or r2.get("inserted", 0):
|
||||
_log.info(f"[Startup] Events bootstrapped — macro: +{r1.get('inserted',0)}, eco: +{r2.get('inserted',0)}")
|
||||
r3 = bootstrap_impact_categories()
|
||||
if r1.get("inserted", 0) or r2.get("inserted", 0) or r3.get("inserted", 0):
|
||||
_log.info(f"[Startup] Bootstrapped — macro: +{r1.get('inserted',0)}, eco: +{r2.get('inserted',0)}, categories: +{r3.get('inserted',0)}")
|
||||
except Exception as _e:
|
||||
_log.warning(f"[Startup] Event bootstrap failed: {_e}")
|
||||
# Start auto-cycle scheduler if enabled
|
||||
@@ -135,6 +138,7 @@ app.include_router(pattern_lab_router.router)
|
||||
app.include_router(specialist_desks_router.router)
|
||||
app.include_router(timeline_router.router)
|
||||
app.include_router(instruments_router.router)
|
||||
app.include_router(impact_router.router)
|
||||
|
||||
|
||||
@app.get("/")
|
||||
|
||||
155
backend/routers/impact.py
Normal file
155
backend/routers/impact.py
Normal file
@@ -0,0 +1,155 @@
|
||||
"""
|
||||
Impact Monitor — instrument impact evaluation for market events and news.
|
||||
Prefix: /api/impact
|
||||
"""
|
||||
import logging
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from fastapi import APIRouter, HTTPException, Query
|
||||
from pydantic import BaseModel
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
router = APIRouter(prefix="/api/impact", tags=["impact"])
|
||||
|
||||
|
||||
# ── Schemas ───────────────────────────────────────────────────────────────────
|
||||
|
||||
class ImpactAdjustment(BaseModel):
|
||||
adjusted_score: float
|
||||
adjusted_direction: str
|
||||
override_rationale: str = ""
|
||||
|
||||
|
||||
class CategoryUpdate(BaseModel):
|
||||
type: str
|
||||
sub_type: str = ""
|
||||
description: str = ""
|
||||
default_impacts: List[Dict[str, Any]] = []
|
||||
|
||||
|
||||
# ── Categories ────────────────────────────────────────────────────────────────
|
||||
|
||||
@router.get("/categories")
|
||||
def list_categories() -> List[Dict[str, Any]]:
|
||||
from services.database import get_all_event_categories
|
||||
import json
|
||||
cats = get_all_event_categories()
|
||||
for c in cats:
|
||||
try:
|
||||
c["default_impacts"] = json.loads(c.get("default_impacts") or "[]")
|
||||
except Exception:
|
||||
c["default_impacts"] = []
|
||||
return cats
|
||||
|
||||
|
||||
@router.put("/categories/{name}")
|
||||
def update_category(name: str, body: CategoryUpdate) -> Dict[str, Any]:
|
||||
from services.database import upsert_event_category
|
||||
import json
|
||||
cat_id = upsert_event_category({
|
||||
"name": name,
|
||||
"type": body.type,
|
||||
"sub_type": body.sub_type,
|
||||
"description": body.description,
|
||||
"default_impacts": json.dumps(body.default_impacts),
|
||||
})
|
||||
return {"id": cat_id, "name": name, "status": "updated"}
|
||||
|
||||
|
||||
# ── Evaluate ──────────────────────────────────────────────────────────────────
|
||||
|
||||
@router.post("/evaluate/event/{event_id}")
|
||||
def evaluate_event(
|
||||
event_id: int,
|
||||
force: bool = Query(False, description="Re-evaluate even if already done"),
|
||||
) -> Dict[str, Any]:
|
||||
"""Trigger AI impact evaluation for a market event."""
|
||||
from services.impact_service import evaluate_event_impacts
|
||||
try:
|
||||
result = evaluate_event_impacts(event_id, force=force)
|
||||
if "error" in result:
|
||||
raise HTTPException(400, result["error"])
|
||||
return result
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"[impact] evaluate_event {event_id} failed: {e}")
|
||||
raise HTTPException(500, str(e))
|
||||
|
||||
|
||||
@router.post("/evaluate/bulk")
|
||||
def evaluate_bulk(
|
||||
event_ids: List[int],
|
||||
force: bool = Query(False),
|
||||
) -> Dict[str, Any]:
|
||||
"""Evaluate multiple events sequentially."""
|
||||
from services.impact_service import evaluate_event_impacts
|
||||
results = []
|
||||
errors = []
|
||||
for eid in event_ids[:20]: # cap at 20 per call
|
||||
try:
|
||||
r = evaluate_event_impacts(eid, force=force)
|
||||
if "error" not in r:
|
||||
results.append({"event_id": eid, "n_instruments": r.get("n_instruments", 0)})
|
||||
else:
|
||||
errors.append({"event_id": eid, "error": r["error"]})
|
||||
except Exception as e:
|
||||
errors.append({"event_id": eid, "error": str(e)})
|
||||
return {"evaluated": len(results), "errors": len(errors), "results": results, "error_details": errors}
|
||||
|
||||
|
||||
# ── Monitor ───────────────────────────────────────────────────────────────────
|
||||
|
||||
@router.get("/monitor")
|
||||
def get_monitor(
|
||||
days: int = Query(7, ge=1, le=90, description="Fenêtre temporelle en jours"),
|
||||
min_score: float = Query(0.3, ge=0.0, le=1.0, description="Score minimum d'impact"),
|
||||
) -> Dict[str, Any]:
|
||||
"""Weekly impact monitor — evaluated events with instrument scores."""
|
||||
from services.impact_service import get_impact_monitor_data
|
||||
try:
|
||||
return get_impact_monitor_data(days=days, min_score=min_score)
|
||||
except Exception as e:
|
||||
logger.error(f"[impact] monitor failed: {e}")
|
||||
raise HTTPException(500, str(e))
|
||||
|
||||
|
||||
@router.get("/event/{event_id}")
|
||||
def get_event_impacts(event_id: int) -> Dict[str, Any]:
|
||||
"""Get stored impacts for a specific event."""
|
||||
from services.database import get_impacts_for_source, get_all_market_events
|
||||
impacts = get_impacts_for_source("event", event_id)
|
||||
events = get_all_market_events()
|
||||
event = next((e for e in events if e["id"] == event_id), None)
|
||||
return {
|
||||
"event_id": event_id,
|
||||
"event": event,
|
||||
"impacts": impacts,
|
||||
"evaluated": len(impacts) > 0,
|
||||
}
|
||||
|
||||
|
||||
# ── Adjust ────────────────────────────────────────────────────────────────────
|
||||
|
||||
@router.put("/adjust/{impact_id}")
|
||||
def adjust_impact(impact_id: int, body: ImpactAdjustment) -> Dict[str, Any]:
|
||||
"""Manually override an AI-estimated impact score/direction."""
|
||||
from services.database import update_impact_adjustment
|
||||
ok = update_impact_adjustment(
|
||||
impact_id,
|
||||
body.adjusted_score,
|
||||
body.adjusted_direction,
|
||||
body.override_rationale,
|
||||
)
|
||||
if not ok:
|
||||
raise HTTPException(404, f"Impact {impact_id} not found")
|
||||
return {"id": impact_id, "status": "adjusted"}
|
||||
|
||||
|
||||
# ── Bootstrap ─────────────────────────────────────────────────────────────────
|
||||
|
||||
@router.post("/bootstrap-categories")
|
||||
def bootstrap_categories(force: bool = False) -> Dict[str, Any]:
|
||||
"""Seed default impact categories."""
|
||||
from services.impact_categories_bootstrap import bootstrap_impact_categories
|
||||
return bootstrap_impact_categories(force=force)
|
||||
@@ -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()
|
||||
|
||||
350
backend/services/impact_categories_bootstrap.py
Normal file
350
backend/services/impact_categories_bootstrap.py
Normal file
@@ -0,0 +1,350 @@
|
||||
"""
|
||||
Bootstrap default event categories with instrument impact sensitivities.
|
||||
Each category has default_impacts: list of {instrument_id, sensitivity (0-1),
|
||||
typical_direction, notes}.
|
||||
sensitivity = how strongly this instrument type is typically affected.
|
||||
typical_direction = usual bias (bullish/bearish/neutral/depends_on_outcome).
|
||||
"""
|
||||
from typing import List, Dict, Any
|
||||
|
||||
ALL_INSTRUMENTS = [
|
||||
"SPY","QQQ","IWM","EEM","EFA",
|
||||
"GLD","SLV","USO","UNG","TLT",
|
||||
"HYG","EURUSD=X","USDJPY=X","GBPUSD=X",
|
||||
"VXX","AAPL","NVDA","GS","XOM","BTC-USD",
|
||||
]
|
||||
|
||||
CATEGORIES: List[Dict[str, Any]] = [
|
||||
|
||||
# ═══════════════════════════════════════════════════════
|
||||
# EVENT CALENDAR
|
||||
# ═══════════════════════════════════════════════════════
|
||||
|
||||
{
|
||||
"name": "FOMC — Décision de taux",
|
||||
"type": "event_calendar",
|
||||
"sub_type": "FOMC",
|
||||
"description": "Décision du Federal Open Market Committee sur les taux directeurs US.",
|
||||
"default_impacts": [
|
||||
{"instrument_id": "SPY", "sensitivity": 0.88, "typical_direction": "depends_on_outcome", "notes": "Hausse = baisse. Baisse = hausse. Surprise domine."},
|
||||
{"instrument_id": "QQQ", "sensitivity": 0.90, "typical_direction": "depends_on_outcome", "notes": "Très sensible aux taux réels — plus volatile que SPY sur les FOMC."},
|
||||
{"instrument_id": "IWM", "sensitivity": 0.80, "typical_direction": "depends_on_outcome", "notes": "Small caps très sensibles au crédit et aux taux courts."},
|
||||
{"instrument_id": "TLT", "sensitivity": 0.95, "typical_direction": "depends_on_outcome", "notes": "Relation directe : hausse = TLT baisse, pivot = TLT monte."},
|
||||
{"instrument_id": "HYG", "sensitivity": 0.82, "typical_direction": "depends_on_outcome", "notes": "Spreads HY se compriment (dovish) ou s'écartent (hawkish)."},
|
||||
{"instrument_id": "GLD", "sensitivity": 0.78, "typical_direction": "depends_on_outcome", "notes": "Or inverse aux taux réels. Dovish = gold up. Hawkish = gold down."},
|
||||
{"instrument_id": "EURUSD=X", "sensitivity": 0.85, "typical_direction": "depends_on_outcome", "notes": "Différentiel Fed/BCE. Hawkish Fed = USD fort = EUR baisse."},
|
||||
{"instrument_id": "USDJPY=X", "sensitivity": 0.88, "typical_direction": "depends_on_outcome", "notes": "Différentiel US-JP. Hawkish Fed = USD/JPY monte."},
|
||||
{"instrument_id": "GBPUSD=X", "sensitivity": 0.72, "typical_direction": "depends_on_outcome", "notes": "Impact via USD strength/weakness."},
|
||||
{"instrument_id": "VXX", "sensitivity": 0.80, "typical_direction": "depends_on_outcome", "notes": "Vol spike si surprise. Compression si résultat attendu."},
|
||||
{"instrument_id": "BTC-USD", "sensitivity": 0.72, "typical_direction": "depends_on_outcome", "notes": "Actif risqué/liquidité. Dovish Fed = BTC monte."},
|
||||
{"instrument_id": "GS", "sensitivity": 0.78, "typical_direction": "depends_on_outcome", "notes": "Banque sensible à la courbe des taux et aux conditions crédit."},
|
||||
{"instrument_id": "EEM", "sensitivity": 0.70, "typical_direction": "depends_on_outcome", "notes": "USD fort = pression sur EM (dette USD). Dovish = flux EM."},
|
||||
]
|
||||
},
|
||||
|
||||
{
|
||||
"name": "NFP — Non-Farm Payrolls",
|
||||
"type": "event_calendar",
|
||||
"sub_type": "NFP",
|
||||
"description": "Publication mensuelle de l'emploi non-agricole US.",
|
||||
"default_impacts": [
|
||||
{"instrument_id": "SPY", "sensitivity": 0.78, "typical_direction": "depends_on_outcome", "notes": "Beat = bon pour actions si pas de peur de la Fed. Miss = récession fear."},
|
||||
{"instrument_id": "QQQ", "sensitivity": 0.75, "typical_direction": "depends_on_outcome", "notes": "Moins sensible que SPY sauf si choc majeur."},
|
||||
{"instrument_id": "IWM", "sensitivity": 0.80, "typical_direction": "bullish_if_beat", "notes": "Small caps très sensibles à la croissance domestique."},
|
||||
{"instrument_id": "TLT", "sensitivity": 0.82, "typical_direction": "depends_on_outcome", "notes": "Beat = TLT baisse (Fed plus hawkish). Miss = TLT monte (récession trade)."},
|
||||
{"instrument_id": "HYG", "sensitivity": 0.75, "typical_direction": "bullish_if_beat", "notes": "Bon emploi = défauts faibles = HY spreads se compriment."},
|
||||
{"instrument_id": "EURUSD=X", "sensitivity": 0.72, "typical_direction": "depends_on_outcome", "notes": "Beat = USD fort. Miss = USD faible."},
|
||||
{"instrument_id": "USDJPY=X", "sensitivity": 0.75, "typical_direction": "depends_on_outcome", "notes": "Beat = USD/JPY monte."},
|
||||
{"instrument_id": "GLD", "sensitivity": 0.65, "typical_direction": "depends_on_outcome", "notes": "Beat = or baisse (taux montent). Miss = or monte (récession hedge)."},
|
||||
{"instrument_id": "VXX", "sensitivity": 0.70, "typical_direction": "depends_on_outcome", "notes": "Miss majeur = vol spike. Beat en ligne = vol compression."},
|
||||
]
|
||||
},
|
||||
|
||||
{
|
||||
"name": "CPI — Inflation US",
|
||||
"type": "event_calendar",
|
||||
"sub_type": "CPI",
|
||||
"description": "Publication mensuelle de l'indice des prix à la consommation US.",
|
||||
"default_impacts": [
|
||||
{"instrument_id": "SPY", "sensitivity": 0.82, "typical_direction": "depends_on_outcome", "notes": "CPI > attendu = hawkish Fed = actions baissent."},
|
||||
{"instrument_id": "QQQ", "sensitivity": 0.85, "typical_direction": "depends_on_outcome", "notes": "QQQ plus sensible que SPY via taux réels."},
|
||||
{"instrument_id": "TLT", "sensitivity": 0.90, "typical_direction": "depends_on_outcome", "notes": "CPI > attendu = TLT vend massivement. En-dessous = TLT rally."},
|
||||
{"instrument_id": "GLD", "sensitivity": 0.80, "typical_direction": "depends_on_outcome", "notes": "CPI > attendu = d'abord bearish or (taux réels montent). Mais LT = hedge."},
|
||||
{"instrument_id": "EURUSD=X", "sensitivity": 0.75, "typical_direction": "depends_on_outcome", "notes": "CPI hot = USD fort = EUR baisse."},
|
||||
{"instrument_id": "USDJPY=X", "sensitivity": 0.78, "typical_direction": "depends_on_outcome", "notes": "CPI hot = Fed hawkish = USD/JPY monte."},
|
||||
{"instrument_id": "HYG", "sensitivity": 0.72, "typical_direction": "depends_on_outcome", "notes": "CPI hot = risque de hausse des taux = spreads s'écartent."},
|
||||
{"instrument_id": "VXX", "sensitivity": 0.75, "typical_direction": "depends_on_outcome", "notes": "Surprise majeure dans les 2 sens = vol spike."},
|
||||
{"instrument_id": "BTC-USD", "sensitivity": 0.65, "typical_direction": "depends_on_outcome", "notes": "CPI hot = taux montent = liquidité se réduit = BTC baisse."},
|
||||
]
|
||||
},
|
||||
|
||||
{
|
||||
"name": "GDP — Croissance US",
|
||||
"type": "event_calendar",
|
||||
"sub_type": "GDP",
|
||||
"description": "Publication trimestrielle du PIB américain (estimé, révisé, final).",
|
||||
"default_impacts": [
|
||||
{"instrument_id": "SPY", "sensitivity": 0.78, "typical_direction": "bullish_if_beat", "notes": "Croissance forte = bénéfices. Récession = bear market."},
|
||||
{"instrument_id": "QQQ", "sensitivity": 0.75, "typical_direction": "bullish_if_beat", "notes": "Corrélé mais moins direct que SPY."},
|
||||
{"instrument_id": "IWM", "sensitivity": 0.85, "typical_direction": "bullish_if_beat", "notes": "Small caps très corrélées au cycle domestique."},
|
||||
{"instrument_id": "TLT", "sensitivity": 0.78, "typical_direction": "bearish_if_beat", "notes": "Croissance forte = taux montent = TLT baisse."},
|
||||
{"instrument_id": "HYG", "sensitivity": 0.80, "typical_direction": "bullish_if_beat", "notes": "Croissance forte = défauts faibles = HY se comprime."},
|
||||
{"instrument_id": "GLD", "sensitivity": 0.60, "typical_direction": "depends_on_outcome", "notes": "Impact indirect via taux réels."},
|
||||
{"instrument_id": "EURUSD=X", "sensitivity": 0.65, "typical_direction": "depends_on_outcome", "notes": "PIB fort = USD fort si surprise."},
|
||||
{"instrument_id": "VXX", "sensitivity": 0.72, "typical_direction": "depends_on_outcome", "notes": "Récession technique = vol spike. Beat = vol compression."},
|
||||
{"instrument_id": "GS", "sensitivity": 0.75, "typical_direction": "bullish_if_beat", "notes": "Croissance forte = M&A, trading, activité bancaire."},
|
||||
{"instrument_id": "XOM", "sensitivity": 0.68, "typical_direction": "bullish_if_beat", "notes": "Croissance = demande énergétique plus forte."},
|
||||
]
|
||||
},
|
||||
|
||||
{
|
||||
"name": "PCE — Core PCE (indicateur Fed)",
|
||||
"type": "event_calendar",
|
||||
"sub_type": "PCE",
|
||||
"description": "Core PCE Price Index — indicateur d'inflation de référence de la Fed.",
|
||||
"default_impacts": [
|
||||
{"instrument_id": "SPY", "sensitivity": 0.75, "typical_direction": "depends_on_outcome", "notes": "Similaire au CPI mais moins volatile. Impact si surprise notable."},
|
||||
{"instrument_id": "TLT", "sensitivity": 0.82, "typical_direction": "depends_on_outcome", "notes": "Hot PCE = taux montent = TLT vend."},
|
||||
{"instrument_id": "GLD", "sensitivity": 0.72, "typical_direction": "depends_on_outcome", "notes": "Impact via taux réels."},
|
||||
{"instrument_id": "EURUSD=X", "sensitivity": 0.68, "typical_direction": "depends_on_outcome", "notes": "Hot PCE = Fed hawkish = USD fort."},
|
||||
{"instrument_id": "QQQ", "sensitivity": 0.78, "typical_direction": "depends_on_outcome", "notes": "QQQ très sensible aux taux réels."},
|
||||
]
|
||||
},
|
||||
|
||||
{
|
||||
"name": "ISM Manufacturing",
|
||||
"type": "event_calendar",
|
||||
"sub_type": "ISM",
|
||||
"description": "ISM Manufacturing PMI — activité industrielle US (>50 expansion, <50 contraction).",
|
||||
"default_impacts": [
|
||||
{"instrument_id": "IWM", "sensitivity": 0.80, "typical_direction": "bullish_if_beat", "notes": "Très corrélé à l'industrie domestique."},
|
||||
{"instrument_id": "SPY", "sensitivity": 0.68, "typical_direction": "bullish_if_beat", "notes": "Indicateur de cycle économique."},
|
||||
{"instrument_id": "USO", "sensitivity": 0.65, "typical_direction": "bullish_if_beat", "notes": "Production industrielle forte = demande énergie."},
|
||||
{"instrument_id": "XOM", "sensitivity": 0.65, "typical_direction": "bullish_if_beat", "notes": "Similaire à USO."},
|
||||
{"instrument_id": "TLT", "sensitivity": 0.65, "typical_direction": "bearish_if_beat", "notes": "Expansion = risque de hausse taux."},
|
||||
{"instrument_id": "VXX", "sensitivity": 0.62, "typical_direction": "depends_on_outcome", "notes": "Miss sévère déclenche vol."},
|
||||
]
|
||||
},
|
||||
|
||||
{
|
||||
"name": "BOJ — Décision de taux",
|
||||
"type": "event_calendar",
|
||||
"sub_type": "BOJ",
|
||||
"description": "Décision de politique monétaire de la Banque du Japon — YCC, NIRP, normalisation.",
|
||||
"default_impacts": [
|
||||
{"instrument_id": "USDJPY=X", "sensitivity": 0.97, "typical_direction": "depends_on_outcome", "notes": "DRIVER PRINCIPAL. Hausse surprise = JPY fort = USD/JPY chute."},
|
||||
{"instrument_id": "EFA", "sensitivity": 0.82, "typical_direction": "depends_on_outcome", "notes": "Japon = composante majeure de l'EAFE."},
|
||||
{"instrument_id": "GLD", "sensitivity": 0.68, "typical_direction": "depends_on_outcome", "notes": "Normalisation BOJ = or peut monter (diversification réserves)."},
|
||||
{"instrument_id": "TLT", "sensitivity": 0.72, "typical_direction": "depends_on_outcome", "notes": "BOJ hawkish = hausse taux Japon = JGB concurrence UST."},
|
||||
{"instrument_id": "VXX", "sensitivity": 0.80, "typical_direction": "bullish_if_surprise","notes": "BOJ surprise = carry trade unwind = vol spike mondial."},
|
||||
{"instrument_id": "SPY", "sensitivity": 0.65, "typical_direction": "depends_on_outcome", "notes": "Via carry trade unwind (BOJ hawkish → vente actifs risqués)."},
|
||||
{"instrument_id": "BTC-USD", "sensitivity": 0.58, "typical_direction": "bearish_if_hawkish", "notes": "Risk-off si carry unwind."},
|
||||
]
|
||||
},
|
||||
|
||||
{
|
||||
"name": "ECB — Décision de taux",
|
||||
"type": "event_calendar",
|
||||
"sub_type": "ECB",
|
||||
"description": "Décision de politique monétaire de la Banque Centrale Européenne.",
|
||||
"default_impacts": [
|
||||
{"instrument_id": "EURUSD=X", "sensitivity": 0.95, "typical_direction": "depends_on_outcome", "notes": "DRIVER PRINCIPAL. Hawkish ECB = EUR fort. Dovish = EUR faible."},
|
||||
{"instrument_id": "GBPUSD=X", "sensitivity": 0.72, "typical_direction": "depends_on_outcome", "notes": "Impact via USD et sentiment zone euro."},
|
||||
{"instrument_id": "EFA", "sensitivity": 0.85, "typical_direction": "depends_on_outcome", "notes": "Zone euro = composante majeure EAFE."},
|
||||
{"instrument_id": "TLT", "sensitivity": 0.65, "typical_direction": "depends_on_outcome", "notes": "Politique BCE influence flux globaux vers Treasuries."},
|
||||
{"instrument_id": "GLD", "sensitivity": 0.60, "typical_direction": "depends_on_outcome", "notes": "BCE hawkish = EUR fort = or en EUR baisse."},
|
||||
{"instrument_id": "VXX", "sensitivity": 0.62, "typical_direction": "depends_on_outcome", "notes": "Surprise ECB peut déclencher vol."},
|
||||
]
|
||||
},
|
||||
|
||||
{
|
||||
"name": "BOE — Décision de taux",
|
||||
"type": "event_calendar",
|
||||
"sub_type": "BOE",
|
||||
"description": "Décision de politique monétaire de la Bank of England.",
|
||||
"default_impacts": [
|
||||
{"instrument_id": "GBPUSD=X", "sensitivity": 0.95, "typical_direction": "depends_on_outcome", "notes": "DRIVER PRINCIPAL. Hawkish BOE = GBP fort. Dovish = GBP faible."},
|
||||
{"instrument_id": "EURUSD=X", "sensitivity": 0.55, "typical_direction": "depends_on_outcome", "notes": "Impact indirect via sentiment EUR/GBP."},
|
||||
{"instrument_id": "TLT", "sensitivity": 0.52, "typical_direction": "depends_on_outcome", "notes": "Impact global limité sauf si crise gilts (2022)."},
|
||||
{"instrument_id": "VXX", "sensitivity": 0.55, "typical_direction": "depends_on_outcome", "notes": "Crise gilt-style = vol spike."},
|
||||
]
|
||||
},
|
||||
|
||||
{
|
||||
"name": "OPEC+ — Décision de production",
|
||||
"type": "event_calendar",
|
||||
"sub_type": "OPEC",
|
||||
"description": "Réunion OPEC+ : décision de quotas de production pétrolière.",
|
||||
"default_impacts": [
|
||||
{"instrument_id": "USO", "sensitivity": 0.95, "typical_direction": "depends_on_outcome", "notes": "DRIVER PRINCIPAL. Coupes = oil monte. Augmentation = oil baisse."},
|
||||
{"instrument_id": "XOM", "sensitivity": 0.88, "typical_direction": "depends_on_outcome", "notes": "Prix pétrole = cash-flow XOM."},
|
||||
{"instrument_id": "EEM", "sensitivity": 0.68, "typical_direction": "depends_on_outcome", "notes": "Exportateurs EM bénéficient des coupes."},
|
||||
{"instrument_id": "GLD", "sensitivity": 0.55, "typical_direction": "depends_on_outcome", "notes": "Oil choc = inflation = gold monte parfois."},
|
||||
{"instrument_id": "EURUSD=X", "sensitivity": 0.58, "typical_direction": "depends_on_outcome", "notes": "Pétrole cher = pression sur l'Europe importatrice = EUR faible."},
|
||||
{"instrument_id": "VXX", "sensitivity": 0.65, "typical_direction": "depends_on_outcome", "notes": "Surprise coupe majeure = spike vol court terme."},
|
||||
{"instrument_id": "SPY", "sensitivity": 0.60, "typical_direction": "depends_on_outcome", "notes": "Oil choc = inflation = stagflation fear."},
|
||||
{"instrument_id": "TLT", "sensitivity": 0.58, "typical_direction": "depends_on_outcome", "notes": "Oil choc = inflation = taux montent = TLT baisse."},
|
||||
]
|
||||
},
|
||||
|
||||
{
|
||||
"name": "EIA — Stocks pétroliers",
|
||||
"type": "event_calendar",
|
||||
"sub_type": "EIA",
|
||||
"description": "Rapport hebdomadaire EIA sur les stocks de pétrole brut US.",
|
||||
"default_impacts": [
|
||||
{"instrument_id": "USO", "sensitivity": 0.82, "typical_direction": "depends_on_outcome", "notes": "Build (hausse stocks) = baissier. Draw (baisse stocks) = haussier."},
|
||||
{"instrument_id": "XOM", "sensitivity": 0.72, "typical_direction": "depends_on_outcome", "notes": "Corrélé au prix pétrole."},
|
||||
{"instrument_id": "UNG", "sensitivity": 0.45, "typical_direction": "neutral", "notes": "Impact indirect via sentiment énergie."},
|
||||
]
|
||||
},
|
||||
|
||||
# ═══════════════════════════════════════════════════════
|
||||
# GEOPOLITICAL
|
||||
# ═══════════════════════════════════════════════════════
|
||||
|
||||
{
|
||||
"name": "Guerre — Moyen-Orient",
|
||||
"type": "geopolitical",
|
||||
"sub_type": "War",
|
||||
"description": "Conflit militaire ou escalade au Moyen-Orient (Iran, Israël, Yemen, Golfe Persique).",
|
||||
"default_impacts": [
|
||||
{"instrument_id": "USO", "sensitivity": 0.92, "typical_direction": "bullish", "notes": "DRIVER PRINCIPAL. Menace sur les livraisons du Golfe Persique."},
|
||||
{"instrument_id": "GLD", "sensitivity": 0.85, "typical_direction": "bullish", "notes": "Safe haven classique en crise géopolitique."},
|
||||
{"instrument_id": "VXX", "sensitivity": 0.88, "typical_direction": "bullish", "notes": "Choc géopolitique = vol spike immédiat."},
|
||||
{"instrument_id": "XOM", "sensitivity": 0.80, "typical_direction": "bullish", "notes": "Prime géopolitique sur le pétrole."},
|
||||
{"instrument_id": "SPY", "sensitivity": 0.75, "typical_direction": "bearish", "notes": "Risk-off. Ampleur dépend de la durée et de l'escalade."},
|
||||
{"instrument_id": "QQQ", "sensitivity": 0.72, "typical_direction": "bearish", "notes": "Tech vend en risk-off."},
|
||||
{"instrument_id": "TLT", "sensitivity": 0.72, "typical_direction": "bullish", "notes": "Fuite vers la qualité."},
|
||||
{"instrument_id": "EURUSD=X", "sensitivity": 0.68, "typical_direction": "bearish", "notes": "USD safe haven. EUR exposé via énergie."},
|
||||
{"instrument_id": "EEM", "sensitivity": 0.72, "typical_direction": "bearish", "notes": "Risk-off pèse sur EM. Pays importateurs pétrole souffrent."},
|
||||
{"instrument_id": "USDJPY=X", "sensitivity": 0.65, "typical_direction": "bearish", "notes": "JPY safe haven = USD/JPY baisse."},
|
||||
{"instrument_id": "BTC-USD", "sensitivity": 0.60, "typical_direction": "bearish", "notes": "Risk-off en phase initiale. Puis possible refuge si dévaluation USD."},
|
||||
]
|
||||
},
|
||||
|
||||
{
|
||||
"name": "Guerre — Russie / Ukraine",
|
||||
"type": "geopolitical",
|
||||
"sub_type": "War",
|
||||
"description": "Escalade ou désescalade du conflit russo-ukrainien. Impact sur énergie, céréales, EM.",
|
||||
"default_impacts": [
|
||||
{"instrument_id": "USO", "sensitivity": 0.85, "typical_direction": "bullish", "notes": "Russie = grand exportateur. Sanctions = offre réduite."},
|
||||
{"instrument_id": "UNG", "sensitivity": 0.80, "typical_direction": "bullish", "notes": "Gaz russe = composante clé approvisionnement Europe."},
|
||||
{"instrument_id": "GLD", "sensitivity": 0.82, "typical_direction": "bullish", "notes": "Safe haven + incertitude systémique."},
|
||||
{"instrument_id": "EFA", "sensitivity": 0.78, "typical_direction": "bearish", "notes": "Europe exposée énergétiquement et économiquement."},
|
||||
{"instrument_id": "EURUSD=X", "sensitivity": 0.80, "typical_direction": "bearish", "notes": "EUR exposé au choc énergétique et à la croissance européenne."},
|
||||
{"instrument_id": "VXX", "sensitivity": 0.85, "typical_direction": "bullish", "notes": "Escalade = spike vol immédiat."},
|
||||
{"instrument_id": "SPY", "sensitivity": 0.68, "typical_direction": "bearish", "notes": "Risk-off, plus modéré car US moins exposé."},
|
||||
{"instrument_id": "EEM", "sensitivity": 0.72, "typical_direction": "bearish", "notes": "EM exportateurs céréales et énergie impactés."},
|
||||
{"instrument_id": "TLT", "sensitivity": 0.68, "typical_direction": "bullish", "notes": "Fuite vers qualité en escalade."},
|
||||
{"instrument_id": "XOM", "sensitivity": 0.72, "typical_direction": "bullish", "notes": "Prix pétrole/gaz monte = XOM bénéficie."},
|
||||
]
|
||||
},
|
||||
|
||||
{
|
||||
"name": "Sanctions — Pétrole / Énergie",
|
||||
"type": "geopolitical",
|
||||
"sub_type": "Sanctions",
|
||||
"description": "Sanctions énergétiques (Russie, Iran, Venezuela) ou perturbation supply pétrolier.",
|
||||
"default_impacts": [
|
||||
{"instrument_id": "USO", "sensitivity": 0.90, "typical_direction": "bullish", "notes": "Offre réduite = prix monte."},
|
||||
{"instrument_id": "XOM", "sensitivity": 0.82, "typical_direction": "bullish", "notes": "Bénéfice du prix élevé et du remplacement de supply."},
|
||||
{"instrument_id": "GLD", "sensitivity": 0.68, "typical_direction": "bullish", "notes": "Inflation énergie = or monte."},
|
||||
{"instrument_id": "TLT", "sensitivity": 0.62, "typical_direction": "bearish", "notes": "Inflation énergie = taux montent."},
|
||||
{"instrument_id": "SPY", "sensitivity": 0.65, "typical_direction": "bearish", "notes": "Stagflation si choc sévère."},
|
||||
{"instrument_id": "EURUSD=X", "sensitivity": 0.68, "typical_direction": "bearish", "notes": "Europe plus exposée que US."},
|
||||
]
|
||||
},
|
||||
|
||||
{
|
||||
"name": "Chine — Taiwan / Tensions géopolitiques",
|
||||
"type": "geopolitical",
|
||||
"sub_type": "China",
|
||||
"description": "Escalade militaire ou diplomatique Chine-Taiwan, mers de Chine.",
|
||||
"default_impacts": [
|
||||
{"instrument_id": "VXX", "sensitivity": 0.95, "typical_direction": "bullish", "notes": "Choc systémique maximal si escalade."},
|
||||
{"instrument_id": "NVDA", "sensitivity": 0.92, "typical_direction": "bearish", "notes": "Taiwan = TSMC. Supply chain semi-conducteurs bloquée."},
|
||||
{"instrument_id": "QQQ", "sensitivity": 0.85, "typical_direction": "bearish", "notes": "Tech exposé via supply chain Asie."},
|
||||
{"instrument_id": "SPY", "sensitivity": 0.80, "typical_direction": "bearish", "notes": "Risk-off systémique."},
|
||||
{"instrument_id": "GLD", "sensitivity": 0.88, "typical_direction": "bullish", "notes": "Safe haven ultime."},
|
||||
{"instrument_id": "TLT", "sensitivity": 0.75, "typical_direction": "bullish", "notes": "Fuite vers qualité."},
|
||||
{"instrument_id": "EEM", "sensitivity": 0.90, "typical_direction": "bearish", "notes": "Chine = composante majeure EEM."},
|
||||
{"instrument_id": "AAPL", "sensitivity": 0.85, "typical_direction": "bearish", "notes": "Apple : fabrication en Chine + ventes chinoises menacées."},
|
||||
{"instrument_id": "USDJPY=X", "sensitivity": 0.70, "typical_direction": "bearish", "notes": "JPY safe haven."},
|
||||
{"instrument_id": "BTC-USD", "sensitivity": 0.65, "typical_direction": "bearish", "notes": "Risk-off initial."},
|
||||
]
|
||||
},
|
||||
|
||||
{
|
||||
"name": "Guerre commerciale / Tarifs douaniers",
|
||||
"type": "geopolitical",
|
||||
"sub_type": "TradeWar",
|
||||
"description": "Annonce de tarifs douaniers, escalade guerre commerciale US-Chine ou US-UE.",
|
||||
"default_impacts": [
|
||||
{"instrument_id": "EEM", "sensitivity": 0.85, "typical_direction": "bearish", "notes": "Exportateurs EM très exposés."},
|
||||
{"instrument_id": "QQQ", "sensitivity": 0.80, "typical_direction": "bearish", "notes": "Tech exposé via supply chain et marchés chinois."},
|
||||
{"instrument_id": "AAPL", "sensitivity": 0.88, "typical_direction": "bearish", "notes": "Apple : tarifs sur produits fabriqués en Chine."},
|
||||
{"instrument_id": "NVDA", "sensitivity": 0.82, "typical_direction": "bearish", "notes": "Export controls sur chips."},
|
||||
{"instrument_id": "SPY", "sensitivity": 0.75, "typical_direction": "bearish", "notes": "Stagflation domestique + récession mondiale."},
|
||||
{"instrument_id": "GLD", "sensitivity": 0.72, "typical_direction": "bullish", "notes": "Hedge inflation + incertitude."},
|
||||
{"instrument_id": "TLT", "sensitivity": 0.65, "typical_direction": "depends_on_outcome", "notes": "Récession = haussier TLT. Inflation = baissier."},
|
||||
{"instrument_id": "EURUSD=X", "sensitivity": 0.68, "typical_direction": "bearish", "notes": "USD peut se renforcer en position de force."},
|
||||
{"instrument_id": "VXX", "sensitivity": 0.80, "typical_direction": "bullish", "notes": "Incertitude = vol monte."},
|
||||
{"instrument_id": "USO", "sensitivity": 0.60, "typical_direction": "bearish", "notes": "Récession mondiale = demande pétrole baisse."},
|
||||
{"instrument_id": "XOM", "sensitivity": 0.58, "typical_direction": "bearish", "notes": "Même logique que USO."},
|
||||
]
|
||||
},
|
||||
|
||||
{
|
||||
"name": "Crise bancaire / Stress systémique",
|
||||
"type": "geopolitical",
|
||||
"sub_type": "Banking",
|
||||
"description": "Faillite bancaire, crise de liquidité, risque systémique (type SVB, Lehman, gilts UK).",
|
||||
"default_impacts": [
|
||||
{"instrument_id": "VXX", "sensitivity": 0.97, "typical_direction": "bullish", "notes": "Crise systémique = vol spike maximal."},
|
||||
{"instrument_id": "HYG", "sensitivity": 0.92, "typical_direction": "bearish", "notes": "Spreads HY explosent en crise de crédit."},
|
||||
{"instrument_id": "GS", "sensitivity": 0.90, "typical_direction": "bearish", "notes": "Banques directement exposées."},
|
||||
{"instrument_id": "GLD", "sensitivity": 0.85, "typical_direction": "bullish", "notes": "Safe haven ultime."},
|
||||
{"instrument_id": "TLT", "sensitivity": 0.80, "typical_direction": "bullish", "notes": "Fuite vers la qualité + attente pivot Fed."},
|
||||
{"instrument_id": "SPY", "sensitivity": 0.82, "typical_direction": "bearish", "notes": "Contagion systémique."},
|
||||
{"instrument_id": "IWM", "sensitivity": 0.88, "typical_direction": "bearish", "notes": "Small caps très dépendantes du crédit bancaire régional."},
|
||||
{"instrument_id": "BTC-USD", "sensitivity": 0.72, "typical_direction": "depends_on_outcome", "notes": "Initial sell-off, puis possible rally si pivot Fed."},
|
||||
{"instrument_id": "EURUSD=X", "sensitivity": 0.72, "typical_direction": "depends_on_outcome", "notes": "USD safe haven. EUR exposé si crise zone euro."},
|
||||
{"instrument_id": "USDJPY=X", "sensitivity": 0.70, "typical_direction": "bearish", "notes": "Risk-off = JPY monte."},
|
||||
]
|
||||
},
|
||||
|
||||
{
|
||||
"name": "Stimulus Chine / PBOC",
|
||||
"type": "geopolitical",
|
||||
"sub_type": "China",
|
||||
"description": "Annonce de stimulus économique par Pékin ou décision d'assouplissement de la PBOC.",
|
||||
"default_impacts": [
|
||||
{"instrument_id": "EEM", "sensitivity": 0.92, "typical_direction": "bullish", "notes": "Chine = moteur principal des émergents."},
|
||||
{"instrument_id": "GLD", "sensitivity": 0.75, "typical_direction": "bullish", "notes": "Stimulus = inflation + demande bijouterie/physique."},
|
||||
{"instrument_id": "USO", "sensitivity": 0.80, "typical_direction": "bullish", "notes": "Chine = 1er importateur pétrole. Stimulus = demande monte."},
|
||||
{"instrument_id": "SLV", "sensitivity": 0.78, "typical_direction": "bullish", "notes": "Chine = 1er consommateur industriel argent."},
|
||||
{"instrument_id": "XOM", "sensitivity": 0.70, "typical_direction": "bullish", "notes": "Demande pétrole Chine monte."},
|
||||
{"instrument_id": "EFA", "sensitivity": 0.70, "typical_direction": "bullish", "notes": "Europe/Japon exportent vers Chine."},
|
||||
{"instrument_id": "SPY", "sensitivity": 0.60, "typical_direction": "bullish", "notes": "Sentiment global améliore."},
|
||||
{"instrument_id": "EURUSD=X", "sensitivity": 0.58, "typical_direction": "bullish", "notes": "Risk-on = USD faible."},
|
||||
]
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
def bootstrap_impact_categories(force: bool = False) -> Dict[str, Any]:
|
||||
"""Insert default impact categories. Idempotent if force=False."""
|
||||
from services.database import upsert_event_category, get_all_event_categories
|
||||
|
||||
existing = {c["name"] for c in get_all_event_categories()}
|
||||
inserted = skipped = 0
|
||||
for cat in CATEGORIES:
|
||||
if not force and cat["name"] in existing:
|
||||
skipped += 1
|
||||
continue
|
||||
upsert_event_category(cat)
|
||||
inserted += 1
|
||||
|
||||
return {"inserted": inserted, "skipped": skipped, "total": len(CATEGORIES)}
|
||||
256
backend/services/impact_service.py
Normal file
256
backend/services/impact_service.py
Normal file
@@ -0,0 +1,256 @@
|
||||
"""
|
||||
Impact evaluation service.
|
||||
Given a market_event (calendar or geopolitical), calls the AI to estimate
|
||||
instrument-level impacts (score 0-1, direction, rationale, confidence).
|
||||
Results are persisted in instrument_impacts table.
|
||||
"""
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
ALL_INSTRUMENTS = [
|
||||
"SPY","QQQ","IWM","EEM","EFA",
|
||||
"GLD","SLV","USO","UNG","TLT",
|
||||
"HYG","EURUSD=X","USDJPY=X","GBPUSD=X",
|
||||
"VXX","AAPL","NVDA","GS","XOM","BTC-USD",
|
||||
]
|
||||
|
||||
INSTRUMENT_NAMES = {
|
||||
"SPY": "S&P 500 ETF", "QQQ": "Nasdaq 100 ETF", "IWM": "Russell 2000 ETF",
|
||||
"EEM": "Emerging Markets ETF", "EFA": "MSCI EAFE ETF",
|
||||
"GLD": "Gold ETF", "SLV": "Silver ETF", "USO": "WTI Oil ETF",
|
||||
"UNG": "Natural Gas ETF", "TLT": "US 20Y Treasury ETF",
|
||||
"HYG": "High Yield Credit ETF", "EURUSD=X": "EUR/USD",
|
||||
"USDJPY=X": "USD/JPY", "GBPUSD=X": "GBP/USD",
|
||||
"VXX": "VIX Tracker", "AAPL": "Apple Inc",
|
||||
"NVDA": "NVIDIA Corp", "GS": "Goldman Sachs",
|
||||
"XOM": "ExxonMobil", "BTC-USD": "Bitcoin",
|
||||
}
|
||||
|
||||
_INST_LIST_STR = ", ".join(f"{k} ({v})" for k, v in INSTRUMENT_NAMES.items())
|
||||
|
||||
|
||||
def _get_api_key() -> str:
|
||||
key = os.environ.get("OPENAI_API_KEY", "")
|
||||
if not key:
|
||||
from services.database import get_config
|
||||
key = get_config("openai_api_key") or ""
|
||||
return key
|
||||
|
||||
|
||||
def _build_prompt(event: Dict[str, Any], category_defaults: Optional[List[Dict]] = None) -> str:
|
||||
cat_context = ""
|
||||
if category_defaults:
|
||||
top = sorted(category_defaults, key=lambda x: x.get("sensitivity", 0), reverse=True)[:8]
|
||||
lines = [f" • {d['instrument_id']}: sensibilité {d.get('sensitivity',0):.0%}, direction usuelle = {d.get('typical_direction','?')}" for d in top]
|
||||
cat_context = "\nImpacts par défaut de cette catégorie (ajuste si l'événement est atypique) :\n" + "\n".join(lines)
|
||||
|
||||
return f"""Tu es un analyste macro senior spécialisé en options et trading multi-actifs.
|
||||
|
||||
ÉVÉNEMENT À ANALYSER :
|
||||
- Nom : {event.get('name', '')}
|
||||
- Date : {event.get('start_date', '')}
|
||||
- Type : {event.get('category', '')} / {event.get('sub_type', '')}
|
||||
- Description : {event.get('description', '')}
|
||||
- Impact attendu (résumé) : {event.get('market_impact', '')}
|
||||
{cat_context}
|
||||
|
||||
MISSION :
|
||||
Évalue l'impact potentiel de cet événement sur chacun des 20 instruments suivants.
|
||||
Instruments disponibles : {_INST_LIST_STR}
|
||||
|
||||
Pour chaque instrument IMPACTÉ (score >= 0.15), retourne :
|
||||
- instrument_id : identifiant exact (ex: "SPY", "EURUSD=X", "BTC-USD")
|
||||
- impact_score : 0.0 (aucun) → 1.0 (impact majeur, mouvement > 2%)
|
||||
- direction : "bullish" | "bearish" | "neutral" | "depends_on_outcome"
|
||||
- rationale : 1 phrase MAX expliquant le mécanisme
|
||||
- confidence : 0.0-1.0 (ta certitude sur l'estimation)
|
||||
|
||||
Critères de scoring :
|
||||
- 0.0-0.2 : négligeable (bruit)
|
||||
- 0.2-0.4 : faible (< 0.5% mouvement)
|
||||
- 0.4-0.6 : modéré (0.5-1% mouvement)
|
||||
- 0.6-0.8 : fort (1-2% mouvement)
|
||||
- 0.8-1.0 : majeur (> 2%, event driver primaire)
|
||||
|
||||
FORMAT JSON STRICT (pas de texte hors du JSON) :
|
||||
{{
|
||||
"impacts": [
|
||||
{{"instrument_id": "SPY", "impact_score": 0.85, "direction": "bearish", "rationale": "Hausse taux comprime les multiples P/E", "confidence": 0.90}}
|
||||
],
|
||||
"summary": "Contexte macro de cet événement en 1 phrase",
|
||||
"key_driver": "Instrument le plus impacté et pourquoi"
|
||||
}}"""
|
||||
|
||||
|
||||
def evaluate_event_impacts(
|
||||
event_id: int,
|
||||
force: bool = False,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Evaluate instrument impacts for a market_event via AI.
|
||||
Saves results to instrument_impacts table.
|
||||
Returns the list of impacts + AI summary.
|
||||
"""
|
||||
from services.database import (
|
||||
get_all_market_events, get_event_category,
|
||||
get_impacts_for_source, delete_impacts_for_source,
|
||||
save_instrument_impacts,
|
||||
)
|
||||
|
||||
# Fetch event
|
||||
events = get_all_market_events()
|
||||
event = next((e for e in events if e["id"] == event_id), None)
|
||||
if not event:
|
||||
return {"error": f"Event {event_id} not found"}
|
||||
|
||||
# Check if already evaluated
|
||||
existing = get_impacts_for_source("event", event_id)
|
||||
if existing and not force:
|
||||
return {
|
||||
"event_id": event_id,
|
||||
"impacts": existing,
|
||||
"summary": "",
|
||||
"from_cache": True,
|
||||
"n_instruments": len(existing),
|
||||
}
|
||||
|
||||
if force and existing:
|
||||
delete_impacts_for_source("event", event_id)
|
||||
|
||||
# Get category defaults for context
|
||||
category_defaults = None
|
||||
cat_name = event.get("sub_type") or event.get("category", "")
|
||||
if cat_name:
|
||||
for possible in [
|
||||
cat_name,
|
||||
f"FOMC — Décision de taux" if "FOMC" in cat_name.upper() else None,
|
||||
f"NFP — Non-Farm Payrolls" if "NFP" in cat_name.upper() else None,
|
||||
]:
|
||||
if possible:
|
||||
cat = get_event_category(possible)
|
||||
if cat:
|
||||
try:
|
||||
category_defaults = json.loads(cat.get("default_impacts") or "[]")
|
||||
except Exception:
|
||||
pass
|
||||
break
|
||||
|
||||
api_key = _get_api_key()
|
||||
if not api_key:
|
||||
return {"error": "No OpenAI API key configured"}
|
||||
|
||||
prompt = _build_prompt(event, category_defaults)
|
||||
|
||||
try:
|
||||
from openai import OpenAI
|
||||
client = OpenAI(api_key=api_key)
|
||||
response = client.chat.completions.create(
|
||||
model="gpt-4o-mini",
|
||||
messages=[{"role": "user", "content": prompt}],
|
||||
response_format={"type": "json_object"},
|
||||
temperature=0.25,
|
||||
max_tokens=1200,
|
||||
)
|
||||
raw = json.loads(response.choices[0].message.content)
|
||||
except Exception as e:
|
||||
logger.error(f"[impact_service] AI call failed for event {event_id}: {e}")
|
||||
return {"error": str(e)}
|
||||
|
||||
ai_impacts = raw.get("impacts", [])
|
||||
summary = raw.get("summary", "")
|
||||
key_driver = raw.get("key_driver", "")
|
||||
|
||||
# Persist
|
||||
to_save = []
|
||||
for imp in ai_impacts:
|
||||
if imp.get("instrument_id") not in ALL_INSTRUMENTS:
|
||||
continue
|
||||
to_save.append({
|
||||
"source_type": "event",
|
||||
"source_id": event_id,
|
||||
"source_name": event["name"],
|
||||
"source_date": event["start_date"],
|
||||
"category_name": cat_name,
|
||||
"instrument_id": imp["instrument_id"],
|
||||
"impact_score": float(imp.get("impact_score", 0.5)),
|
||||
"direction": imp.get("direction", "neutral"),
|
||||
"rationale": imp.get("rationale", ""),
|
||||
"confidence": float(imp.get("confidence", 0.7)),
|
||||
"ai_generated": 1,
|
||||
})
|
||||
|
||||
save_instrument_impacts(to_save)
|
||||
saved = get_impacts_for_source("event", event_id)
|
||||
|
||||
return {
|
||||
"event_id": event_id,
|
||||
"event_name": event["name"],
|
||||
"impacts": saved,
|
||||
"summary": summary,
|
||||
"key_driver": key_driver,
|
||||
"n_instruments": len(saved),
|
||||
"from_cache": False,
|
||||
}
|
||||
|
||||
|
||||
def get_impact_monitor_data(days: int = 7, min_score: float = 0.3) -> Dict[str, Any]:
|
||||
"""
|
||||
Return all evaluated sources (events + news) from the last N days,
|
||||
filtered by min_score, with their instrument impacts.
|
||||
Also includes unevaluated events for quick launch.
|
||||
"""
|
||||
from services.database import (
|
||||
get_weekly_impact_sources, get_all_market_events,
|
||||
get_impacts_for_source,
|
||||
)
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
cutoff = (datetime.utcnow() - timedelta(days=days)).strftime("%Y-%m-%d")
|
||||
|
||||
# Evaluated sources
|
||||
evaluated = get_weekly_impact_sources(days=days, min_score=min_score)
|
||||
|
||||
# All recent events not yet evaluated
|
||||
all_events = get_all_market_events()
|
||||
evaluated_ids = {(s["source_type"], s.get("source_id")) for s in evaluated}
|
||||
|
||||
unevaluated = []
|
||||
for ev in all_events:
|
||||
if ev.get("start_date", "") < cutoff:
|
||||
continue
|
||||
if ("event", ev["id"]) not in evaluated_ids:
|
||||
unevaluated.append({
|
||||
"id": ev["id"],
|
||||
"name": ev["name"],
|
||||
"start_date": ev["start_date"],
|
||||
"category": ev.get("category",""),
|
||||
"sub_type": ev.get("sub_type",""),
|
||||
"impact_score": ev.get("impact_score", 0.5),
|
||||
})
|
||||
|
||||
# Top impacted instruments this period
|
||||
from collections import defaultdict
|
||||
inst_scores: Dict[str, List[float]] = defaultdict(list)
|
||||
for source in evaluated:
|
||||
for imp in source.get("impacts", []):
|
||||
score = imp.get("adjusted_score") or imp.get("impact_score", 0)
|
||||
if score >= min_score:
|
||||
inst_scores[imp["instrument_id"]].append(score)
|
||||
|
||||
top_instruments = sorted(
|
||||
[{"instrument_id": k, "avg_score": sum(v)/len(v), "n_events": len(v)}
|
||||
for k, v in inst_scores.items()],
|
||||
key=lambda x: x["avg_score"] * x["n_events"], reverse=True
|
||||
)[:10]
|
||||
|
||||
return {
|
||||
"evaluated": evaluated,
|
||||
"unevaluated": sorted(unevaluated, key=lambda x: x["start_date"], reverse=True),
|
||||
"top_instruments": top_instruments,
|
||||
"period_days": days,
|
||||
"min_score": min_score,
|
||||
}
|
||||
Reference in New Issue
Block a user