""" 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)