Files
OpenFin/backend/routers/impact.py
OpenSquared 0b1fcff49c feat: Market Events — catégories, cleanup sidebar, rename
- Supprime Timeline & ImpactMonitor du routing (redirects /impact + /timeline → /market-events)
- Renomme 'Instrument Snap.' → 'Instrument Analysis' dans la sidebar
- Onglet 'Catégories & Defaults' dans Market Events: CRUD complet (créer/éditer/supprimer)
  chaque catégorie a une table d'impacts par défaut (instrument, sensibilité, direction, notes)
- impact_service: meilleur matching catégorie (sub_type fuzzy + type fallback)
- DB: delete_event_category() + DELETE /api/impact/categories/{name}

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-25 21:11:24 +02:00

165 lines
6.3 KiB
Python

"""
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.delete("/categories/{name}")
def delete_category(name: str) -> Dict[str, Any]:
from services.database import delete_event_category
ok = delete_event_category(name)
if not ok:
raise HTTPException(404, f"Category '{name}' not found")
return {"status": "deleted", "name": name}
@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)