- 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>
165 lines
6.3 KiB
Python
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)
|